From 27f95f3c80f3233d5e69315c1ee74d7b08567f72 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Wed, 5 Oct 2022 01:41:31 +0900 Subject: [PATCH] MountStubsCleaner: preserve timestamps Fix issue 3148 Signed-off-by: Akihiro Suda --- client/client_test.go | 77 ++++++++++++++++++++++++++++++++++++ executor/stubs.go | 26 +++++++++++- util/system/atime_unix.go | 21 ++++++++++ util/system/atime_windows.go | 17 ++++++++ 4 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 util/system/atime_unix.go create mode 100644 util/system/atime_windows.go diff --git a/client/client_test.go b/client/client_test.go index f061e86991877..2344db77e5dee 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -183,6 +183,7 @@ func TestIntegration(t *testing.T) { testSBOMScan, testSBOMScanSingleRef, testMultipleCacheExports, + testMountStubsTimestamp, ) } @@ -7927,6 +7928,82 @@ func testMultipleCacheExports(t *testing.T, sb integration.Sandbox) { ensureFileContents(t, filepath.Join(destDir, "unique"), string(uniqueFile)) } +// https://github.com/moby/buildkit/issues/3148 +func testMountStubsTimestamp(t *testing.T, sb integration.Sandbox) { + c, err := New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + const sourceDateEpoch = int64(1234567890) // Fri Feb 13 11:31:30 PM UTC 2009 + st := llb.Image("busybox:latest").Run( + llb.Args([]string{"/bin/touch", fmt.Sprintf("--date=@%d", sourceDateEpoch), + "/bin", + "/etc", + "/var", + "/var/foo", + "/tmp", + "/tmp/foo2", + "/tmp/foo2/bar", + }), + llb.AddMount("/var/foo", llb.Scratch(), llb.Tmpfs()), + llb.AddMount("/tmp/foo2/bar", llb.Scratch(), llb.Tmpfs()), + ) + def, err := st.Marshal(sb.Context()) + require.NoError(t, err) + + tmpDir := t.TempDir() + tarFile := filepath.Join(tmpDir, "out.tar") + tarFileW, err := os.Create(tarFile) + require.NoError(t, err) + defer tarFileW.Close() + + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterTar, + Output: fixedWriteCloser(tarFileW), + }, + }, + }, nil) + require.NoError(t, err) + tarFileW.Close() + + tarFileR, err := os.Open(tarFile) + require.NoError(t, err) + defer tarFileR.Close() + tarR := tar.NewReader(tarFileR) + touched := map[string]*tar.Header{ + "bin/": nil, // Regular dir + "etc/": nil, // Parent of file mounts (etc/{resolv.conf, hosts}) + "var/": nil, // Parent of dir mount (var/foo) + "tmp/": nil, // Grandparent of dir mount (tmp/foo2/bar) + // No support for reproducing the timestamps of mount point directories such as /dev, + // because the touched timestamp value is lost when the mount is unmounted. + } + mustNotExist := map[string]struct{}{ + "var/foo": {}, // Created on mounting var/foo, and removed on unmounting it + "tmp/foo2": {}, // Created on mounting tmp/foo2/bar, and removed on unmounting it + } + for { + hd, err := tarR.Next() + if errors.Is(err, io.EOF) { + break + } + require.NoError(t, err) + if _, ok := mustNotExist[hd.Name]; ok { + t.Errorf("must not exist %q: %+v", hd.Name, hd) + } + if x, ok := touched[hd.Name]; ok && x == nil { + touched[hd.Name] = hd + } + } + for name, hd := range touched { + t.Logf("Verifying %q (%+v)", name, hd) + require.NotNil(t, hd, name) + require.Equal(t, sourceDateEpoch, hd.ModTime.Unix(), name) + } +} + func ensureFile(t *testing.T, path string) { st, err := os.Stat(path) require.NoError(t, err, "expected file at %s", path) diff --git a/executor/stubs.go b/executor/stubs.go index 2c13b13053a4f..167db203f6570 100644 --- a/executor/stubs.go +++ b/executor/stubs.go @@ -7,6 +7,8 @@ import ( "syscall" "github.com/containerd/continuity/fs" + "github.com/moby/buildkit/util/system" + "github.com/sirupsen/logrus" ) func MountStubsCleaner(dir string, mounts []Mount) func() { @@ -43,7 +45,29 @@ func MountStubsCleaner(dir string, mounts []Mount) func() { if st.Size() != 0 { continue } - os.Remove(p) + // Back up the timestamps of the dir for reproducible builds + // https://github.com/moby/buildkit/issues/3148 + dir := filepath.Dir(p) + dirSt, err := os.Stat(dir) + if err != nil { + logrus.WithError(err).Warnf("Failed to stat %q (parent of mount stub %q)", dir, p) + continue + } + mtime := dirSt.ModTime() + atime, err := system.Atime(dirSt) + if err != nil { + logrus.WithError(err).Warnf("Failed to stat atime of %q (parent of mount stub %q)", dir, p) + atime = mtime + } + + if err := os.Remove(p); err != nil { + logrus.WithError(err).Warnf("Failed to remove mount stub %q", p) + } + + // Restore the timestamps of the dir + if err := os.Chtimes(dir, atime, mtime); err != nil { + logrus.WithError(err).Warnf("Failed to restore time time mount stub timestamp (os.Chtimes(%q, %v, %v))", dir, atime, mtime) + } } } } diff --git a/util/system/atime_unix.go b/util/system/atime_unix.go new file mode 100644 index 0000000000000..d3f44aa53a324 --- /dev/null +++ b/util/system/atime_unix.go @@ -0,0 +1,21 @@ +//go:build !windows +// +build !windows + +package system + +import ( + "fmt" + iofs "io/fs" + "syscall" + "time" + + "github.com/containerd/continuity/fs" +) + +func Atime(st iofs.FileInfo) (time.Time, error) { + stSys, ok := st.Sys().(*syscall.Stat_t) + if !ok { + return time.Time{}, fmt.Errorf("expected st.Sys() to be *syscall.Stat_t, got %T", st.Sys()) + } + return fs.StatATimeAsTime(stSys), nil +} diff --git a/util/system/atime_windows.go b/util/system/atime_windows.go new file mode 100644 index 0000000000000..808408b613cfb --- /dev/null +++ b/util/system/atime_windows.go @@ -0,0 +1,17 @@ +package system + +import ( + "fmt" + iofs "io/fs" + "syscall" + "time" +) + +func Atime(st iofs.FileInfo) (time.Time, error) { + stSys, ok := st.Sys().(*syscall.Win32FileAttributeData) + if !ok { + return time.Time{}, fmt.Errorf("expected st.Sys() to be *syscall.Win32FileAttributeData, got %T", st.Sys()) + } + // ref: https://github.com/golang/go/blob/go1.19.2/src/os/types_windows.go#L230 + return time.Unix(0, stSys.LastAccessTime.Nanoseconds()), nil +}