Skip to content

vendor: github.com/moby/go-archive main / v0.3.0-dev#7111

Draft
thaJeztah wants to merge 3 commits into
docker:masterfrom
thaJeztah:bump_go_archive
Draft

vendor: github.com/moby/go-archive main / v0.3.0-dev#7111
thaJeztah wants to merge 3 commits into
docker:masterfrom
thaJeztah:bump_go_archive

Conversation

@thaJeztah

Copy link
Copy Markdown
Member

vendor: github.com/moby/go-archive main / v0.3.0-dev

full diff: moby/go-archive@v0.2.0...91b7e54

vendor: github.com/moby/go-archive with hardening

full diff: ctalledo/go-archive@91b7e54...df2ce62

- Human readable description for the release notes

- A picture of a cute animal (not mandatory but encouraged)

full diff: moby/go-archive@v0.2.0...91b7e54

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
full diff: ctalledo/go-archive@91b7e54...df2ce62

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@docker-agent docker-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assessment: 🟡 NEEDS ATTENTION

This vendor update of github.com/moby/go-archive introduces significant security hardening — os.Root-based filesystem operations with openat(2) semantics, filepath.IsLocal path validation, and improved AUFS whiteout handling. The overall direction is a clear improvement.

Two security findings in the new code warrant attention before merging:

  1. TOCTOU in opaque-whiteout walk (diff.go:151): os.RemoveAll(p) uses an absolute path outside the os.Root containment boundary, unlike every other removal in the same function.
  2. Empty tar entry name (diff.go:62): A tar entry with an empty name normalizes to "." (the extraction root), passes filepath.IsLocal, and can overwrite root directory metadata.

Comment thread vendor/github.com/moby/go-archive/diff.go Outdated
// Strip a leading "/" so absolute entries stay root-relative, then
// Clean while keeping any ".." so the IsLocal check below rejects
// escapes instead of silently rewriting them.
hdr.Name = path.Clean(strings.TrimLeft(hdr.Name, "/"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[medium/security] Empty tar entry name normalized to "." passes filepath.IsLocal, enabling overwrite of extraction root metadata

The normalization pipeline:

hdr.Name = path.Clean(strings.TrimLeft(hdr.Name, "/"))
if !filepath.IsLocal(hdr.Name) {
    return 0, breakoutError(fmt.Errorf("invalid entry name %q", hdr.Name))
}

For a tar entry with hdr.Name == "":

  • strings.TrimLeft("", "/")""
  • path.Clean("")"."
  • filepath.IsLocal(".")true ✓ (passes!)

The entry is then treated as a directory-type "." entry, which maps to the extraction root itself. A crafted tar archive could include a TypeDir entry with an empty name carrying arbitrary Uid, Gid, Mode, or ModTime values and silently overwrite the root directory's metadata after extraction.

Suggested fix: add an explicit rejection of the post-clean "." name (or reject the empty pre-clean name):

hdr.Name = path.Clean(strings.TrimLeft(hdr.Name, "/"))
if !filepath.IsLocal(hdr.Name) || hdr.Name == "." {
    return 0, breakoutError(fmt.Errorf("invalid entry name %q", hdr.Name))
}

(The same pattern applies in archive.go's extractTar path, which uses identical normalization logic and should receive the same fix.)

Confidence Score
🟡 moderate 67/100

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fix! The updated code handles this comprehensively — the early check on lines 58-60 rejects empty and "." entries before cleaning, and the post-clean validation on line 65 provides defense-in-depth for any path that normalizes to ".".

This approach is cleaner than my suggestion since it separates concerns: the early check handles the specific empty/root-dir case, while the later check focuses on path traversal safety.

@thaJeztah
thaJeztah requested a review from docker-agent July 17, 2026 11:46

@docker-agent docker-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assessment: 🟢 APPROVE

This vendor update introduces -based containment for tar extraction (using semantics), replaces manual path-traversal checks with , hardens device-number overflow detection, and updates the implementation to use a parent-directory file descriptor. The security model is sound and the hardening changes are a clear improvement over the previous string-prefix checks.

if hdr.Typeflag == tar.TypeLink {
if fi, err := os.Lstat(hdr.Linkname); err == nil && (fi.Mode()&os.ModeSymlink == 0) {
if err := os.Chmod(path, hdrInfo.Mode()); err != nil {
if fi, err := root.Lstat(hdr.Linkname); err == nil && (fi.Mode()&os.ModeSymlink == 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] handleLChmod passes raw hdr.Linkname to root.Lstat without normalization

In handleLChmod, when the tar entry is a hardlink (TypeLink), the function calls root.Lstat(hdr.Linkname) using the raw, uncleaned linkname from the tar header. At the same createTarFile call site, the parallel chtimes code already does root.Lstat(filepath.FromSlash(hdr.Linkname)) — converting separators — and the TypeLink switch case cleans the linkname via path.Clean before validation, but stores the result in a local linkname variable rather than writing it back to hdr.Linkname.

Trigger: A tar entry with TypeLink and a Linkname containing redundant path elements (e.g., "./a/b", "a//b") passes the filepath.IsLocal guard in createTarFile (correctly), but the uncleaned form is then passed to handleLChmod's root.Lstat. On Linux/macOS, ./a/b resolves identically to a/b so there is no behavioral difference. On all platforms, the consequence of an unexpected Lstat failure is only a silently skipped root.Chmod — not a security issue.

Impact: Minor: at most a missed Chmod on the hardlink's target in a degenerate case. No security consequence since os.Root contains any traversal via openat(2) semantics.

Suggested fix — mirror the filepath.FromSlash used by the chtimes code immediately below:

Suggested change
if fi, err := root.Lstat(hdr.Linkname); err == nil && (fi.Mode()&os.ModeSymlink == 0) {
if fi, err := root.Lstat(filepath.FromSlash(hdr.Linkname)); err == nil && (fi.Mode()&os.ModeSymlink == 0) {
Confidence Score
🟡 moderate 62/100

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a unix file, so filepath.FromSlash is effectively a no-op, but I updated for consistency

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're absolutely right — on Unix filepath.FromSlash is a no-op since the separator is already /. Thanks for updating it for consistency with the chtimes pattern. The alignment makes the code easier to reason about even if there's no functional difference here.

@docker-agent docker-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assessment: 🟢 APPROVE

This PR vendors github.com/moby/go-archive from v0.2.0 to a pre-release dev version (v0.2.1-0.20260716170833 via a replace directive pointing to ctalledo's fork). The core changes are security hardening of the tar extraction path: Unpack and createTarFile now use Go's os.Root API (openat-backed bounded filesystem operations) instead of raw os.* calls with manual prefix checks, and path traversal validation is strengthened with filepath.IsLocal after path.Clean.

The hardening changes are well-structured. The os.Root API provides the primary containment guarantee for all path operations. All drafter findings were low-severity and, on deeper analysis, confirmed to be either correct behavior or no-bug outcomes. No high or medium severity issues were identified.

Note: vendor.mod contains a // FIXME comment indicating this uses a personal fork (ctalledo/go-archive) for testing moby/go-archive#45. This is explicitly called out in the PR description and is expected for a dev-version vendor bump.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

@docker-agent docker-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assessment: 🔴 CRITICAL

This PR vendors an updated github.com/moby/go-archive (v0.3.0-dev) with security hardening, but the vendor.mod includes a replace directive that substitutes the canonical module with a personal testing fork (ctalledo/go-archive). This was intentionally marked FIXME and must be removed before merging.

Lower-confidence findings (not posted inline)

None

Comment thread vendor.mod
)

// FIXME(thaJeztah): testing https://github.com/moby/go-archive/pull/45
replace github.com/moby/go-archive => github.com/ctalledo/go-archive v0.2.1-0.20260717121834-2bc39549db4e

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[high] replace directive points to a personal testing fork instead of the canonical moby/go-archive

The PR adds a replace directive in vendor.mod that redirects github.com/moby/go-archive to github.com/ctalledo/go-archive v0.2.1-0.20260717121834-2bc39549db4e. The adjacent FIXME(thaJeztah) comment explicitly marks this as a temporary testing state while waiting for moby/go-archive#45 to land.

Impact if merged: Every build of docker/cli at this commit uses code from the unreviewed personal fork (ctalledo/go-archive) rather than the canonical moby/go-archive — a supply-chain risk that defeats the stated goal of the PR. The require line correctly points to the official pre-release (v0.2.1-0.20260716170833-91b7e546daf2), but the replace directive overrides it at build time.

Resolution: Remove the replace directive (and the accompanying FIXME comment) before merging, once PR #45 in moby/go-archive is merged and the canonical module is published.

Confidence Score
🟢 strong 100/100

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants