[1/1] rootless: always map nobody/nogroup as dedicated block

Message ID 20260820151029.802935-1-felix.moessbauer@siemens.com
State Under Review
Headers show
Series [1/1] rootless: always map nobody/nogroup as dedicated block | expand

Commit Message

Felix Moessbauer Aug. 20, 2026, 3:10 p.m. UTC
The nobody/nogroup ids were mapped as part of the linear id range,
with an extra single-id mapping added only if the range was too small
to contain them. The extra mapping was guarded by "uid_cnt < nobody_id",
but the linear range ends at uid_cnt-2, so it covers nobody only for
uid_cnt >= 65536. For a count of 65534 or 65535 neither the linear range
nor the guard mapped nobody.

Relying on the linear range to cover nobody is unsafe in general. If that
range covers the outer id that --map-root-user maps to, unshare punches a
hole out of the mapping, and per unshare(1) this "may result in the
highest user ID of the mapping not being mapped". That highest id is
exactly nobody. This happens whenever the subid range overlaps the id of
the calling user, e.g. with

  builder:0:65536

where the mapped block 1..65534 contains the builder's own uid. The
resulting namespace then silently lacks a nobody mapping, while a range
like 100000:65536 is unaffected and hides the problem.

Map nobody/nogroup as a separate block at the top of the range and cap
the linear range at nobody_id-1. The separate block does not overlap
--map-root-user, so it survives, and a hole now at most costs id 65533.

Fixes: 4fedb1ae ("add support for fully rootless builds")
Signed-off-by: Felix Moessbauer <felix.moessbauer@siemens.com>
---
 meta/classes-global/base.bbclass | 16 ++++++++++------
 1 file changed, 10 insertions(+), 6 deletions(-)

Patch

diff --git a/meta/classes-global/base.bbclass b/meta/classes-global/base.bbclass
index f074eee6..c56b6b1a 100644
--- a/meta/classes-global/base.bbclass
+++ b/meta/classes-global/base.bbclass
@@ -464,15 +464,19 @@  def run_privileged_cmd(d):
         nobody_subid = uid_base + uid_cnt - 1
         gid_base, gid_cnt = get_subid_range('/etc/subgid', d)
         nogroup_subid = gid_base + gid_cnt - 1
+        # nobody/nogroup need a dedicated block at the top of the range. As part
+        # of the linear range they are lost whenever that range covers the outer
+        # id of --map-root-user, because unshare then punches out a hole and
+        # drops the highest id of the range.
+        uid_linear_cnt = min(uid_cnt - 2, nobody_id - 1)
+        gid_linear_cnt = min(gid_cnt - 2, nobody_id - 1)
         cmd = 'unshare --mount --pid --uts --ipc --user' \
               ' --kill-child' \
               ' --setuid 0 --setgid 0 --fork' \
-              f' --map-users  1:{uid_base+1}:{uid_cnt-2}' \
-              f' --map-groups 1:{gid_base+1}:{gid_cnt-2}'
-        if uid_cnt < nobody_id:
-            cmd += f' --map-users  {nobody_id}:{nobody_subid}:1'
-        if gid_cnt < nobody_id:
-            cmd += f' --map-groups {nobody_id}:{nogroup_subid}:1'
+              f' --map-users  1:{uid_base+1}:{uid_linear_cnt}' \
+              f' --map-groups 1:{gid_base+1}:{gid_linear_cnt}' \
+              f' --map-users  {nobody_id}:{nobody_subid}:1' \
+              f' --map-groups {nobody_id}:{nogroup_subid}:1'
         cmd += " --map-root-user"
     else:
         cmd = 'sudo -E'