Skip to content

dockerfile: reject --chown by name with --link at parse time - #7134

Merged
tonistiigi merged 1 commit into
moby:masterfrom
KR-Ravindra:fix/copy-link-chown-numeric
Sep 17, 2026
Merged

tonistiigi merged 1 commit into
moby:masterfrom
KR-Ravindra:fix/copy-link-chown-numeric

Conversation

@KR-Ravindra

@KR-Ravindra KR-Ravindra commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

Problem

COPY --link --chown=<user>[:<group>] with a non-numeric user or group is accepted by the Dockerfile frontend, but on any worker that supports MergeOp the build fails late in the solver with

failed to solve: invalid user index: -1

Nothing in that message points at --chown, so people end up either dropping --link or hard-coding UIDs after a lot of trial and error (see the issue thread and the linked buildx issues). Only numeric --chown=UID[:GID] (and root) work with --link.

Root cause

  • frontend/dockerfile/dockerfile2llb/convert_copy.go:54-56: llb.WithUser(cfg.chown) is added unconditionally. WithUser (client/llb/fileop.go:231) turns anything that is not an integer or root into a UserOpt{Name: ...}, which marshals as a pb.UserOpt_ByName whose Input is the FileOp base index.
  • frontend/dockerfile/dockerfile2llb/convert_copy.go:334-349: when --link is set, --chmod is empty and the worker supports CapMergeOp, the FileOp is executed on llb.Scratch() and the result is merged on top of the stage. The base of that FileOp is pb.Empty (-1), so the marshalled owner is byName:{name:"foo" input:-1} (verified by marshalling the frontend output in a unit test).
  • solver/llbsolver/ops/file.go:289 and :534 reject ByName.Input < 0 with invalid user index.

Workers without MergeOp never take this path: they fall back to a regular d.state.File(...) copy where names resolve against the stage's /etc/passwd, which is why the classic graphdriver backend of the docker driver is unaffected. docker build with the containerd image store enables MergeOp and takes the same path.

Fix

In dispatchCopy, whenever --link is set, reject a --chown that refers to a user or group by name with an error that names the flag and the remedy:

--chown=foo:bar: user and group names can't be resolved when used with --link, use numeric uid[:gid] instead

The check reuses llb.WithUser to decide what counts as "by name", so it can never disagree with how the value is actually marshalled (root and numeric IDs still pass). It does not look at opt.llbCaps or at the --chmod fallback: a Dockerfile with --link --chown=<name> is rejected the same way on every worker, instead of depending on which copy path the current daemon takes (review feedback). The --chown section of frontend/dockerfile/docs/reference.md now states the restriction.

No new linter rule: the build would fail anyway on this path, so a hard error at frontend time is strictly more informative than a warning followed by the solver error.

How tested

New unit test TestCopyLinkChownByName in frontend/dockerfile/dockerfile2llb/convert_test.go (no daemon needed; it enables CapMergeOp via pb.Caps.CapSet(pb.Caps.All())).

Before the fix:

--- FAIL: TestCopyLinkChownByName (0.00s)
    --- FAIL: TestCopyLinkChownByName/user_and_group_names (0.00s)
        convert_test.go:502: Error: An error is expected but got nil.
    --- FAIL: TestCopyLinkChownByName/user_name (0.00s)
        convert_test.go:502: Error: An error is expected but got nil.
    --- FAIL: TestCopyLinkChownByName/group_name (0.00s)
        convert_test.go:502: Error: An error is expected but got nil.
FAIL	github.com/moby/buildkit/frontend/dockerfile/dockerfile2llb	0.012s

Marshalling the frontend output for FROM scratch / COPY --link --chown=foo:bar a /b before the fix showed the FileOp copy owner as user=byName:{name:"foo" input:-1} group=byName:{name:"bar" input:-1}, i.e. exactly what the solver rejects.

After the fix:

--- PASS: TestCopyLinkChownByName (0.00s)
    --- PASS: TestCopyLinkChownByName/numeric (0.00s)
    --- PASS: TestCopyLinkChownByName/root (0.00s)
    --- PASS: TestCopyLinkChownByName/user_name (0.00s)
    --- PASS: TestCopyLinkChownByName/user_and_group_names (0.00s)
    --- PASS: TestCopyLinkChownByName/group_name (0.00s)
    --- PASS: TestCopyLinkChownByName/user_name_without_merge_op (0.00s)
    --- PASS: TestCopyLinkChownByName/user_name_with_chmod (0.00s)
    --- PASS: TestCopyLinkChownByName/user_name_without_link (0.00s)
ok  	github.com/moby/buildkit/frontend/dockerfile/dockerfile2llb	0.869s

Also: go test ./frontend/dockerfile/dockerfile2llb/ ./frontend/dockerfile/instructions/ pass, gofmt -l clean, go vet clean, golangci-lint run ./frontend/dockerfile/dockerfile2llb/ reports 0 issues, go build ./frontend/dockerfile/... ./cmd/... succeeds.

Links

This change was prepared with an AI agent operated by KR-Ravindra, who reviewed and tested it.

@KR-Ravindra

Copy link
Copy Markdown
Contributor Author

Round 1 self-review.

Checked before marking ready:

  1. Not a duplicate: no other PR references COPY --link not compatible with chown #2987; the cross-references on the issue are downstream workarounds and the docs proposal docs: proposal to raise awareness about an unexpected behavior of COPY --link #4964.
  2. The line references in the description match master (convert_copy.go 54-56 and 334-349, fileop.go:231, file.go:289 and :534). cfg.link, cfg.chmod and cfg.opt.llbCaps are not modified between the hoisted useLink and its original use, so the link path itself is unchanged.
  3. On 9cc2f54: go test ./frontend/dockerfile/dockerfile2llb/ ./frontend/dockerfile/instructions/, go vet, gofmt -l and go build ./frontend/dockerfile/... ./cmd/... pass; TestCopyLinkChownByName fails on master as described.
  4. Question for maintainers rather than a change request: a hard error is the smallest fix. Resolving names against the stage rootfs, as discussed in COPY --link not compatible with chown #2987, would be the longer-term option if that is preferred.

CI workflows are waiting for first-contributor approval.

@KR-Ravindra
KR-Ravindra marked this pull request as ready for review September 8, 2026 21:09
}

// cfg.opt.llbCaps can be nil in unit tests
useLink := cfg.opt.llbCaps != nil && cfg.opt.llbCaps.Supports(pb.CapMergeOp) == nil && cfg.link && cfg.chmod == ""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should not depend on what capabilities the current daemon happens to support (opt.llbCaps), but Dockerfile behavior should be consistent.

When --link is used the COPY/ADD FileOp runs against llb.Scratch() and
the result is merged on top of the stage. llb.WithUser() marshals a
non-numeric --chown value as a by-name lookup against the FileOp base,
which for scratch is input -1, so the solver later fails with "invalid
user index: -1" and nothing points back to the offending --chown.

Validate this in dispatchCopy whenever --link is set and return an
error naming the flag and asking for numeric uid[:gid]. The check does
not depend on the worker's capabilities or on the --chmod fallback, so
a Dockerfile is accepted or rejected the same way everywhere. Numeric
IDs and "root" keep working. Document the restriction in the --chown
section.

Signed-off-by: KR Ravindra <42912207+KR-Ravindra@users.noreply.github.com>
@KR-Ravindra
KR-Ravindra force-pushed the fix/copy-link-chown-numeric branch from 9cc2f54 to e3f5f64 Compare September 10, 2026 23:07
@KR-Ravindra

Copy link
Copy Markdown
Contributor Author

@tonistiigi done in e3f5f64: the check is now cfg.link && chownByName(cfg.chown) in dispatchCopy, independent of opt.llbCaps and of the --chmod fallback, so COPY --link --chown=<name> is rejected the same way on every worker. The test table gained the no-MergeOp, --chmod and no---link cases; commit message and description updated to match. PTAL.

@tonistiigi
tonistiigi merged commit 9d350c0 into moby:master Sep 17, 2026
276 of 277 checks passed
@crazy-max crazy-max added this to the v0.34.0 milestone Sep 17, 2026
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