From 42a80fcfe49cabbf9b75f460f0da13a30ec6488a Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 16 Dec 2024 06:57:17 +0000 Subject: [PATCH] bunpen: expose the bare / tmpfs at /unbacked, to allow for debugging ramdisk usage --- pkgs/by-name/bunpen/Makefile | 3 +- pkgs/by-name/bunpen/integration_test | 115 +++++++++++++++++--- pkgs/by-name/bunpen/package.nix | 4 + pkgs/by-name/bunpen/restrict/ns/mount_ns.ha | 17 +++ 4 files changed, 119 insertions(+), 20 deletions(-) diff --git a/pkgs/by-name/bunpen/Makefile b/pkgs/by-name/bunpen/Makefile index d6f08d498..154a50d42 100644 --- a/pkgs/by-name/bunpen/Makefile +++ b/pkgs/by-name/bunpen/Makefile @@ -24,7 +24,6 @@ install: test: hare test - # N.B.: integration_test relies on bashisms, not just `sh`. - PATH=$(PWD):$(PATH) bash ./integration_test + PATH=$(PWD):$(PATH) ./integration_test .PHONY: all install test diff --git a/pkgs/by-name/bunpen/integration_test b/pkgs/by-name/bunpen/integration_test index 2c39ea6f3..410ad25b6 100755 --- a/pkgs/by-name/bunpen/integration_test +++ b/pkgs/by-name/bunpen/integration_test @@ -1,8 +1,12 @@ #!/usr/bin/env bash # vim: set shiftwidth=2 : +# XXX: `bash` shebang because this relies on bashisms, not just `sh`! +set -o pipefail set -eu +SELF=$(realpath $0) + # we can't rely on /usr/bin/env existing in the nix build environment env=$(which env) test=$(which test) @@ -19,7 +23,7 @@ captureExitStatus() { } test_01_invoke_01_trivial() { - bunpen --bunpen-path / "$env" true + bunpen --bunpen-debug=4 --bunpen-path / "$env" true } test_01_invoke_02_by_path() { @@ -202,33 +206,108 @@ test_09_reap_children() { ps x | grep -E 'Zs +[0-9]+:[0-9]+ \[true\] ' && return 1 || return 0 } +test_10_tmpfs_root_is_introspectable() { + echo from_parent > test_file0 + bunpen --bunpen-path /nix/store --bunpen-path $PWD bash -c "test -f $PWD/test_file0 && echo 'from_sandbox' > /test_file1 && sleep 2" & + local sbox_pid=$! + sleep 0.5 -rc=0 -succeeded=() -failed=() -for f in $(declare -F); do - if [[ "$f" =~ ^test_* ]]; then - mkdir "$f" + # /proc/$pid/root is the rootfs as seen from inside the sandbox. + # within the sandbox, `/unbacked` holds the actual rootfs, without anyting mounted over it; + # i.e., the portion of the fstree which is backed only by the tmpfs-root + test ! -e /proc/$sbox_pid/root/unbacked/$PWD/test_file0 + test /proc/$sbox_pid/root/$PWD/test_file0 + local test_file1=$(cat /proc/$sbox_pid/root/unbacked/test_file1) + + # diagnostics, should the test fail: + echo -n "/proc/$sbox_pid: " && ls /proc/$sbox_pid + echo -n "/proc/$sbox_pid/root: " && ls /proc/$sbox_pid/root + echo -n "/proc/$sbox_pid/root/unbacked: " && ls /proc/$sbox_pid/root/unbacked + echo -n "/proc/$sbox_pid/root/unbacked/test_file1: " && echo "$test_file1" + echo -n "/proc/$sbox_pid/root/unbacked/$PWD: " && ls -l /proc/$sbox_pid/root/unbacked/$PWD + + test "$test_file1" = from_sandbox +} + +runTests() { + local testsToRun=("$@") + rc=0 + succeeded=() + failed=() + basedir=$PWD + for f in "${testsToRun[@]}"; do + mkdir "$basedir/$f" echo -n "$f: ..." - if (cd "$f"; "$f"); then + cd "$basedir/$f" + if $SELF "$f" > "$basedir/$f/stdall" 2>&1; then echo " SUCCESS" succeeded+=("$f") else rc=1 echo " FAIL" + cat "$basedir/$f/stdall" failed+=("$f") fi + done + + if [[ "${#failed[@]}" != 0 ]]; then + echo + echo "FAILED TESTS:" + fi + + for t in "${failed[@]}"; do + echo "- $t" + done + + echo "${#succeeded[@]} tests succeeded" + test -n "${#succeeded[@]}" || return 1 + return "$rc" +} + + +self_test_succeed() { + true +} +self_test_fail() { + false +} +self_test_pipe_fail() { + true | false | true +} +self_test_step_fail() { + true + false + true +} + +selfTest() { + # bash is dumb; make sure that if a test would fail, the toplevel test runner would also fail + if "$SELF" self_test_succeed \ + && ! "$SELF" self_test_fail \ + && ! "$SELF" self_test_pipe_fail \ + && ! "$SELF" self_test_step_fail \ + && ! "$SELF" self_test_succeed self_test_fail self_test_succeed \ + ; then + echo "self-test: success" + else + echo "self-test: FAIL" + exit 1 + fi +} + +allTests=() +for f in $(declare -F); do + if [[ "$f" =~ ^test_* ]]; then + allTests+=("$f") fi done -if [[ -n "${#failed[@]}" ]]; then - echo - echo "FAILED TESTS:" +if [ $# = 1 ]; then + # run the test inline + "$@" +elif [ $# = 0 ]; then + selfTest + runTests "${allTests[@]}" +else + runTests "$@" fi - -for t in "${failed[@]}"; do - echo "- $t" -done - -test -n "${#succeeded[@]}" -exit "$rc" diff --git a/pkgs/by-name/bunpen/package.nix b/pkgs/by-name/bunpen/package.nix index b2c289fab..495bd1e37 100644 --- a/pkgs/by-name/bunpen/package.nix +++ b/pkgs/by-name/bunpen/package.nix @@ -34,6 +34,10 @@ doCheck = true; + postPatch = '' + patchShebangs --build integration_test + ''; + meta = { description = "userspace sandbox helper"; longDescription = '' diff --git a/pkgs/by-name/bunpen/restrict/ns/mount_ns.ha b/pkgs/by-name/bunpen/restrict/ns/mount_ns.ha index 6ccc58746..8e38a32c6 100644 --- a/pkgs/by-name/bunpen/restrict/ns/mount_ns.ha +++ b/pkgs/by-name/bunpen/restrict/ns/mount_ns.ha @@ -41,6 +41,23 @@ fn isolate_paths(what: *restrict::resources) void = { // errors::ext::check("[namespace] mount -t tmpfs tmpfs new", rt::ext::mount("tmpfs", "new", "tmpfs", rt::ext::mount_flag::NODEV | rt::ext::mount_flag::NOSUID, null)); // errors::ext::check("[namespace] mount -o rbind new new", rt::ext::mount("new", "new", "", rt::ext::mount_flag::BIND | rt::ext::mount_flag::REC, null)); + // bind the new `/` tmpfs also to `/unbacked`; this is entirely optional, to + // aid with troubleshooting: + // `/unbacked` is a shallow bind of `/`, so provides a view into which files + // are backed only by RAM. + // admins can debug unexpected RAM use by querying (from outside the sandbox): + // - `for p in /proc/*; do if [ "$p" != /proc/self -a "$p" != /proc/thread-self -a -d "$p/root/unbacked" ]; then du -h "$p/root/unbacked"; fi; done | sort -h` + errors::ext::check("[namespace] mkdir new/unbacked", rt::mkdir("new/unbacked", 0o755)); + errors::ext::check("[namespace] mount new new/unbacked", + rt::ext::mount( + "new", + "new/unbacked", + "", + rt::ext::mount_flag::BIND, + null, + ) + ); + // try to mount a new /proc. // - this is "safe" because we're not doing anything // the sandboxed program can't do. IOW, if this is unsafe, then the downstream