#!@runtimeShell@ isDebug="$SANE_SANDBOX_DEBUG" test -n "$isDebug" && set -x isDisable="$SANE_SANDBOX_DISABLE" profileDirs=(@profileDirs@) cliArgs=() cliPathArgs=() autodetect= profilesNamed=() rootPaths=() homePaths=() capabilities=() net= dns=() method= firejailFlags=() bwrapFlags=() landlockPaths= capshCapsArg= debug() { [ -n "$isDebug" ] && printf "[debug] %s" "$1" >&2 } loadProfileByPath() { # profile format is simply a list of arguments one would pass to this sane-sandboxed script itself, # with one argument per line readarray -t _profArgs < <(cat "$1") parseArgs "${_profArgs[@]}" } tryLoadProfileByName() { _profile="$1" if [ "${_profile:0:1}" = "/" ]; then # absolute path to profile. # consider it an error if it doesn't exist. # in general, prefer to use `--sane-sandbox-profile-dir` and specify the profile by name. # doing so maximizes compatibility with anything else that uses the name, like firejail. loadProfileByPath "$_profile" else profilesNamed+=("$_profile") for _profileDir in "${profileDirs[@]}"; do _profilePath="$_profileDir/$_profile.profile" debug "try profile at path: '$_profilePath'" if [ -f "$_profilePath" ]; then loadProfileByPath "$_profilePath" break fi done fi } # convert e.g. `file:///Local%20Users/foo.mp3` to `file:///Local Users/foo.mp3` urldecode() { # source: : "${*//+/ }" echo -e "${_//%/\\x}" } tryArgAsPath() { _arg="$1" _path= if [ "${_arg:0:1}" = "/" ]; then # absolute path _path="$_arg" elif [ "${_arg:0:8}" = "file:///" ]; then # URI to an absolute path which is presumably on this vfs # commonly found when xdg-open/mimeo passes a path on to an application # if URIs to relative paths exist, this implementation doesn't support them _path="/$(urldecode "${_arg:8}")" else # assume relative path _path="$(pwd)/$_arg" fi if [ -e "$_path" ]; then cliPathArgs+=("$_path") fi } ## parse CLI args into the variables declared above ## args not intended for this helper are put into $parseArgsExtra parseArgs() { parseArgsExtra=() while [ "$#" -ne 0 ]; do _arg="$1" shift case "$_arg" in (--) # rest of args are for the CLI, and not for us. # consider two cases: # - sane-sandboxed --sane-sandbox-flag1 -- /nix/store/.../mpv --arg0 arg1 # - sane-sandboxed /nix/store/.../mpv --arg0 -- arg1 # in the first case, we swallow the -- and treat the rest as CLI args. # in the second case, the -- is *probably* intended for the application. # but it could be meant for us. do the most conservative thing here # and stop our own parsing, and also forward the -- to the wrapped binary. # # this mode of argument parsing is clearly ambiguous, it's probably worth reducing our own API in the future if [ -n "$parseArgsExtra" ]; then parseArgsExtra+=("--") fi parseArgsExtra+=("$@") break ;; (--sane-sandbox-debug) isDebug=1 set -x ;; (--sane-sandbox-replace-cli) # keep the sandbox flags, but clear any earlier CLI args. # this lets the user do things like `mpv --sane-sandbox-replace-cli sh` to enter a shell # with the sandbox that `mpv` would see. parseArgsExtra=() ;; (--sane-sandbox-disable) isDisable=1 ;; (--sane-sandbox-method) method="$1" shift ;; (--sane-sandbox-autodetect) # autodetect: crawl the CLI program's args & bind any which look like paths into the sandbox. # this is handy for e.g. media players or document viewers. # it's best combined with some two-tiered thing. # e.g. first drop to the broadest path set of interest (Music,Videos,tmp, ...), then drop via autodetect. autodetect=1 ;; (--sane-sandbox-cap) _cap="$1" shift capabilities+=("$_cap") ;; (--sane-sandbox-dns) # N.B.: these named temporary variables ensure that `set -x` causes $1 to be printed _dns="$1" shift dns+=("$_dns") ;; (--sane-sandbox-firejail-arg) _fjFlag="$1" shift firejailFlags+=("$_fjFlag") ;; (--sane-sandbox-bwrap-arg) _bwrapFlag="$1" shift bwrapFlags+=("$_bwrapFlag") ;; (--sane-sandbox-net) net="$1" shift ;; (--sane-sandbox-home-path) _path="$1" shift homePaths+=("$_path") ;; (--sane-sandbox-path) _path="$1" shift rootPaths+=("$_path") ;; (--sand-sandbox-add-pwd) _path="$(pwd)" rootPaths+=("$_path") ;; (--sane-sandbox-profile) tryLoadProfileByName "$1" shift ;; (--sane-sandbox-profile-dir) _dir="$1" shift profileDirs+=("$_dir") ;; (*) parseArgsExtra+=("$_arg") ;; esac done } ## FIREJAIL BACKEND firejailName= firejailProfile= firejailIngestRootPath() { # XXX: firejail flat-out refuses to whitelist certain root paths # this exception list is non-exhaustive [ "$1" != "/bin" ] && [ "$1" != "/etc" ] && firejailFlags+=("--noblacklist=$1" "--whitelist=$1") } firejailIngestHomePath() { firejailFlags+=("--noblacklist="'${HOME}/'"$1" "--whitelist="'${HOME}/'"$1") } firejailIngestNet() { firejailFlags+=("--net=$1") } firejailIngestDns() { firejailFlags+=("--dns=$1") } firejailIngestProfile() { if [ -z "$firejailName" ]; then firejailName="$1" fi if [ -z "$firejailProfile" ]; then _fjProfileDirs=(@firejailProfileDirs@) for _fjProfileDir in "${_fjProfileDirs[@]}"; do _fjProfile="$_fjProfileDir/$1.profile" debug "try firejail profile at path: '$_fjProfile'" if [ -f "$_fjProfile" ]; then firejailProfile="$_fjProfile" fi done fi } firejailExec() { if [ -n "$firejailName" ]; then firejailFlags+=("--join-or-start=$firejailName") fi if [ -n "$firejailProfile" ]; then firejailFlags+=("--profile=$firejailProfile") fi PATH="$PATH:@firejail@/bin" exec firejail "${firejailFlags[@]}" -- "${cliArgs[@]}" } ## BUBBLEWRAP BACKEND bwrapIngestRootPath() { # N.B.: use --dev-bind-try instead of --dev-bind for platform-specific paths like /run/opengl-driver-32 # which don't exist on aarch64, as the -try variant will gracefully fail (i.e. not bind it). # N.B.: `test -r` for paths like /mnt/servo-media, which may otherwise break bwrap when offline with # "bwrap: Can't get type of source /mnt/...: Input/output error" # HOWEVER, paths such as `/run/secrets` are not readable, so don't do that (or, try `test -e` if this becomes a problem again). # test -r "$1" && bwrapFlags+=("--dev-bind-try" "$1" "$1") bwrapFlags+=("--dev-bind-try" "$1" "$1") } bwrapIngestHomePath() { _path="$HOME/$1" # `-try` version of binding is still desireable for user files. # although it'd be nice if all program directories could be required to exist, some things are scoped poorly. # e.g. ~/.local/share/historic.json for wike's history. i don't want to give it all of ~/.local/share, and i don't want it to fail if its history file doesn't exist. bwrapFlags+=("--dev-bind-try" "$_path" "$_path") } bwrapIngestProfile() { debug "bwrap doesn't implement profiles" } bwrapIngestCapability() { bwrapFlags+=("--cap-add" "cap_$1") } # WIP bwrapExec() { PATH="$PATH:@bubblewrap@/bin" exec bwrap --dev /dev --proc /proc --tmpfs /tmp "${bwrapFlags[@]}" -- "${cliArgs[@]}" } ## LANDLOCK BACKEND landlockIngestRootPath() { # TODO: escape colons if [ -e "$1" ]; then # landlock is fd-based and requires `open`ing the path; # sandboxer will error if that part fails. if [ -z "$landlockPaths" ]; then # avoid leading :, which would otherwise cause a "no such file" error. landlockPaths="$1" else landlockPaths="$landlockPaths:$1" fi fi } landlockIngestHomePath() { landlockIngestRootPath "$HOME/$1" } landlockIngestProfile() { debug "landlock doesn't implement profiles" } landlockIngestCapability() { capshonlyIngestCapability "$1" } landlockExec() { # other sandboxing methods would create fake /dev, /proc, /tmp filesystems # but landlock can't do that. so bind a minimal number of assumed-to-exist files. # note that most applications actually do start without these, but maybe produce weird errors during their lifetime. # typical failure mode: # - /tmp: application can't perform its task # - /dev/{null,random,urandom,zero}: application warns but works around it landlockIngestRootPath '/dev/null' landlockIngestRootPath '/dev/random' landlockIngestRootPath '/dev/urandom' landlockIngestRootPath '/dev/zero' landlockIngestRootPath '/tmp' # /dev/{stderr,stdin,stdout} are links to /proc/self/fd/N # and /proc/self is a link to /proc/. # there seems to be an issue, observed with wireshark, in binding these. # maybe i bound the symlinks but not the actual data being pointed to. # if you want to bind /dev/std*, then also bind all of /proc. # landlockIngestRootPath '/proc/self' # landlockIngestRootPath "/proc/$$" # landlockIngestRootPath '/dev/stderr' # landlockIngestRootPath '/dev/stdin' # landlockIngestRootPath '/dev/stdout' # landlock sandboxer has no native support for capabilities (except that it sets nonewprivs), # so trampoline through `capsh` as well, to drop privs. # N.B: capsh passes its arg to bash (via /nix/store/.../bash), which means you have to `-c "my command"` to # invoke the actual user command. PATH="$PATH:@landlockSandboxer@/bin:@libcap@/bin" LL_FS_RO= LL_FS_RW="$landlockPaths" exec \ sandboxer \ capsh "--caps=$capshCapsArg" -- \ -c "${cliArgs[*]}" } ## CAPSH-ONLY BACKEND # this backend exists because apps which are natively bwrap may complain about having ambient privileges. # then, run them in a capsh sandbox, which ignores any path sandboxing and just lowers privs to what's needed. capshonlyIngestRootPath() { debug "capshonly doesn't implement root paths" } capshonlyIngestHomePath() { debug "capshonly doesn't implement home paths" } capshonlyIngestProfile() { debug "capshonly doesn't implement profiles" } capshonlyIngestCapability() { # N.B. `capsh` parsing of `--caps=X` arg is idiosyncratic: # - valid: `capsh --caps=CAP_FOO,CAP_BAR=eip -- ` # - valid: `capsh --caps= -- ` # - invalid: `capsh --caps=CAP_FOO,CAP_BAR -- ` # - invalid: `capsh --caps==eip -- ` if [ -z "$capshCapsArg" ]; then capshCapsArg="cap_$1=eip" else capshCapsArg="cap_$1,$capshCapsArg" fi } capshonlyExec() { PATH="$PATH:@libcap@/bin" exec \ capsh "--caps=$capshCapsArg" -- \ -c "${cliArgs[*]}" } ## BACKEND HANDOFF test -n "$SANE_SANDBOX_PREPEND" && parseArgs $SANE_SANDBOX_PREPEND parseArgs "$@" cliArgs+=("${parseArgsExtra[@]}") test -n "$SANE_SANDBOX_APPEND" && parseArgs $SANE_SANDBOX_APPEND test -n "$isDisable" && exec "${cliArgs[@]}" ### convert generic args into sandbox-specific args # order matters: for firejail, early args override the later --profile args for _path in "${rootPaths[@]}"; do "$method"IngestRootPath "$_path" done for _path in "${homePaths[@]}"; do "$method"IngestHomePath "$_path" done if [ -n "$autodetect" ]; then for _arg in "${cliArgs[@]:1}"; do tryArgAsPath "$_arg" done for _path in "${cliPathArgs[@]}"; do # TODO: might want to also mount the directory *above* this file, # to access e.g. adjacent album art in the media's folder. "$method"IngestRootPath "$_path" done fi for _cap in "${capabilities[@]}"; do "$method"IngestCapability "$_cap" done if [ -n "$net" ]; then "$method"IngestNet "$net" fi for _addr in "${dns[@]}"; do "$method"IngestDns "$_addr" done for _prof in "${profilesNamed[@]}"; do "$method"IngestProfile "$_prof" done # variables meant to be inherited # N.B.: SANE_SANDBOX_DEBUG FREQUENTLY BREAKS APPLICATIONS WHICH PARSE STDOUT # example is wireshark parsing stdout of dumpcap; # in such a case invoke the app with --sane-sandbox-debug instead of the env var. export SANE_SANDBOX_DEBUG="$SANE_SANDBOX_DEBUG" export SANE_SANDBOX_DISABLE="$SANE_SANDBOX_DISABLE" export SANE_SANDBOX_PREPEND="$SANE_SANDBOX_PREPEND" export SANE_SANDBOX_APPEND="$SANE_SANDBOX_APPEND" "$method"Exec echo "sandbox glue failed for method='$method'" exit 1