diff --git a/modules/programs/sane-sandboxed b/modules/programs/sane-sandboxed index c6ff400f..c8ac959f 100644 --- a/modules/programs/sane-sandboxed +++ b/modules/programs/sane-sandboxed @@ -1,28 +1,8 @@ -#!@runtimeShell@ +#!@bash@/bin/bash + +## EARLY DEBUG HOOKS -profileDirs=() isDebug= -isDisable= - -cliArgs=() -cliPathArgs=() -autodetect= -profilesNamed=() -paths=() -capabilities=() -net= -keepPidspace= -dns=() -method= -extraEnv=() - -# backend-specific state: -firejailFlags=() -bwrapUnshareNet=(--unshare-net) -bwrapUnsharePid=(--unshare-pid) -bwrapFlags=() -landlockPaths= -capshCapsArg= enableDebug() { isDebug=1 @@ -33,11 +13,145 @@ 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= + +### 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" +# - "parent" +# - "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=() +# set non-empty if this process may want to query /proc/$PID/... of _other_ processes. +keepPidspace= +# 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 +extraEnv=() +# profilesNamed: list of profile names we've successfully loaded +profilesNamed=() + +# arguments to forward onto a specific backend (if that backend is active) +firejailFlags=() +bwrapFlags=() + + +## UTILITIES/BOILERPLATE + +# 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() { + _npUnparsed="$1" + _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%%/*}" + _npUnparsed="${_npUnparsed/$_npThisComp}" + 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 + _npOut= + for _npComp in "${_npComps[@]}"; do + _npOut="$_npOut/$_npComp" + done + if [ -z "$_npOut" ]; then + echo "/" + else + echo "$_npOut" + fi +} + +# normPath() { +# # 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. +# normPath() { +# # `man realpath`: +# # --logical: resolve `..` before dereferencing symlinks +# # --no-symlinks: don't follow symlinks +# # --canonicalize-missing: don't error if path components don't exist +# normPathOut="$(realpath --logical --no-symlinks --canonicalize-missing "$1")" +# } + +# return the path to this file or directory's parent, even if the input doesn't exist. +parent() { + normPath "$1/.." + # normPath "$(dirname "$1")" +} + # `locate ` => print the full path to `` if it's on PATH, else print `` locate() { command -v "$1" || echo "$2" } +# convert e.g. `file:///Local%20Users/foo.mp3` to `file:///Local Users/foo.mp3` +urldecode() { + # source: + : "${*//+/ }" + echo -e "${_//%/\\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 @@ -73,21 +187,10 @@ initDefaultProfileDirs() { done } -# convert e.g. `file:///Local%20Users/foo.mp3` to `file:///Local Users/foo.mp3` -urldecode() { - # source: - : "${*//+/ }" - echo -e "${_//%/\\x}" -} - -# return the path to this file or directory's parent, even if the input doesn't exist. -parent() { - realpath --logical --no-symlinks --canonicalize-missing "$1/.." -} - # 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" @@ -127,29 +230,16 @@ tryArgAsPath() { return else # assume relative path - _path="$(pwd)/$_arg" + _path="$PWD/$_arg" fi tryPath "$_path" "$_how" } -# remove duplicate //, reduce '.' and '..' (naively). -# chomps trailing slashes. -# does not resolve symlinks, nor check for existence of any component of the path. -normPath() { - realpath --logical --no-symlinks --canonicalize-missing "$1" -} -ensureTrailingSlash() { - if [ "${1:-1}" = "/" ]; then - printf "%s" "$1" - else - printf "%s/" "$1" - fi -} - -## parse CLI args into the variables declared above -## args not intended for this helper are put into $parseArgsExtra +## 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 @@ -238,7 +328,7 @@ parseArgs() { paths+=("$_path") ;; (--sane-sandbox-add-pwd) - _path="$(pwd)" + _path="$PWD" paths+=("$_path") ;; (--sane-sandbox-profile) @@ -316,6 +406,9 @@ firejailExec() { ## BUBBLEWRAP BACKEND +bwrapUnshareNet=(--unshare-net) +bwrapUnsharePid=(--unshare-pid) + bwrapSetup() { debug "bwrapSetup: noop" } @@ -344,7 +437,6 @@ bwrapIngestProfile() { bwrapIngestCapability() { bwrapFlags+=("--cap-add" "cap_$1") } -# WIP bwrapExec() { # --unshare-all implies the following: @@ -363,6 +455,9 @@ bwrapExec() { ## 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. @@ -430,6 +525,8 @@ landlockExec() { # 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" } @@ -563,9 +660,6 @@ canonicalizePaths() { ### parse arguments, with consideration of any which may be injected via the environment parseArgsAndEnvironment() { - if [ -n "$SANE_SANDBOX_DEBUG" ]; then - enableDebug - fi if [ -n "$SANE_SANDBOX_DISABLE" ]; then isDisable=1 fi diff --git a/modules/programs/sane-sandboxed.nix b/modules/programs/sane-sandboxed.nix index 39478db3..973ced77 100644 --- a/modules/programs/sane-sandboxed.nix +++ b/modules/programs/sane-sandboxed.nix @@ -1,9 +1,9 @@ { lib, stdenv +, bash , bubblewrap , firejail , landlock-sandboxer , libcap -, runtimeShell , substituteAll , profileDir ? "/share/sane-sandboxed/profiles" }: @@ -11,7 +11,7 @@ let sane-sandboxed = substituteAll { src = ./sane-sandboxed; - inherit bubblewrap firejail libcap runtimeShell; + inherit bash bubblewrap firejail libcap; landlockSandboxer = landlock-sandboxer; firejailProfileDirs = "/run/current-system/sw/etc/firejail /etc/firejail ${firejail}/etc/firejail"; };