From 864e75afce494adee92c1856ac5852a22998763d Mon Sep 17 00:00:00 2001 From: Colin Date: Tue, 29 Oct 2024 05:59:01 +0000 Subject: [PATCH] sanebox: purge --- hosts/common/programs/assorted.nix | 2 +- hosts/common/programs/default.nix | 1 - hosts/common/programs/mmcli.nix | 1 - hosts/common/programs/sane-scripts.nix | 3 - hosts/common/programs/sanebox.nix | 32 - hosts/common/programs/sway/config | 10 - modules/persist/stores/private/default.nix | 2 +- modules/programs/default.nix | 23 +- modules/programs/make-sandbox-args.nix | 17 +- modules/programs/make-sandboxed.nix | 67 +- modules/services/clightning.nix | 2 +- modules/vpn.nix | 2 +- pkgs/by-name/sanebox/package.nix | 67 -- pkgs/by-name/sanebox/sanebox | 1217 -------------------- scripts/check-backups | 2 +- 15 files changed, 35 insertions(+), 1413 deletions(-) delete mode 100644 hosts/common/programs/sanebox.nix delete mode 100644 pkgs/by-name/sanebox/package.nix delete mode 100755 pkgs/by-name/sanebox/sanebox diff --git a/hosts/common/programs/assorted.nix b/hosts/common/programs/assorted.nix index c900999fe..917abbfb6 100644 --- a/hosts/common/programs/assorted.nix +++ b/hosts/common/programs/assorted.nix @@ -625,7 +625,7 @@ in gnome-calendar.sandbox.whitelistDbus = [ "user" ]; # gnome-disks - # XXX(2024-09-02): fails to show any disks even when run as `SANEBOX_DISABLE=1 sudo -E gnome-disks`. + # XXX(2024-09-02): fails to show any disks even when run as `BUNPEN_DISABLE=1 sudo -E gnome-disks`. gnome-disk-utility.buildCost = 1; gnome-disk-utility.sandbox.whitelistDbus = [ "system" ]; gnome-disk-utility.sandbox.whitelistWayland = true; diff --git a/hosts/common/programs/default.nix b/hosts/common/programs/default.nix index e8e24e020..9161e6a20 100644 --- a/hosts/common/programs/default.nix +++ b/hosts/common/programs/default.nix @@ -157,7 +157,6 @@ ./sane-secrets-unlock.nix ./sane-sysload.nix ./sane-theme.nix - ./sanebox.nix ./satellite.nix ./schlock.nix ./seatd.nix diff --git a/hosts/common/programs/mmcli.nix b/hosts/common/programs/mmcli.nix index 6d2a5b3a1..d59376109 100644 --- a/hosts/common/programs/mmcli.nix +++ b/hosts/common/programs/mmcli.nix @@ -22,7 +22,6 @@ mainProgram = "mmcli"; }; }); - sandbox.method = "bwrap"; #< TODO: get it working with bunpen sandbox.whitelistDbus = [ "system" diff --git a/hosts/common/programs/sane-scripts.nix b/hosts/common/programs/sane-scripts.nix index ce296be6d..a7d7ff3ba 100644 --- a/hosts/common/programs/sane-scripts.nix +++ b/hosts/common/programs/sane-scripts.nix @@ -74,7 +74,6 @@ in "sane-scripts.clone".sandbox.method = null; #< TODO: sandbox "sane-scripts.dev-cargo-loop".sandbox = { - method = "bwrap"; net = "clearnet"; whitelistPwd = true; extraPaths = [ @@ -106,7 +105,6 @@ in # because `mount` is a cap_sys_admin syscall, there's no great way to mount stuff dynamically like this. # instead, we put ourselves in a mount namespace, do the mount, and drop into a shell or run a command. # this actually has an OK side effect, that the mount isn't shared, and so we avoid contention/interleaving that would cause the ending `umount` to fail. - method = "bwrap"; # cap_sys_admin is needed to mount stuff. # ordinarily /run/wrappers/bin/mount would do that via setuid, but sandboxes have no_new_privs by default. capabilities = [ "sys_admin" ]; @@ -168,7 +166,6 @@ in }; "sane-scripts.stop-all-servo".sandbox = { - method = "bwrap"; whitelistSystemctl = true; }; diff --git a/hosts/common/programs/sanebox.nix b/hosts/common/programs/sanebox.nix deleted file mode 100644 index ef332103f..000000000 --- a/hosts/common/programs/sanebox.nix +++ /dev/null @@ -1,32 +0,0 @@ -{ config, lib, pkgs, ... }: -let - cfg = config.sane.programs; -in -{ - sane.programs.sanebox = { - packageUnwrapped = (pkgs.sanebox.override { - bubblewrap = cfg.bubblewrap.package; - iproute2 = cfg.iproute2.package; - iptables = cfg.iptables.package; - libcap = cfg.capsh.package; #< the sandboxer doesn't use any other libcap binaries - passt = cfg.passt.package; - landlock-sandboxer = cfg.landlock-sandboxer.package; - # landlock-sandboxer = pkgs.landlock-sandboxer.override { - # # not strictly necessary (landlock ABI is versioned), however when sandboxer version != kernel version, - # # the sandboxer may nag about one or the other wanting to be updated. - # linux = config.boot.kernelPackages.kernel; - # }; - }).overrideAttrs (base: { - # create a directory which holds just the `sanebox` so that we - # can add sanebox as a dependency to binaries via `PATH=/run/current-system/libexec/sanebox` without forcing rebuild every time sanebox changes - postInstall = '' - mkdir -p $out/libexec/sanebox - ln -s $out/bin/sanebox $out/libexec/sanebox/sanebox - ''; - }); - - sandbox.enable = false; - }; - - environment.pathsToLink = lib.mkIf cfg.sanebox.enabled [ "/libexec/sanebox" ]; -} diff --git a/hosts/common/programs/sway/config b/hosts/common/programs/sway/config index 8680f0d83..342b4a76d 100644 --- a/hosts/common/programs/sway/config +++ b/hosts/common/programs/sway/config @@ -267,16 +267,6 @@ exec --no-startup-id mv $XDG_RUNTIME_DIR/$WAYLAND_DISPLAY.lock $XDG_RUNTIME_DIR/ # the double-$ means to set the variable at *runtime*, not at "compile-time" (so that it doesn't impact the line immediately above us set $$WAYLAND_DISPLAY "$(echo $DESIRED_WAYLAND_DISPLAY)" - -# manually export PATH here, since all my user services need that, and sanebox implementation depends on it. -# also, manually export XDG_DATA_DIRS. glib fails in weird ways (e.g. thinks everything is application/x-octet-stream mime type) without it. -# for more, see: -# -# XXX: dbus-update-activation-environment --systemd is ASYNCHRONOUS. it returns before the systemd environment is actually updated. -# hence, call `systemctl import-environment` ourselves. i could probably remove the dbus stuff and be safe, but at least for now it's an OK backup. -# exec --no-startup-id systemctl --user import-environment PATH XDG_DATA_DIRS DISPLAY WAYLAND_DISPLAY SWAYSOCK XDG_CURRENT_DESKTOP -# exec --no-startup-id dbus-update-activation-environment --systemd PATH XDG_DATA_DIRS DISPLAY WAYLAND_DISPLAY SWAYSOCK XDG_CURRENT_DESKTOP - # previously: `include /etc/sway/config.d/*` was needed for xdg-desktop-portal-* to work. # stock nixos `programs.sway` would setup /etc/sway/config.d with additional variables to import to the dbus env. # but now i'm doing that manually: diff --git a/modules/persist/stores/private/default.nix b/modules/persist/stores/private/default.nix index bc55d83cc..e23a70239 100644 --- a/modules/persist/stores/private/default.nix +++ b/modules/persist/stores/private/default.nix @@ -65,7 +65,7 @@ lib.mkIf config.sane.persist.enable sandbox.tryKeepUsers = true; sandbox.keepPids = true; sandbox.extraPaths = [ - "/run/gocryptfs/private.key" #< TODO: teach sanebox about `-o FLAG1=VALUE1,FLAG2=VALUE2` style of argument passing, then use `existing` autodetect, and remove this + "/run/gocryptfs/private.key" #< TODO: teach sandbox about `-o FLAG1=VALUE1,FLAG2=VALUE2` style of argument passing, then use `existing` autodetect, and remove this ]; suggestedPrograms = [ "gocryptfs" ]; }; diff --git a/modules/programs/default.nix b/modules/programs/default.nix index 5c7124cc2..1c0dc00fa 100644 --- a/modules/programs/default.nix +++ b/modules/programs/default.nix @@ -100,13 +100,7 @@ let allowedRunPaths = lib.unique allowedRunPaths; }; in - (makeSandboxed.override { - sanebox = if sandbox.method == "bunpen" then - pkgs.bunpen - else - pkgs.sanebox - ; - }) { + makeSandboxed { inherit pkgName package; inherit (sandbox) embedSandboxer @@ -293,7 +287,7 @@ let ''; }; sandbox.method = mkOption { - type = types.nullOr (types.enum [ "bunpen" "bwrap" "capshonly" "pastaonly" "landlock" ]); + type = types.nullOr (types.enum [ "bunpen" ]); default = "bunpen"; description = '' how/whether to sandbox all binaries in the package. @@ -495,8 +489,7 @@ let description = '' extra arguments to pass to the sandbox wrapper. example: [ - "--sanebox-dns" - "1.1.1.1" + "--bunpen-keep-pid" ] ''; }; @@ -526,15 +519,7 @@ let wrapPkg name config config.packageUnwrapped ; suggestedPrograms = lib.mkIf saneCfg.sandbox.enable ( - lib.optionals (config.sandbox.method == "bwrap") [ - "sanebox" "bubblewrap" "passt" "iproute2" "iptables" - ] ++ lib.optionals (config.sandbox.method == "landlock") [ - "sanebox" "landlock-sandboxer" "capsh" - ] ++ lib.optionals (config.sandbox.method == "pastaonly") [ - "sanebox" "passt" "iproute2" "iptables" "capsh" - ] ++ lib.optionals (config.sandbox.method == "capshonly") [ - "sanebox" "capsh" - ] ++ lib.optionals (config.sandbox.method == "bunpen") [ + lib.optionals (config.sandbox.method == "bunpen") [ "bunpen" ] ); diff --git a/modules/programs/make-sandbox-args.nix b/modules/programs/make-sandbox-args.nix index f9f5426d2..3a2c7c753 100644 --- a/modules/programs/make-sandbox-args.nix +++ b/modules/programs/make-sandbox-args.nix @@ -1,20 +1,5 @@ { lib }: let - saneboxGenerators = { - autodetectCliPaths = style: [ "--sanebox-autodetect" style ]; - capability = cap: [ "--sanebox-cap" cap ]; - dns = addr: [ "--sanebox-dns" addr ]; - keepIpc = [ "--sanebox-keep-namespace" "ipc" ]; - keepPids = [ "--sanebox-keep-namespace" "pid" ]; - tryKeepUsers = [ "--sanebox-keep-namespace" "user" ]; - method = method: [ "--sanebox-method" method ]; - netDev = netDev: [ "--sanebox-net-dev" netDev ]; - netGateway = netGateway: [ "--sanebox-net-gateway" netGateway ]; - path.unqualified = p: [ "--sanebox-path" p ]; - path.home = p: [ "--sanebox-home-path" p ]; - path.run = p: [ "--sanebox-run-path" p ]; - whitelistPwd = [ "--sanebox-add-pwd" ]; - }; bunpenGenerators = { autodetectCliPaths = style: [ "--bunpen-autodetect" style ]; capability = cap: [ "--bunpen-cap" cap ]; @@ -72,7 +57,7 @@ let gen = if method == "bunpen" then bunpenGenerators else - saneboxGenerators + bunpenGenerators ; allowPaths = flavor: paths: lib.flatten (builtins.map gen.path."${flavor}" paths); diff --git a/modules/programs/make-sandboxed.nix b/modules/programs/make-sandboxed.nix index aee796767..eddcba7bc 100644 --- a/modules/programs/make-sandboxed.nix +++ b/modules/programs/make-sandboxed.nix @@ -2,6 +2,7 @@ lib, stdenv, buildPackages, + bunpen, file, gnugrep, gnused, @@ -9,15 +10,14 @@ makeBinaryWrapper, makeShellWrapper, runCommandLocal, - sanebox, writeShellScriptBin, xorg, }: let - fakeSaneSandboxed = writeShellScriptBin sanebox.meta.mainProgram '' - # behave like the real sanebox with SANEBOX_DISABLE=1, - # but in a manner which avoids taking a dependency on the real sanebox. - # the primary use for this is to allow a package's `check` phase to work even when sanebox isn't available. + fakeSaneSandboxed = writeShellScriptBin bunpen.meta.mainProgram '' + # behave like the real bunpen with BUNPEN_DISABLE=1, + # but in a manner which avoids taking a dependency on the real bunpen. + # the primary use for this is to allow a package's `check` phase to work even when bunpen isn't available (which allows for faster iteration). _origArgs=($@) # throw away all arguments until we find the path to the binary which is being sandboxed @@ -25,12 +25,12 @@ let shift done if [ "$#" -eq 0 ]; then - >&2 echo "${sanebox.meta.mainProgram} (mock): failed to parse args: ''${_origArgs[*]}" + >&2 echo "${bunpen.meta.mainProgram} (mock): failed to parse args: ''${_origArgs[*]}" exit 1 fi - if [ -z "$SANEBOX_DISABLE" ]; then - >&2 echo "${sanebox.meta.mainProgram} (mock): not called with SANEBOX_DISABLE=1; unsure how to sandbox: ''${_origArgs[*]}" + if [ -z "$BUNPEN_DISABLE" ]; then + >&2 echo "${bunpen.meta.mainProgram} (mock): not called with BUNPEN_DISABLE=1; unsure how to sandbox: ''${_origArgs[*]}" exit 1 fi # assume that every argument after the binary name is an argument for the binary and not for the sandboxer. @@ -45,14 +45,14 @@ let # take an existing package, which may have a `bin/` folder as well as `share/` etc, # and patch the `bin/` items in-place - sandboxBinariesInPlace = sanebox': extraSandboxArgs: pkgName: pkg: pkg.overrideAttrs (unwrapped: { + sandboxBinariesInPlace = bunpen': extraSandboxArgs: pkgName: pkg: pkg.overrideAttrs (unwrapped: { # disable the sandbox and inject a minimal fake sandboxer which understands that flag, # in order to support packages which invoke sandboxed apps in their check phase. # note that it's not just for packages which invoke their *own* binaries in check phase, # but also packages which invoke OTHER PACKAGES' sandboxed binaries. # hence, put the fake sandbox in nativeBuildInputs instead of nativeCheckInputs. env = (unwrapped.env or {}) // { - SANEBOX_DISABLE = 1; + BUNPEN_DISABLE = 1; }; outputs = unwrapped.outputs or [ "out" ]; nativeBuildInputs = [ @@ -64,14 +64,14 @@ let makeShellWrapper ] ++ (unwrapped.nativeBuildInputs or []); disallowedReferences = (unwrapped.disallowedReferences or []) ++ [ - # the fake sandbox gates itself behind SANEBOX_DISABLE, so if it did end up deployed + # the fake sandbox gates itself behind BUNPEN_DISABLE, so if it did end up deployed # then it wouldn't permit anything not already permitted. but it would still be annoying. fakeSaneSandboxed ]; postFixup = (unwrapped.postFixup or "") + '' assertExecutable() { - : # my programs refer to sanebox by name, not path, which triggers an over-eager assertion in nixpkgs (so, mask that) + : # my programs refer to bunpen by name, not path, which triggers an over-eager assertion in nixpkgs (so, mask that) } makeDocumentedCWrapper() { # this is identical to nixpkgs' implementation, only replace execv with execvp, the latter which looks for the executable on PATH. @@ -101,27 +101,11 @@ let mv "$_dir/$_name" "$_dir/.sandboxed/" fi - if [ -n "${sanebox.interpreter or ""}" ]; then - # N.B.: double `escapeShellArg`: once for the shell wrapper, and again for runtime because the shell wrapper doesn't escape. - # spotcheck this by seeing if animatch (requires a path "Holy Pangolin") works - makeShellWrapper ${sanebox'} "$_dir/$_name" \ - --suffix PATH : /run/current-system/sw/libexec/${sanebox.pname} \ - --inherit-argv0 \ - ${lib.escapeShellArgs (lib.flatten (builtins.map (f: [ "--add-flags" (lib.escapeShellArg f) ]) extraSandboxArgs))} \ - --add-flags "$_dir/.sandboxed/$_name" - - # `exec`ing a script with an interpreter will smash $0. instead, source it to preserve $0: - # - - substituteInPlace "$_dir/$_name" \ - --replace-fail 'exec -a "$0" ' 'source ' - else - # we can use a binary shell wrapper since the wrapper's environment is capable of forwarding argv[0]. - makeBinaryWrapper ${sanebox'} "$_dir/$_name" \ - --suffix PATH : /run/current-system/sw/libexec/${sanebox.pname} \ - --inherit-argv0 \ - ${lib.escapeShellArgs (lib.flatten (builtins.map (f: [ "--add-flags" f ]) extraSandboxArgs))} \ - --add-flags "$_dir/.sandboxed/$_name" - fi + makeBinaryWrapper ${bunpen'} "$_dir/$_name" \ + --suffix PATH : /run/current-system/sw/libexec/${bunpen.pname} \ + --inherit-argv0 \ + ${lib.escapeShellArgs (lib.flatten (builtins.map (f: [ "--add-flags" f ]) extraSandboxArgs))} \ + --add-flags "$_dir/.sandboxed/$_name" } derefWhileInSameOutput() { @@ -415,7 +399,7 @@ let }; passthru = (prevAttrs.passthru or {}) // extraPassthru // { checkSandboxed = runCommandLocal "${pkgName}-check-sandboxed" { - nativeBuildInputs = [ file gnugrep sanebox ]; + nativeBuildInputs = [ bunpen file gnugrep ]; buildInputs = builtins.map (out: finalAttrs.finalPackage."${out}") (finalAttrs.outputs or [ "out" ]); } '' set -e @@ -426,7 +410,7 @@ let local dir="$1" local binname="$2" echo "checking if $dir/$binname is sandboxed" - echo " sandboxer is ${sanebox.name}" + echo " sandboxer is ${bunpen.name}" echo " PATH=$PATH" # XXX: call by full path because some binaries (e.g. util-linux) would otherwise # be shadowed by things the nix builder implicitly puts on PATH. @@ -434,8 +418,7 @@ let # if the file doesn't have an interpreter, assume it's directly invokable by qemu (hence, the intentional lack of quotes around `interpreter`) set -x local realbin="$(realpath $dir/$binname)" - local interpreter=$(file "$realbin" | grep --only-matching "a /nix/.* script" | cut -d" " -f2 || echo "") - echo 'echo "printing for test"' | ${stdenv.hostPlatform.emulator buildPackages} $interpreter "$dir/$binname" --sanebox-net-dev all --sanebox-dns default --sanebox-net-gateway default --sanebox-replace-cli /bin/sh --bunpen-drop-shell \ + echo 'echo "printing for test"' | ${stdenv.hostPlatform.emulator buildPackages} "$dir/$binname" --bunpen-drop-shell \ | grep "printing for test" _numExec=$(( $_numExec + 1 )) } @@ -482,12 +465,12 @@ let make-sandboxed = { pkgName, package, wrapperType, embedSandboxer ? false, extraSandboxerArgs ? [], passthru ? {} }: let unsandboxed = package; - sanebox' = if embedSandboxer then + bunpen' = if embedSandboxer then # optionally hard-code the sandboxer. this forces rebuilds, but allows deep iteration w/o deploys. - lib.getExe sanebox + lib.getExe bunpen else #v prefer to load by bin name to reduce rebuilds - sanebox.meta.mainProgram + bunpen.meta.mainProgram ; # two ways i could wrap a package in a sandbox: @@ -498,14 +481,14 @@ let # regardless of which one is chosen here, all other options are exposed via `passthru`. sandboxedBy = { inplace = sandboxBinariesInPlace - sanebox' + bunpen' extraSandboxerArgs pkgName (makeHookable unsandboxed); wrappedDerivation = let sandboxedBin = sandboxBinariesInPlace - sanebox' + bunpen' extraSandboxerArgs pkgName (symlinkBinaries pkgName unsandboxed); diff --git a/modules/services/clightning.nix b/modules/services/clightning.nix index e56277ff7..341fa0454 100644 --- a/modules/services/clightning.nix +++ b/modules/services/clightning.nix @@ -175,7 +175,7 @@ in serviceConfig.RestrictSUIDSGID = true; serviceConfig.SystemCallArchitectures = "native"; - #VVV relaxed because it uses bwrap sandboxing (sanebox) + #VVV relaxed because my sandbox wrapper uses namespaces serviceConfig.RestrictNamespaces = false; serviceConfig.ProcSubset = "all"; serviceConfig.ProtectHostname = false; diff --git a/modules/vpn.nix b/modules/vpn.nix index 464af8586..38d16d19f 100644 --- a/modules/vpn.nix +++ b/modules/vpn.nix @@ -22,7 +22,7 @@ # 3. to apply a VPN to internet traffic selectively, just proxy an applications traffic into the VPN device # 3a. use a network namespace and a userspace TCP stack (e.g. pasta/slirp4netns). # 3b. attach the VPN device to a bridge device, then connect that to a network namespace by using a veth pair. -# 3c. juse use `sanebox`, which abstracts the above options. +# 3c. juse use `bunpen`, which abstracts the above options. { config, lib, sane-lib, ... }: let diff --git a/pkgs/by-name/sanebox/package.nix b/pkgs/by-name/sanebox/package.nix deleted file mode 100644 index e60287ee5..000000000 --- a/pkgs/by-name/sanebox/package.nix +++ /dev/null @@ -1,67 +0,0 @@ -{ lib, stdenv -, bash -, bubblewrap -, coreutils -, iproute2 -, iptables -, landlock-sandboxer -, libcap -, passt -, substituteAll -, profileDir ? "/share/sanebox/profiles" -}: -stdenv.mkDerivation { - pname = "sanebox"; - version = "0.1"; - - src = ./sanebox; - dontUnpack = true; - - buildInputs = [ - bash # for cross builds, to ensure #!/bin/sh is substituted - ]; - - buildPhase = '' - runHook preBuild - substitute $src sanebox \ - --replace-fail '@bwrap@' '${lib.getExe bubblewrap}' \ - --replace-fail '@capsh@' '${lib.getExe' libcap "capsh"}' \ - --replace-fail '@env@' '${lib.getExe' coreutils "env"}' \ - --replace-fail '@ip@' '${lib.getExe' iproute2 "ip"}' \ - --replace-fail '@iptables@' '${lib.getExe' iptables "iptables"}' \ - --replace-fail '@landlockSandboxer@' '${lib.getExe landlock-sandboxer}' \ - --replace-fail '@pasta@' '${lib.getExe' passt "pasta"}' \ - --replace-fail '@readlink@' '${lib.getExe' coreutils "readlink"}' \ - - runHook postBuild - ''; - - installPhase = '' - runHook preInstall - install -d "$out" - install -d "$out/bin" - install -m 755 sanebox $out/bin/sanebox - runHook postInstall - ''; - - passthru = { - interpreter = lib.getBin bash; - runtimeDeps = [ - bubblewrap - coreutils - landlock-sandboxer - libcap - passt - ]; - }; - - meta = { - description = '' - helper program to run some other program in a sandbox. - factoring this out allows: - 1. to abstract over the particular sandbox implementation (bwrap, landlock, ...). - 2. to modify sandbox settings without forcing a rebuild of the sandboxed package. - ''; - mainProgram = "sanebox"; - }; -} diff --git a/pkgs/by-name/sanebox/sanebox b/pkgs/by-name/sanebox/sanebox deleted file mode 100755 index 868a7d1ca..000000000 --- a/pkgs/by-name/sanebox/sanebox +++ /dev/null @@ -1,1217 +0,0 @@ -#!/bin/sh - -## BUILD-TIME SUBSTITUTIONS -### _FALLBACK: if `` isn't on PATH, then use this instead -BWRAP_FALLBACK='@bwrap@' -CAPSH_FALLBACK='@capsh@' -ENV_FALLBACK='@env@' -IP_FALLBACK='@ip@' -IPTABLES_FALLBACK='@iptables@' -LANDLOCK_SANDBOXER_FALLBACK='@landlockSandboxer@' -PASTA_FALLBACK='@pasta@' -READLINK_FALLBACK='@readlink@' - - -## EARLY DEBUG HOOKS - -isDebug= - -enableDebug() { - isDebug=1 - set -x -} - -debug() { - if [ -n "$isDebug" ]; then - printf "[debug] %s" "$1" >&2 - fi -} - -# if requested, enable debugging as early as possible -if [ -n "$SANEBOX_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 - -# 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= -# 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 -cliArgs=() -# type of sandbox to use -# - "bwrap" -# - "landlock" -# - "capshonly" -# - "pastaonly" -# - "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) -# - "existingDir" (directory only; no files) -# - "existingFile" (file only; no directories) -# - "parent" -# - "existingOrParent" -# - "existingDirOrParent" -# - "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" -# - "net" -# - "pid": if this process may wany to query /proc/$PID/... of parent/sibling processes. -# - "user": only applicable if running as root -# - "uts" -# - "all": as if all the above were specified -keepNamespace=() -# name of some network device to make available to the sandbox, if any. -# or "all" to keep all devices available -netDev= -# IPv4 address of the default gateway associated with the bridged network device (usually that's just the VPN's IP addr) -netGateway=default -# list of IP addresses to use for DNS servers inside the sandbox (not supported by all backends) -dns=() -argv0= -case "${0:-8}" in - ("sanebox"|"/sanebox") - ;; - (*) - argv0="$0" - ;; -esac - -# arguments to forward onto a specific backend (if that backend is active) -bwrapArgs=() -capshArgs=() -pastaArgs=() - -usage() { - echo 'sanebox: run a program inside a sandbox' - echo 'USAGE: sanebox [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 ' --sanebox-help' - echo ' show this message' - echo ' --sanebox-debug' - echo ' print debug messages to stderr' - echo ' --sanebox-replace-cli ' - echo ' invoke under the sandbox instead of any program previously listed' - echo ' also clears and earlier arguments intended for the program' - echo ' --sanebox-disable' - echo ' invoke the program directly, instead of inside a sandbox' - echo ' --sanebox-dry-run' - echo ' show what would be `exec`uted but do not perform any action' - echo ' --sanebox-method ' - echo ' use a specific sandboxer' - echo ' --sanebox-autodetect ' - echo ' add files which appear later as CLI arguments into the sandbox' - echo ' --sanebox-cap ' - echo ' allow the sandboxed program to use the provided linux capability (both inside and outside the sandbox)' - echo ' special cap "all" to preserve all capabilities possible' - echo ' --sanebox-bwrap-arg ' - echo ' --sanebox-capsh-arg ' - echo ' --sanebox-pasta-arg ' - echo ' --sanebox-net-dev |all' - echo ' --sanebox-net-gateway ' - echo ' --sanebox-dns |host' - echo ' --sanebox-keep-namespace ' - echo ' do not unshare the provided linux namespace' - echo ' --sanebox-path ' - echo ' allow access to the host within the sandbox' - echo ' path is interpreted relative to the working directory if not absolute' - echo ' --sanebox-home-path ' - echo ' allow access to the host , relative to HOME' - echo ' --sanebox-run-path ' - echo ' allow access to the host , relative to XDG_RUNTIME_DIR' - echo ' --sanebox-add-pwd' - echo ' shorthand for `--sanebox-path $PWD`' - echo - echo 'the following environment variables are also considered and propagated to children:' - echo ' SANEBOX_DISABLE=1' - echo ' equivalent to `--sanebox-disable`' - echo ' SANEBOX_DEBUG=1' - echo ' equivalent to `--sanebox-debug`, but activates earlier' - echo ' SANEBOX_PREPEND=...' - echo ' act as though the provided arg string appeared at the start of the CLI' - echo ' SANEBOX_APPEND=...' - echo ' act as though the provided arg string appeared at the end of the CLI' -} - - -## UTILITIES/BOILERPLATE - -# `relativeToPwd out-var path` -# if `path` is absolute, returns `path` -# otherwise, joins `path` onto `$PWD` -relativeToPwd() { - local outvar=$1 - local path=$2 - if [ "${path:0:1}" == "/" ]; then - declare -g "$outvar"="$path" - else - declare -g "$outvar"="$PWD/path" - fi -} - -# `splitHead `: 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 -# chomps trailing slashes. -# does not resolve symlinks, nor check for existence of any component of the path. -normPath() { - local outVar=$1 - local unparsed=$2 - local comps=() - while [ -n "$unparsed" ]; do - splitHead _npThisComp _npUnparsed "$unparsed" - unparsed=$_npUnparsed - local thisComp=$_npThisComp - - case $thisComp in - (/..) - # "go up" path component => delete the leaf dir (if any) - if [ ${#comps[@]} -ne 0 ]; then - unset comps[-1] - fi - ;; - (/. | / | "") ;; - (*) - # normal, non-empty path component => append it - comps+=("$thisComp") - ;; - esac - done - - # join the components - if [ ${#comps[@]} -eq 0 ]; then - declare -g "$outVar"="/" - else - local joined= - for comp in "${comps[@]}"; do - joined=$joined$comp - done - declare -g "$outVar"="$joined" - 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 ` => if `` is on PATH, then return that, else -locate() { - local outVar="$1" - local bin="$2" - local loc="$3" - # N.B.: `bin=$(command -v $bin)` would make more sense, but it forks. - for dir in ${PATH//:/ }; do - if [ -e "$dir/$bin" ]; then - loc="$dir/$bin" - break - fi - done - declare -g "$outVar"="$loc" -} - -# `urldecode outVar ` -# convert e.g. `file:///Local%20Users/foo.mp3` to `file:///Local Users/foo.mp3` -urldecode() { - local outVar=$1 - shift - - # source: - # replace each `+` with space - local i=${*//+/ } - # then replace each `%` with `\x` - # and have `echo` evaluate the escape sequences - local decoded=$(echo -e "${i//%/\\x}") - declare -g "$outVar"="$decoded" -} - -# `contains needle ${haystack[0]} ${haystack[1]} ...` -# evals `true` if the first argument is equal to any of the other args -contains() { - local needle=$1 - shift - for item in "$@"; do - if [ "$needle" = "$item" ]; then - return 0 - fi - done - return 1 -} - -# `readlinkOnce outVar path`: writes the link target to outVar -# or sets outVar="" if path isn't a link. -# unlink `derefOnce`, this only acts if the leaf of `path` is a symlink, -# not if it's an ordinary entry within a symlinked directory. -readlinkOnce() { - local outVar=$1 - local path=$2 - local linkTarget= - if [ -v "linkCache[$path]" ]; then - linkTarget=${linkCache[$path]} - elif [ -L "$path" ]; then - # path is a link, but not in the cache - locate _readlink "readlink" "$READLINK_FALLBACK" - linkTarget=$("$_readlink" "$path") - # insert it into the cache, in case we traverse it again - linkCache[$path]=$linkTarget - else - # remember for later that this path doesn't represent a link. - # empty target is used to indicate a non-symlink (i.e. ordinary file/directory). - # i think a symlink can *technically* point to "" (via `symlink` syscall), but `ln -s` doesn't allow it - linkCache[$path]= - fi - declare -g "$outVar"="$linkTarget" -} - -# `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= - - local walked= - local unwalked=$source - while [ -n "$unwalked" ]; do - splitHead _head _unwalked "$unwalked" - unwalked=$_unwalked - walked=$walked$_head - - readlinkOnce _linkTarget "$walked" - if [ -n "$_linkTarget" ]; then - target=$_linkTarget$unwalked - break - fi - done - - # make absolute - if [ -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" -} - - -## HELPERS - -# subroutine of `tryArgAsPath` for after the arg has been converted into a valid (but possibly not existing) path. -# adds an entry to `paths` and evals `true` on success; -# evals `false` if the path couldn't be added, for any reason. -tryPath() { - local path=$1 - local how=$2 - - case $how in - (existing) - # the caller wants to access either a file, or a directory (possibly a symlink to such a thing) - if [ -e "$path" ]; then - relativeToPwd _absPath "$path" - paths+=("$_absPath") - return 0 - fi - return 1 - ;; - (existingDir) - # the caller wants to access a directory, and explicitly *not* a file (though it could be a symlink *to a directory*) - if [ -d "$path" ]; then - relativeToPwd _absPath "$path" - paths+=("$_absPath") - return 0 - fi - return 1 - ;; - (existingFile) - # 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 - relativeToPwd _absPath "$path" - paths+=("$_absPath") - return 0 - fi - return 1 - ;; - (parent) - # the caller wants access to the entire directory containing this directory regardless of the file's existence. - parent _tryPathParent "$path" - tryPath "$_tryPathParent" "existing" - ;; - (existingOrParent) - # 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" - ;; - (existingDirOrParent) - # the caller wants access to the directory, or write access to the parent directory in case the path doesn't exist or is a file - tryPath "$path" "existingDir" || tryPath "$path" "parent" - ;; - (existingFileOrParent) - # 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" - ;; - esac -} - -# if the argument looks path-like, then add it to paths. -# this function ingests absolute, relative, or file:///-type URIs. -# but it converts any such path into an absolute path before adding it to paths. -tryArgAsPath() { - local arg=$1 - local how=$2 - # norecurseFlag is used internally by this function when it recurses - local norecurseFlag=$3 - local path= - case $arg in - (/*) - # absolute path - path=$arg - ;; - (file:///*) - # 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}" - path=$_path - ;; - (*) - # could be a CLI argument or a relative path - # want to handle: - # - `--file=$path` - # - `file=$path` - # - `$path` - if [ -z "$norecurseFlag" ]; then - local pathInFlag=${arg#*=} - if [ "$pathInFlag" != "$arg" ]; then - tryArgAsPath "$pathInFlag" "$how" --norecurse - # 0.01% chance this was a path which contained an equal sign and not a flag, but don't handle that for now: - return - fi - fi - - if [ "${arg:0:1}" = "-" ]; then - # 99% chance it's a CLI argument. if not, use `./-<...>` - return - fi - - # try it as a relative path - path=$PWD/$arg - ;; - esac - - 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 - local arg=$1 - shift - case $arg in - (--) - # rest of args are for the CLI, and not for us. - # consider two cases: - # - sanebox --sanebox-flag1 -- /nix/store/.../mpv --arg0 arg1 - # - sanebox /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 - ;; - (--sanebox-help) - usage - exit 0 - ;; - (--sanebox-debug) - enableDebug - ;; - (--sanebox-replace-cli) - # keep the sandbox flags, but clear any earlier CLI args. - # this lets the user do things like `mpv --sanebox-replace-cli sh` to enter a shell - # with the sandbox that `mpv` would see. - parseArgsExtra=() - argv0= - ;; - (--sanebox-disable) - isDisable=1 - ;; - (--sanebox-dry-run) - isDryRun=1 - ;; - (--sanebox-method) - method=$1 - shift - ;; - (--sanebox-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 - ;; - (--sanebox-cap) - # N.B.: these named temporary variables ensure that "set -x" causes $1 to be printed - local cap=$1 - shift - if [ "$cap" = all ]; then - # this tries to remain exhaustive, but new capabilities are occassionally added to the kernel: - # add anything here as it's found to be missing - capabilities+=( - audit_control audit_read audit_write block_suspend bpf checkpoint_restore - chown dac_override dac_read_search fowner fsetid ipc_lock - ipc_owner kill lease linux_immutable mac_admin mac_override - mknod net_admin net_bind_service net_broadcast net_raw perfmon - setfcap setgid setpcap setuid sys_admin sys_boot - sys_chroot sys_module sys_nice sys_pacct sys_ptrace sys_rawio - sys_resource sys_time sys_tty_config syslog wake_alarm - ) - else - capabilities+=("$cap") - fi - ;; - (--sanebox-bwrap-arg) - local bwrapArg=$1 - shift - bwrapArgs+=("$bwrapArg") - ;; - (--sanebox-capsh-arg) - local capshArg=$1 - shift - capshArgs+=("$capshArg") - ;; - (--sanebox-pasta-arg) - local pastaArg=$1 - shift - pastaArgs+=("$pastaArg") - ;; - (--sanebox-net-dev) - netDev=$1 - shift - ;; - (--sanebox-net-gateway) - netGateway=$1 - shift - ;; - (--sanebox-dns) - local dnsServer=$1 - shift - if [ "$dnsServer" = default ]; then - dns=() - else - dns+=("$dnsServer") - fi - ;; - (--sanebox-keep-namespace) - local namespace=$1 - shift - if [ "$namespace" = all ]; then - keepNamespace+=("cgroup" "ipc" "net" "pid" "user" "uts") - else - keepNamespace+=("$namespace") - fi - ;; - (--sanebox-path) - local path=$1 - shift - relativeToPwd _absPath "$path" - paths+=("$_absPath") - ;; - (--sanebox-home-path) - local path=$1 - shift - paths+=("$HOME/$path") - ;; - (--sanebox-run-path) - local path=$1 - shift - paths+=("$XDG_RUNTIME_DIR/$path") - ;; - (--sanebox-add-pwd) - paths+=("$PWD") - ;; - (*) - parseArgsExtra+=("$arg") - ;; - esac - done -} - - -## BUBBLEWRAP BACKEND - -bwrapUnshareCgroup=(--unshare-cgroup) -bwrapUnshareIpc=(--unshare-ipc) -bwrapUnshareNet=(--unshare-net) -bwrapUnsharePid=(--unshare-pid) -bwrapUnshareUts=(--unshare-user) -bwrapUnshareUts=(--unshare-uts) -bwrapVirtualizeDev=(--dev /dev) -bwrapVirtualizeProc=(--proc /proc) -bwrapVirtualizeTmp=(--tmpfs /tmp) -bwrapUsePasta= - -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" && bwrapArgs+=("--dev-bind-try" "$1" "$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. - readlinkOnce linkTarget "$1" - if [ -n "$linkTarget" ]; then - bwrapArgs+=("--symlink" "$linkTarget" "$1") - else - bwrapArgs+=("--dev-bind-try" "$1" "$1") - fi - - # 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. - case $1 in - (/) - bwrapVirtualizeDev=() - bwrapVirtualizeProc=() - bwrapVirtualizeTmp=() - ;; - (/dev) - bwrapVirtualizeDev=() - ;; - (/proc) - bwrapVirtualizeProc=() - ;; - (/tmp) - bwrapVirtualizeTmp=() - ;; - esac -} -bwrapIngestNetDev() { - local dev="$1" - bwrapUnshareNet=() - if [ "$dev" != "all" ]; then - bwrapUsePasta=1 - pastaonlyIngestNetDev "$dev" - fi -} -bwrapIngestNetGateway() { - bwrapUsePasta=1 - pastaonlyIngestNetGateway "$1" -} -bwrapIngestDns() { - bwrapUsePasta=1 - pastaonlyIngestDns "$1" -} -bwrapIngestKeepNamespace() { - case $1 in - (cgroup) - bwrapUnshareCgroup=() - ;; - (ipc) - bwrapUnshareIpc=() - ;; - (net) - bwrapUnshareNet=() - ;; - (pid) - bwrapUnsharePid=() - ;; - (user) - bwrapUnshareUser=() - ;; - (uts) - bwrapUnshareUts=() - ;; - esac -} -bwrapIngestCapability() { - bwrapArgs+=("--cap-add" "cap_$1") - # a program run inside a user namespace has no capabilities outside the namespace. - # so, disable the user namespace. - # N.B.: this only applies to root. non-root users still get a user namespace, because that's required in order to do any of the other namespacing. - # bwrapUnshareUser=() -} - -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 `bwrapArgs` contains --share-net, this 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" "$BWRAP_FALLBACK" - if [ -n "$bwrapUsePasta" ]; then - # pasta drops us into an environment where we're root, but some apps complain if run as root. - # TODO: this really belongs on the `pastaonlyGetCli` side. - # TODO: i think we need to add `/dev/net/tun` to the namespace for nested pasta calls to work? - bwrapArgs+=( - # --unshare-user is necessary for --uid to work when called as pseudo root - --unshare-user - --uid "$UID" - --gid "${GROUPS[0]}" - ) - fi - - cliArgs=( - "$_bwrap" - --argv0 "$argv0" - "${bwrapUnshareCgroup[@]}" - "${bwrapUnshareIpc[@]}" - "${bwrapUnshareNet[@]}" - "${bwrapUnsharePid[@]}" - "${bwrapUnshareUser[@]}" - "${bwrapUnshareUts[@]}" - "${bwrapVirtualizeDev[@]}" "${bwrapVirtualizeProc[@]}" "${bwrapVirtualizeTmp[@]}" - "${bwrapArgs[@]}" -- - "${cliArgs[@]}" - ) - if [ -n "$bwrapUsePasta" ]; then - # not critical, but it's less confusing if we have pasta execute bwrap *as* "bwrap" - local _realArgv="$argv0" - argv0=bwrap - pastaonlyGetCli - argv0="$_realArgv" - fi -} - - -## LANDLOCK BACKEND - -landlockPaths= -landlockNetFlags=(LL_TCP_BIND= LL_TCP_CONNECT=) - -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 - # - /dev/fd/*: application fails to open its stdin/stdout/etc - paths+=( - /dev/fd - /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/. - # 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 -} -landlockIngestNetDev() { - local dev="$1" - if [ "$dev" == "all" ]; then - landlockNetFlags=() - else - debug "landlockIngestNetDev: $dev: unsupported (landlock cannot selectively restrict devices)" - fi -} -landlockIngestNetGateway() { - debug "landlockIngestNetGateway: noop" -} -landlockIngestDns() { - debug "landlockIngestDns: noop" -} -landlockIngestKeepNamespace() { - debug "landlockIngestKeepNamespace: noop" -} -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 "landlock-sandboxer" "$LANDLOCK_SANDBOXER_FALLBACK" - locate _env "env" "$ENV_FALLBACK" - capshonlyGetCli - cliArgs=("$_env" LL_FS_RO= LL_FS_RW="$landlockPaths" "${landlockNetFlags[@]}" - "$_sandboxer" - "${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. - -# all=: means to clear all capabilities -capshCapsArg="all=" - -capshonlySetup() { - debug "capshonlySetup: noop" -} -capshonlyIngestPath() { - debug "capshonlyIngestPath: stubbed" -} -capshonlyIngestNetDev() { - debug "capshonlyIngestNetDev: '$1': stubbed (capsh network is always unrestricted)" -} -capshonlyIngestNetGateway() { - debug "capshonlyIngestNetGateway: '$1': stubbed (capsh network is always unrestricted)" -} -capshonlyIngestDns() { - debug "capshonlyIngestDns: '$1': stubbed (capsh network is always unrestricted)" -} -capshonlyIngestKeepNamespace() { - debug "capshonlyIngestKeepNamespace: noop" -} -capshonlyIngestCapability() { - # N.B. `capsh` parsing of `--caps=X` arg is idiosyncratic: - # - valid: `capsh --caps=CAP_FOO,CAP_BAR=eip -- ` - # - valid: `capsh --caps= -- ` - # - invalid: `capsh --caps=CAP_FOO,CAP_BAR -- ` - # - invalid: `capsh --caps==eip -- ` - # - # `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. - locate _capsh "capsh" "$CAPSH_FALLBACK" - - local hasP= - local hasI= - if "$_capsh" "--has-a=cap_$1" 2>/dev/null; then - # XXX: this ambient special case could probably be removed: - # a capability can't be ambient without also being I and P, IIUC. - hasP=1 - hasI=1 - else - if "$_capsh" "--has-p=cap_$1" 2>/dev/null; then - hasP=1 - fi - if "$_capsh" "--has-i=cap_$1" 2>/dev/null; then - hasI=1 - fi - fi - if [ -n "$hasI" ] || [ -n "$hasP" ]; then - # hasP means "able to add to E or I set. - # so, if we have the cap in *either* P or I, then we can place it in I here. - # only if we have it in P can we add it to P and E. - local ext=i - if [ -n "$hasP" ]; then - ext="e${ext}p" - fi - capshCapsArg="$capshCapsArg cap_$1+$ext" - else - debug "capsh: don't have capability $1" - fi -} - -capshonlyGetCli() { - locate _capsh "capsh" "$CAPSH_FALLBACK" - locate _env "env" "$ENV_FALLBACK" - - cliArgs=( - "$_capsh" "--caps=$capshCapsArg" --no-new-privs --shell="$_env" "${capshArgs[@]}" -- -a "$argv0" "${cliArgs[@]}" - ) -} - - -## PASTA-ONLY BACKEND -# this backend exists mostly as a helper for the bwrap backend - -pastaNetSetup= -pastaOutboundPorts=() -pastaonlySetup() { - debug "pastaonlySetup: noop" -} -pastaonlyIngestPath() { - debug "pastaonlyIngestPath: noop" -} -pastaonlyIngestNetDev() { - local dev=$1 - case $dev in - (all) - ;; - (*) - pastaArgs+=(--outbound-if4 "$dev") - ;; - esac -} -pastaonlyIngestNetGateway() { - pastaArgs+=(--gateway "$1") -} -pastaonlyIngestDns() { - local dns=$1 - case "$dns" in - (host) - # use the host's DNS resolver - if ! contains 53 "${pastaOutboundPorts[@]}"; then - pastaOutboundPorts+=(53) - fi - ;; - (*) - locate _iptables "iptables" "$IPTABLES_FALLBACK" - locate _ip "ip" "$IP_FALLBACK" - # NAT DNS requests to localhost to the VPN's DNS resolver - # claim the whole 127.0.0.x space, because some setups place the DNS on a different address of localhost. - pastaNetSetup="$_iptables -A OUTPUT -t nat -p udp --dport 53 -m iprange --dst-range 127.0.0.1-127.0.0.255 -j DNAT --to-destination $1:53; $pastaNetSetup" - pastaNetSetup="$_ip addr del 127.0.0.1/8 dev lo; $pastaNetSetup" - ;; - esac -} -pastaonlyIngestKeepNamespace() { - : -} -pastaonlyIngestCapability() { - : -} -pastaonlyGetCli() { - cliArgs=( - "/bin/sh" "-c" - "$pastaNetSetup exec"' "$@"' - "$argv0" # first argument after `-c "..."` is interpreted by bash as the argv - "${cliArgs[@]}" - ) - locate _pasta "pasta" "$PASTA_FALLBACK" - - if [ "$UID" = 0 ]; then - # default pasta will change to `nobody` if invoked as root, but there are times i actually want to run as root. - pastaArgs+=(--runas 0) - fi - - local pastaOutboundPortsStr=none - if [ "${#pastaOutboundPorts[@]}" -ne 0 ]; then - pastaOutboundPortsStr="${pastaOutboundPorts[*]}" - pastaOutboundPortsStr="${pastaOutboundPortsStr// /,}" - fi - - cliArgs=( - "$_pasta" --ipv4-only -U "$pastaOutboundPortsStr" -T "$pastaOutboundPortsStr" -u none -t none --config-net - "${pastaArgs[@]}" -- - "${cliArgs[@]}" - ) -} - - -## NONE BACKEND -# this backend exists only to allow benchmarking -noneSetup() { - : -} -noneIngestPath() { - : -} -noneIngestNetDev() { - : -} -noneIngestNetGateway() { - : -} -noneIngestDns() { - : -} -noneIngestKeepNamespace() { - : -} -noneIngestCapability() { - : -} -noneGetCli() { - cliArgs=(-a "$argv0" "${cliArgs[@]}") -} - - -## ARGUMENT POST-PROCESSING - -loadLinkCache() { - if ! [ -e /etc/sanebox/symlink-cache ]; then - # don't error if the link cache is inaccessible. - # this can happen during nix builds e.g. - return - fi - - # readarray -t: reads some file into an array; each line becomes one element - readarray -t _linkCacheArray < /etc/sanebox/symlink-cache - for link in "${_linkCacheArray[@]}"; do - # XXX: bash doesn't give a good way to escape the tab character, but that's what we're splitting on here. - local from=${link%% *} - local to=${link##* } - linkCache[$from]=$to - done -} - -### 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 - 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. -canonicalizePaths() { - # remove '//' and simplify '.', '..' paths, into canonical absolute logical paths. - local canonPaths=() - for path in "${paths[@]}"; do - normPath _canonPath "$path" - canonPaths+=("$_canonPath") - done - paths=("${canonPaths[@]}") -} -expandLink() { - derefOnce _linkTarget "$1" - if [ -n "$_linkTarget" ]; then - # add + expand the symlink further, but take care to avoid infinite recursion - if ! contains "$_linkTarget" "${paths[@]}"; then - paths+=("$_linkTarget") - expandLink "$_linkTarget" - fi - fi -} -### expand `paths` until it contains no symlinks whose target isn't also in `paths` -expandLinks() { - for path in "${paths[@]}"; do - expandLink "$path" - done -} -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= - for other in "${paths[@]}"; do - if [[ "$path" =~ ^"$other"/ ]] || [ "$other" = / -a "$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. - local canonicalizedPaths=() - for path in "${toplevelPaths[@]}"; do - if ! contains "$path" "${canonicalizedPaths[@]}"; then - canonicalizedPaths+=("$path") - fi - done - paths=("${canonicalizedPaths[@]}") -} - - -## 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 "$SANEBOX_DISABLE" ]; then - isDisable=1 - fi - - if [ -n "$SANEBOX_PREPEND" ]; then - parseArgs $SANEBOX_PREPEND - fi - - parseArgs "$@" - cliArgs+=("${parseArgsExtra[@]}") - if [ -z "$argv0" ]; then - argv0=${cliArgs[0]} - fi - - if [ -n "$SANEBOX_APPEND" ]; then - parseArgs $SANEBOX_APPEND - fi -} - -### convert generic args into sandbox-specific args -ingestForBackend() { - for path in "${paths[@]}"; do - "$method"IngestPath "$path" - done - - for cap in "${capabilities[@]}"; do - "$method"IngestCapability "$cap" - done - - if [ -n "$netDev" ]; then - "$method"IngestNetDev "$netDev" - fi - - if [ "$netGateway" != default ]; then - "$method"IngestNetGateway "$netGateway" - fi - - for addr in "${dns[@]}"; do - "$method"IngestDns "$addr" - done - - for ns in "${keepNamespace[@]}"; do - "$method"IngestKeepNamespace "$ns" - done -} - - -## TOPLEVEL EXECUTION -# no code evaluated before this point should be dependent on user args / environment. - -# TODO: use `set -e`, only i'm using `return 1` in places for control flow. -set +e - -parseArgsAndEnvironment "$@" - -# variables meant to be inherited -# N.B.: SANEBOX_DEBUG FREQUENTLY BREAKS APPLICATIONS WHICH PARSE STDOUT -# example is wireshark parsing stdout of dumpcap; -# in such a case invoke the app with --sanebox-debug instead of the env var. -export SANEBOX_DEBUG=$SANEBOX_DEBUG -export SANEBOX_DISABLE=$SANEBOX_DISABLE -export SANEBOX_PREPEND=$SANEBOX_PREPEND -export SANEBOX_APPEND=$SANEBOX_APPEND - -if [ -z "$isDisable" ]; then - loadLinkCache - # method-specific setup could add additional paths that need binding, so do that before canonicalization - "$method"Setup - maybeAutodetectPaths - canonicalizePaths - expandLinks - removeSubpaths - - 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 diff --git a/scripts/check-backups b/scripts/check-backups index 4b144b200..483742bad 100755 --- a/scripts/check-backups +++ b/scripts/check-backups @@ -1,5 +1,5 @@ #!/usr/bin/env nix-shell -#!nix-shell -i bash -p bash -p nettools -p openssh -p rsync -p sane-scripts.vpn -p sanebox +#!nix-shell -i bash -p bash -p nettools -p openssh -p rsync -p sane-scripts.vpn # secret should include RN_USER source /run/secrets/rsync-net-env