diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 502645cb..5096abee 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -63,6 +63,8 @@ jobs: run: DOCKER_BUILDKIT=1 docker build -t rootlesskit:test-integration --target test-integration . - name: "Integration test: exit-code" run: docker run --rm --privileged rootlesskit:test-integration ./integration-exit-code.sh + - name: "Integration test: no-new-privs" + run: docker run --rm --privileged rootlesskit:test-integration ./integration-no-new-privs.sh - name: "Integration test: propagation" run: docker run --rm --privileged rootlesskit:test-integration ./integration-propagation.sh - name: "Integration test: propagation (with `mount --make-rshared /`)" diff --git a/hack/integration-no-new-privs.sh b/hack/integration-no-new-privs.sh new file mode 100755 index 00000000..b42bbfdc --- /dev/null +++ b/hack/integration-no-new-privs.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# Regression test for https://github.com/rootless-containers/rootlesskit/issues/551 +source $(realpath $(dirname $0))/common.inc.sh + +# The regular newuidmap/newgidmap are setuid or have file caps, so they cannot work with no_new_privs. +# Use unprivileged stand-ins that map only the current UID/GID. +tmp=$(mktemp -d) +trap "rm -rf ${tmp}" EXIT +cat >${tmp}/newuidmap <<'EOS' +#!/bin/sh +echo "$2 $3 $4" >/proc/$1/uid_map +EOS +cat >${tmp}/newgidmap <<'EOS' +#!/bin/sh +echo deny >/proc/$1/setgroups +echo "$2 $3 $4" >/proc/$1/gid_map +EOS +chmod +x ${tmp}/newuidmap ${tmp}/newgidmap +export PATH=${tmp}:${PATH} + +function test_no_new_privs() { + args="$@" + INFO "Testing no_new_privs for args=${args}" + # Without the fix, rootlesskit hangs, so use timeout. + out=$(setpriv --no-new-privs timeout 30 $ROOTLESSKIT $args sh -ec 'echo "uid=$(id -u)"; grep -E "^(NoNewPrivs|CapEff|CapAmb):" /proc/self/status') + echo "${out}" + for expected in "uid=0" "NoNewPrivs:.1" "CapEff:.[0-9a-f]*[1-9a-f][0-9a-f]*$" "CapAmb:.0*$"; do + if ! echo "${out}" | grep -qE "${expected}"; then + ERROR "expected to match ${expected}" + exit 1 + fi + done +} + +test_no_new_privs --pidns=false +test_no_new_privs --pidns=true diff --git a/pkg/child/child.go b/pkg/child/child.go index eefc9ea9..2542157b 100644 --- a/pkg/child/child.go +++ b/pkg/child/child.go @@ -314,16 +314,40 @@ func hasCaps() (bool, error) { return data.Effective != 0, nil } -// gainCaps gains the caps inside the user namespace. -// The caps are gained on re-execution after the child's uid_map and gid_map are fully written. -func gainCaps() error { - pid := os.Getpid() +// dropInheritableAndAmbientCaps prevents the ambient caps (set by the parent) from leaking to the workload. +func dropInheritableAndAmbientCaps() error { + // EINVAL means that the kernel does not support ambient caps (added in Linux 4.3) + if err := unix.Prctl(unix.PR_CAP_AMBIENT, unix.PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0); err != nil && err != unix.EINVAL { + return fmt.Errorf("failed to clear the ambient caps: %w", err) + } + hdr := unix.CapUserHeader{ + Version: unix.LINUX_CAPABILITY_VERSION_3, + } + var data [2]unix.CapUserData + if err := unix.Capget(&hdr, &data[0]); err != nil { + return fmt.Errorf("failed to get the current caps: %w", err) + } + data[0].Inheritable, data[1].Inheritable = 0, 0 + if err := unix.Capset(&hdr, &data[0]); err != nil { + return fmt.Errorf("failed to clear the inheritable caps: %w", err) + } + return nil +} + +func reexecCountEnvName(pid int) string { pidns, err := statPIDNS(pid) if err != nil { logrus.WithError(err).Debug("Failed to stat pidns (negligible when unsharing pidns)") pidns = 0 } - envName := fmt.Sprintf("_ROOTLESSKIT_REEXEC_COUNT_%d_%d", pidns, pid) + return fmt.Sprintf("_ROOTLESSKIT_REEXEC_COUNT_%d_%d", pidns, pid) +} + +// gainCaps gains the caps inside the user namespace. +// The caps are gained on re-execution after the child's uid_map and gid_map are fully written. +func gainCaps() error { + pid := os.Getpid() + envName := reexecCountEnvName(pid) logrus.Debugf("Re-executing the RootlessKit child process (PID=%d) to gain the caps", pid) var envValueInt int @@ -378,9 +402,11 @@ func Child(opt Opt) error { msg *messages.Message err error ) - if ok, err := hasCaps(); err != nil { - return err - } else if !ok { + // The parent starts the child with ambient caps, so the child usually has the caps + // already, even when no_new_privs is set. Otherwise the caps are gained by re-execution. + // A re-executed child has already completed the handshake below. + reexecEnvName := reexecCountEnvName(os.Getpid()) + if os.Getenv(reexecEnvName) == "" { _, err = messages.WaitFor(pipeR, messages.Name(messages.ParentHello{})) if err != nil { return err @@ -399,11 +425,17 @@ func Child(opt Opt) error { if err != nil { return err } - + } + if ok, err := hasCaps(); err != nil { + return err + } else if !ok { // gainCaps re-executes the process on success and only returns on failure, // so reaching here always means an error. return fmt.Errorf("failed to gain the caps inside the user namespace: %w", gainCaps()) } + if err := dropInheritableAndAmbientCaps(); err != nil { + return err + } if opt.MountProcfs { if err := mountProcfs(); err != nil { diff --git a/pkg/parent/parent.go b/pkg/parent/parent.go index 8fa96251..8ae475e7 100644 --- a/pkg/parent/parent.go +++ b/pkg/parent/parent.go @@ -177,6 +177,9 @@ func Parent(opt Opt) error { cmd.SysProcAttr = &syscall.SysProcAttr{ Pdeathsig: syscall.SIGKILL, Cloneflags: syscall.CLONE_NEWUSER | syscall.CLONE_NEWNS, + // The child keeps the caps across exec, even when no_new_privs is set + // (where the child cannot gain the caps by re-execution after uid_map is written). + AmbientCaps: allCaps(), } if opt.NetworkDriver != nil { @@ -545,3 +548,22 @@ func InitStateDir(stateDir string) error { } return nil } + +// allCaps returns all the capabilities supported by the kernel, +// or nil if the kernel does not support ambient caps (added in Linux 4.3). +func allCaps() []uintptr { + if _, err := unix.PrctlRetInt(unix.PR_CAP_AMBIENT, unix.PR_CAP_AMBIENT_IS_SET, 0, 0, 0); err == unix.EINVAL { + return nil + } + last := unix.CAP_LAST_CAP + if b, err := os.ReadFile("/proc/sys/kernel/cap_last_cap"); err == nil { + if n, err := strconv.Atoi(strings.TrimSpace(string(b))); err == nil { + last = n + } + } + caps := make([]uintptr, 0, last+1) + for i := 0; i <= last; i++ { + caps = append(caps, uintptr(i)) + } + return caps +}