diff --git a/client/mergediff_test.go b/client/mergediff_test.go index 4aa0cf39adf6..adeeeb81bc99 100644 --- a/client/mergediff_test.go +++ b/client/mergediff_test.go @@ -428,6 +428,33 @@ func diffOpTestCases() (tests []integration.Test) { ), }) + // Check that deleting together a file from the base and another one from a merge + // does not result in a crash. + deleteFileAfterMergeCmds := []string{ + "rm /unmodifiedDir/deleteFile1 /unmodifiedDir/deleteFile2", + } + + extraContent := llb.Scratch(). + File(llb.Mkdir("/unmodifiedDir", 0755)). + File(llb.Mkfile("/unmodifiedDir/deleteFile2", 0644, []byte("foo"))) + + tests = append(tests, verifyContents{ + name: "TestDiffDeleteFilesAfterMerge", + state: llb.Diff( + base(), + runShell(llb.Merge([]llb.State{ + base(), + extraContent, + }), + joinCmds( + deleteFileAfterMergeCmds, + )...)), + contents: mergeContents( + apply( + fstest.CreateDir("/unmodifiedDir", 0755)), + ), + }) + // Opaque dirs should be converted to the "explicit whiteout" format, as described in the OCI image spec: // https://github.com/opencontainers/image-spec/blob/main/layer.md#opaque-whiteout opaqueDirCmds := []string{ diff --git a/snapshot/diffapply_unix.go b/snapshot/diffapply_unix.go index 72395d33a668..ddba5d117f7a 100644 --- a/snapshot/diffapply_unix.go +++ b/snapshot/diffapply_unix.go @@ -724,7 +724,10 @@ func (d *differ) overlayChanges(ctx context.Context, handle func(context.Context return errors.Errorf("unhandled stat type for %+v", srcfi) } - if !srcfi.IsDir() && c.srcStat.Nlink > 1 { + // Changes with Delete kind may share the same inode even if they are unrelated. + // Skip them to avoid creating hardlinks between whiteouts as whiteouts are not + // always created and may leave the hardlink dangling. + if !srcfi.IsDir() && c.srcStat.Nlink > 1 && c.kind != fs.ChangeKindDelete { if linkSubPath, ok := d.inodes[statInode(c.srcStat)]; ok { c.linkSubPath = linkSubPath } else {