diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 14af5427..fe2e6680 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -52,6 +52,8 @@ jobs: run: docker run --rm --privileged --sysctl net.ipv6.conf.all.disable_ipv6=0 rootlesskit:test-integration ./integration-ipv6.sh - name: "Integration test: systemd socket activation" run: docker run --rm --net=none --privileged rootlesskit:test-integration ./integration-systemd-socket.sh + - name: "Integration test: pdeathsig" + run: docker run --rm --privileged rootlesskit:test-integration ./integration-pdeathsig.sh - name: "Integration test: Network (network driver=slirp4netns)" run: | docker run --rm --privileged rootlesskit:test-integration ./integration-net.sh slirp4netns diff --git a/hack/integration-pdeathsig.sh b/hack/integration-pdeathsig.sh new file mode 100755 index 00000000..593baa7e --- /dev/null +++ b/hack/integration-pdeathsig.sh @@ -0,0 +1,125 @@ +#!/bin/bash +# Test script to verify Pdeathsig behavior using rootlesskit itself +# This script: +# 1. Uses rootlesskit to spawn a long-running process +# 2. Kills the rootlesskit parent process +# 3. Verifies that the child process is killed as expected +# 4. Tests both with --reaper true and --reaper false + +source $(realpath $(dirname $0))/common.inc.sh + +INFO "Starting Pdeathsig test using rootlesskit..." + +# Function to run the test with a specific reaper setting +run_test() { + local reaper_setting=$1 + INFO "Testing with --reaper $reaper_setting" + + # Create a temporary directory for test artifacts + TEMP_DIR=$(mktemp -d) + INFO "Created temporary directory: $TEMP_DIR" + + # Create a marker file that will be touched by the child process if it's still alive + MARKER_FILE="$TEMP_DIR/child_still_alive" + + # Create a script that will be executed by rootlesskit + CHILD_SCRIPT="$TEMP_DIR/child_script.sh" + cat > "$CHILD_SCRIPT" << 'EOF' +#!/bin/bash +echo "Child process started with PID: $$" +echo "Parent PID: $PPID" + +# Register a trap to handle signals +trap 'echo "Child received signal, exiting"; exit 1' TERM INT + +# Run for 30 seconds, checking if parent is still alive +for i in {1..30}; do + echo "Child still running (iteration $i)..." + + # Check if parent has changed (died) + CURRENT_PPID=$(ps -o ppid= -p $$) + if [ "$CURRENT_PPID" != "$PPID" ]; then + echo "Parent changed from $PPID to $CURRENT_PPID" + if [ "$CURRENT_PPID" = "1" ]; then + echo "Parent is now init (PID 1), parent has died" + echo "Child should be killed by Pdeathsig, but if you see this message, it wasn't" + touch MARKER_FILE_PLACEHOLDER + exit 1 + fi + fi + + sleep 1 +done + +# If we reach here, the child wasn't killed +echo "Child completed normally (this shouldn't happen if Pdeathsig is working)" +touch MARKER_FILE_PLACEHOLDER +EOF + + # Replace the placeholder with the actual marker file path + sed -i "s|MARKER_FILE_PLACEHOLDER|$MARKER_FILE|g" "$CHILD_SCRIPT" + chmod +x "$CHILD_SCRIPT" + + # Start rootlesskit with the child script + INFO "Starting rootlesskit with --reaper $reaper_setting..." + if [ "$reaper_setting" = "true" ]; then + $ROOTLESSKIT --reaper $reaper_setting --pidns "$CHILD_SCRIPT" & + else + $ROOTLESSKIT --reaper $reaper_setting "$CHILD_SCRIPT" & + fi + ROOTLESSKIT_PID=$! + INFO "Rootlesskit started with PID: $ROOTLESSKIT_PID" + + # Wait a moment for the child to start + sleep 2 + + # Find the child process + ROOTLESSKIT_CHILD_PID=$(pgrep -P $ROOTLESSKIT_PID) + if [ -z "$ROOTLESSKIT_CHILD_PID" ]; then + ERROR "Failed to find rootlesskit child process" + return 1 + fi + INFO "Found rootlesskit child process with PID: $ROOTLESSKIT_CHILD_PID" + + # Kill the rootlesskit process + INFO "Killing rootlesskit process (PID: $ROOTLESSKIT_PID)..." + kill -9 $ROOTLESSKIT_PID + + # Wait a moment for the rootlesskit child to be killed + sleep 2 + + # Check if the rootlesskit child process is still running + if ps -p $ROOTLESSKIT_CHILD_PID > /dev/null; then + ERROR "FAIL: Rootlesskit Child process (PID: $ROOTLESSKIT_CHILD_PID) is still running after rootlesskit parent was killed" + kill -9 $ROOTLESSKIT_CHILD_PID # Clean up + return 1 + else + INFO "PASS: Rootlesskit Child process (PID: $ROOTLESSKIT_CHILD_PID) was killed as expected" + fi + + # Check if the marker file exists + if [ -f "$MARKER_FILE" ]; then + ERROR "FAIL: Marker file exists, which means the child process wasn't killed by Pdeathsig" + return 1 + else + INFO "PASS: Marker file doesn't exist, which means the child process was killed by Pdeathsig" + fi + + INFO "Test with --reaper $reaper_setting completed successfully!" + rm -rf "$TEMP_DIR" + return 0 +} + +# Run tests with both reaper settings +if ! run_test "true"; then + ERROR "Test with --reaper true failed" + exit 1 +fi + +if ! run_test "false"; then + ERROR "Test with --reaper false failed" + exit 1 +fi + +INFO "All tests completed successfully!" +exit 0 diff --git a/pkg/child/child.go b/pkg/child/child.go index d32cb675..e23e6e9c 100644 --- a/pkg/child/child.go +++ b/pkg/child/child.go @@ -442,12 +442,6 @@ func Child(opt Opt) error { // The parent calls child with Pdeathsig, but it is cleared when newuidmap SUID binary is called // https://github.com/rootless-containers/rootlesskit/issues/65#issuecomment-492343646 - runtime.LockOSThread() - err = unix.Prctl(unix.PR_SET_PDEATHSIG, uintptr(unix.SIGKILL), 0, 0, 0) - runtime.UnlockOSThread() - if err != nil { - return err - } os.Unsetenv(opt.PipeFDEnvKey) if err := pipeR.Close(); err != nil { return fmt.Errorf("failed to close fd %d: %w", pipeFD, err) @@ -483,17 +477,50 @@ func Child(opt Opt) error { if err != nil { return err } + + // Create a channel to receive errors from the goroutine + cmdErrCh := make(chan error, 1) + if opt.Reaper { - if err := runAndReap(cmd); err != nil { + // Launch a goroutine to execute the command with Pdeathsig + go func() { + // Lock the goroutine to the OS thread + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + // Set the parent death signal + if err := unix.Prctl(unix.PR_SET_PDEATHSIG, uintptr(unix.SIGKILL), 0, 0, 0); err != nil { + cmdErrCh <- err + return + } + + // Run the command + cmdErrCh <- runAndReap(cmd) + }() + + // Wait for the command to complete + if err := <-cmdErrCh; err != nil { return fmt.Errorf("command %v exited: %w", opt.TargetCmd, err) } } else { - if err := cmd.Start(); err != nil { - return fmt.Errorf("command %v exited: %w", opt.TargetCmd, err) - } - sigc := sigproxy.ForwardAllSignals(context.TODO(), cmd.Process.Pid) - defer sigproxysignal.StopCatch(sigc) - if err := cmd.Wait(); err != nil { + // Launch a goroutine to execute the command with Pdeathsig + go func() { + // Lock the goroutine to the OS thread + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + // Set the parent death signal + if err := unix.Prctl(unix.PR_SET_PDEATHSIG, uintptr(unix.SIGKILL), 0, 0, 0); err != nil { + cmdErrCh <- err + return + } + + // Run the command without reaping + cmdErrCh <- runWithoutReap(cmd) + }() + + // Wait for the command to complete + if err := <-cmdErrCh; err != nil { return fmt.Errorf("command %v exited: %w", opt.TargetCmd, err) } } @@ -514,6 +541,16 @@ func setMountPropagation(propagation string) error { return nil } +func runWithoutReap(cmd *exec.Cmd) error { + cmd.SysProcAttr.Setsid = true + if err := cmd.Start(); err != nil { + return err + } + sigc := sigproxy.ForwardAllSignals(context.TODO(), cmd.Process.Pid) + defer sigproxysignal.StopCatch(sigc) + return cmd.Wait() +} + func runAndReap(cmd *exec.Cmd) error { c := make(chan os.Signal, 32) signal.Notify(c, syscall.SIGCHLD)