Wren Linux

wren-linux / tools / test-wren-sandbox.sh

#!/bin/bash
# Checks bin/wren-sandbox, the start-up steps firefox, incognito, tor-browser and mullvad-browser
# share (the lock, the private folder, the restricted Wayland socket, the sound pipe, taking it
# all back), on their own, with pretend wayland-restrict, setfacl, sudo and pw-cat. The launchers'
# own tests check what each one hands its sandbox. Nothing of this computer changes. Run by
# test.sh, or:  bash tools/test-wren-sandbox.sh
# shellcheck disable=SC2016  # the inner shells expand their own variables
HERE=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
LIB=$HERE/bin/wren-sandbox
T=$(mktemp -d) || { echo "  FAIL  couldn't make a temp folder (is /tmp full?)"; exit 1; }
trap 'rm -rf "$T"' EXIT
fails=0
check() { # DESCRIPTION CONDITION-COMMAND...
    local what=$1
    shift
    if "$@" >/dev/null 2>&1; then printf '  ok    %s\n' "$what"; else printf '  FAIL  %s\n' "$what"; fails=$((fails + 1)); fi
}
not() { ! "$@"; }

mkdir -p "$T/fake" "$T/home/.local/bin" "$T/run" "$T/shm"
printf '#!/bin/bash\necho "setfacl $*" >>"%s/calls"\n' "$T" >"$T/fake/setfacl"
# wayland-restrict: says ready (the real one makes the socket), and waits for its input to close
printf '#!/bin/bash\n: >"$1"\necho ready\ncat >/dev/null\n' >"$T/fake/wayland-restrict"
printf '#!/bin/bash\necho "pw-cat $*" >>"%s/calls"\ncat >/dev/null\n' "$T" >"$T/fake/pw-cat"
printf '#!/bin/bash\n[ "$1 $2" = "-n -u" ] && [ ! -e "%s/no-rule" ]\n' "$T" >"$T/fake/sudo"
chmod +x "$T"/fake/*
: >"$T/calls"

# withlib COMMANDS...: runs them with the library loaded, in a shell of its own; die says why and stops
withlib() {
    env HOME="$T/home" PATH="$T/fake:/usr/bin:/bin" WREN_SHARE_ROOT="$T/shm" bash -c '
        die() { echo "died: $*"; exit 1; }
        . "$1"; shift
        rt='"$T"'/run user=$(id -un)
        eval "$*"' _ "$LIB" "$@" 2>&1
}

# ---- the session
check "ws_session: your own sway session, with a plain name" bash -c '! grep -q . <<<"$1"' _ "$(WAYLAND_DISPLAY=wayland-1 XDG_RUNTIME_DIR=$T/run withlib 'ws_session' 2>&1 | grep -v "^died: can.t find")"
check "...no session: it says so" grep -q 'needs your sway session' <<<"$(unset WAYLAND_DISPLAY; XDG_RUNTIME_DIR="$T/run" withlib 'ws_session')"
check "...a name with a folder in it: refused" grep -q 'plain name' <<<"$(WAYLAND_DISPLAY=a/b XDG_RUNTIME_DIR="$T/run" withlib 'ws_session')"
: >"$T/run/wayland-1"
check "...a socket that isn't there: refused" grep -q "can't find your Wayland socket" <<<"$(WAYLAND_DISPLAY=wayland-1 XDG_RUNTIME_DIR="$T/run" withlib 'ws_session')"
check "ws_session refuses to run as root" grep -q 'run it as your normal user, not root' "$LIB"

# ---- the tools and the rule
check "ws_tools: wayland-restrict from ~/.local/bin first" bash -c '[ "$1" = "$2/home/.local/bin/wayland-restrict" ]' _ "$(cp "$T/fake/wayland-restrict" "$T/home/.local/bin/"; withlib 'ws_tools how; echo $restrict')" "$T"
rm -f "$T/home/.local/bin/wayland-restrict"
check "...else the one on the PATH" bash -c '[ "$1" = "$2/fake/wayland-restrict" ]' _ "$(withlib 'ws_tools how; echo $restrict')" "$T"
check "...none: it says what to run" grep -q 'died: wayland-restrict is missing. Run: wrenctl install x' <<<"$(env HOME="$T/home" PATH="$T/nofake:/usr/bin:/bin" bash -c 'die() { echo "died: $*"; exit 1; }; . "$1"; ws_tools "wrenctl install x"' _ "$LIB" 2>&1)"
check "ws_allowed: the rule is there: nothing said" test -z "$(withlib 'ws_allowed /x how')"
touch "$T/no-rule"
check "...no rule: it says what to run" grep -q "died: your user isn't set up to start it. Run: wrenctl part y" <<<"$(withlib 'ws_allowed /x "wrenctl part y"')"
rm -f "$T/no-rule"

# ---- one at a time
cat >"$T/lock.sh" <<'EOF'
. "$1"
die() { echo "died: $*"; exit 1; }
if ws_lock "$2"; then echo got; sleep "${3:-0}"; else echo busy; fi
EOF
(env PATH="/usr/bin:/bin" bash "$T/lock.sh" "$LIB" "$T/run/l.lock" 3 >"$T/first" 2>&1 &)
/bin/sleep 0.7
second=$(env PATH="/usr/bin:/bin" bash "$T/lock.sh" "$LIB" "$T/run/l.lock" 2>&1)
check "ws_lock: the first one gets it" grep -qx got "$T/first"
check "...a second, while it is held, doesn't" test "$second" = busy
/bin/sleep 3.3
check "...and it can again once the first has ended" test "$(env PATH="/usr/bin:/bin" bash "$T/lock.sh" "$LIB" "$T/run/l.lock" 2>&1)" = got

# ---- the private folder
mkdir -p "$T/shm/wren-fx.OLD1" "$T/shm/wren-other.KEEP"
out=$(withlib 'ws_share fx; echo "$share"; ls -ld "$share" | cut -c1-10')
check "ws_share: a new folder with a name nobody can guess, in memory, yours alone" bash -c '[[ $(sed -n 1p <<<"$1") =~ ^'"$T"'/shm/wren-fx\.[A-Za-z0-9]{8}$ ]] && [ "$(sed -n 2p <<<"$1")" = drwx------ ]' _ "$out"
check "...what an earlier run of the same sandbox left is cleared" test ! -e "$T/shm/wren-fx.OLD1"
check "...the folder of another sandbox is not" test -e "$T/shm/wren-other.KEEP"
check "...and it is gone when the command ends" bash -c '! ls "$1"/wren-fx.* >/dev/null 2>&1' _ "$T/shm"
check "...a place in memory that isn't there: it says so" grep -q "isn't available" <<<"$(env HOME="$T/home" PATH="$T/fake:/usr/bin:/bin" WREN_SHARE_ROOT="$T/nowhere" bash -c 'die() { echo "died: $*"; exit 1; }; . "$1"; rt=/x user=u; ws_share a' _ "$LIB" 2>&1)"

# ---- the restricted Wayland socket
: >"$T/calls"
out=$(withlib 'ws_share fx; ws_tools how; ws_traps; ws_wayland fx-wayland fx; echo "$sock"; test -e "$sock" && echo made')
check "ws_wayland: the socket is in the private folder" bash -c '[[ $(sed -n 1p <<<"$1") =~ ^'"$T"'/shm/wren-fx\.[A-Za-z0-9]+/fx-wayland$ ]] && grep -qx made <<<"$1"' _ "$out"
check "...the sandbox may pass through the folder and use the socket, nothing else" bash -c 'grep -qE "^setfacl -m .*:--x '"$T"'/shm/wren-fx\.[A-Za-z0-9]+$" "$1" && grep -qE "^setfacl -m .*:rw- '"$T"'/shm/wren-fx\.[A-Za-z0-9]+/fx-wayland$" "$1" && [ "$(grep -c "^setfacl -m" "$1")" = 2 ]' _ "$T/calls"
printf '#!/bin/bash\nsleep 30\n' >"$T/fake/wayland-restrict"
check "...a wayland-restrict that never says ready: it says so, and ends" grep -q "couldn't make a restricted Wayland socket" <<<"$(withlib 'ws_share fx; ws_tools how; ws_traps; ws_wayland fx-wayland fx' 2>&1)"
printf '#!/bin/bash\n: >"$1"\necho ready\ncat >/dev/null\n' >"$T/fake/wayland-restrict"

# ---- the sound pipe
: >"$T/calls"
check "ws_audio: off asks for nothing: no pipe (-)" test "$(withlib 'ws_share fx; ws_audio false fx-audio.fifo; echo "$audio_arg"')" = -
check "...on, but no sound server of yours: no pipe" test "$(withlib 'ws_share fx; ws_audio true fx-audio.fifo; echo "$audio_arg"')" = -
python3 -I -c 'import socket,sys; socket.socket(socket.AF_UNIX).bind(sys.argv[1])' "$T/run/pipewire-0"
out=$(withlib 'ws_share fx; ws_traps; ws_audio true fx-audio.fifo Movie; echo "$audio_arg"; exec 7>"$share/fx-audio.fifo"; /bin/sleep 0.5; stat -c "%a %F" "$share/fx-audio.fifo"')
check "...on, with one: a pipe named so, yours alone" bash -c '[ "$(sed -n 1p <<<"$1")" = fx-audio.fifo ] && [ "$(sed -n 2p <<<"$1")" = "600 fifo" ]' _ "$out"
check "...a player of yours reads it as raw sound, a normal stream in your volume control" grep -q 'pw-cat -p -a --format s16 --rate 48000 --channels 2 --media-role Movie -' "$T/calls"
check "...and the sandbox may use that pipe only" grep -qE '^setfacl -m u:[^:]+:rw- .*/fx-audio.fifo$' "$T/calls"

# ---- taking it all back
out=$(withlib '
    ws_extra_cleanup() { echo extra-ran; }
    ws_share fx; ws_tools how; ws_traps; ws_wayland fx-wayland fx; ws_audio true fx-audio.fifo
    echo "wl=$ws_wl_pid player=$ws_player share=$share"
    ws_cleanup
    kill -0 "$ws_wl_pid" 2>/dev/null && echo wl-alive || echo wl-gone
    [ -e "$share" ] && echo share-there || echo share-gone')
check "ws_cleanup: the socket's maker and the player are ended" grep -q 'wl-gone' <<<"$out"
check "...the folder goes" grep -q 'share-gone' <<<"$out"
check "...and what the command itself has to undo is undone (ws_extra_cleanup)" grep -q 'extra-ran' <<<"$out"

# ---- installing it
check "the commands that use it read it from root's folder" bash -c 'for f in firefox incognito browser-sandbox; do grep -q "/usr/local/libexec/wren-sandbox" "$1/bin/$f" || exit 1; done' _ "$HERE"
check "each installer puts it in place, takes it out with the last command, and its update notices" bash -c '
    for f in extras/firefox-sandbox.sh parts/incognito.sh extras/tor-browser.sh extras/mullvad-browser-sandbox.sh; do
        grep -qx "[[:space:]]*install_sandbox_lib" "$1/$f" && grep -qx "[[:space:]]*remove_sandbox_lib" "$1/$f" && grep -q "^# files:.*bin/wren-sandbox" "$1/$f" || { echo "$f"; exit 1; }
    done' _ "$HERE"
check "it goes only when none of the commands is left" bash -c '
    f=$(sed -n "/^remove_sandbox_lib() {/,/^}/p" "$1/lib/core.sh")
    for c in firefox incognito browser-sandbox; do grep -q "/usr/local/bin/$c" <<<"$f" || exit 1; done' _ "$HERE"
check "a stale folder is cleared only if it is yours (the folder for these is in a place all users write to)" grep -q 'find "$WS_SHARE_ROOT" -maxdepth 1 -user "$(id -un)"' "$LIB"
check "it is passed by shellcheck" bash -c 'command -v shellcheck >/dev/null || exit 0; shellcheck -x "$1"' _ "$LIB"

[ "$fails" -eq 0 ]