sane-sandboxed: introduce a symlink cache to reduce readlink
calls even more
it's all a bit silly. i still do a bunch of -L tests: i just avoid the costly readlink fork :|
This commit is contained in:
parent
11ddce043d
commit
660ba94c7c
|
@ -43,6 +43,7 @@ let
|
|||
makeProfile = pkgs.callPackage ./make-sandbox-profile.nix { };
|
||||
makeSandboxed = pkgs.callPackage ./make-sandboxed.nix { sane-sandboxed = config.sane.programs.sane-sandboxed.package; };
|
||||
|
||||
# TODO: much of this can be simplified now that the sandbox helper uses a symlink cache
|
||||
# removeStorePaths: [ str ] -> [ str ], but remove store paths, because nix evals aren't allowed to contain any (for purity reasons?)
|
||||
removeStorePaths = paths: lib.filter (p: !(lib.hasPrefix "/nix/store" p)) paths;
|
||||
|
||||
|
@ -54,6 +55,14 @@ let
|
|||
expandSymlinksOnce = paths: lib.unique (paths ++ removeStorePaths (makeCanonical (derefSymlinks paths)));
|
||||
expandSymlinks = paths: lib.converge expandSymlinksOnce paths;
|
||||
|
||||
# given some paths, walk all of these and keep only the paths/ancestors which are symlinks
|
||||
keepOnlySymlinks = paths: lib.filter
|
||||
(p: ((config.sane.fs."${p}" or {}).symlink or null) != null)
|
||||
(lib.concatMap (p: path-lib.walk "/" p) paths)
|
||||
;
|
||||
# symlinkToAttrs: [ str ] -> Attrs such that `attrs."${symlink}" = symlinkTarget`.
|
||||
symlinksToAttrs = paths: lib.genAttrs paths (p: config.sane.fs."${p}".symlink.target);
|
||||
|
||||
vpn = lib.findSingle (v: v.default) null null (builtins.attrValues config.sane.vpn);
|
||||
|
||||
sandboxProfilesFor = userName: let
|
||||
|
@ -101,7 +110,35 @@ let
|
|||
vpn.dns
|
||||
else
|
||||
null;
|
||||
allowedPaths = expandSymlinks allowedPaths;
|
||||
inherit allowedPaths;
|
||||
|
||||
symlinkCache = {
|
||||
"/bin/sh" = config.environment.binsh;
|
||||
"${builtins.unsafeDiscardStringContext config.environment.binsh}" = "bash";
|
||||
"/usr/bin/env" = config.environment.usrbinenv;
|
||||
"${builtins.unsafeDiscardStringContext config.environment.usrbinenv}" = "coreutils";
|
||||
# "/run/current-system" = "${config.system.build.toplevel}";
|
||||
} // lib.optionalAttrs config.hardware.opengl.enable {
|
||||
"/run/opengl-driver" = let
|
||||
gl = config.hardware.opengl;
|
||||
# from: <repo:nixos/nixpkgs:nixos/modules/hardware/opengl.nix>
|
||||
package = pkgs.buildEnv {
|
||||
name = "opengl-drivers";
|
||||
paths = [ gl.package ] ++ gl.extraPackages;
|
||||
};
|
||||
in "${package}";
|
||||
} // lib.optionalAttrs (config.hardware.opengl.enable && config.hardware.opengl.driSupport32Bit) {
|
||||
"/run/opengl-driver-32" = let
|
||||
gl = config.hardware.opengl;
|
||||
# from: <repo:nixos/nixpkgs:nixos/modules/hardware/opengl.nix>
|
||||
package = pkgs.buildEnv {
|
||||
name = "opengl-drivers-32bit";
|
||||
paths = [ gl.package32 ] ++ gl.extraPackages32;
|
||||
};
|
||||
in "${package}";
|
||||
} // (
|
||||
symlinksToAttrs (keepOnlySymlinks (expandSymlinks allowedPaths))
|
||||
);
|
||||
};
|
||||
defaultProfile = sandboxProfilesFor config.sane.defaultUser;
|
||||
makeSandboxedArgs = {
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
{ pkgName
|
||||
, method
|
||||
, allowedPaths ? []
|
||||
, symlinkCache ? {}
|
||||
, autodetectCliPaths ? false
|
||||
, capabilities ? []
|
||||
, dns ? null
|
||||
|
@ -18,6 +19,13 @@ let
|
|||
];
|
||||
allowPaths = paths: lib.flatten (builtins.map allowPath paths);
|
||||
|
||||
cacheLink = from: to: [
|
||||
"--sane-sandbox-cache-symlink"
|
||||
from
|
||||
to
|
||||
];
|
||||
cacheLinks = links: lib.flatten (lib.mapAttrsToList cacheLink links);
|
||||
|
||||
capabilityFlags = lib.flatten (builtins.map (c: [ "--sane-sandbox-cap" c ]) capabilities);
|
||||
|
||||
netItems = lib.optionals (netDev != null) [
|
||||
|
@ -38,6 +46,7 @@ let
|
|||
++ capabilityFlags
|
||||
++ lib.optionals (autodetectCliPaths != null) [ "--sane-sandbox-autodetect" autodetectCliPaths ]
|
||||
++ lib.optionals whitelistPwd [ "--sane-sandbox-add-pwd" ]
|
||||
++ cacheLinks symlinkCache
|
||||
++ extraConfig;
|
||||
|
||||
in
|
||||
|
|
|
@ -37,6 +37,9 @@ profileDirs=()
|
|||
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=
|
||||
# linkCache: associative array mapping canonical symlinks to canonical targets
|
||||
# used to speed up `readlink` operations
|
||||
declare -A linkCache
|
||||
|
||||
### values derived directly from $argv
|
||||
# cliArgs: the actual command we'll be running inside the sandbox
|
||||
|
@ -120,6 +123,9 @@ usage() {
|
|||
echo ' shorthand for `--sane-sandbox-path $PWD`'
|
||||
echo ' --sane-sandbox-profile <profile>'
|
||||
echo ' --sane-sandbox-profile-dir <dir>'
|
||||
echo ' --sane-sandbox-cache-symlink <from> <to>'
|
||||
echo ' assume that <from> is a symlink to <to>'
|
||||
echo ' performance optimization to avoid spawning a readlink subshell'
|
||||
echo
|
||||
echo 'the following environment variables are also considered and propagated to children:'
|
||||
echo ' SANE_SANDBOX_DISABLE=1'
|
||||
|
@ -148,6 +154,30 @@ relativeToPwd() {
|
|||
fi
|
||||
}
|
||||
|
||||
# `splitHead <headVar> <tailVar> <input>`: write the top-level directory to `headVar` and set `tailVar` to the remaining path.
|
||||
# input is assumed to be a full path.
|
||||
# both outputs inherit the leading slash (except if the path has only one item, in which case `tailVar=`.
|
||||
# example: splitHead myHead myTail /path/to/thing
|
||||
# myHead=/path
|
||||
# myTail=/to/thing
|
||||
# example: splitHead myHead myTail /top
|
||||
# myHead=/top
|
||||
# myTail=
|
||||
splitHead() {
|
||||
local outHead="$1"
|
||||
local outTail="$2"
|
||||
local path="$3"
|
||||
|
||||
# chomp leading `/`
|
||||
path="${path:1}"
|
||||
local leadingComp="${path%%/*}"
|
||||
local compLen="${#leadingComp}"
|
||||
local tail="${path:$compLen}"
|
||||
|
||||
declare -g "$outHead"="/$leadingComp"
|
||||
declare -g "$outTail"="$tail"
|
||||
}
|
||||
|
||||
# `normPath outVar "$path"`
|
||||
# remove duplicate //, reduce '.' and '..' (naively).
|
||||
# expects a full path as input
|
||||
|
@ -155,26 +185,18 @@ relativeToPwd() {
|
|||
# does not resolve symlinks, nor check for existence of any component of the path.
|
||||
normPath() {
|
||||
local npOut="$1"
|
||||
local npUnparsed="$2"
|
||||
_npUnparsed="$2"
|
||||
local 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
|
||||
while [ -n "$_npUnparsed" ]; do
|
||||
splitHead _npThisComp _npUnparsed "$_npUnparsed"
|
||||
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
|
||||
elif [ "$_npThisComp" != "/." ] && [ "$_npThisComp" != "/" ] && [ "$_npThisComp" != "" ]; then
|
||||
# normal, non-empty path component => append it
|
||||
npComps+=("$npThisComp")
|
||||
npComps+=("$_npThisComp")
|
||||
fi
|
||||
done
|
||||
|
||||
|
@ -184,7 +206,7 @@ normPath() {
|
|||
else
|
||||
local npJoined=
|
||||
for npComp in "${npComps[@]}"; do
|
||||
npJoined="$npJoined/$npComp"
|
||||
npJoined="$npJoined$npComp"
|
||||
done
|
||||
declare -g "$npOut"="$npJoined"
|
||||
fi
|
||||
|
@ -252,15 +274,44 @@ contains() {
|
|||
return 1
|
||||
}
|
||||
|
||||
# `derefOnce outvar path`: assume `path` is a symlink and set `outvar` to its target.
|
||||
# the result is always an absolute path (relative symlinks are transformed).
|
||||
# `derefOnce outVar path`: walks from `/` to `path` and derefs the first symlink it encounters.
|
||||
# the dereferenced equivalent of `path` is written to `outVar`.
|
||||
# the dereferenced path may yet contain more unresolved symlinks.
|
||||
# if no links are encountered, then `outVar` is set empty.
|
||||
derefOnce() {
|
||||
local outVar="$1"
|
||||
local source="$2"
|
||||
local target="$(readlink "$source")"
|
||||
local flag="$3"
|
||||
local target=
|
||||
|
||||
local walked=
|
||||
_unwalked="$source"
|
||||
while [ -n "$_unwalked" ]; do
|
||||
splitHead _head _unwalked "$_unwalked"
|
||||
walked="$walked$_head"
|
||||
local linkTarget="${linkCache["$walked"]}"
|
||||
if [ -z "$linkTarget" ] && [ -L "$walked" ]; then
|
||||
# path is a link, but not in the cache
|
||||
linkTarget="$(readlink "$walked")"
|
||||
# insert it into the cache, in case we traverse it again
|
||||
linkCache["$walked"]="$linkTarget"
|
||||
fi
|
||||
if [ -n "$linkTarget" ]; then
|
||||
target="$linkTarget$_unwalked"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# make absolute
|
||||
if [ "${target:0:1}" != "/" ]; then
|
||||
target="$(dirname "$source")/$target"
|
||||
if [ "$flag" != '--no-canon' ] && [ -n "$target" ]; then
|
||||
if [ "${target:0:1}" != "/" ]; then
|
||||
# `walked` is a relative link.
|
||||
# then, the link is relative to the parent directory of `walked`
|
||||
target="$walked/../$target"
|
||||
fi
|
||||
# canonicalize
|
||||
normPath _normTarget "$target"
|
||||
target="$_normTarget"
|
||||
fi
|
||||
declare -g "$outVar"="$target"
|
||||
}
|
||||
|
@ -490,6 +541,14 @@ parseArgs() {
|
|||
shift
|
||||
profileDirs=("$dir" "${profileDirs[@]}")
|
||||
;;
|
||||
(--sane-sandbox-cache-symlink)
|
||||
local from="$1"
|
||||
shift
|
||||
local to="$1"
|
||||
shift
|
||||
relativeToPwd _absFrom "$from"
|
||||
linkCache["$_absFrom"]="$to"
|
||||
;;
|
||||
(*)
|
||||
parseArgsExtra+=("$arg")
|
||||
;;
|
||||
|
@ -579,8 +638,10 @@ bwrapIngestPath() {
|
|||
# or maybe configure remote mounts to somehow never hang.
|
||||
# test -r "$1" && bwrapFlags+=("--dev-bind-try" "$1" "$1")
|
||||
if [ -L "$1" ]; then
|
||||
local target="$(readlink "$1")"
|
||||
bwrapFlags+=("--symlink" "$target" "$1")
|
||||
# N.B.: test specifically whether this path is a link, not whether it's a non-symlink under a symlink'd dir.
|
||||
# this way, the filetype of this path is *always* the same both inside and outside the sandbox.
|
||||
derefOnce _target "$1" '--no-canon'
|
||||
bwrapFlags+=("--symlink" "$_target" "$1")
|
||||
else
|
||||
bwrapFlags+=("--dev-bind-try" "$1" "$1")
|
||||
fi
|
||||
|
@ -809,19 +870,20 @@ maybeAutodetectPaths() {
|
|||
# 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.
|
||||
local canonPaths=()
|
||||
for path in "${paths[@]}"; do
|
||||
normPath _canonPath "$path"
|
||||
paths+=("$_canonPath")
|
||||
canonPaths+=("$_canonPath")
|
||||
done
|
||||
paths=("${canonPaths[@]}")
|
||||
}
|
||||
expandLink() {
|
||||
if [ -L "$1" ]; then
|
||||
derefOnce _linkTarget "$1"
|
||||
derefOnce _linkTarget "$1"
|
||||
if [ -n "$_linkTarget" ]; then
|
||||
# add + expand the symlink further, but take care to avoid infinite recursion
|
||||
normPath _canonTarget "$_linkTarget"
|
||||
if ! contains "$_canonTarget" "${paths[@]}"; then
|
||||
paths+=("$_canonTarget")
|
||||
expandLink "$_canonTarget"
|
||||
if ! contains "$_linkTarget" "${paths[@]}"; then
|
||||
paths+=("$_linkTarget")
|
||||
expandLink "$_linkTarget"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
@ -833,6 +895,7 @@ expandLinks() {
|
|||
}
|
||||
removeSubpaths() {
|
||||
# remove subpaths, but the result might include duplicates.
|
||||
# TODO: make this not be O(n^2)!
|
||||
local toplevelPaths=()
|
||||
for path in "${paths[@]}"; do
|
||||
local isSubpath=
|
||||
|
|
Loading…
Reference in New Issue
Block a user