Skip to content

user: fix ToHost treating a non-root uid equal to the remapped-root base as the remapped root - #242

Open
okhowang wants to merge 1 commit into
moby:mainfrom
okhowang:fix/identity-mapping-tohost-low-base-remap
Open

okhowang wants to merge 1 commit into
moby:mainfrom
okhowang:fix/identity-mapping-tohost-low-base-remap

Conversation

@okhowang

@okhowang okhowang commented Aug 4, 2026 •

Copy link
Copy Markdown

fixes #241

ToHost translated container ids to host ids, but special-cased the
container root by skipping the id-map lookup whenever the container uid
matched the host remapped-root base (RootPair). That comparison was
wrong: the input is a container-namespace id, so it must never be
compared against a host value.

When the subuid/subgid base is below 65536 (e.g. 'rootless:1000:65536',
used to align dind-rootless container uids), a non-root container uid
that equals the base (e.g. 1000) was incorrectly left unmapped and
therefore appeared as root inside the container.

Drop the special case entirely and always translate every id through
toHost. toHost(0) already resolves to the host remapped-root base (which
is exactly what RootPair returns), so the container root is handled
correctly without any guard. An empty (nil) mapping is treated as
identity.

Add a regression test covering standard, low-base, and empty mappings.

@okhowang okhowang changed the title user: fix ToHost treating a non-root uid equal to the remapped-root b… user: fix ToHost treating a non-root uid equal to the remapped-root base as the remapped root Aug 4, 2026
@okhowang
okhowang force-pushed the fix/identity-mapping-tohost-low-base-remap branch 3 times, most recently from 89c80ea to 5d658c5 Compare August 4, 2026 11:43
@kolyshkin

Copy link
Copy Markdown
Collaborator

The fix looks correct to me, but @thaJeztah's concern is valid: there is one caller in moby that depends on the current behavior.

The classic builder's COPY --from copies files from another build stage or image, whose layers (with graphdriver and userns-remap) are already owned by host (remapped) IDs, yet it remaps them again via Archiver.CopyWithTar / CopyFileWithTar → ToHost. Today this works for root-owned files only because of the special case being removed here (non-root files already fail with container ID 100100 cannot be mapped to a host ID). With this PR, it would fail for all files.

I've opened moby/moby#53859 to fix that on the moby side. Other ToHost callers in moby and buildkit pass container IDs, so they only benefit from this fix.

A few nits for this PR:

  1. On a uid mapping error, ToHost now returns ruid, 0, err, i.e. gid 0 (root). Better to return -1, -1, err, consistent with ToContainer (and with what was suggested in [userns-remap] Bug: ToHost incorrectly maps non-root container UID to root when it collides with RootPair #241).
  2. The doc comment for ToHost can be much shorter, something like "ToHost returns the host UID and GID for the container uid and gid. An empty mapping is treated as identity."
  3. It'd be nice to have a test case documenting the behavior change, i.e. that passing the (already remapped) host root ID now results in an error.

Comment thread user/idtools.go Outdated
rgid, err = toHost(gid, i.GIDMaps)
ruid, err := toHost(uid, i.UIDMaps)
if err != nil {
return ruid, 0, err

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Better to return -1, -1, err here, consistent with ToContainer (and with what was suggested in #241).

Comment thread user/idtools.go Outdated
Comment on lines +120 to +132
// ToHost returns the host UID and GID for the container uid, gid.
// Remapping is only performed if the ids aren't already the remapped root ids
//
// Every container id is translated through the id map. The container root
// (id 0) maps to the host remapped-root base (the ParentID of the map entry
// covering id 0), because toHost(0) resolves to that base--which is exactly
// what [IdentityMapping.RootPair] returns, so no special case is needed.
// An empty (nil) mapping is treated as identity.
//
// Callers must pass container-namespace ids. ToHost does not treat an id as
// "already remapped" based on the host remapped-root value: doing so would
// incorrectly leave a non-root container uid that happens to equal the host
// remapped-root base unmapped (and therefore owned by the remapped root inside
// the container).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This does not need to be that long. Something like

Suggested change
// ToHost returns the host UID and GID for the container uid, gid.
// Remapping is only performed if the ids aren't already the remapped root ids
//
// Every container id is translated through the id map. The container root
// (id 0) maps to the host remapped-root base (the ParentID of the map entry
// covering id 0), because toHost(0) resolves to that base--which is exactly
// what [IdentityMapping.RootPair] returns, so no special case is needed.
// An empty (nil) mapping is treated as identity.
//
// Callers must pass container-namespace ids. ToHost does not treat an id as
// "already remapped" based on the host remapped-root value: doing so would
// incorrectly leave a non-root container uid that happens to equal the host
// remapped-root base unmapped (and therefore owned by the remapped root inside
// the container).
// ToHost returns the host UID and GID for the container uid and gid.
// An empty mapping is treated as identity.

would do just fine.

@kolyshkin

Copy link
Copy Markdown
Collaborator

3. It'd be nice to have a test case documenting the behavior change, i.e. that passing the (already remapped) host root ID now results in an error.

Something like this:

func TestToHostRemappedRoot(t *testing.T) {
      // ToHost expects container IDs. A host ID, such as the remapped root
      // one, is not treated as already remapped, and so it is either mapped
      // again (if it is in the container range) or results in an error.
      idMap := []IDMap{{ID: 0, ParentID: 100000, Count: 65536}}
      m := IdentityMapping{UIDMaps: idMap, GIDMaps: idMap}
      ruid, rgid := m.RootPair()

      uid, gid, err := m.ToHost(ruid, rgid)
      if err == nil {
              t.Fatalf("expected an error, got uid %d, gid %d", uid, gid)
      }
}

…se unmapped

ToHost translated container ids to host ids, but special-cased the
container root by skipping the id-map lookup whenever the container uid
matched the host remapped-root base (RootPair). That comparison was
wrong: the input is a container-namespace id, so it must never be
compared against a host value.

When the subuid/subgid base is below 65536 (e.g. 'rootless:1000:65536',
used to align dind-rootless container uids), a non-root container uid
that equals the base (e.g. 1000) was incorrectly left unmapped and
therefore appeared as root inside the container.

Drop the special case entirely and always translate every id through
toHost. toHost(0) already resolves to the host remapped-root base (which
is exactly what RootPair returns), so the container root is handled
correctly without any guard. An empty (nil) mapping is treated as
identity.

Add a regression test covering standard, low-base, and empty mappings.

Signed-off-by: okhowang(王沛文) <okhowang@tencent.com>
@okhowang
okhowang force-pushed the fix/identity-mapping-tohost-low-base-remap branch from 5d658c5 to 66f3976 Compare October 8, 2026 03:28

@kolyshkin kolyshkin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

AFAIK this one is ready; PTAL @vvoland @thaJeztah

@kolyshkin

Copy link
Copy Markdown
Collaborator

(of course, user release notes should come with an explanation about this one)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants