nix-files/modules/programs/sane-sandboxed

889 lines
29 KiB
Bash

#!@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: <https://unix.stackexchange.com/a/558605>
# 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 <bin>'
echo ' invoke <bin> 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 <bwrap|capshonly|firejail|landlock|none>'
echo ' use a specific sandboxer'
echo ' --sane-sandbox-autodetect <existing|existingFile|existingFileOrParent|existingOrParent|parent>'
echo ' add files which appear later as CLI arguments into the sandbox'
echo ' --sane-sandbox-cap <sys_admin|net_raw|net_admin|...>'
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 <server>'
echo ' --sane-sandbox-firejail-arg <arg>'
echo ' --sane-sandbox-bwrap-arg <arg>'
echo ' --sane-sandbox-net <iface>'
echo ' --sane-sandbox-keep-namespace <cgroup|ipc|pid|uts|all>'
echo ' do not unshare the provided linux namespace'
echo ' --sane-sandbox-path <path>'
echo ' allow access to the host <path> within the sandbox'
echo ' --sane-sandbox-add-pwd'
echo ' shorthand for `--sane-sandbox-path $PWD`'
echo ' --sane-sandbox-profile <profile>'
echo ' --sane-sandbox-profile-dir <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 <first></re/st/...>
# 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 <bin-name> </path/to/default>` => if `<bin-name>` is on PATH, then return that, else </path/to/default>
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 <uri>`
# convert e.g. `file:///Local%20Users/foo.mp3` to `file:///Local Users/foo.mp3`
urldecode() {
local outVar="$1"
shift
# source: <https://stackoverflow.com/q/6250698>
# 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/<N>.
# 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 -- <cmd>`
# - valid: `capsh --caps= -- <cmd>`
# - invalid: `capsh --caps=CAP_FOO,CAP_BAR -- <cmd>`
# - invalid: `capsh --caps==eip -- <cmd>`
#
# `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