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:
parent
67395bdcd3
commit
f10f1ee7b1
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue
Block a user