bunpen: expose the bare / tmpfs at /unbacked, to allow for debugging ramdisk usage

This commit is contained in:
2024-12-16 06:57:17 +00:00
parent ad319417b5
commit 42a80fcfe4
4 changed files with 119 additions and 20 deletions

View File

@@ -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

View File

@@ -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\] <defunct>' && 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"

View File

@@ -34,6 +34,10 @@
doCheck = true;
postPatch = ''
patchShebangs --build integration_test
'';
meta = {
description = "userspace sandbox helper";
longDescription = ''

View File

@@ -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