Skip to content

fix: open the window unsandboxed where Ubuntu denies the namespace's id maps - #590

Merged
omartelo merged 5 commits into
mainfrom
fix/userns-probe-uid-map
Sep 11, 2026
Merged

omartelo merged 5 commits into
mainfrom
fix/userns-probe-uid-map

Conversation

@omartelo

@omartelo omartelo commented Sep 11, 2026 •

Copy link
Copy Markdown
Collaborator

Follows #587, which put the setuid bit on the helper Chromium actually reads.

The probe said yes where Chromium says no

can_create_user_namespace() only called unshare(CLONE_NEWUSER) in a forked child. Ubuntu 23.10 and later (kernel.apparmor_restrict_unprivileged_userns=1) let that call succeed and deny what follows inside the namespace. lich-shell therefore kept the sandbox on, Chromium's own probe failed, Chromium fell back to the setuid helper, and a helper that is not root 4755 (any tarball) aborted the browser process with SIGTRAP, exit 133. The ceilings bullet promised the opposite: a tarball on Ubuntu runs unsandboxed under Chrome's warning bar.

The child now does what Chromium 150's does

Read from sandbox/linux/services/credentials.cc on branch-heads/7871 (CEF 150.0.10 is Chromium 150.0.7871.101), Credentials::CanCreateProcessInNewUserNS: enter a user namespace, write deny to /proc/self/setgroups where the kernel has it, map its own gid and then uid one wide, drop every capability with capset, and unshare(CLONE_NEWUSER) once more. The map lines are formatted before the fork; the child makes raw syscalls and _exit only.

Not mirrored: Chromium's GetRESIds precondition (real, effective and saved ids agree). Only a setuid or setgid lich-shell fails it, and Chromium then refuses to start with the sandbox on at all.

Proof

Local, emulating the shape. This machine has no AppArmor, so Landlock stands in: setpriv --landlock-access fs:write-file with write access allowed beneath every top-level directory except /proc. Under it unshare --user true exits 0 and unshare --user --map-root-user true fails with cannot open /proc/self/uid_map: Permission denied, the denial bwrap reported on a real Ubuntu desktop. The window, launched with --ozone-platform=headless and no display:

Build Under the rig Without it
before exit 133, FATAL:setuid_sandbox_host.cc:166 The SUID sandbox helper binary was found, but is not configured correctly not rerun
after opening the window unsandboxed, still up at 20 s no such line, still up at 20 s

Real AppArmor, in CI. GitHub's ubuntu-24.04 runner enforces the restriction (actions/runner-images#10443; the image change to lift it, actions/runner-images#11489, was closed). The new step in the shell job checks that the sysctl reads 1, assembles the window the way a tarball ships it, and requires it to stay up unsandboxed. It landed one commit ahead of the fix, so the two runs are the before and after:

  • ecc66eed, no fix: the sysctl read 1, then the window did not open unsandboxed (exit 133) over FATAL:sandbox/linux/suid/client/setuid_sandbox_host.cc:166 The SUID sandbox helper binary was found, but is not configured correctly ... /home/runner/work/_temp/shell/cef/chrome-sandbox, the crash Ubuntu users hit.
  • ca72f197, the fix: the step passes, the window up and unsandboxed at 20 s.

Still unproven: a real Ubuntu desktop session, and #587's package path (a root 4755 helper keeping the sandbox on under the same policy).

Test plan

  • cargo fmt --check, cargo clippy --release --all-targets -- -D warnings, cargo test --release (14 passed)
  • New: maps_only_its_own_id_into_the_namespace; the fork itself is the documented coverage exception
  • Landlock rig: before exits 133 on the helper FATAL, after opens unsandboxed and stays up, after without the rig keeps the sandbox
  • CI on ecc66ee (the step on main, no fix): the step fails on the runner with exit 133 and the helper FATAL
  • CI on ca72f19 (the fix): the step passes
  • A tarball on an Ubuntu 24.04 desktop opens under Chrome's warning bar

Chromium looks for chrome-sandbox in cef/, beside libcef.so, not beside
lich-shell. The deb, rpm and AUR packages installed a root-owned 4755 copy
beside lich-shell and left the one in cef/ at 755, so on a desktop that
denies unprivileged user namespaces (Ubuntu's AppArmor policy) Chromium
found an unusable helper and aborted the window with SIGTRAP.

nfpm cannot mark a file inside the window's tree setuid: a second entry for
the same destination collides, and the archlinux packager drops a setuid bit
read from the source file. The mode is set from the install script instead,
wired to post_upgrade for pacman too. The AUR PKGBUILD chmods the copy in
cef/, and lich-shell's --no-sandbox decision checks that same file.
ubuntu-latest enforces kernel.apparmor_restrict_unprivileged_userns, the
policy that lets lich-shell enter a user namespace and denies the id maps
inside it. The step assembles the window the way a tarball ships it, with a
helper nobody made setuid, and asserts it opens unsandboxed and stays up.

Committed ahead of the fix, so the run on this commit shows the crash on a
real AppArmor kernel.
lich-shell decided whether Chromium could sandbox by forking a child that only
called unshare(CLONE_NEWUSER). Ubuntu's AppArmor policy lets that call succeed
and denies the id maps inside the namespace, so the shell kept the sandbox on,
Chromium's own probe failed, and its fallback to a setuid helper that a
tarball cannot own aborted the window with SIGTRAP.

The child now follows Credentials::CanCreateProcessInNewUserNS as Chromium
150 has it: deny setgroups, map its own gid and uid, drop every capability,
enter a second user namespace. Where any step fails the window opens with
--no-sandbox, as docs/ceilings.md promises for a tarball on Ubuntu.
@omartelo
omartelo merged commit cf87d6d into main Sep 11, 2026
7 checks passed
@omartelo
omartelo deleted the fix/userns-probe-uid-map branch September 11, 2026 18:13
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.

1 participant