make-sandboxed: handle /opt-style packaging, with toplevels linked into /bin, a bit better

This commit is contained in:
2024-08-15 03:05:49 +00:00
parent 87e9856497
commit fd6959230f

View File

@@ -12,6 +12,7 @@
symlinkJoin, symlinkJoin,
writeShellScriptBin, writeShellScriptBin,
writeTextFile, writeTextFile,
xorg,
}: }:
let let
fakeSaneSandboxed = writeShellScriptBin "sanebox" '' fakeSaneSandboxed = writeShellScriptBin "sanebox" ''
@@ -72,8 +73,7 @@ let
postFixup = (unwrapped.postFixup or "") + '' postFixup = (unwrapped.postFixup or "") + ''
assertExecutable() { 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() { # makeDocumentedCWrapper() {
# # this is identical to nixpkgs' implementation, only replace execv with execvp, the latter which looks for the executable on PATH. # # 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() { sandboxWrap() {
local _dir="$1" local _dir="$1"
local _name="$2" local _name="$2"
echo "sandboxWrap $_dir/$_name"
# N.B.: unlike stock `wrapProgram`, we place the unwrapped binary in a subdirectory and *preserve its 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) # 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 ' --replace-fail 'exec ' 'source '
} }
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() { crawlAndWrap() {
local _dir="$1" local output="$1"
for _p in $(ls "$_dir/"); do local _dir="$2"
if [ -x "$_dir/$_p" ] && ! [ -d "$_dir/$_p" ]; then echo "crawlAndWrap $_dir"
sandboxWrap "$_dir" "$_p" local items=($(ls -a "$_dir/"))
elif [ -d "$_dir/$_p" ]; then for item in "''${items[@]}"; do
crawlAndWrap "$_dir/$_p" 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 fi
# ignore all non-binaries # ignore all non-binaries
fi
done done
} }
@@ -126,10 +177,10 @@ let
local outdir=''${!output} local outdir=''${!output}
echo "scanning output '$output' at $outdir for binaries to sandbox" echo "scanning output '$output' at $outdir for binaries to sandbox"
if [ -e "$outdir/bin" ]; then if [ -e "$outdir/bin" ]; then
crawlAndWrap "$outdir/bin" crawlAndWrap "$outdir" "$outdir/bin"
fi fi
if [ -e "$outdir/libexec" ]; then if [ -e "$outdir/libexec" ]; then
crawlAndWrap "$outdir/libexec" crawlAndWrap "$outdir" "$outdir/libexec"
fi fi
done done
''; '';
@@ -145,20 +196,57 @@ let
; ;
# helper used for `wrapperType == "wrappedDerivation"` which simply symlinks all a package's binaries into a new derivation # 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 set -e
if [ -e "${package}/bin" ]; then symlinkPath() {
mkdir -p "$out/bin" if [ -e "$out/$1" ]; then
${buildPackages.xorg.lndir}/bin/lndir "${package}/bin" "$out/bin" : # 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 fi
if [ "$(readlink ${package}/sbin)" == "bin" ]; then elif [ -d "${package}/$1" ]; then
# weird packages like wpa_supplicant depend on a sbin/ -> bin symlink in their service files echo "handling $1: descending into directory"
ln -s bin "$out/sbin" mkdir -p "$out/$1"
items=($(ls -a "${package}/$1"))
for item in "''${items[@]}"; do
if [ "$item" != . ] && [ "$item" != .. ]; then
symlinkPath "$1/$item"
fi fi
if [ -e "${package}/libexec" ]; then done
mkdir -p "$out/libexec" elif [ -e "${package}/$1" ]; then
${buildPackages.xorg.lndir}/bin/lndir "${package}/libexec" "$out/libexec" echo "handling $1: symlinking ordinary file"
ln -s "${package}/$1" "$out/$1"
fi fi
}
symlinkPath bin
symlinkPath sbin
symlinkPath libexec
# allow downstream wrapping to hook this (and thereby actually wrap the binaries) # allow downstream wrapping to hook this (and thereby actually wrap the binaries)
runHook postFixup runHook postFixup
'').overrideAttrs (_: { '').overrideAttrs (_: {
@@ -191,6 +279,28 @@ let
mv ./substituteResult "$_outPath" mv ./substituteResult "$_outPath"
fi 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 # fixup a few files i understand well enough
for d in \ for d in \
$out/etc/xdg/autostart/*.desktop \ $out/etc/xdg/autostart/*.desktop \
@@ -213,13 +323,15 @@ let
# further, since the sandboxed binaries intentionally reference the unsandboxed binaries, # further, since the sandboxed binaries intentionally reference the unsandboxed binaries,
# we have to patch those out as a way to whitelist them. # we have to patch those out as a way to whitelist them.
checkSandboxed = let checkSandboxed = let
sandboxedNonBin = fixHardcodedRefs unsandboxed "/dev/null" unsandboxedNonBin; sandboxedNonBin = fixHardcodedRefs unsandboxed sandboxedBin unsandboxedNonBin;
in runCommandLocal "${sandboxedNonBin.name}-check-sandboxed" in runCommandLocal "${sandboxedNonBin.name}-check-sandboxed"
{ disallowedReferences = [ unsandboxed ]; } { disallowedReferences = [ unsandboxed ]; }
''
# dereference every symlink, ensuring that whatever data is behind it does not reference non-sandboxed binaries. # 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. # 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. # 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.
''
cp -R --dereference "${sandboxedNonBin}" "$out" # IF YOUR BUILD FAILS HERE, TRY SANDBOXING WITH "inplace" 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, # patch them to use the sandboxed binaries,
# and add some passthru metadata to enforce no lingering references to the unsandboxed binaries. # and add some passthru metadata to enforce no lingering references to the unsandboxed binaries.
sandboxNonBinaries = pkgName: unsandboxed: sandboxedBin: let sandboxNonBinaries = pkgName: unsandboxed: sandboxedBin: let
sandboxedWithoutFixedRefs = (runCommandLocal "${pkgName}-sandboxed-non-binary" {} '' sandboxedWithoutFixedRefs = (runCommandLocal "${pkgName}-sandboxed-non-binary" {
nativeBuildInputs = [ xorg.lndir ];
} ''
set -e set -e
mkdir "$out" mkdir "$out"
# link in a limited subset of the directories. # link in a limited subset of the directories.
@@ -239,7 +353,7 @@ let
for dir in etc share; do for dir in etc share; do
if [ -e "${unsandboxed}/$dir" ]; then if [ -e "${unsandboxed}/$dir" ]; then
mkdir "$out/$dir" mkdir "$out/$dir"
${buildPackages.xorg.lndir}/bin/lndir "${unsandboxed}/$dir" "$out/$dir" lndir "${unsandboxed}/$dir" "$out/$dir"
fi fi
done done
runHook postInstall runHook postInstall
@@ -249,7 +363,7 @@ let
}); });
in fixHardcodedRefs unsandboxed sandboxedBin sandboxedWithoutFixedRefs; 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`. # populate passthru attributes the caller expects, like `checkSandboxed`.
fixupMetaAndPassthru = pkgName: pkg: extraPassthru: pkg.overrideAttrs (finalAttrs: prevAttrs: let fixupMetaAndPassthru = pkgName: pkg: extraPassthru: pkg.overrideAttrs (finalAttrs: prevAttrs: let
nonBin = (prevAttrs.passthru or {}).sandboxedNonBin or {}; nonBin = (prevAttrs.passthru or {}).sandboxedNonBin or {};