diff --git a/modules/programs/make-sandboxed.nix b/modules/programs/make-sandboxed.nix index 4f1c0d11b..891e0386f 100644 --- a/modules/programs/make-sandboxed.nix +++ b/modules/programs/make-sandboxed.nix @@ -12,6 +12,7 @@ symlinkJoin, writeShellScriptBin, writeTextFile, + xorg, }: let fakeSaneSandboxed = writeShellScriptBin "sanebox" '' @@ -72,8 +73,7 @@ let 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 sanebox 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. @@ -87,6 +87,7 @@ let sandboxWrap() { local _dir="$1" local _name="$2" + echo "sandboxWrap $_dir/$_name" # N.B.: unlike stock `wrapProgram`, we place the unwrapped binary in a subdirectory and *preserve its name*. # the upside of this is that for applications which read "$0" to decide what to do (e.g. busybox, git) @@ -110,15 +111,65 @@ let --replace-fail 'exec ' 'source ' } - crawlAndWrap() { - local _dir="$1" - for _p in $(ls "$_dir/"); do - if [ -x "$_dir/$_p" ] && ! [ -d "$_dir/$_p" ]; then - sandboxWrap "$_dir" "$_p" - elif [ -d "$_dir/$_p" ]; then - crawlAndWrap "$_dir/$_p" + derefWhileInSameOutput() { + local output="$1" + local item="$2" + if [ -L "$item" ]; then + local target=$(readlink "$item") + if [[ "$target" =~ ^"$output"/ ]]; then + # absolute link back into the same package + item=$(derefWhileInSameOutput "$output" "$target") + elif [[ "$target" =~ ^/nix/store/ ]]; then + : # absolute link to another package: we're done + else + # relative link + local parent=$(dirname "$item") + target="$parent/$target" + item=$(derefWhileInSameOutput "$output" "$target") + fi + fi + echo "$item" + } + findUnwrapped() { + if [ -L "$1" ]; then + echo "$1" + else + local dir_=$(dirname "$1") + local file_=$(basename "$1") + local sandboxed="$dir_/.sandboxed/$file_" + local unwrapped="$dir_/.''${file_}-unwrapped" + if grep -q "$sandboxed" "$1"; then + echo "/dev/null" #< already sandboxed + elif grep -q "$unwrapped" "$1"; then + echo $(findUnwrapped "$unwrapped") + else + echo "$1" + fi + fi + } + + crawlAndWrap() { + local output="$1" + local _dir="$2" + echo "crawlAndWrap $_dir" + local items=($(ls -a "$_dir/")) + for item in "''${items[@]}"; do + if [ "$item" != . ] && [ "$item" != .. ]; then + local target="$_dir/$item" + if [ -x "$target" ] && ! [ -d "$target" ]; then + # in the case of symlinks, deref until we find the real file, or the symlink points outside the package + target=$(derefWhileInSameOutput "$output" "$target") + target=$(findUnwrapped "$target") + if [ "$target" != /dev/null ]; then + local parent=$(dirname "$target") + local bin=$(basename "$target") + sandboxWrap "$parent" "$bin" + fi + elif [ -d "$target" ]; then + crawlAndWrap "$output" "$target" + fi + # ignore all non-binaries fi - # ignore all non-binaries done } @@ -126,10 +177,10 @@ let local outdir=''${!output} echo "scanning output '$output' at $outdir for binaries to sandbox" if [ -e "$outdir/bin" ]; then - crawlAndWrap "$outdir/bin" + crawlAndWrap "$outdir" "$outdir/bin" fi if [ -e "$outdir/libexec" ]; then - crawlAndWrap "$outdir/libexec" + crawlAndWrap "$outdir" "$outdir/libexec" fi done ''; @@ -145,20 +196,57 @@ let ; # helper used for `wrapperType == "wrappedDerivation"` which simply symlinks all a package's binaries into a new derivation - symlinkBinaries = pkgName: package: (runCommandLocal "${pkgName}-bin-only" {} '' + symlinkBinaries = pkgName: package: (runCommandLocal "${pkgName}-bin-only" { + nativeBuildInputs = [ gnused ]; + } '' set -e - if [ -e "${package}/bin" ]; then - mkdir -p "$out/bin" - ${buildPackages.xorg.lndir}/bin/lndir "${package}/bin" "$out/bin" - fi - if [ "$(readlink ${package}/sbin)" == "bin" ]; then - # weird packages like wpa_supplicant depend on a sbin/ -> bin symlink in their service files - ln -s bin "$out/sbin" - fi - if [ -e "${package}/libexec" ]; then - mkdir -p "$out/libexec" - ${buildPackages.xorg.lndir}/bin/lndir "${package}/libexec" "$out/libexec" - fi + symlinkPath() { + if [ -e "$out/$1" ]; then + : # already linked. may happen when e.g. the package has bin/foo, and sbin -> bin. + elif ! [ -x "${package}/$1" ]; then + : # not a binary, nor a directory (-x) which could contain binaries + elif [ -L "${package}/$1" ]; then + local target=$(readlink "${package}/$1") + if [[ "$target" =~ ^${package}/ ]]; then + # absolute link back into the same package + echo "handling $1: descending into absolute symlink to same package: $target" + target=$(echo "$target" | sed 's:${package}/::') + ln -s "$out/$target" "$out/$1" + # create/link the backing path + # N.B.: if some leading component of the backing path is also a symlink... this might not work as expected. + local parent=$(dirname "$out/$target") + mkdir -p "$parent" + symlinkPath "$target" + elif [[ "$target" =~ ^/nix/store/ ]]; then + # absolute link to another package + echo "handling $1: symlinking absolute store path: $target" + ln -s "$target" "$out/$1" + else + # relative link + echo "handling $1: descending into relative symlink: $target" + ln -s "$target" "$out/$1" + local parent=$(dirname "$1") + local derefParent=$(dirname "$out/$parent/$target") + $(set -x && mkdir -p "$derefParent") + symlinkPath "$parent/$target" + fi + elif [ -d "${package}/$1" ]; then + echo "handling $1: descending into directory" + mkdir -p "$out/$1" + items=($(ls -a "${package}/$1")) + for item in "''${items[@]}"; do + if [ "$item" != . ] && [ "$item" != .. ]; then + symlinkPath "$1/$item" + fi + done + elif [ -e "${package}/$1" ]; then + echo "handling $1: symlinking ordinary file" + ln -s "${package}/$1" "$out/$1" + fi + } + symlinkPath bin + symlinkPath sbin + symlinkPath libexec # allow downstream wrapping to hook this (and thereby actually wrap the binaries) runHook postFixup '').overrideAttrs (_: { @@ -191,6 +279,28 @@ let mv ./substituteResult "$_outPath" fi } + + # remove any files which exist in sandoxedBin (makes it possible to sandbox /opt-style packages) + # also remove any files which would be "hidden". mostly useful for /opt-style packages which contain nix-wrapped binaries. + removeUnwanted() { + local file_=$(basename "$1") + if [[ "$file_" == .* ]]; then + rm -r "$out/$1" + elif [ -f "$out/$1" ] || [ -L "$out/$1" ]; then + if [ -e "${sandboxedBin}/$1" ]; then + rm "$out/$1" + fi + elif [ -d "$out/$1" ]; then + local files=($(ls -a "$out/$1")) + for item in "''${files[@]}"; do + if [ "$item" != . ] && [ "$item" != .. ]; then + removeUnwanted "$1/$item" + fi + done + fi + } + removeUnwanted "" + # fixup a few files i understand well enough for d in \ $out/etc/xdg/autostart/*.desktop \ @@ -213,13 +323,15 @@ let # further, since the sandboxed binaries intentionally reference the unsandboxed binaries, # we have to patch those out as a way to whitelist them. checkSandboxed = let - sandboxedNonBin = fixHardcodedRefs unsandboxed "/dev/null" unsandboxedNonBin; + sandboxedNonBin = fixHardcodedRefs unsandboxed sandboxedBin unsandboxedNonBin; in runCommandLocal "${sandboxedNonBin.name}-check-sandboxed" { disallowedReferences = [ unsandboxed ]; } + # dereference every symlink, ensuring that whatever data is behind it does not reference non-sandboxed binaries. + # the dereference *can* fail, in case it's a relative symlink that refers to a part of the non-binaries we don't patch. + # in such case, this could lead to weird brokenness (e.g. no icons/images), so failing is reasonable. + # N.B.: this `checkSandboxed` protects against accidentally referencing unsandboxed binaries from data files (.deskop, .service, etc). + # there's an *additional* `checkSandboxed` further below which invokes every executable in the final package to make sure the binaries are truly sandboxed. '' - # dereference every symlink, ensuring that whatever data is behind it does not reference non-sandboxed binaries. - # the dereference *can* fail, in case it's a relative symlink that refers to a part of the non-binaries we don't patch. - # in such case, this could lead to weird brokenness (e.g. no icons/images), so failing is reasonable. cp -R --dereference "${sandboxedNonBin}" "$out" # IF YOUR BUILD FAILS HERE, TRY SANDBOXING WITH "inplace" '' ; @@ -230,7 +342,9 @@ let # patch them to use the sandboxed binaries, # and add some passthru metadata to enforce no lingering references to the unsandboxed binaries. sandboxNonBinaries = pkgName: unsandboxed: sandboxedBin: let - sandboxedWithoutFixedRefs = (runCommandLocal "${pkgName}-sandboxed-non-binary" {} '' + sandboxedWithoutFixedRefs = (runCommandLocal "${pkgName}-sandboxed-non-binary" { + nativeBuildInputs = [ xorg.lndir ]; + } '' set -e mkdir "$out" # link in a limited subset of the directories. @@ -239,7 +353,7 @@ let for dir in etc share; do if [ -e "${unsandboxed}/$dir" ]; then mkdir "$out/$dir" - ${buildPackages.xorg.lndir}/bin/lndir "${unsandboxed}/$dir" "$out/$dir" + lndir "${unsandboxed}/$dir" "$out/$dir" fi done runHook postInstall @@ -249,7 +363,7 @@ let }); in fixHardcodedRefs unsandboxed sandboxedBin sandboxedWithoutFixedRefs; - # take the nearly-final sandboxed package, with binaries and and else, and + # take the nearly-final sandboxed package, with binaries and all else, and # populate passthru attributes the caller expects, like `checkSandboxed`. fixupMetaAndPassthru = pkgName: pkg: extraPassthru: pkg.overrideAttrs (finalAttrs: prevAttrs: let nonBin = (prevAttrs.passthru or {}).sandboxedNonBin or {};