wren-linux / bin / wren-sandbox
# shellcheck shell=bash
# shellcheck disable=SC2154 # user, rt, restrict: set by the caller, as the header says
# wren-sandbox: written by Wren Linux. Installed as /usr/local/libexec/wren-sandbox, root's, by
# the scripts that install the commands that start a sandbox (firefox, incognito, tor-browser and
# mullvad-browser: extras/firefox-sandbox.sh, parts/incognito.sh, lib/browser-sandbox.sh). It is
# not run: those commands source it, as your user, and take the steps they all share from here.
# One copy of the start-up code that keeps a sandbox from your session, so a fix reaches every
# sandbox at once, and nothing is copied slightly differently into one of them.
#
# What each command still does itself: who it runs as, its own lock and folder names, what it hands
# its sandbox besides the screen and the sound (downloads, a picture, the theme), and the session
# script it starts. The caller defines die(), and these variables before using the functions:
# user the sandbox's user share its private folder (ws_share makes it)
# rt your runtime folder restrict wayland-restrict (ws_tools finds it)
# A caller that has something more to undo defines ws_extra_cleanup.
WS_SHARE_ROOT=${WREN_SHARE_ROOT:-/dev/shm}
ws_player='' ws_wl_pid=''
# ws_session: your own sway session is there, and what it is handed is a plain name
ws_session() {
[ "$(id -u)" -ne 0 ] || die "run it as your normal user, not root."
[ -n "${WAYLAND_DISPLAY:-}" ] || die "it needs your sway session; start sway first."
rt=${XDG_RUNTIME_DIR:-}
[ -n "$rt" ] && [ -d "$rt" ] || die "XDG_RUNTIME_DIR isn't set."
case $WAYLAND_DISPLAY in
*/*) die "WAYLAND_DISPLAY should be a plain name like wayland-1." ;;
esac
[ -S "$rt/$WAYLAND_DISPLAY" ] || die "can't find your Wayland socket ($rt/$WAYLAND_DISPLAY)."
}
# ws_tools HOW: the tools it needs. HOW is what to run if wayland-restrict is missing.
# wayland-restrict is yours, in ~/.local/bin: taken from there, so a link from a program whose PATH
# leaves that out (Wren Linux's own scripts) still opens.
ws_tools() {
restrict=$HOME/.local/bin/wayland-restrict
[ -x "$restrict" ] || restrict=$(command -v wayland-restrict) || die "wayland-restrict is missing. Run: $1"
command -v setfacl >/dev/null || die "setfacl is missing (package acl)."
}
# ws_allowed SESSION HOW: your user may run SESSION as $user (the one rule in /etc/sudoers.d)
ws_allowed() {
sudo -n -u "$user" -l "$1" >/dev/null 2>&1 || die "your user isn't set up to start it. Run: $2"
}
# ws_lock FILE: one at a time (a second would take back the access the first one needs). Fails if
# it is open already, and holds the lock on descriptor 9 until this ends.
ws_lock() {
command -v flock >/dev/null || die "flock is missing (package util-linux)."
exec 9>"$1"
flock -n 9
}
# ws_share PREFIX: what the sandbox is handed (the restricted socket, the sound pipe, ...) goes in a
# new folder of its own, with a name nobody can guess, and passage is given to that alone. Never
# through your runtime folder: that also leads to every socket in it, and your audio server's
# (pipewire-0, pulse/native) are open to anyone who reaches them, with your microphones and camera
# behind them. The session script accepts only such a folder. Sets share.
ws_share() {
[ -d "$WS_SHARE_ROOT" ] || die "$WS_SHARE_ROOT (memory) isn't available."
# Left by a crash or a power cut (the lock says this one isn't running)
find "$WS_SHARE_ROOT" -maxdepth 1 -user "$(id -un)" -name "wren-$1.*" -exec rm -rf {} + 2>/dev/null || true
share=$(mktemp -d "$WS_SHARE_ROOT/wren-$1.XXXXXXXX") || die "couldn't make a folder in $WS_SHARE_ROOT."
trap 'rm -rf "$share"' EXIT
# An older version's passage, if it ended without taking it back
setfacl -x "u:$user" "$rt" 2>/dev/null || true
}
# ws_cleanup: all it was handed is taken back
ws_cleanup() {
if [ -n "$ws_player" ]; then kill "$ws_player" 2>/dev/null || true; fi
if [ -n "$ws_wl_pid" ]; then kill "$ws_wl_pid" 2>/dev/null || true; fi
rm -rf "$share"
if declare -F ws_extra_cleanup >/dev/null; then ws_extra_cleanup; fi
}
# ws_traps: take it all back however this ends
ws_traps() {
trap ws_cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM HUP
}
# ws_wayland SOCKET RESTRICT-NAME: the restricted Wayland socket, SHARE/SOCKET, made by wayland-restrict
# (it can show a window, but sway doesn't offer it the screen-reading, typing or session-lock
# protocols), and passage to it for the sandbox. Sets sock.
# 9>&-: wayland-restrict must not inherit the lock. It ends when ws_cleanup kills it, or when this
# script dies (its standard input closes).
ws_wayland() {
local line
sock=$share/$1
coproc WL { "$restrict" "$sock" "$2" 9>&-; }
ws_wl_pid=$WL_PID
IFS= read -r -t 10 line <&"${WL[0]}" && [ "$line" = ready ] ||
die "couldn't make a restricted Wayland socket (needs sway 1.9 or newer; see above)."
setfacl -m "u:$user:--x" "$share"
setfacl -m "u:$user:rw-" "$sock"
}
# ws_audio ENABLED FIFO-NAME MEDIA-ROLE: the sound pipe: the sandbox writes, a player of yours
# reads and plays it, so the sandbox never gets to your audio server or your microphone. Only raw
# sound can come through. Sets audio_arg to the pipe's name, or - for none.
# 9>&-: the player must not inherit the lock, or it outlives us holding it.
ws_audio() {
audio_arg=-
[ "$1" = true ] && command -v pw-cat >/dev/null && [ -S "$rt/pipewire-0" ] || return 0
local fifo=$share/$2
mkfifo -m 600 "$fifo"
setfacl -m "u:$user:rw-" "$fifo"
( exec pw-cat -p -a --format s16 --rate 48000 --channels 2 --media-role "${3:-Movie}" - <"$fifo" 9>&- ) &
ws_player=$!
audio_arg=$2
}