Skip to content

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

Open
KR-Ravindra wants to merge 1 commit into
moby:masterfrom
KR-Ravindra:fix/copy-link-chown-numeric
Open

dockerfile: reject --chown by name with --link at parse time#7134
KR-Ravindra wants to merge 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

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, compute the "linked layer" condition once and, when it holds, 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). Behaviour is unchanged for numeric IDs, for --link combined with --chmod, and for workers without MergeOp. 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/user_name_without_merge_op (0.00s)
    --- PASS: TestCopyLinkChownByName/numeric (0.00s)
    --- PASS: TestCopyLinkChownByName/group_name (0.00s)
    --- PASS: TestCopyLinkChownByName/root (0.00s)
    --- PASS: TestCopyLinkChownByName/user_name (0.00s)
    --- PASS: TestCopyLinkChownByName/user_and_group_names (0.00s)
ok  	github.com/moby/buildkit/frontend/dockerfile/dockerfile2llb	0.013s

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.

When --link is used and the worker supports MergeOp, 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 when the link path is taken and return
an error naming the flag and asking for numeric uid[:gid]. Numeric IDs
and "root" keep working, and workers without MergeOp are unchanged
because they fall back to a regular copy where names resolve against
the stage filesystem. Document the restriction in the --chown section.

Signed-off-by: KR Ravindra <42912207+KR-Ravindra@users.noreply.github.com>
@KR-Ravindra

Copy link
Copy Markdown
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
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.

1 participant