modules/programs: extend wrapperType="wrappedDerivation" to handle common share/ items

This commit is contained in:
2024-01-29 11:42:47 +00:00
parent 6f86e61a00
commit 7af970f38c
2 changed files with 160 additions and 108 deletions

View File

@@ -256,7 +256,8 @@ let
binaries wrap the binaries in the original derivation with a sandbox. binaries wrap the binaries in the original derivation with a sandbox.
"inplace" is more reliable, but "wrappedDerivation" is more lightweight (doesn't force any rebuilds). "inplace" is more reliable, but "wrappedDerivation" is more lightweight (doesn't force any rebuilds).
the biggest gap in "wrappedDerivation" is that it doesn't handle .desktop files; just the binaries. the biggest gap in "wrappedDerivation" is that it doesn't link anything outside `bin/`, except for
some limited (verified safe) support for `share/applications/*.desktop`
"wrappedDerivation" is mostly good for prototyping. "wrappedDerivation" is mostly good for prototyping.
''; '';
}; };

View File

@@ -2,6 +2,7 @@
, runCommand , runCommand
, runtimeShell , runtimeShell
, sane-sandboxed , sane-sandboxed
, symlinkJoin
, writeShellScriptBin , writeShellScriptBin
, writeTextFile , writeTextFile
}: }:
@@ -28,15 +29,146 @@ let
# assume that every argument after the binary name is an argument for the binary and not for the sandboxer. # assume that every argument after the binary name is an argument for the binary and not for the sandboxer.
exec "$@" exec "$@"
''; '';
makeHookable = pkg:
if ((pkg.override or {}).__functionArgs or {}) ? runCommand then
pkg.override {
runCommand = name: env: cmd: runCommand name env (cmd + lib.optionalString (name == pkg.name) ''
# if the package is a runCommand (common for wrappers), then patch it to call our `postFixup` hook, first
runHook postFixup
'');
}
else
# assume the package already calls postFixup (if not, we error during system-level build)
pkg;
# take an existing package, which may have a `bin/` folder as well as `share/` etc,
# and patch the `bin/` items in-place
sandboxBinariesInPlace = binMap: sane-sandboxed': extraSandboxArgsStr: 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 {}) // {
SANE_SANDBOX_DISABLE = 1;
};
nativeBuildInputs = (unwrapped.nativeBuildInputs or []) ++ [
fakeSaneSandboxed
];
disallowedReferences = (unwrapped.disallowedReferences or []) ++ [
# the fake sandbox gates itself behind SANE_SANDBOX_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 "") + ''
getProfileFromBinMap() {
case "$1" in
${builtins.concatStringsSep "\n" (lib.mapAttrsToList
(bin: profile: ''
(${bin})
echo "${profile}"
;;
'')
binMap
)}
(*)
;;
esac
}
sandboxWrap() {
_name="$1"
_profileFromBinMap="$(getProfileFromBinMap $_name)"
_profiles=("$_profileFromBinMap" "$_name" "${pkgName}" "${unwrapped.pname or ""}" "${unwrapped.name or ""}")
# filter to just the unique profiles
_profileArgs=(${extraSandboxArgsStr})
for _profile in "''${_profiles[@]}"; do
if [ -n "$_profile" ] && ! [[ " ''${_profileArgs[@]} " =~ " $_profile " ]]; then
_profileArgs+=("--sane-sandbox-profile" "$_profile")
fi
done
# N.B.: unlike `makeWrapper`, 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)
# they work as expected without any special hacks.
# if desired, makeWrapper-style naming could be achieved by leveraging `exec -a <original_name>`.
mkdir -p "$out/bin/.sandboxed"
mv "$out/bin/$_name" "$out/bin/.sandboxed/"
cat <<EOF >> "$out/bin/$_name"
#!${runtimeShell}
exec ${sane-sandboxed'} \
''${_profileArgs[@]} \
"$out/bin/.sandboxed/$_name" "\$@"
EOF
chmod +x "$out/bin/$_name"
}
for _p in $(ls "$out/bin/"); do
sandboxWrap "$_p"
done
'';
});
# 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: runCommand "${pkgName}-sandboxed" {} '' symlinkBinaries = pkgName: package: runCommand "${pkgName}-bin-only" {} ''
mkdir -p "$out/bin" mkdir -p "$out/bin"
for d in $(ls "${package}/bin"); do for d in $(ls "${package}/bin"); do
ln -s "${package}/bin/$d" "$out/bin/$d" ln -s "${package}/bin/$d" "$out/bin/$d"
done done
# postFixup can do the actual wrapping # allow downstream wrapping to hook this (and thereby actually wrap the binaries)
runHook postFixup runHook postFixup
''; '';
# helper used for `wrapperType == "wrappedDerivation"` which copies over the .desktop files
# and ensures that they don't point to the unwrapped versions.
# other important files it preserves:
# - share/applications
# - share/dbus-1 (frequently a source of leaked references!)
# - share/icons
# - share/man
# - share/mime
# TODO: it'd be nice to just symlink these instead, but then we couldn't leverage `disallowedReferences` like this.
copyNonBinaries = pkgName: package: runCommand "${pkgName}-sandboxed-non-binary" {
disallowedReferences = [ package ];
} ''
mkdir "$out"
if [ -e "${package}/share" ]; then
cp -R "${package}/share" "$out/"
fi
'';
# take the nearly-final sandboxed package, with binaries and and else, and
# populate passthru attributes the caller expects, like `sandboxProfiles` and `checkSandboxed`.
fixupMetaAndPassthru = pkgName: pkg: sandboxProfiles: pkg.overrideAttrs (orig: let
final = fixupMetaAndPassthru pkgName pkg sandboxProfiles;
in {
meta = (orig.meta or {}) // {
# take precedence over non-sandboxed versions of the same binary.
priority = ((orig.meta or {}).priority or 0) - 1;
};
passthru = (pkg.passthru or {}) // {
inherit sandboxProfiles;
checkSandboxed = runCommand "${pkgName}-check-sandboxed" {} ''
# invoke each binary in a way only the sandbox wrapper will recognize,
# ensuring that every binary has in fact been wrapped.
_numExec=0
for b in ${final}/bin/*; do
echo "checking if $b is sandboxed"
PATH="${final}/bin:${sane-sandboxed}/bin:$PATH" \
SANE_SANDBOX_DISABLE=1 \
"$b" --sane-sandbox-replace-cli echo "printing for test" \
| grep "printing for test"
_numExec=$(( $_numExec + 1 ))
done
echo "successfully tested $_numExec binaries"
test "$_numExec" -ne 0 && touch "$out"
'';
};
});
in in
{ pkgName, package, method, wrapperType, vpn ? null, allowedHomePaths ? [], allowedRootPaths ? [], autodetectCliPaths ? false, binMap ? {}, capabilities ? [], embedProfile ? false, embedSandboxer ? false, extraConfig ? [], whitelistPwd ? false }: { pkgName, package, method, wrapperType, vpn ? null, allowedHomePaths ? [], allowedRootPaths ? [], autodetectCliPaths ? false, binMap ? {}, capabilities ? [], embedProfile ? false, embedSandboxer ? false, extraConfig ? [], whitelistPwd ? false }:
let let
@@ -95,112 +227,31 @@ let
# here we switch between the options. # here we switch between the options.
# note that no.2 ("wrappedDerivation") *doesn't support .desktop files yet*. # note that no.2 ("wrappedDerivation") *doesn't support .desktop files yet*.
# the final package simply doesn't include .desktop files, only bin/. # the final package simply doesn't include .desktop files, only bin/.
package' = if wrapperType == "inplace" then packageWrapped = if wrapperType == "inplace" then
if ((package.override or {}).__functionArgs or {}) ? runCommand then sandboxBinariesInPlace
package.override { binMap
runCommand = name: env: cmd: runCommand name env (cmd + lib.optionalString (name == package.name) '' sane-sandboxed'
# if the package is a runCommand (common for wrappers), then patch it to call our `postFixup` hook, first maybeEmbedProfilesDir
runHook postFixup pkgName
''); (makeHookable package)
}
else
package
else if wrapperType == "wrappedDerivation" then else if wrapperType == "wrappedDerivation" then
symlinkBinaries pkgName package let
binariesOnly = symlinkBinaries pkgName package;
binariesWrapped = sandboxBinariesInPlace
binMap
sane-sandboxed'
maybeEmbedProfilesDir
pkgName
binariesOnly;
in
symlinkJoin {
name = "${pkgName}-sandboxed-all";
paths = [
binariesWrapped
(copyNonBinaries pkgName package)
];
}
else else
builtins.throw "unknown wrapperType: ${wrapperType}"; builtins.throw "unknown wrapperType: ${wrapperType}";
packageWrapped = package'.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 {}) // {
SANE_SANDBOX_DISABLE = 1;
};
nativeBuildInputs = (unwrapped.nativeBuildInputs or []) ++ [
fakeSaneSandboxed
];
disallowedReferences = (unwrapped.disallowedReferences or []) ++ [
# the fake sandbox gates itself behind SANE_SANDBOX_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 "") + ''
getProfileFromBinMap() {
case "$1" in
${builtins.concatStringsSep "\n" (lib.mapAttrsToList
(bin: profile: ''
(${bin})
echo "${profile}"
;;
'')
binMap
)}
(*)
;;
esac
}
sandboxWrap() {
_name="$1"
_profileFromBinMap="$(getProfileFromBinMap $_name)"
_profiles=("$_profileFromBinMap" "$_name" "${pkgName}" "${unwrapped.pname or ""}" "${unwrapped.name or ""}")
# filter to just the unique profiles
_profileArgs=(${maybeEmbedProfilesDir})
for _profile in "''${_profiles[@]}"; do
if [ -n "$_profile" ] && ! [[ " ''${_profileArgs[@]} " =~ " $_profile " ]]; then
_profileArgs+=("--sane-sandbox-profile" "$_profile")
fi
done
# N.B.: unlike `makeWrapper`, 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)
# they work as expected without any special hacks.
# if desired, makeWrapper-style naming could be achieved by leveraging `exec -a <original_name>`.
mkdir -p "$out/bin/.sandboxed"
mv "$out/bin/$_name" "$out/bin/.sandboxed/"
cat <<EOF >> "$out/bin/$_name"
#!${runtimeShell}
exec ${sane-sandboxed'} \
''${_profileArgs[@]} \
"$out/bin/.sandboxed/$_name" "\$@"
EOF
chmod +x "$out/bin/$_name"
}
for _p in $(ls "$out/bin/"); do
sandboxWrap "$_p"
done
'';
meta = (unwrapped.meta or {}) // {
# take precedence over non-sandboxed versions of the same binary.
priority = ((unwrapped.meta or {}).priority or 0) - 1;
};
passthru = (unwrapped.passthru or {}) // {
checkSandboxed = runCommand "${pkgName}-check-sandboxed" {} ''
# invoke each binary in a way only the sandbox wrapper will recognize,
# ensuring that every binary has in fact been wrapped.
_numExec=0
for b in ${packageWrapped}/bin/*; do
echo "checking if $b is sandboxed"
PATH="${packageWrapped}/bin:${sane-sandboxed}/bin:$PATH" \
SANE_SANDBOX_DISABLE=1 \
"$b" --sane-sandbox-replace-cli echo "printing for test" \
| grep "printing for test"
_numExec=$(( $_numExec + 1 ))
done
echo "successfully tested $_numExec binaries"
test "$_numExec" -ne 0 && touch "$out"
'';
sandboxProfiles = sandboxProfilesPkg;
};
});
in in
packageWrapped fixupMetaAndPassthru pkgName packageWrapped sandboxProfilesPkg