Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 /`)"
Expand Down
36 changes: 36 additions & 0 deletions hack/integration-no-new-privs.sh
Original file line number Diff line number Diff line change
@@ -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
50 changes: 41 additions & 9 deletions pkg/child/child.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down
22 changes: 22 additions & 0 deletions pkg/parent/parent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Comment thread
ricardobranco777 marked this conversation as resolved.
}

if opt.NetworkDriver != nil {
Expand Down Expand Up @@ -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
}