modules/programs: sane-sandboxed: optimize "normPath" to not invoke subshells

each subshell causes like 5ms just on my laptop, which really adds up.
this implementation still forks internally, but doesn't exec.
runtime decreases from 150ms -> 90ms for
`time librewolf --sane-sandbox-replace-cli true`
This commit is contained in:
Colin 2024-02-18 12:07:19 +00:00
parent 67395bdcd3
commit f10f1ee7b1
2 changed files with 153 additions and 59 deletions

View File

@ -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: <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=
### 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 <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%%/*}"
_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 <bin-name> </path/to/default>` => print the full path to `<bin-name>` if it's on PATH, else print `</path/to/default>`
locate() {
command -v "$1" || echo "$2"
}
# convert e.g. `file:///Local%20Users/foo.mp3` to `file:///Local Users/foo.mp3`
urldecode() {
# source: <https://stackoverflow.com/q/6250698>
: "${*//+/ }"
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: <https://stackoverflow.com/q/6250698>
: "${*//+/ }"
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

View File

@ -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";
};