wren-linux / bin / media-sandbox
#!/bin/bash
# media-sandbox COMMAND [ARGS...]: written by Wren Linux extras/media.sh (also extras/pdf-viewer.sh)
# Runs imv, mpv, zathura or GIMP inside bubblewrap, so a booby-trapped image,
# video or PDF can't read your files. The program sees only the files and
# folders you name on its command line (read-only), its own empty home, and
# your screen. It has no network unless you give mpv (or imv, zathura) a URL,
# and your sound is available to mpv only. GIMP, which saves its work, also
# gets ~/Pictures read-write, its own settings (~/.config/GIMP) and ~/Downloads
# read-only, and never the network.
# It gets a restricted Wayland socket (bin/wayland-restrict), not your real
# one: sway doesn't offer it the screen-reading or typing protocols. With the
# network it shares this computer's network namespace, where XWayland's abstract
# socket can be reached, so it then starts through wren-no-abstract-sockets.
# Set WREN_NO_SANDBOX=1 to run the program plainly.
set -euo pipefail
die() { printf 'media-sandbox: %s\n' "$*" >&2; exit 1; }
[ $# -ge 1 ] || die "usage: media-sandbox COMMAND [ARGS...]"
cmd=$1
shift
if [ "${WREN_NO_SANDBOX:-}" = 1 ]; then exec "$cmd" "$@"; fi
command -v bwrap >/dev/null || die "bubblewrap isn't installed (bash extras/media.sh adds it). WREN_NO_SANDBOX=1 runs without it."
[ -n "${WAYLAND_DISPLAY:-}" ] && [ -n "${XDG_RUNTIME_DIR:-}" ] || die "needs your sway session (WAYLAND_DISPLAY and XDG_RUNTIME_DIR)."
case $WAYLAND_DISPLAY in */*) die "WAYLAND_DISPLAY should be a plain name like wayland-1." ;; esac
rt=$XDG_RUNTIME_DIR
# 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
restrict=$HOME/.local/bin/wayland-restrict
[ -x "$restrict" ] || restrict=$(command -v wayland-restrict) || die "wayland-restrict is missing. Run: wrenctl install media"
sock=$rt/media-sandbox-wayland.$$
# Dies with this script (its standard input closes); the trap tidies the socket file
coproc WL { exec "$restrict" "$sock" media-sandbox; }
trap 'kill "$WL_PID" 2>/dev/null || true; rm -f "$sock"' EXIT
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). WREN_NO_SANDBOX=1 runs without the sandbox."
args=(--unshare-all --die-with-parent --new-session --clearenv
--ro-bind /usr /usr --symlink usr/bin /bin --symlink usr/sbin /sbin
--symlink usr/lib /lib --symlink usr/lib64 /lib64
--ro-bind /etc /etc --ro-bind /sys /sys
--proc /proc --dev /dev --tmpfs /tmp
--tmpfs "$HOME" --tmpfs "$rt"
--setenv PATH /usr/bin:/bin --setenv HOME "$HOME" --setenv XDG_RUNTIME_DIR "$rt"
--setenv WAYLAND_DISPLAY "$WAYLAND_DISPLAY"
--ro-bind "$sock" "$rt/$WAYLAND_DISPLAY")
[ -e /dev/dri ] && args+=(--dev-bind /dev/dri /dev/dri)
for v in LANG LC_ALL TERM XCURSOR_THEME XCURSOR_SIZE XDG_CURRENT_DESKTOP; do
[ -z "${!v:-}" ] || args+=(--setenv "$v" "${!v}")
done
# the program's own settings, read-only, if there are any
for d in "$HOME/.config/mpv" "$HOME/.config/imv" "$HOME/.config/zathura"; do
[ -d "$d" ] && args+=(--ro-bind "$d" "$d")
done
# GIMP: ~/Pictures to open and save in, its own settings, ~/Downloads to open from
name=$(basename -- "$cmd")
gimp=false
case $name in gimp | gimp-3.0) gimp=true ;; esac
if $gimp; then
mkdir -p "$HOME/Pictures" "$HOME/.config/GIMP"
args+=(--bind "$HOME/Pictures" "$HOME/Pictures" --bind "$HOME/.config/GIMP" "$HOME/.config/GIMP")
[ -d "$HOME/Downloads" ] && args+=(--ro-bind "$HOME/Downloads" "$HOME/Downloads")
fi
# sound for mpv: your audio server's socket (it could also listen to the microphone)
if [ "$(basename -- "$cmd")" = mpv ]; then
for s in pipewire-0 pulse/native; do
[ -S "$rt/$s" ] && { [ "$s" = pulse/native ] && args+=(--dir "$rt/pulse"); args+=(--ro-bind "$rt/$s" "$rt/$s"); }
done
fi
# every file or folder named on the command line, read-only; a URL (other than
# file://) gives mpv the network
network=false
newargs=()
for a in "$@"; do
# file:///path is a file, not the network: treat it as the path
case $a in file://*) a=${a#file://} ;; esac
case $a in
# an option that points at a file:// address gets no network either
-*=file://*) newargs+=("$a") ;;
# (never for GIMP: it has no business on the network)
*://*) $gimp || { network=true; args+=(--share-net --ro-bind-try /etc/ssl /etc/ssl); }; newargs+=("$a") ;;
# an option can name a file too (mpv --sub-file=subs.srt)
-*=*)
v=${a#*=}
if [ -e "$v" ]; then
p=$(realpath -- "$v")
args+=(--ro-bind "$p" "$p")
newargs+=("${a%%=*}=$p")
else
newargs+=("$a")
fi ;;
-*) newargs+=("$a") ;;
*)
if [ -e "$a" ]; then
p=$(realpath -- "$a")
# (for GIMP, ~/Pictures and ~/Downloads are already there, as they are)
case $p in
"$HOME/Pictures"/* | "$HOME/Downloads"/*) $gimp || args+=(--ro-bind "$p" "$p") ;;
*) args+=(--ro-bind "$p" "$p") ;;
esac
newargs+=("$p")
else
newargs+=("$a")
fi ;;
esac
done
# With the network, never without the wrapper (a sandbox from before it was added)
wrapper=()
if $network; then
noabs=/usr/local/libexec/wren-no-abstract-sockets
[ -x "$noabs" ] || die "$noabs is missing, and it's needed for a web address. Run: wrenctl install media"
wrapper=("$noabs")
fi
# not exec: the restricted socket has to stay up while the program runs
rc=0
"${wrapper[@]}" bwrap "${args[@]}" -- "$cmd" "${newargs[@]}" || rc=$?
exit "$rc"