From 35147208d9be8a7718436bf26475de5bc14aa355 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Wed, 5 Aug 2026 09:27:43 +0200 Subject: [PATCH] chrootarchive: use archive untar implementation on Windows Windows does not support chroot-based extraction, so there is no need to duplicate archive's decompression and untar handling. Move the existing chroot-based handler to the Unix implementation. On Windows, retain the existing destination setup, then delegate directly to archive.Untar or archive.UntarUncompressed. This keeps the Unix behavior unchanged while removing the redundant Windows invokeUnpack path. Signed-off-by: Sebastiaan van Stijn --- chrootarchive/archive.go | 39 -------------------------------- chrootarchive/archive_unix.go | 39 ++++++++++++++++++++++++++++++++ chrootarchive/archive_windows.go | 29 +++++++++++++++++++++--- 3 files changed, 65 insertions(+), 42 deletions(-) diff --git a/chrootarchive/archive.go b/chrootarchive/archive.go index 1e79837..9876100 100644 --- a/chrootarchive/archive.go +++ b/chrootarchive/archive.go @@ -1,15 +1,11 @@ package chrootarchive import ( - "errors" "io" - "os" - "path/filepath" "github.com/moby/sys/user" "github.com/moby/go-archive" - "github.com/moby/go-archive/compression" ) // NewArchiver returns a new Archiver which uses chrootarchive.Untar @@ -51,41 +47,6 @@ func UntarUncompressed(tarArchive io.Reader, dest string, options *archive.TarOp return untarHandler(tarArchive, dest, options, false, dest) } -// Handler for teasing out the automatic decompression -func untarHandler(tarArchive io.Reader, dest string, options *archive.TarOptions, decompress bool, root string) error { - if tarArchive == nil { - return errors.New("empty archive") - } - if options == nil { - options = &archive.TarOptions{} - } - - // If dest is inside a root then directory is created within chroot by extractor. - // This case is only currently used by cp. - if dest == root { - uid, gid := options.IDMap.RootPair() - - dest = filepath.Clean(dest) - if _, err := os.Stat(dest); os.IsNotExist(err) { - if err := user.MkdirAllAndChown(dest, 0o755, uid, gid, user.WithOnlyNew); err != nil { - return err - } - } - } - - r := io.NopCloser(tarArchive) - if decompress { - decompressedArchive, err := compression.DecompressStream(tarArchive) - if err != nil { - return err - } - defer decompressedArchive.Close() - r = decompressedArchive - } - - return invokeUnpack(r, dest, options, root) -} - // Tar tars the requested path while chrooted to the specified root. func Tar(srcPath string, options *archive.TarOptions, root string) (io.ReadCloser, error) { if options == nil { diff --git a/chrootarchive/archive_unix.go b/chrootarchive/archive_unix.go index e2b8d50..c8c64b2 100644 --- a/chrootarchive/archive_unix.go +++ b/chrootarchive/archive_unix.go @@ -5,12 +5,51 @@ package chrootarchive import ( "errors" "io" + "os" "path/filepath" "strings" "github.com/moby/go-archive" + "github.com/moby/go-archive/compression" + "github.com/moby/sys/user" ) +// Handler for teasing out the automatic decompression +func untarHandler(tarArchive io.Reader, dest string, options *archive.TarOptions, decompress bool, root string) error { + if tarArchive == nil { + return errors.New("empty archive") + } + if options == nil { + options = &archive.TarOptions{} + } + + // Create dest here only if it is the root itself; paths below the root are + // created by the extractor after entering the chroot. + // This case is only currently used by cp. + if dest == root { + uid, gid := options.IDMap.RootPair() + + dest = filepath.Clean(dest) + if _, err := os.Stat(dest); os.IsNotExist(err) { + if err := user.MkdirAllAndChown(dest, 0o755, uid, gid, user.WithOnlyNew); err != nil { + return err + } + } + } + + r := io.NopCloser(tarArchive) + if decompress { + decompressedArchive, err := compression.DecompressStream(tarArchive) + if err != nil { + return err + } + defer decompressedArchive.Close() + r = decompressedArchive + } + + return invokeUnpack(r, dest, options, root) +} + func invokeUnpack(decompressedArchive io.Reader, dest string, options *archive.TarOptions, root string) error { relDest, err := resolvePathInChroot(root, dest) if err != nil { diff --git a/chrootarchive/archive_windows.go b/chrootarchive/archive_windows.go index 078341f..88e2aab 100644 --- a/chrootarchive/archive_windows.go +++ b/chrootarchive/archive_windows.go @@ -1,7 +1,10 @@ package chrootarchive import ( + "errors" "io" + "os" + "path/filepath" "strings" "github.com/moby/go-archive" @@ -25,14 +28,34 @@ func addLongPathPrefix(srcPath string) string { return longPathPrefix + srcPath } -func invokeUnpack(decompressedArchive io.ReadCloser, dest string, options *archive.TarOptions, root string) error { +// Handler for teasing out the automatic decompression +func untarHandler(tarArchive io.Reader, dest string, options *archive.TarOptions, decompress bool, root string) error { + if tarArchive == nil { + return errors.New("empty archive") + } + + // Create dest here only if it is the root itself; paths below the root are + // created by the extractor after entering the chroot. + // This case is only currently used by cp. + if dest == root { + dest = filepath.Clean(dest) + if _, err := os.Stat(dest); os.IsNotExist(err) { + if err := os.MkdirAll(dest, 0); err != nil { + return err + } + } + } + // Windows is different to Linux here because Windows does not support // chroot. Hence there is no point sandboxing a chrooted process to // do the unpack. We call inline instead within the daemon process. - return archive.Unpack(decompressedArchive, addLongPathPrefix(dest), options) + if decompress { + return archive.Untar(tarArchive, addLongPathPrefix(dest), options) + } + return archive.UntarUncompressed(tarArchive, addLongPathPrefix(dest), options) } -func invokePack(srcPath string, options *archive.TarOptions, root string) (io.ReadCloser, error) { +func invokePack(srcPath string, options *archive.TarOptions, _ string) (io.ReadCloser, error) { // Windows is different to Linux here because Windows does not support // chroot. Hence there is no point sandboxing a chrooted process to // do the pack. We call inline instead within the daemon process.