diff --git a/CLAUDE.md b/CLAUDE.md index 3e2809e..a8f8a0d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,6 +35,12 @@ Source layout (all under `src/`): - `bwrap.{h,cpp}` — `detect_bwrap_unshare_args()` probes the kernel (via a forked `unshare(2)` per namespace type) for which `--unshare-xxx` flags `bwrap` can actually use; `build_bwrap_args()`/`run_bwrap()` assemble and run the sandboxed command. + Never requests `--unshare-user` when running as root: root doesn't need a fresh + user namespace for privilege, and bwrap's own single-mapping uid/gid setup for one + triggers the kernel's unprivileged-userns setgroups() restriction, which showed up + as every other supplementary group collapsing to the overflow gid ("nobody") in + `id`, and `su` inside the sandbox failing with "can't set groups: Operation not + permitted". - `process.{h,cpp}` — argv-based subprocess helpers (fork/execvp, no shell): `run_process()` captures stdout (used for `containers-storage` calls), `run_process_foreground()` inherits all of stdio (used for the interactive `bwrap` diff --git a/src/bwrap.cpp b/src/bwrap.cpp index 56861f5..aba125d 100644 --- a/src/bwrap.cpp +++ b/src/bwrap.cpp @@ -116,7 +116,18 @@ std::vector detect_bwrap_unshare_args() { bool user_ns_supported = kernel_supports_namespace(CLONE_NEWUSER); spdlog::debug("namespace user: {}", user_ns_supported ? "supported" : "not supported"); - if (user_ns_supported) { + + // Root already has full privilege without a new user namespace, and bwrap's own + // uid/gid mapping into one (a single trivial mapping, since we don't pass + // --uid/--gid/subuid ranges) forces the kernel's unprivileged-userns + // setgroups() restriction: every other supplementary group collapses to the + // overflow gid (65534/"nobody"), and setgroups() calls inside the sandbox (e.g. + // `su`) fail with "Operation not permitted". Observed directly: `id` inside a + // root-launched sandbox showed "nobody" repeated once per real supplementary + // group, and `su git` failed exactly this way. So only request --unshare-user + // (and only combine the other probes with it) when not root. + bool combine_with_user_ns = user_ns_supported && geteuid() != 0; + if (combine_with_user_ns) { args.push_back("--unshare-user"); } @@ -126,9 +137,8 @@ std::vector detect_bwrap_unshare_args() { } // As a regular (non-root) user, most namespace types can only be unshared // together with a fresh user namespace (which supplies the capabilities - // needed), not in isolation -- so combine them here whenever the user - // namespace probe above succeeded. - int flags = probe.clone_flag | (user_ns_supported ? CLONE_NEWUSER : 0); + // needed), not in isolation -- so combine them here whenever it applies. + int flags = probe.clone_flag | (combine_with_user_ns ? CLONE_NEWUSER : 0); bool supported = kernel_supports_namespace(flags); spdlog::debug("namespace {}: {}", probe.name, supported ? "supported" : "not supported"); if (supported) {