moby / moby/sys

[userns-remap] Bug: ToHost incorrectly maps non-root container UID to root when it collides with RootPair

Open Beginner friendly
#241 3 comments 0 reactions 0 assignees View on GitHub

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/userIdentityMapping.ToHost() method

Steps to Reproduce

  1. Configure Docker with userns-remap and /etc/subuid:

    rootless:1000:65536
    
  2. Pull an image that contains a non-root user with UID=1000 (e.g., mcr.microsoft.com/playwright:v1.55.1-jammy which creates a pwuser with UID 1000 via adduser).

  3. 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=1000 is the default first non-root user on many Linux distributions (Ubuntu/Debian)
  • subuid ranges 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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.