#!@bash@/bin/bash ## EARLY DEBUG HOOKS isDebug= enableDebug() { isDebug=1 set -x } debug() { [ -n "$isDebug" ] && printf "[debug] %s" "$1" >&2 } # if requested, enable debugging as early as possible if [ -n "$SANE_SANDBOX_DEBUG" ]; then enableDebug fi ## INTERPRETER CONFIGURATION # enable native implementations for common utils like `realpath` # to get a pretty meaningful speedup. # see: # XXX 2024/02/17: nixpkgs bash initializes this to nonsense FHS directories: # "/usr/local/lib/bash:/usr/lib/bash:/opt/local/lib/bash:/usr/pkg/lib/bash:/opt/pkg/lib/bash" # BASH_LOADABLES_PATH="$BASH_LOADABLES_PATH:@bash@/lib/bash" # enable -f realpath realpath # enable -f dirname dirname ## MUTABLE GLOBAL VARIABLES AND HELPER FUNCTIONS profileDirs=() # isDisable: set non-empty to invoke the binary without any sandboxing isDisable= # isDryRun: don't actually execute the program or sandbox: just print the command which would be run (and which the user may run from their own shell) isDryRun= ### values derived directly from $argv # cliArgs: the actual command we'll be running inside the sandbox cliArgs=() # type of sandbox to use # - "bwrap" # - "landlock" # - "capshonly" # - "firejail" # - "none" method= # autodetect: set non-empty to add any path-like entities intended for the binary's CLI, into its sandbox. # - "existing" (file or directory) # - "existingFile" (file only; no directories) # - "parent" # - "existingOrParent" # - "existingFileOrParent" autodetect= # paths: list of paths to make available inside the sandbox. # this could contain duplicates, non-canonical paths (`a/../b`), paths that don't exist, etc. paths=() # linux capabilities to provide to the sandbox, like `sys_admin` (no `cap_` prefix here) capabilities=() # keepNamespace: # - "cgroup" # - "ipc" # - "pid": if this process may wany to query /proc/$PID/... of parent/sibling processes. # - "uts" # - "all": as if all the above were specified keepNamespace=() # name of some network device to make available to the sandbox, if any. net= # list of IP addresses to use for DNS servers inside the sandbox (firejail only) dns=() # list of `VAR=VALUE` environment variables to add to the sandboxed program's environment portalEnv=() # profilesNamed: list of profile names we've successfully loaded profilesNamed=() # arguments to forward onto a specific backend (if that backend is active) firejailFlags=() bwrapFlags=() usage() { echo 'sane-sandboxed: run a program inside a sandbox' echo 'USAGE: sane-sandboxed [sandbox-arg ...] program [sandbox-arg|program-arg ...] [--] [program-arg ...]' echo '' echo 'sandbox args and program args may be intermixed, but the first `--` anywhere signals the end of the sandbox args and the start of program args' echo echo 'sandbox args:' echo ' --sane-sandbox-help' echo ' show this message' echo ' --sane-sandbox-debug' echo ' print debug messages to stderr' echo ' --sane-sandbox-replace-cli ' echo ' invoke under the sandbox instead of any program previously listed' echo ' also clears and earlier arguments intended for the program' echo ' --sane-sandbox-disable' echo ' invoke the program directly, instead of inside a sandbox' echo ' --sane-sandbox-dry-run' echo ' show what would be `exec`uted but do not perform any action' echo ' --sane-sandbox-method ' echo ' use a specific sandboxer' echo ' --sane-sandbox-autodetect ' echo ' add files which appear later as CLI arguments into the sandbox' echo ' --sane-sandbox-cap ' echo ' allow the sandboxed program to use the provided linux capability (both inside and outside the sandbox)' echo ' --sane-sandbox-portal' echo ' set environment variables so that the sandboxed program will attempt to use xdg-desktop-portal for operations like opening files' echo ' --sane-sandbox-no-portal' echo ' undo a previous `--sane-sandbox-portal` arg' echo ' --sane-sandbox-dns ' echo ' --sane-sandbox-firejail-arg ' echo ' --sane-sandbox-bwrap-arg ' echo ' --sane-sandbox-net ' echo ' --sane-sandbox-keep-namespace ' echo ' do not unshare the provided linux namespace' echo ' --sane-sandbox-path ' echo ' allow access to the host within the sandbox' echo ' --sane-sandbox-add-pwd' echo ' shorthand for `--sane-sandbox-path $PWD`' echo ' --sane-sandbox-profile ' echo ' --sane-sandbox-profile-dir ' echo echo 'the following environment variables are also considered and propagated to children:' echo ' SANE_SANDBOX_DISABLE=1' echo ' equivalent to `--sane-sandbox-disable`' echo ' SANE_SANDBOX_DEBUG=1' echo ' equivalent to `--sane-sandbox-debug`, but activates earlier' echo ' SANE_SANDBOX_PREPEND=...' echo ' act as though the provided arg string appeared at the start of the CLI' echo ' SANE_SANDBOX_APPEND=...' echo ' act as though the provided arg string appeared at the end of the CLI' } ## UTILITIES/BOILERPLATE # `normPath outVar "$path"` # remove duplicate //, reduce '.' and '..' (naively). # expects a full path as input # chomps trailing slashes. # does not resolve symlinks, nor check for existence of any component of the path. normPath() { _npOut="$1" _npUnparsed="$2" _npComps=() while [ -n "$_npUnparsed" ]; do # chomp leading `/` _npUnparsed="${_npUnparsed:1}" # split into # in the case of `//`, or more, _npThisComp is empty, # and we push nothing to _npComps, # but we're guaranteed to make progress. _npThisComp="${_npUnparsed%%/*}" _npThisLen="${#_npThisComp}" _npUnparsed="${_npUnparsed:$_npThisLen}" if [ "$_npThisComp" = ".." ]; then # "go up" path component => delete the leaf dir (if any) if [ ${#_npComps[@]} -ne 0 ]; then unset _npComps[-1] fi elif [ "$_npThisComp" != "." ] && [ -n "$_npThisComp" ]; then # normal, non-empty path component => append it _npComps+=("$_npThisComp") fi done # join the components if [ ${#_npComps[@]} -eq 0 ]; then declare -g "$_npOut"="/" else _npJoined= for _npComp in "${_npComps[@]}"; do _npJoined="$_npJoined/$_npComp" done declare -g "$_npOut"="$_npJoined" fi } # normPathBashBuiltin() { # # normPath implementation optimized by using bash builtins (`help realpath`): # # -s: canonicalize `.` and `..` without resolving symlinks # # -c: enforce that each component exist (intentionally omitted) # # XXX: THIS DOESN'T WORK: if bash is asked to canonicalize a path with symlinks, it either (1) resolves the symlink (default) or (2) aborts (-s). there's no equivalent to coreutils # realpath -s "$1" # } # legacy coreutils normPath: definitive, but slow (requires a fork/exec subshell). # bash `normPath` aims to be equivalent to this one. # normPathCoreutils() { # # `man realpath`: # # --logical: resolve `..` before dereferencing symlinks # # --no-symlinks: don't follow symlinks # # --canonicalize-missing: don't error if path components don't exist # realpath --logical --no-symlinks --canonicalize-missing "$1" # } # `parent outVar "$path"` # return the path to this file or directory's parent, even if the input doesn't exist. parent() { normPath "$1" "$2/.." } # `locate outVar ` => if `` is on PATH, then return that, else locate() { # N.B.: explicitly avoid returning the output of `command -v`, for optimization. # unlike other bash builtins, `x="$(command -v y)"` forks, whereas just `command -v y` does not. if command -v "$2" > /dev/null; then declare -g "$1"="$2" else declare -g "$1"="$3" fi } # `urldecode outVar ` # convert e.g. `file:///Local%20Users/foo.mp3` to `file:///Local Users/foo.mp3` urldecode() { local outVar="$1" shift # source: # replace each `+` with space local i="${*//+/ }" # then replace each `%` with `\x` # and have `echo` evaluate the escape sequences declare -g "$outVar"="$(echo -e "${i//%/\\x}")" } ## HELPERS 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 < "$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 } initDefaultProfileDirs() { for d in ${XDG_DATA_DIRS//:/ }; do profileDirs+=("$d/sane-sandboxed/profiles") done } # subroutine of `tryArgAsPath` for after the arg has been converted into a valid (but possibly not existing) path. # adds an entry to `cliPathArgs` and evals `true` on success; # evals `false` if the path couldn't be added, for any reason. cliPathArgs=() tryPath() { _path="$1" _how="$2" if [ "$_how" = "existing" ]; then # the caller wants to access either a file, or a directory (possibly a symlink to such a thing) if [ -e "$_path" ]; then cliPathArgs+=("$_path") true fi false elif [ "$_how" = "existingFile" ]; then # the caller wants to access a file, and explicitly *not* a directory (though it could be a symlink *to a file*) if [ -f "$_path" ]; then cliPathArgs+=("$_path") true fi false elif [ "$_how" = "parent" ]; then # the caller wants access to the entire directory containing this directory regardless of the file's existence. parent _tryPathParent "$_path" tryPath "$_tryPathParent" "existing" elif [ "$_how" = "existingOrParent" ]; then # the caller wants access to the path, or write access to the parent directory so it may create the path if it doesn't exist. tryPath "$_path" "existing" || tryPath "$_path" "parent" elif [ "$_how" = "existingFileOrParent" ]; then # the caller wants access to the file, or write access to the parent directory so it may create the file if it doesn't exist. tryPath "$_path" "existingFile" || tryPath "$_path" "parent" fi } # if the argument looks path-like, then add it to cliPathArgs. # this function ingests absolute, relative, or file:///-type URIs. # but it converts any such path into an absolute path before adding it to cliPathArgs. tryArgAsPath() { _arg="$1" _how="$2" _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 urldecode _path "${_arg:7}" elif [ "${_path:0:1}" = "-" ]; then # 99% chance it's a CLI argument. if not, use `./-<...>` return else # assume relative path _path="$PWD/$_arg" fi tryPath "$_path" "$_how" } ## ARGV PARSING LOOP # parse CLI args into the variables declared above # args not intended for this helper are put into $parseArgsExtra parseArgsExtra=() parseArgs() { 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-help) usage exit 0 ;; (--sane-sandbox-debug) enableDebug ;; (--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-dry-run) isDryRun=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" shift ;; (--sane-sandbox-cap) _cap="$1" shift capabilities+=("$_cap") ;; (--sane-sandbox-portal) # instruct glib/gtk apps to perform actions such as opening external files via dbus calls to org.freedesktop.portal.*. # note that GIO_USE_PORTALS primarily acts as a *fallback*: apps only open files via the portal if they don't know how to themelves. # this switch is typically accompanied by removing all MIME associations from the app's view, then. # GTK_USE_PORTALS is the old name, beginning to be phased out as of 2023-10-02 portalEnv=("GIO_USE_PORTALS=1" "GTK_USE_PORTAL=1" "NIXOS_XDG_OPEN_USE_PORTAL=1") ;; (--sane-sandbox-no-portal) # override a previous --sane-sandbox-portal call portalEnv=() ;; (--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-keep-namespace) _namespace="$1" shift if [ "$_namespace" = all ]; then keepNamespace+=("cgroup" "ipc" "pid" "uts") else keepNamespace+=("$_namespace") fi ;; (--sane-sandbox-path) _path="$1" shift paths+=("$_path") ;; (--sane-sandbox-add-pwd) _path="$PWD" paths+=("$_path") ;; (--sane-sandbox-profile) # load the profile *immediately*, inline. # this way, user arguments which come later on the CLI can override any profile config, v.s. if profile loading were defered. tryLoadProfileByName "$1" shift ;; (--sane-sandbox-profile-dir) # add another directory in which to search for profiles, # and give it *greater* precedence than the existing search directories (i.e. override the default profile) _dir="$1" shift profileDirs=("$_dir" "${profileDirs[@]}") ;; (*) parseArgsExtra+=("$_arg") ;; esac done } ## FIREJAIL BACKEND firejailName= firejailProfile= firejailSetup() { debug "firejailSetup: noop" } firejailIngestPath() { # 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") } firejailIngestNet() { firejailFlags+=("--net=$1") } firejailIngestDns() { firejailFlags+=("--dns=$1") } firejailIngestKeepNamespace() { debug "firejailIngestKeepNamespace: noop" } 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 } firejailGetCli() { if [ -n "$firejailName" ]; then firejailFlags+=("--join-or-start=$firejailName") fi if [ -n "$firejailProfile" ]; then firejailFlags+=("--profile=$firejailProfile") fi locate _firejail "firejail" "@firejail@/bin/firejail" cliArgs=( "$_firejail" "${firejailFlags[@]}" -- env "${portalEnv[@]}" "${cliArgs[@]}" ) } ## BUBBLEWRAP BACKEND bwrapUnshareCgroup=(--unshare-cgroup) bwrapUnshareIpc=(--unshare-ipc) bwrapUnshareNet=(--unshare-net) bwrapUnsharePid=(--unshare-pid) bwrapUnshareUts=(--unshare-uts) bwrapVirtualizeDev=(--dev /dev) bwrapVirtualizeProc=(--proc /proc) bwrapVirtualizeTmp=(--tmpfs /tmp) bwrapSetup() { debug "bwrapSetup: noop" } bwrapIngestPath() { # 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). # HOWEVER, `test -e` hangs (for ~10s?) on broken mount points or mount subpaths. it handles mount superpaths fine. e.g.: # - /mnt/servo/media/Pictures -> prone to hanging (subdir of mount) # - /mnt/servo/media -> prone to hanging (root mount point) # - /mnt/servo -> never hangs # may be possible to place ever mount in a subdir, and mount the super dir? # or maybe configure remote mounts to somehow never hang. # test -r "$1" && bwrapFlags+=("--dev-bind-try" "$1" "$1") bwrapFlags+=("--dev-bind-try" "$1" "$1") # default to virtualizing a few directories in a way that's safe (doesn't impact outside environment) # and maximizes compatibility with apps. but if explicitly asked for the directory, then remove the virtual # device and bind it as normal. if [ "$1" = / ]; then bwrapVirtualizeDev=() bwrapVirtualizeProc=() bwrapVirtualizeTmp=() elif [ "$1" = /dev ]; then bwrapVirtualizeDev=() elif [ "$1" = /proc ]; then bwrapVirtualizeProc=() elif [ "$1" = /tmp ]; then bwrapVirtualizeTmp=() fi } bwrapIngestNet() { debug "bwrapIngestNet: enabling full net access for '$1' because don't know how to restrict it more narrowly" bwrapUnshareNet=() } bwrapIngestKeepNamespace() { case "$1" in (cgroup) bwrapUnshareCgroup=() ;; (ipc) bwrapUnshareIpc=() ;; (pid) bwrapUnsharePid=() ;; (uts) bwrapUnshareUts=() ;; esac } bwrapIngestProfile() { debug "bwrapIngestProfile: stubbed" } bwrapIngestCapability() { bwrapFlags+=("--cap-add" "cap_$1") } bwrapGetCli() { # --unshare-all implies the following: # --unshare-pid: mean that the /proc mount does not expose /proc/$PID/ for every other process on the machine. # --unshare-net creates a new net namespace with only the loopback interface. # if `bwrapFlags` contains --share-net, thiss is canceled and the program sees an unsandboxed network. # --unshare-ipc # --unshare-cgroup # --unshare-uts # --unshare-user (implicit to every non-suid call to bwrap) locate _bwrap "bwrap" "@bubblewrap@/bin/bwrap" cliArgs=( "$_bwrap" "${bwrapUnshareCgroup[@]}" "${bwrapUnshareIpc[@]}" "${bwrapUnshareNet[@]}" "${bwrapUnsharePid[@]}" "${bwrapUnshareUser[@]}" "${bwrapUnshareUts[@]}" "${bwrapVirtualizeDev[@]}" "${bwrapVirtualizeProc[@]}" "${bwrapVirtualizeTmp[@]}" "${bwrapFlags[@]}" -- env "${portalEnv[@]}" "${cliArgs[@]}" ) } ## LANDLOCK BACKEND landlockPaths= landlockSetup() { # 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 paths+=(\ /dev/null /dev/random /dev/urandom /dev/zero /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. # /proc/self # "/proc/$$" # /dev/stderr # /dev/stdin # /dev/stdout } landlockIngestPath() { # 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 } landlockIngestNet() { debug "landlockIngestNet: '$1': stubbed (landlock network is always unrestricted)" } landlockIngestKeepNamespace() { debug "landlockIngestKeepNamespace: noop" } landlockIngestProfile() { debug "landlockIngestProfile: stubbed" } landlockIngestCapability() { capshonlyIngestCapability "$1" } landlockGetCli() { # 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. locate _sandboxer "sandboxer" "@landlockSandboxer@/bin/sandboxer" locate _capsh "capsh" "@libcap@/bin/capsh" cliArgs=(env LL_FS_RO= LL_FS_RW="$landlockPaths" "$_sandboxer" "$_capsh" "--caps=$capshCapsArg" --no-new-privs --shell="/usr/bin/env" -- "${portalEnv[@]}" "${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. capshCapsArg= capshonlySetup() { debug "capshonlySetup: noop" } capshonlyIngestPath() { debug "capshonlyIngestPath: stubbed" } capshonlyIngestNet() { debug "capshonlyIngestNet: '$1': stubbed (capsh network is always unrestricted)" } capshonlyIngestKeepNamespace() { debug "capshonlyIngestKeepNamespace: noop" } capshonlyIngestProfile() { debug "capshonlyIngestProfile: stubbed" } 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 -- ` # # `capsh --caps=CAP_FOO=eip -- true` will fail if we don't have CAP_FOO, # but for my use i'd still like to try running the command even if i can't grant it all capabilities. # therefore, only grant it those capabilities i know will succeed. if capsh "--has-p=cap_$1" 2>/dev/null; then if [ -z "$capshCapsArg" ]; then capshCapsArg="cap_$1=eip" else capshCapsArg="cap_$1,$capshCapsArg" fi else debug "capsh: don't have capability $1" fi } capshonlyGetCli() { locate _capsh "capsh" "@libcap@/bin/capsh" cliArgs=( "$_capsh" "--caps=$capshCapsArg" --no-new-privs --shell="/usr/bin/env" -- "${portalEnv[@]}" "${cliArgs[@]}" ) } ## NONE BACKEND # this backend exists only to allow benchmarking noneSetup() { : } noneIngestPath() { : } noneIngestNet() { : } noneIngestKeepNamespace() { : } noneIngestProfile() { : } noneIngestCapability() { : } noneGetCli() { : } ## ARGUMENT POST-PROCESSING ### autodetect: if one of the CLI args looks like a path, that could be an input or output file # so allow access to it. maybeAutodetectPaths() { if [ -n "$autodetect" ]; then for _arg in "${cliArgs[@]:1}"; do tryArgAsPath "$_arg" "$autodetect" 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. paths+=("$_path") done fi } ### path sorting: if the app has access both to /FOO and /FOO/BAR, some backends get confused. # notably bwrap, --bind /FOO /FOO followed by --bind /FOO/BAR /FOO/BAR results in /FOO being accessible but /FOO/BAR *not*. # so reduce the paths to the minimal set which includes those requested. # for more sophisticated (i.e. complex) backends like firejail, this may break subpaths which were blacklisted earlier. canonicalizePaths() { # remove '//' and simplify '.', '..' paths, into canonical absolute logical paths. _normPaths=() for _path in "${paths[@]}"; do normPath _canonPath "$_path" _normPaths+=("$_canonPath") done # remove subpaths, but the result might include duplicates. _toplevelPaths=() for _path in "${_normPaths[@]}"; do _isSubpath= for _other in "${_normPaths[@]}"; do if [[ "$_path" =~ ^$_other/.* ]] || [ "$_other" = "/" ] && [ "$_path" != "/" ]; then # N.B.: $_path lacks a trailing slash, so this never matches self. # UNLESS $_path or $_other is exactly `/`, which we special-case. _isSubpath=1 fi done if [ -z "$_isSubpath" ]; then _toplevelPaths+=("$_path") fi done # remove duplicated paths. canonicalizedPaths=() for _path in "${_toplevelPaths[@]}"; do _isAlreadyListed= for _other in "${canonicalizedPaths[@]}"; do if [ "$_path" = "$_other" ]; then _isAlreadyListed=1 fi done if [ -z "$_isAlreadyListed" ]; then canonicalizedPaths+=("$_path") fi done } ## TOPLEVEL ADAPTERS # - convert CLI args/env into internal structures # - convert internal structures into backend-specific structures ### parse arguments, with consideration of any which may be injected via the environment parseArgsAndEnvironment() { if [ -n "$SANE_SANDBOX_DISABLE" ]; then isDisable=1 fi test -n "$SANE_SANDBOX_PREPEND" && parseArgs $SANE_SANDBOX_PREPEND parseArgs "$@" cliArgs+=("${parseArgsExtra[@]}") test -n "$SANE_SANDBOX_APPEND" && parseArgs $SANE_SANDBOX_APPEND } ### convert generic args into sandbox-specific args # order matters: for firejail, early args override the later --profile args ingestForBackend() { for _path in "${canonicalizedPaths[@]}"; do "$method"IngestPath "$_path" done 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 _ns in "${keepNamespace[@]}"; do "$method"IngestKeepNamespace "$_ns" done for _prof in "${profilesNamed[@]}"; do "$method"IngestProfile "$_prof" done } ## TOPLEVEL EXECUTION # no code evaluated before this point should be dependent on user args / environment. initDefaultProfileDirs parseArgsAndEnvironment "$@" # 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" if [ -z "$isDisable" ]; then # method-specific setup could add additional paths that need binding, so do that before canonicalization "$method"Setup maybeAutodetectPaths canonicalizePaths ingestForBackend "$method"GetCli fi if [ -n "$isDryRun" ]; then echo "dry-run: ${cliArgs[*]}" exit 0 fi exec "${cliArgs[@]}" echo "sandbox glue failed for method='$method'" exit 1