From 88b8a25a1ee00ea9b5667f3ee5050e59679ed3ed Mon Sep 17 00:00:00 2001 From: Jack Decker <24392469+jackowfish@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:07:38 -0400 Subject: [PATCH] tmpfs: stream file contents when writing a tar archive --- pkg/safemem/io.go | 14 ++ pkg/safemem/io_test.go | 14 ++ pkg/sentry/fsimpl/tmpfs/fscheckpoint.go | 27 ++-- pkg/sentry/fsimpl/tmpfs/tar.go | 107 ++++++--------- pkg/sentry/fsimpl/tmpfs/tar_test.go | 174 ++++++++++++++++++++---- pkg/sentry/fsimpl/tmpfs/tmpfs.go | 11 +- 6 files changed, 231 insertions(+), 116 deletions(-) diff --git a/pkg/safemem/io.go b/pkg/safemem/io.go index ecbfdf39b1d..33b522a93eb 100644 --- a/pkg/safemem/io.go +++ b/pkg/safemem/io.go @@ -146,6 +146,20 @@ func (r ToIOReader) Read(dst []byte) (int, error) { return int(n), err } +// ToIOWriter implements io.Writer for a (safemem.)Writer. +// +// ToIOWriter will return a successful partial write iff Writer.WriteFromBlocks +// does so. +type ToIOWriter struct { + Writer Writer +} + +// Write implements io.Writer.Write. +func (w ToIOWriter) Write(src []byte) (int, error) { + n, err := w.Writer.WriteFromBlocks(BlockSeqOf(BlockFromSafeSlice(src))) + return int(n), err +} + // FromIOReader implements Reader for an io.Reader by repeatedly invoking // io.Reader.Read until it returns an error or partial read. This is not // thread-safe. diff --git a/pkg/safemem/io_test.go b/pkg/safemem/io_test.go index db4cd6352cd..cd1a0e74e99 100644 --- a/pkg/safemem/io_test.go +++ b/pkg/safemem/io_test.go @@ -197,3 +197,17 @@ func TestWriteFullToBlocks(t *testing.T) { t.Errorf("dst: got %q, wanted %q", got, want) } } + +func TestToIOWriter(t *testing.T) { + dsts := makeBlocks(make([]byte, 3), make([]byte, 3)) + w := ToIOWriter{&BlockSeqWriter{BlockSeqFromSlice(dsts)}} + n, err := w.Write([]byte("foobar")) + if wantN := 6; n != wantN || err != nil { + t.Errorf("Write: got (%v, %v), wanted (%v, nil)", n, err, wantN) + } + for i, want := range [][]byte{[]byte("foo"), []byte("bar")} { + if got := dsts[i].ToSlice(); !bytes.Equal(got, want) { + t.Errorf("dsts[%d]: got %q, wanted %q", i, got, want) + } + } +} diff --git a/pkg/sentry/fsimpl/tmpfs/fscheckpoint.go b/pkg/sentry/fsimpl/tmpfs/fscheckpoint.go index c711813ce45..808b90fe5ce 100644 --- a/pkg/sentry/fsimpl/tmpfs/fscheckpoint.go +++ b/pkg/sentry/fsimpl/tmpfs/fscheckpoint.go @@ -114,11 +114,10 @@ func (cb *fsckptTarWriterCallbacks) regularFileWrite(ctx context.Context, rf *re // fsckptTarReaderCallbacks implements tarReaderCallbacks by storing MemoryFile // offsets containing regular file data in the tar archive. type fsckptTarReaderCallbacks struct { - fs *filesystem - regularFiles map[*tar.Header]*fsckptRegularFile + fs *filesystem } -func (cb *fsckptTarReaderCallbacks) regularFileRead(ctx context.Context, hdr *tar.Header, tr *tar.Reader) error { +func (cb *fsckptTarReaderCallbacks) regularFileRead(ctx context.Context, hdr *tar.Header, tr *tar.Reader, rf *regularFile) error { if hdr.Size < 8 { return fmt.Errorf("header size %d too small for regular file size", hdr.Size) } @@ -131,29 +130,21 @@ func (cb *fsckptTarReaderCallbacks) regularFileRead(ctx context.Context, hdr *ta if _, err := io.ReadFull(tr, buf[:]); err != nil { return fmt.Errorf("failed to read file size from tar: %w", err) } - crf := &fsckptRegularFile{ - size: binary.LittleEndian.Uint64(buf[:]), - data: make([]fsckptRegularFileSegment, remSize/segSize), - } - if _, err := ReadCheckpointRegularFileSegmentSlice(tr, crf.data); err != nil { + size := binary.LittleEndian.Uint64(buf[:]) + segs := make([]fsckptRegularFileSegment, remSize/segSize) + if _, err := ReadCheckpointRegularFileSegmentSlice(tr, segs); err != nil { return fmt.Errorf("failed to read file segments from tar: %w", err) } - cb.regularFiles[hdr] = crf - return nil -} - -func (cb *fsckptTarReaderCallbacks) regularFileSetContents(ctx context.Context, hdr *tar.Header, rf *regularFile) error { - crf := cb.regularFiles[hdr] rf.inode.mu.Lock() defer rf.inode.mu.Unlock() rf.dataMu.Lock() defer rf.dataMu.Unlock() - rf.size.Store(uint64(crf.size)) + rf.size.Store(size) gap := rf.data.FirstGap() n := uint64(0) - for _, rfseg := range crf.data { - gap = rf.data.Insert(gap, memmap.MappableRange{rfseg.Start, rfseg.End}, rfseg.Value).NextGap() - n += (rfseg.End - rfseg.Start) / hostarch.PageSize + for _, seg := range segs { + gap = rf.data.Insert(gap, memmap.MappableRange{seg.Start, seg.End}, seg.Value).NextGap() + n += (seg.End - seg.Start) / hostarch.PageSize } if !cb.fs.accountPages(n) { return fmt.Errorf("restored filesystem would exceed size limit of %d pages", cb.fs.maxSizeInPages) diff --git a/pkg/sentry/fsimpl/tmpfs/tar.go b/pkg/sentry/fsimpl/tmpfs/tar.go index 54d92bccb11..8d2413d0ff4 100644 --- a/pkg/sentry/fsimpl/tmpfs/tar.go +++ b/pkg/sentry/fsimpl/tmpfs/tar.go @@ -16,7 +16,6 @@ package tmpfs import ( "archive/tar" - "bytes" "encoding/base64" "fmt" "io" @@ -29,9 +28,9 @@ import ( "gvisor.dev/gvisor/pkg/abi/linux" "gvisor.dev/gvisor/pkg/context" "gvisor.dev/gvisor/pkg/log" + "gvisor.dev/gvisor/pkg/safemem" "gvisor.dev/gvisor/pkg/sentry/kernel/auth" "gvisor.dev/gvisor/pkg/sentry/vfs" - "gvisor.dev/gvisor/pkg/usermem" ) // tarRead creates the corresponding dentry and its children from the given @@ -56,6 +55,16 @@ func (fs *filesystem) readFromTar(ctx context.Context, tr *tar.Reader, cb tarRea fileToHeader := map[string]*tar.Header{} symlinkToHeader := map[string]*tar.Header{} linkToHeader := map[string]*tar.Header{} + // Regular file contents are streamed from tr into a detached inode as each + // entry is read, since tr is sequential. The inode is attached to its parent + // directory below, once all directories exist. + headerToRegularFile := map[*tar.Header]*inode{} + defer func() { + // Release regular files that were not attached because of an error. + for _, ino := range headerToRegularFile { + ino.decRef(ctx) + } + }() for { header, err := tr.Next() if err != nil { @@ -69,9 +78,14 @@ func (fs *filesystem) readFromTar(ctx context.Context, tr *tar.Reader, cb tarRea case tar.TypeDir: directoryToHeader[header.Name] = header case tar.TypeReg: - if err := cb.regularFileRead(ctx, header, tr); err != nil { + ino, err := fs.newRegularFile(auth.KUID(header.Uid), auth.KGID(header.Gid), linux.FileMode(header.Mode), nil /* parentDir */) + if err != nil { return err } + headerToRegularFile[header] = ino + if err := cb.regularFileRead(ctx, header, tr, ino.impl.(*regularFile)); err != nil { + return fmt.Errorf("failed to read file %v: %w", header.Name, err) + } fileToHeader[header.Name] = header case tar.TypeFifo, tar.TypeBlock, tar.TypeChar: fileToHeader[header.Name] = header @@ -91,9 +105,10 @@ func (fs *filesystem) readFromTar(ctx context.Context, tr *tar.Reader, cb tarRea } // Re-create all regular files, FIFOs, block devices, and character devices. for path, hdr := range fileToHeader { - if err := fs.mknodFromTar(ctx, hdr, pathToInode, cb); err != nil { + if err := fs.mknodFromTar(hdr, pathToInode, headerToRegularFile); err != nil { return fmt.Errorf("failed to make file %v: %w", path, err) } + delete(headerToRegularFile, hdr) } // Re-create all symlinks. for path, hdr := range symlinkToHeader { @@ -194,10 +209,10 @@ func (fs *filesystem) mkdirFromTar(hdr *tar.Header, pathToInode map[string]*inod return childDir.dentry.inode, nil } -// mknodFromTar creates a regular file,FIFO, block device, or character device file using -// the provided header. It also writes the file content to the corresponding regular file if it -// exists. -func (fs *filesystem) mknodFromTar(ctx context.Context, hdr *tar.Header, pathToInode map[string]*inode, cb tarReaderCallbacks) error { +// mknodFromTar creates a FIFO, block device, or character device file using +// the provided header, or attaches the regular file already created for it in +// headerToRegularFile. +func (fs *filesystem) mknodFromTar(hdr *tar.Header, pathToInode map[string]*inode, headerToRegularFile map[*tar.Header]*inode) error { dir, name := filepath.Split(hdr.Name) parentInode, ok := pathToInode[dir] if !ok { @@ -211,7 +226,7 @@ func (fs *filesystem) mknodFromTar(ctx context.Context, hdr *tar.Header, pathToI var err error switch hdr.Typeflag { case tar.TypeReg: - childInode, err = fs.newRegularFile(auth.KUID(hdr.Uid), auth.KGID(hdr.Gid), linux.FileMode(hdr.Mode), nil /* parentDir */) + childInode = headerToRegularFile[hdr] case tar.TypeFifo: childInode, err = fs.newNamedPipe(auth.KUID(hdr.Uid), auth.KGID(hdr.Gid), linux.FileMode(hdr.Mode), nil /* parentDir */) case tar.TypeBlock: @@ -235,14 +250,6 @@ func (fs *filesystem) mknodFromTar(ctx context.Context, hdr *tar.Header, pathToI child := fs.newDentry(childInode) parentDir.insertChildLocked(child, name) pathToInode[hdr.Name] = childInode - - // Write file contents to the corresponding regular files. - if rf, _ := childInode.impl.(*regularFile); rf != nil { - if err := cb.regularFileSetContents(ctx, hdr, rf); err != nil { - return err - } - } - return nil } @@ -311,53 +318,29 @@ func (fs *filesystem) symlinkFromTar(hdr *tar.Header, pathToInode map[string]*in } type tarReaderCallbacks interface { - // regularFileRead reads information about the regular file with header hdr - // from tr. - regularFileRead(ctx context.Context, hdr *tar.Header, tr *tar.Reader) error - - // regularFileSetContents sets the contents and size of rf, using what was - // previously read for hdr. - regularFileSetContents(ctx context.Context, hdr *tar.Header, rf *regularFile) error + // regularFileRead reads the regular file with header hdr from tr, and sets + // the contents and size of rf accordingly. + regularFileRead(ctx context.Context, hdr *tar.Header, tr *tar.Reader, rf *regularFile) error } // tarDefaultReaderCallbacks implements tarReaderCallbacks by reading regular // file contents from the tar archive. -type tarDefaultReaderCallbacks struct { - headerToContent map[*tar.Header]*bytes.Buffer -} +type tarDefaultReaderCallbacks struct{} -func (cb *tarDefaultReaderCallbacks) regularFileRead(ctx context.Context, hdr *tar.Header, tr *tar.Reader) error { - var buf bytes.Buffer - n, err := io.Copy(&buf, tr) - if err != nil { - return fmt.Errorf("failed to read file content: %w", err) - } - if n != hdr.Size { - return fmt.Errorf("failed to read all file content, got %d bytes, want %d", n, hdr.Size) - } - if hdr.Size > 0 { - cb.headerToContent[hdr] = &buf - } - return nil -} - -func (cb *tarDefaultReaderCallbacks) regularFileSetContents(ctx context.Context, hdr *tar.Header, rf *regularFile) error { - buf, ok := cb.headerToContent[hdr] - if !ok { - return nil - } +func (tarDefaultReaderCallbacks) regularFileRead(ctx context.Context, hdr *tar.Header, tr *tar.Reader, rf *regularFile) error { rf.inode.mu.Lock() defer rf.inode.mu.Unlock() - src := usermem.BytesIOSequence(buf.Bytes()) rw := getRegularFileReadWriter(rf, 0, 0) - n, err := src.CopyInTo(ctx, rw) + // Copy in fixed-size chunks, so that memory use does not depend on the size + // of the file. + n, err := io.Copy(safemem.ToIOWriter{Writer: rw}, tr) + putRegularFileReadWriter(rw) if err != nil { return fmt.Errorf("failed to write file content: %w", err) } - if size := int64(len(buf.Bytes())); n != size { - return fmt.Errorf("failed to write all file content to %v, got %d bytes, want %d", hdr.Name, n, size) + if n != hdr.Size { + return fmt.Errorf("failed to write all file content, got %d bytes, want %d", n, hdr.Size) } - putRegularFileReadWriter(rw) return nil } @@ -534,20 +517,18 @@ func (tarDefaultWriterCallbacks) regularFileWrite(ctx context.Context, rf *regul // it is safe to lock here to ensure no concurrent writes occur. rf.inode.mu.Lock() defer rf.inode.mu.Unlock() - data := make([]byte, rf.size.RacyLoad()) - dst := usermem.BytesIOSequence(data) + size := int64(rf.size.RacyLoad()) rw := getRegularFileReadWriter(rf, 0, 0) - n, err := dst.CopyOutFrom(ctx, rw) - putRegularFileReadWriter(rw) - if err != nil && err != io.EOF { - return fmt.Errorf("failed to read file content: %w", err) - } - if n != int64(len(data)) { - return fmt.Errorf("failed to read all file content, got %d bytes, want %d", n, len(data)) - } - if _, err := tw.Write(data); err != nil { + defer putRegularFileReadWriter(rw) + // Copy in fixed-size chunks, so that memory use does not depend on the size + // of the file. ReadToBlocks stops at the file size. + n, err := io.Copy(tw, safemem.ToIOReader{Reader: rw}) + if err != nil { return fmt.Errorf("failed to write file content to tar: %w", err) } + if n != size { + return fmt.Errorf("failed to read all file content, got %d bytes, want %d", n, size) + } return nil } diff --git a/pkg/sentry/fsimpl/tmpfs/tar_test.go b/pkg/sentry/fsimpl/tmpfs/tar_test.go index 3ce00a55cad..b2f1fb8cf61 100644 --- a/pkg/sentry/fsimpl/tmpfs/tar_test.go +++ b/pkg/sentry/fsimpl/tmpfs/tar_test.go @@ -17,28 +17,24 @@ package tmpfs import ( "archive/tar" "bytes" + "encoding/binary" + "fmt" "io" + "math/rand/v2" + "runtime" "strings" "testing" + "gvisor.dev/gvisor/pkg/context" "gvisor.dev/gvisor/pkg/sentry/contexttest" "gvisor.dev/gvisor/pkg/sentry/kernel/auth" "gvisor.dev/gvisor/pkg/sentry/vfs" ) -// TestSourceTarLongSymlinkRelease is a regression test for a bug where -// symlinkFromTar did not call fs.accountPages(1) for symlinks whose target -// length is >= shortSymlinkLen, while (*inode).decRef unconditionally calls -// fs.unaccountPages(1) on teardown for such symlinks. The asymmetry -// underflowed fs.pagesUsed and panicked filesystem.unaccountPages on -// mount-namespace teardown. -func TestSourceTarLongSymlinkRelease(t *testing.T) { - ctx := contexttest.Context(t) - creds := auth.CredentialsFromContext(ctx) - - // Symlink target of length shortSymlinkLen (128) triggers the long-symlink - // accounting path. - longTarget := strings.Repeat("a", shortSymlinkLen) +// tarWithEntries returns a tar archive with a root directory and the given +// entries. +func tarWithEntries(t *testing.T, hdrs []*tar.Header, contents [][]byte) *bytes.Buffer { + t.Helper() var buf bytes.Buffer tw := tar.NewWriter(&buf) if err := tw.WriteHeader(&tar.Header{ @@ -48,18 +44,36 @@ func TestSourceTarLongSymlinkRelease(t *testing.T) { }); err != nil { t.Fatalf("tar.WriteHeader(dir): %v", err) } - if err := tw.WriteHeader(&tar.Header{ - Name: "./longlink", - Typeflag: tar.TypeSymlink, - Linkname: longTarget, - Mode: 0777, - }); err != nil { - t.Fatalf("tar.WriteHeader(symlink): %v", err) + for i, hdr := range hdrs { + if err := tw.WriteHeader(hdr); err != nil { + t.Fatalf("tar.WriteHeader(%s): %v", hdr.Name, err) + } + if _, err := tw.Write(contents[i]); err != nil { + t.Fatalf("tar.Writer.Write(%s): %v", hdr.Name, err) + } } if err := tw.Close(); err != nil { t.Fatalf("tar.Writer.Close: %v", err) } + return &buf +} +// tarWithFile returns a tar archive with a root directory and one regular file. +func tarWithFile(t *testing.T, name string, content []byte) *bytes.Buffer { + t.Helper() + return tarWithEntries(t, []*tar.Header{{ + Name: name, + Typeflag: tar.TypeReg, + Mode: 0644, + Size: int64(len(content)), + }}, [][]byte{content}) +} + +// mountFromTarTestOnly mounts a tmpfs from the tar archive in src. The mount +// is released when the test ends. +func mountFromTarTestOnly(t *testing.T, ctx context.Context, src io.Reader) *filesystem { + t.Helper() + creds := auth.CredentialsFromContext(ctx) vfsObj := &vfs.VirtualFilesystem{} if err := vfsObj.Init(ctx); err != nil { t.Fatalf("VFS init: %v", err) @@ -67,20 +81,128 @@ func TestSourceTarLongSymlinkRelease(t *testing.T) { vfsObj.MustRegisterFilesystemType("tmpfs", FilesystemType{}, &vfs.RegisterFilesystemTypeOptions{ AllowUserMount: true, }) - mntns, err := vfsObj.NewMountNamespace(ctx, creds, "", "tmpfs", &vfs.MountOptions{ GetFilesystemOptions: vfs.GetFilesystemOptions{ InternalData: FilesystemOpts{ - SourceTar: io.NopCloser(&buf), + SourceTar: io.NopCloser(src), }, }, }, nil) if err != nil { t.Fatalf("NewMountNamespace: %v", err) } + t.Cleanup(func() { mntns.DecRef(ctx) }) + root := mntns.Root(ctx) + defer root.DecRef(ctx) + fs, ok := root.Mount().Filesystem().Impl().(*filesystem) + if !ok { + t.Fatalf("root filesystem is %T, want *tmpfs.filesystem", root.Mount().Filesystem().Impl()) + } + return fs +} + +// readFileFromTar returns the content of the named regular file in src. +func readFileFromTar(t *testing.T, src io.Reader, name string) []byte { + t.Helper() + tr := tar.NewReader(src) + for { + hdr, err := tr.Next() + if err == io.EOF { + t.Fatalf("tar archive has no entry for %s", name) + } + if err != nil { + t.Fatalf("tar.Reader.Next: %v", err) + } + if hdr.Name != name { + continue + } + got, err := io.ReadAll(tr) + if err != nil { + t.Fatalf("reading %s from archive: %v", name, err) + } + if int64(len(got)) != hdr.Size { + t.Fatalf("%s header size = %d, but read %d bytes", name, hdr.Size, len(got)) + } + return got + } +} + +// randomBytes returns size bytes that differ between calls. +func randomBytes(size int) []byte { + b := make([]byte, size) + var seed [32]byte + binary.LittleEndian.PutUint64(seed[:8], rand.Uint64()) + rand.NewChaCha8(seed).Read(b) + return b +} + +// TestSourceTarLongSymlinkRelease is a regression test for a pagesUsed +// underflow on teardown. symlinkFromTar did not account the page that decRef +// unaccounts for long symlink targets. +func TestSourceTarLongSymlinkRelease(t *testing.T) { + ctx := contexttest.Context(t) + + // A target of length shortSymlinkLen takes the long-symlink path. + src := tarWithEntries(t, []*tar.Header{{ + Name: "./longlink", + Typeflag: tar.TypeSymlink, + Linkname: strings.Repeat("a", shortSymlinkLen), + Mode: 0777, + }}, [][]byte{nil}) + + // Without the fix, the release on cleanup underflows fs.pagesUsed and panics. + mountFromTarTestOnly(t, ctx, src) +} + +// TestTarRegularFileRoundTrip checks that file contents survive a restore and +// archive. Sizes straddle the 32 KiB io.Copy buffer. +func TestTarRegularFileRoundTrip(t *testing.T) { + for _, size := range []int{0, 1, 32<<10 - 1, 32 << 10, 32<<10 + 1, 4<<20 + 1234} { + t.Run(fmt.Sprint(size), func(t *testing.T) { + ctx := contexttest.Context(t) + want := randomBytes(size) + fs := mountFromTarTestOnly(t, ctx, tarWithFile(t, "./file", want)) + + var out bytes.Buffer + if err := fs.tarWrite(ctx, &out, tarDefaultWriterCallbacks{}); err != nil { + t.Fatalf("tarWrite: %v", err) + } + if got := readFileFromTar(t, &out, "./file"); !bytes.Equal(got, want) { + t.Fatalf("./file content differs after round trip (got %d bytes, want %d)", len(got), len(want)) + } + }) + } +} + +// TestTarRegularFileAllocation checks that restore and archive memory use does +// not scale with file size. +func TestTarRegularFileAllocation(t *testing.T) { + const ( + fileSize = 4 << 20 + maxAlloc = 1 << 20 + ) + ctx := contexttest.Context(t) + src := tarWithFile(t, "./file", randomBytes(fileSize)) + + var before, after runtime.MemStats + runtime.ReadMemStats(&before) + fs := mountFromTarTestOnly(t, ctx, src) + runtime.ReadMemStats(&after) + allocated := after.TotalAlloc - before.TotalAlloc + t.Logf("restoring a %d byte file allocated %d bytes", fileSize, allocated) + if allocated > maxAlloc { + t.Errorf("restoring a %d byte file allocated %d bytes, want at most %d", fileSize, allocated, maxAlloc) + } - // Drop the only reference to trigger filesystem teardown. Without the fix, - // releaseChildrenLocked -> inode.decRef -> fs.unaccountPages(1) underflows - // fs.pagesUsed and panics here. - mntns.DecRef(ctx) + // Discard the archive so that only archiving is measured. + runtime.ReadMemStats(&before) + if err := fs.tarWrite(ctx, io.Discard, tarDefaultWriterCallbacks{}); err != nil { + t.Fatalf("tarWrite: %v", err) + } + runtime.ReadMemStats(&after) + allocated = after.TotalAlloc - before.TotalAlloc + t.Logf("archiving a %d byte file allocated %d bytes", fileSize, allocated) + if allocated > maxAlloc { + t.Errorf("archiving a %d byte file allocated %d bytes, want at most %d", fileSize, allocated, maxAlloc) + } } diff --git a/pkg/sentry/fsimpl/tmpfs/tmpfs.go b/pkg/sentry/fsimpl/tmpfs/tmpfs.go index b4d33ef9245..615136ae7de 100644 --- a/pkg/sentry/fsimpl/tmpfs/tmpfs.go +++ b/pkg/sentry/fsimpl/tmpfs/tmpfs.go @@ -30,8 +30,6 @@ package tmpfs import ( - "archive/tar" - "bytes" "fmt" "io" "math" @@ -437,14 +435,9 @@ func (fstype FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt if tmpfsOptsOk && tmpfsOpts.SourceTar != nil { var cb tarReaderCallbacks if tmpfsOpts.SourceTarFSCheckpoint { - cb = &fsckptTarReaderCallbacks{ - fs: &fs, - regularFiles: make(map[*tar.Header]*fsckptRegularFile), - } + cb = &fsckptTarReaderCallbacks{fs: &fs} } else { - cb = &tarDefaultReaderCallbacks{ - headerToContent: make(map[*tar.Header]*bytes.Buffer), - } + cb = tarDefaultReaderCallbacks{} } timeUntarStart := time.Now() if err := fs.tarRead(ctx, tmpfsOpts.SourceTar, cb); err != nil {