Files
mesh-host/internal/profile/userns.go
T
mesh-tools 64dfa90c3a
mesh/repo-check head 64dfa90c3a6c merged into trunk 454be9b2693d — pass: its merge-check.sh passed
mesh/merge-gate head 64dfa90c3a6c merged into trunk 454be9b2693d — pass: group feat/r4a-sdk-first composed: every machine composes with the change as it di…
mesh/delivery delivered
mesh/delivery-group group feat/r4a-sdk-first delivered: every member is delivered
Report whether an unprivileged user namespace can be made (issue 1308)
A build module's sandbox needs one, and a module can require only a capability the node-engine detects (ADR 0448 section 5). The probe drops to an unprivileged uid first when run as root, since root always can.
2026-10-20 01:12:24 +00:00

62 lines
2.2 KiB
Go

package profile
import (
"fmt"
"os"
"strings"
)
// effectiveUID and readProcFile are replaceable so the unit tests stay off the machine.
var (
effectiveUID = os.Geteuid
readProcFile = func(path string) (string, error) {
b, err := os.ReadFile(path)
return string(b), err
}
)
// unprivilegedID is the uid and gid the probe drops to (nobody on every mesh node).
const unprivilegedID = "65534"
// userNamespaces builds the detector for CapUserNamespaces: it makes a user namespace as an
// unprivileged user. The node-engine runs as root, and root can always make one, so as root the
// probe drops first with setpriv (util-linux). Go's SysProcAttr Cloneflags with a Credential
// would clone before the uid drops and prove nothing, so it is not used.
func userNamespaces(runner Runner) Detector {
command, args := "unshare", []string{"--user", "true"}
if effectiveUID() == 0 {
command = "setpriv"
args = []string{"--reuid=" + unprivilegedID, "--regid=" + unprivilegedID, "--clear-groups", "unshare", "--user", "true"}
}
read := readProcFile // bound now, so a probe cut by the timeout never reads a later replacement
return commandCapability{
name: CapUserNamespaces, command: command, args: args,
why: "makes a user namespace as an unprivileged user — what bubblewrap needs, not a kernel setting read",
interpret: func(_ string, err error) (bool, string) {
if err == nil {
return true, "an unprivileged user can make a user namespace"
}
return false, err.Error() + userNamespaceSettings(read)
},
runner: runner,
}
}
// userNamespaceSettings names the two kernel settings that switch user namespaces off, when
// they can be read. It is added to the reason of an absent verdict only.
func userNamespaceSettings(read func(string) (string, error)) string {
var parts []string
for _, s := range []struct{ path, name string }{
{"/proc/sys/kernel/unprivileged_userns_clone", "unprivileged_userns_clone"},
{"/proc/sys/user/max_user_namespaces", "max_user_namespaces"},
} {
if v, err := read(s.path); err == nil {
parts = append(parts, fmt.Sprintf("%s=%s", s.name, strings.TrimSpace(v)))
}
}
if len(parts) == 0 {
return ""
}
return " (" + strings.Join(parts, ", ") + ")"
}