nixpkgs/pkgs/build-support/build-fhsenv-bubblewrap/default.nix

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

297 lines
9.0 KiB
Nix
Raw Permalink Normal View History

{ lib
, stdenv
, callPackage
, runCommandLocal
, writeShellScript
, glibc
, pkgsi686Linux
, runCommandCC
, coreutils
, bubblewrap
}:
{ runScript ? "bash"
, nativeBuildInputs ? []
, extraInstallCommands ? ""
, meta ? {}
, passthru ? {}
, extraPreBwrapCmds ? ""
, extraBwrapArgs ? []
buildFHSEnv: disable security features by default The implicit contract of buildFHSUserEnv was that it allows to run software built for a typical GNU/Linux distribution (not NixOS) without patching it (patchelf, autoPatchelfHook, etc.). Note that this does not inherently imply running untrusted programs. buildFHSUserEnv was implemented by using chroot and assembling a standard-compliant FHS environment in the new root. As expected, this did not provide any kind of isolation between the system and the programs. However, when it was later reimplemented using bubblewrap (PR #225748), which *is* a security tool, several isolation features involving detaches Linux namespaces were turned on by default. This decision has introduced a number of breakages that are very difficult to debug and trace back to this change. For example: `unshareIPC` breaks software audio mixing in programs using ALSA (dmix) and `unsharePID` breaks gdb, Since: 1. the security features were enable without any clear threat model; 2. `buildFHSEnvBubblewrap` is supposed to be a drop-in replacement of `buildFHSEnvChrootenv` (see the release notes for NixOS 23.05); 3. the change is breaking in several common cases (security does not come for free); 4. the contract was not changed, or at least communicated in a clear way to the users; all security features should be turned off by default. P.S. It would be useful to create a variant of buildFHSEnv that does provide some isolation. This could unshare some namespaces and mount only limited parts of the filesystem. Note that buildFHSEnv mounts every directory in / under the new root, so again, very little is gained by unsharing alone.
2023-09-08 06:58:31 +00:00
, unshareUser ? false
, unshareIpc ? false
, unsharePid ? false
, unshareNet ? false
buildFHSEnv: disable security features by default The implicit contract of buildFHSUserEnv was that it allows to run software built for a typical GNU/Linux distribution (not NixOS) without patching it (patchelf, autoPatchelfHook, etc.). Note that this does not inherently imply running untrusted programs. buildFHSUserEnv was implemented by using chroot and assembling a standard-compliant FHS environment in the new root. As expected, this did not provide any kind of isolation between the system and the programs. However, when it was later reimplemented using bubblewrap (PR #225748), which *is* a security tool, several isolation features involving detaches Linux namespaces were turned on by default. This decision has introduced a number of breakages that are very difficult to debug and trace back to this change. For example: `unshareIPC` breaks software audio mixing in programs using ALSA (dmix) and `unsharePID` breaks gdb, Since: 1. the security features were enable without any clear threat model; 2. `buildFHSEnvBubblewrap` is supposed to be a drop-in replacement of `buildFHSEnvChrootenv` (see the release notes for NixOS 23.05); 3. the change is breaking in several common cases (security does not come for free); 4. the contract was not changed, or at least communicated in a clear way to the users; all security features should be turned off by default. P.S. It would be useful to create a variant of buildFHSEnv that does provide some isolation. This could unshare some namespaces and mount only limited parts of the filesystem. Note that buildFHSEnv mounts every directory in / under the new root, so again, very little is gained by unsharing alone.
2023-09-08 06:58:31 +00:00
, unshareUts ? false
, unshareCgroup ? false
, privateTmp ? false
, dieWithParent ? true
, ...
} @ args:
assert (!args ? pname || !args ? version) -> (args ? name); # You must provide name if pname or version (preferred) is missing.
let
inherit (lib)
concatLines
concatStringsSep
escapeShellArgs
filter
optionalString
splitString
;
inherit (lib.attrsets) removeAttrs;
name = args.name or "${args.pname}-${args.version}";
executableName = args.pname or args.name;
# we don't know which have been supplied, and want to avoid defaulting missing attrs to null. Passed into runCommandLocal
nameAttrs = lib.filterAttrs (key: value: builtins.elem key [ "name" "pname" "version" ]) args;
buildFHSEnv = callPackage ./buildFHSEnv.nix { };
fhsenv = buildFHSEnv (removeAttrs args [
"runScript" "extraInstallCommands" "meta" "passthru" "extraPreBwrapCmds" "extraBwrapArgs" "dieWithParent"
"unshareUser" "unshareCgroup" "unshareUts" "unshareNet" "unsharePid" "unshareIpc" "privateTmp"
]);
etcBindEntries = let
files = [
# NixOS Compatibility
"static"
"nix" # mainly for nixUnstable users, but also for access to nix/netrc
# Shells
"shells"
"bashrc"
"zshenv"
"zshrc"
"zinputrc"
"zprofile"
# Users, Groups, NSS
"passwd"
"group"
"shadow"
"hosts"
"resolv.conf"
"nsswitch.conf"
2021-01-26 00:41:50 +00:00
# User profiles
"profiles"
# Sudo & Su
"login.defs"
"sudoers"
"sudoers.d"
# Time
"localtime"
"zoneinfo"
# Other Core Stuff
"machine-id"
"os-release"
# PAM
"pam.d"
# Fonts
"fonts"
# ALSA
"alsa"
"asound.conf"
# SSL
"ssl/certs"
"ca-certificates"
"pki"
];
in map (path: "/etc/${path}") files;
# Here's the problem case:
# - we need to run bash to run the init script
# - LD_PRELOAD may be set to another dynamic library, requiring us to discover its dependencies
# - oops! ldconfig is part of the init script, and it hasn't run yet
# - everything explodes
#
# In particular, this happens with fhsenvs in fhsenvs, e.g. when running
# a wrapped game from Steam.
#
# So, instead of doing that, we build a tiny static (important!) shim
# that executes ldconfig in a completely clean environment to generate
# the initial cache, and then execs into the "real" init, which is the
# first time we see anything dynamically linked at all.
#
# Also, the real init is placed strategically at /init, so we don't
# have to recompile this every time.
containerInit = runCommandCC "container-init" {
buildInputs = [ stdenv.cc.libc.static or null ];
} ''
$CXX -static -s -o $out ${./container-init.cc}
'';
realInit = run: writeShellScript "${name}-init" ''
source /etc/profile
exec ${run} "$@"
'';
indentLines = str: concatLines (map (s: " " + s) (filter (s: s != "") (splitString "\n" str)));
bwrapCmd = { initArgs ? "" }: ''
ignored=(/nix /dev /proc /etc ${optionalString privateTmp "/tmp"})
ro_mounts=()
symlinks=()
etc_ignored=()
buildFHSEnvBubblewrap: extraPreBwrapCmds after variable initialisation Prior to this commit it was not possible to modify e.g. the list of ignored directories at all, however given that `buildFHSEnvBubblewrap` effectively uses a sandboxing tool (*bwrap*) I feel like this is a missed opportunity. The code in nixpkgs already covers all the knobs that are required to get *Nix* itself to run inside bubblewrap, so why not allow users to make that additional modification? While additional `ro_mounts` and such can be *added* to the bubblewrap invocation, the already mounted directories cannot be removed, and even if shadowed by e.g. a tmpfs mount, this would still allow something inside the sandbox to potentially unmount the tmpfs and access the data. So what this change does is moving the snippet where custom code can be injected down by four lines so that users can actually modify those variables e.g. using `ignored+=( /home /srv /mnt /boot )`. The only cases in which this would break is: - someone using those variable names in `extraPreBwrapCmds` already and relying on them being overwritten; I would consider that chance slim, and the fix would be easy enough - someone using a construct like `false && \` to disable the `ignored` initialisation and effectively working around this limitation; again the chances are slim (even though I know I'd be affected), and the fix would be easy enough (as this change makes the workaround needless anyway so it's an improvement) Signed-off-by: benaryorg <binary@benary.org>
2024-09-25 09:32:41 +00:00
${extraPreBwrapCmds}
# loop through all entries of root in the fhs environment, except its /etc.
for i in ${fhsenv}/*; do
path="/''${i##*/}"
if [[ $path == '/etc' ]]; then
:
elif [[ -L $i ]]; then
symlinks+=(--symlink "$(${coreutils}/bin/readlink "$i")" "$path")
ignored+=("$path")
else
ro_mounts+=(--ro-bind "$i" "$path")
ignored+=("$path")
fi
done
# loop through the entries of /etc in the fhs environment.
if [[ -d ${fhsenv}/etc ]]; then
for i in ${fhsenv}/etc/*; do
path="/''${i##*/}"
# NOTE: we're binding /etc/fonts and /etc/ssl/certs from the host so we
# don't want to override it with a path from the FHS environment.
if [[ $path == '/fonts' || $path == '/ssl' ]]; then
continue
fi
if [[ -L $i ]]; then
symlinks+=(--symlink "$i" "/etc$path")
else
ro_mounts+=(--ro-bind "$i" "/etc$path")
fi
etc_ignored+=("/etc$path")
done
fi
# propagate /etc from the actual host if nested
if [[ -e /.host-etc ]]; then
ro_mounts+=(--ro-bind /.host-etc /.host-etc)
else
ro_mounts+=(--ro-bind /etc /.host-etc)
fi
# link selected etc entries from the actual root
for i in ${escapeShellArgs etcBindEntries}; do
if [[ "''${etc_ignored[@]}" =~ "$i" ]]; then
continue
fi
if [[ -e $i ]]; then
symlinks+=(--symlink "/.host-etc/''${i#/etc/}" "$i")
fi
done
declare -a auto_mounts
# loop through all directories in the root
for dir in /*; do
# if it is a directory and it is not ignored
if [[ -d "$dir" ]] && [[ ! "''${ignored[@]}" =~ "$dir" ]]; then
# add it to the mount list
auto_mounts+=(--bind "$dir" "$dir")
fi
done
declare -a x11_args
# Always mount a tmpfs on /tmp/.X11-unix
# Rationale: https://github.com/flatpak/flatpak/blob/be2de97e862e5ca223da40a895e54e7bf24dbfb9/common/flatpak-run.c#L277
x11_args+=(--tmpfs /tmp/.X11-unix)
# Try to guess X socket path. This doesn't cover _everything_, but it covers some things.
if [[ "$DISPLAY" == *:* ]]; then
# recover display number from $DISPLAY formatted [host]:num[.screen]
display_nr=''${DISPLAY/#*:} # strip host
display_nr=''${display_nr/%.*} # strip screen
local_socket=/tmp/.X11-unix/X$display_nr
x11_args+=(--ro-bind-try "$local_socket" "$local_socket")
fi
${optionalString privateTmp ''
2023-12-21 16:25:21 +00:00
# sddm places XAUTHORITY in /tmp
if [[ "$XAUTHORITY" == /tmp/* ]]; then
x11_args+=(--ro-bind-try "$XAUTHORITY" "$XAUTHORITY")
fi
# dbus-run-session puts the socket in /tmp
IFS=";" read -ra addrs <<<"$DBUS_SESSION_BUS_ADDRESS"
for addr in "''${addrs[@]}"; do
[[ "$addr" == unix:* ]] || continue
IFS="," read -ra parts <<<"''${addr#unix:}"
for part in "''${parts[@]}"; do
printf -v part '%s' "''${part//\\/\\\\}"
printf -v part '%b' "''${part//%/\\x}"
[[ "$part" == path=/tmp/* ]] || continue
x11_args+=(--ro-bind-try "''${part#path=}" "''${part#path=}")
done
done
''}
2023-12-21 16:25:21 +00:00
cmd=(
${bubblewrap}/bin/bwrap
--dev-bind /dev /dev
--proc /proc
--chdir "$(pwd)"
${optionalString unshareUser "--unshare-user"}
${optionalString unshareIpc "--unshare-ipc"}
${optionalString unsharePid "--unshare-pid"}
${optionalString unshareNet "--unshare-net"}
${optionalString unshareUts "--unshare-uts"}
${optionalString unshareCgroup "--unshare-cgroup"}
${optionalString dieWithParent "--die-with-parent"}
--ro-bind /nix /nix
${optionalString privateTmp "--tmpfs /tmp"}
# Our glibc will look for the cache in its own path in `/nix/store`.
# As such, we need a cache to exist there, because pressure-vessel
# depends on the existence of an ld cache. However, adding one
# globally proved to be a bad idea (see #100655), the solution we
# settled on being mounting one via bwrap.
# Also, the cache needs to go to both 32 and 64 bit glibcs, for games
# of both architectures to work.
--tmpfs ${glibc}/etc \
--tmpfs /etc \
--symlink /etc/ld.so.conf ${glibc}/etc/ld.so.conf \
--symlink /etc/ld.so.cache ${glibc}/etc/ld.so.cache \
--ro-bind ${glibc}/etc/rpc ${glibc}/etc/rpc \
--remount-ro ${glibc}/etc \
--symlink ${realInit runScript} /init \
'' + optionalString fhsenv.isMultiBuild (indentLines ''
--tmpfs ${pkgsi686Linux.glibc}/etc \
--symlink /etc/ld.so.conf ${pkgsi686Linux.glibc}/etc/ld.so.conf \
--symlink /etc/ld.so.cache ${pkgsi686Linux.glibc}/etc/ld.so.cache \
--ro-bind ${pkgsi686Linux.glibc}/etc/rpc ${pkgsi686Linux.glibc}/etc/rpc \
--remount-ro ${pkgsi686Linux.glibc}/etc \
'') + ''
"''${ro_mounts[@]}"
"''${symlinks[@]}"
"''${auto_mounts[@]}"
"''${x11_args[@]}"
${concatStringsSep "\n " extraBwrapArgs}
${containerInit} ${initArgs}
)
exec "''${cmd[@]}"
'';
bin = writeShellScript "${name}-bwrap" (bwrapCmd { initArgs = ''"$@"''; });
in runCommandLocal name (nameAttrs // {
inherit nativeBuildInputs meta;
passthru = passthru // {
env = runCommandLocal "${name}-shell-env" {
shellHook = bwrapCmd {};
} ''
echo >&2 ""
echo >&2 "*** User chroot 'env' attributes are intended for interactive nix-shell sessions, not for building! ***"
echo >&2 ""
exit 1
'';
inherit args fhsenv;
};
}) ''
mkdir -p $out/bin
ln -s ${bin} $out/bin/${executableName}
${extraInstallCommands}
''