[userns-remap] Bug: ToHost incorrectly maps non-root container UID to root when it collides with RootPair
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 101
- Forks
- 61
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 5
Description
Summary
When userns-remap is enabled, the ToHost method in moby/sys/user/idtools.go incorrectly skips UID/GID remapping for non-root users whose container UID happens to equal the host-side root UID returned by RootPair(). This causes files owned by such users to be chown'd to the remapped root UID on the host, and thus appear as root:root inside the container.
Affected Component
github.com/moby/sys/user — IdentityMapping.ToHost() method
Steps to Reproduce
-
Configure Docker with
userns-remapand/etc/subuid:rootless:1000:65536 -
Pull an image that contains a non-root user with UID=1000 (e.g.,
mcr.microsoft.com/playwright:v1.55.1-jammywhich creates apwuserwith UID 1000 viaadduser). -
Run the image:
docker run -it --rm mcr.microsoft.com/playwright:v1.55.1-jammy ls -l /home
Expected Behavior
drwxr-x--- 2 pwuser pwuser 57 Sep 23 2025 pwuser
Actual Behavior
drwxr-x--- 2 root root 73 Sep 23 2025 pwuser
Root Cause
In ToHost() (line 109-126 of idtools.go):
func (i IdentityMapping) ToHost(uid, gid int) (int, int, error) {
var err error
ruid, rgid := i.RootPair()
if uid != ruid {
ruid, err = toHost(uid, i.UIDMaps)
if err != nil {
return ruid, rgid, err
}
}
if gid != rgid {
rgid, err = toHost(gid, i.GIDMaps)
}
return ruid, rgid, err
}
The method assumes that if uid == RootPair().UID, then uid must be the container root (UID 0) that has already been mapped, and skips the toHost() call as an optimization. However, this assumption is incorrect when the subuid ParentID coincides with a non-root container UID.
For example, with subuid = rootless:1000:65536:
RootPair()returns(1000, 1000)(host-side root)- Container root (UID 0) maps to host UID 1000
- A non-root user in the image with UID 1000 should map to host UID 2000
But ToHost(1000, 1000) sees uid == ruid (1000 == 1000), skips the toHost() call, and returns (1000, 1000) — the remapped root UID. The file gets chown'd to 1000:1000 on the host, which is the same as the remapped root, and appears as root:root inside the container.
This bug is especially common because:
UID=1000is the default first non-root user on many Linux distributions (Ubuntu/Debian)subuidranges often start at 1000 when configured for rootless Docker- Many Docker images create a user with UID=1000
Suggested Fix
The ToHost method should always perform the toHost() mapping, and only skip it for the actual container root (UID 0) — not for any arbitrary UID that happens to collide with RootPair().
Remove the optimization entirely. The performance impact is negligible:
func (i IdentityMapping) ToHost(uid, gid int) (int, int, error) {
ruid, err := toHost(uid, i.UIDMaps)
if err != nil {
return -1, -1, err
}
rgid, err := toHost(gid, i.GIDMaps)
return ruid, rgid, err
}
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in moby/sys/user/idtools.go at IdentityMapping.ToHost() and inspect how RootPair(), UIDMaps, and GIDMaps are used. Verify the userns-remap reproduction with a subuid range starting at 1000; done means a container UID/GID of 1000 maps to the appropriate non-root host IDs while container root still maps to the remapped root IDs.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- operating-systems
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100