Merge pull request #16030 from abbradar/fhs-refactor

Improvements for FHS user chrootenv
This commit is contained in:
Nikolay Amiantov 2016-06-11 21:04:20 +04:00 committed by GitHub
commit b341de88e9
11 changed files with 99 additions and 308 deletions

View file

@ -171,42 +171,18 @@ c = lib.makeOverridable f { a = 1; b = 2; }</programlisting>
<section xml:id="sec-fhs-environments">
<title>buildFHSChrootEnv/buildFHSUserEnv</title>
<title>buildFHSUserEnv</title>
<para>
<function>buildFHSChrootEnv</function> and
<function>buildFHSUserEnv</function> provide a way to build and run
FHS-compatible lightweight sandboxes. They get their own isolated root with
binded <filename>/nix/store</filename>, so their footprint in terms of disk
<function>buildFHSUserEnv</function> provides a way to build and run
FHS-compatible lightweight sandboxes. It creates an isolated root with
bound <filename>/nix/store</filename>, so its footprint in terms of disk
space needed is quite small. This allows one to run software which is hard or
unfeasible to patch for NixOS -- 3rd-party source trees with FHS assumptions,
games distributed as tarballs, software with integrity checking and/or external
self-updated binaries.
</para>
<para>
<function>buildFHSChrootEnv</function> allows to create persistent
environments, which can be constructed, deconstructed and entered by
multiple users at once. A downside is that it requires
<literal>root</literal> access for both those who create and destroy and
those who enter it. It can be useful to create environments for daemons that
one can enter and observe.
</para>
<para>
<function>buildFHSUserEnv</function> uses Linux namespaces feature to create
self-updated binaries. It uses Linux namespaces feature to create
temporary lightweight environments which are destroyed after all child
processes exit. It does not require root access, and can be useful to create
sandboxes and wrap applications.
</para>
<para>
Those functions both rely on <function>buildFHSEnv</function>, which creates
an actual directory structure given a list of necessary packages and extra
build commands.
<function>buildFHSChrootEnv</function> and <function>buildFHSUserEnv</function>
both accept those arguments which are passed to
<function>buildFHSEnv</function>:
processes exit, without root user rights requirement. Accepted arguments are:
</para>
<variablelist>
@ -220,14 +196,16 @@ c = lib.makeOverridable f { a = 1; b = 2; }</programlisting>
<term><literal>targetPkgs</literal></term>
<listitem><para>Packages to be installed for the main host's architecture
(i.e. x86_64 on x86_64 installations).</para></listitem>
(i.e. x86_64 on x86_64 installations). Along with libraries binaries are also
installed.</para></listitem>
</varlistentry>
<varlistentry>
<term><literal>multiPkgs</literal></term>
<listitem><para>Packages to be installed for all architectures supported by
a host (i.e. i686 and x86_64 on x86_64 installations).</para></listitem>
a host (i.e. i686 and x86_64 on x86_64 installations). Only libraries are
installed by default.</para></listitem>
</varlistentry>
<varlistentry>
@ -240,29 +218,33 @@ c = lib.makeOverridable f { a = 1; b = 2; }</programlisting>
<varlistentry>
<term><literal>extraBuildCommandsMulti</literal></term>
<listitem><para>Like <literal>extraBuildCommandsMulti</literal>, but
<listitem><para>Like <literal>extraBuildCommands</literal>, but
executed only on multilib architectures.</para></listitem>
</varlistentry>
<varlistentry>
<term><literal>extraOutputsToInstall</literal></term>
<listitem><para>Additional derivation outputs to be linked for both
target and multi-architecture packages.</para></listitem>
</varlistentry>
<varlistentry>
<term><literal>extraInstallCommands</literal></term>
<listitem><para>Additional commands to be executed for finalizing the
derivation with runner script.</para></listitem>
</varlistentry>
<varlistentry>
<term><literal>runScript</literal></term>
<listitem><para>A command that would be executed inside the sandbox and
passed all the command line arguments. It defaults to
<literal>bash</literal>.</para></listitem>
</varlistentry>
</variablelist>
<para>
Additionally, <function>buildFHSUserEnv</function> accepts
<literal>runScript</literal> parameter, which is a command that would be
executed inside the sandbox and passed all the command line arguments. It
default to <literal>bash</literal>.
</para>
<para>
It also uses <literal>CHROOTENV_EXTRA_BINDS</literal> environment variable
for binding extra directories in the sandbox to outside places. The format of
the variable is <literal>/mnt=test-mnt:/data</literal>, where
<literal>/mnt</literal> would be mounted as <literal>/test-mnt</literal>
and <literal>/data</literal> would be mounted as <literal>/data</literal>.
<literal>extraBindMounts</literal> array argument to
<function>buildFHSUserEnv</function> function is prepended to this variable.
Latter entries take priority if defined several times -- i.e. in case of
<literal>/data=data1:/data=data2</literal> the actual bind path would be
<literal>/data2</literal>.
</para>
<para>
One can create a simple environment using a <literal>shell.nix</literal>
like that:

View file

@ -1,48 +0,0 @@
{ stdenv } : { env, extraInstallCommands ? "" } :
let
# References to shell scripts that set up or tear down the environment
initSh = ./init.sh.in;
mountSh = ./mount.sh.in;
loadSh = ./load.sh.in;
umountSh = ./umount.sh.in;
destroySh = ./destroy.sh.in;
name = env.pname;
in stdenv.mkDerivation {
name = "${name}-chrootenv";
preferLocalBuild = true;
buildCommand = ''
mkdir -p $out/bin
cd $out/bin
sed -e "s|@chrootEnv@|${env}|g" \
-e "s|@name@|${name}|g" \
-e "s|@shell@|${stdenv.shell}|g" \
${initSh} > init-${name}-chrootenv
chmod +x init-${name}-chrootenv
sed -e "s|@shell@|${stdenv.shell}|g" \
-e "s|@name@|${name}|g" \
${mountSh} > mount-${name}-chrootenv
chmod +x mount-${name}-chrootenv
sed -e "s|@shell@|${stdenv.shell}|g" \
-e "s|@name@|${name}|g" \
${loadSh} > load-${name}-chrootenv
chmod +x load-${name}-chrootenv
sed -e "s|@shell@|${stdenv.shell}|g" \
-e "s|@name@|${name}|g" \
${umountSh} > umount-${name}-chrootenv
chmod +x umount-${name}-chrootenv
sed -e "s|@chrootEnv@|${env}|g" \
-e "s|@shell@|${stdenv.shell}|g" \
-e "s|@name@|${name}|g" \
${destroySh} > destroy-${name}-chrootenv
chmod +x destroy-${name}-chrootenv
${extraInstallCommands}
'';
}

View file

@ -1,22 +0,0 @@
#! @shell@ -e
chrootenvDest=/run/chrootenv/@name@
# Remove bind mount points
rmdir $chrootenvDest/{dev,nix/store,nix,proc,sys,host-etc,home,var,run,tmp}
# Remove symlinks to the software that should be part of the chroot system profile
for i in @chrootEnv@/*
do
if [ "$i" != "@chrootEnv@/etc" ] && [ "$i" != "@chrootEnv@/var" ]
then
rm $chrootenvDest/$(basename $i)
fi
done
# Remove the remaining folders
rm -Rf $chrootenvDest/{etc,root}
rm -Rf /tmp/chrootenv-@name@
# Remove the chroot environment folder
rmdir $chrootenvDest

View file

@ -1,22 +0,0 @@
#! @shell@ -e
chrootenvDest=/run/chrootenv/@name@
# Create some mount points for stuff that must be bind mounted
mkdir -p $chrootenvDest/{nix/store,dev,proc,sys,host-etc,host-tmp,home,var,run}
# Symlink the software that should be part of the chroot system profile
for i in @chrootEnv@/*
do
if [ "$i" != "@chrootEnv@/var" ]
then
ln -s "$i" "$chrootenvDest"
fi
done
# Create root folder
mkdir $chrootenvDest/root
# Create tmp folder
mkdir -m1777 $chrootenvDest/tmp
mkdir -m1777 -p /tmp/chrootenv-@name@

View file

@ -1,13 +0,0 @@
#! @shell@ -e
chrootenvDest=/run/chrootenv/@name@
# Enter the LFS chroot environment
sudo chroot --userspec "$USER:${GROUPS[0]}" --groups "${GROUPS[0]}" $chrootenvDest /usr/bin/env -i \
TERM="$TERM" \
DISPLAY="$DISPLAY" \
HOME="$HOME" \
XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR" \
LANG="$LANG" \
SSL_CERT_FILE="$SSL_CERT_FILE" \
/bin/bash --login

View file

@ -1,34 +0,0 @@
#! @shell@ -e
chrootenvDest=/run/chrootenv/@name@
# Bind mount the Nix store
mount --bind /nix/store $chrootenvDest/nix/store
# Bind mount some kernel related stuff
mount --bind /dev $chrootenvDest/dev
mount --bind /dev/pts $chrootenvDest/dev/pts
mount --bind /dev/shm $chrootenvDest/dev/shm
mount --bind /proc $chrootenvDest/proc
mount --bind /sys $chrootenvDest/sys
# Bind mount home directories
mount --bind /home $chrootenvDest/home
# Bind mount state directories
mount --bind /var $chrootenvDest/var
mount --rbind /run $chrootenvDest/run
# Bind mount the host system's /etc
mount --bind /etc $chrootenvDest/host-etc
# Bind mount the host system's /tmp
mount --bind /tmp $chrootenvDest/host-tmp
# Bind mount /tmp
mount --bind /tmp/chrootenv-@name@ $chrootenvDest/tmp
# Expose sockets in /tmp
for i in /tmp/.*-unix; do
ln -s "/host-tmp/$(basename "$i")" "$chrootenvDest/$i"
done

View file

@ -1,6 +0,0 @@
#! @shell@ -e
chrootenvDest=/run/chrootenv/@name@
# Unmount all (r)bind mounts
umount -l $chrootenvDest/{dev/pts,dev/shm,dev,nix/store,proc,sys,host-etc,host-tmp,home,var,tmp,run}

View file

@ -2,16 +2,15 @@
# Bind mounts hierarchy: from => to (relative)
# If 'to' is nil, path will be the same
mounts = { '/nix/store' => nil,
'/dev' => nil,
mounts = { '/' => 'host',
'/proc' => nil,
'/sys' => nil,
'/etc' => 'host-etc',
'/tmp' => 'host-tmp',
'/home' => nil,
'/nix' => nil,
'/tmp' => nil,
'/var' => nil,
'/run' => nil,
'/root' => nil,
'/dev' => nil,
'/home' => nil,
}
# Propagate environment variables
@ -62,12 +61,15 @@ $mount = make_fcall 'mount', [Fiddle::TYPE_VOIDP,
Fiddle::TYPE_INT
# Read command line args
abort "Usage: chrootenv swdir program args..." unless ARGV.length >= 2
swdir = Pathname.new ARGV[0]
execp = ARGV.drop 1
abort "Usage: chrootenv program args..." unless ARGV.length >= 1
execp = ARGV
# Populate extra mounts
if not ENV["CHROOTENV_EXTRA_BINDS"].nil?
$stderr.puts "CHROOTENV_EXTRA_BINDS is discussed for deprecation."
$stderr.puts "If you have a usecase, please drop a note in issue #16030."
$stderr.puts "Notice that we now bind-mount host FS to '/host' and symlink all directories from it to '/' by default."
for extra in ENV["CHROOTENV_EXTRA_BINDS"].split(':')
paths = extra.split('=')
if not paths.empty?
@ -132,24 +134,6 @@ if $cpid == 0
Dir.chroot root
Dir.chdir '/'
# Symlink swdir hierarchy
mount_dirs = Set.new mounts.map { |_, v| Pathname.new v }
link_swdir = lambda do |swdir, prefix|
swdir.find do |path|
rel = prefix.join path.relative_path_from(swdir)
# Don't symlink anything in binded or symlinked directories
Find.prune if mount_dirs.include? rel or rel.symlink?
if not rel.directory?
# File does not exist; make a symlink and bail out
rel.make_symlink path
Find.prune
end
# Recursively follow symlinks
link_swdir.call path.readlink, rel if path.symlink?
end
end
link_swdir.call swdir, Pathname.new('')
# New environment
new_env = Hash[ envvars.map { |x| [x, ENV[x]] } ]

View file

@ -1,28 +1,29 @@
{ runCommand, lib, writeText, writeScriptBin, stdenv, ruby } :
{ env, runScript ? "bash", extraBindMounts ? [], extraInstallCommands ? "", meta ? {}, passthru ? {} } :
{ callPackage, runCommand, lib, writeScript, stdenv, coreutils, ruby }:
let buildFHSEnv = callPackage ./env.nix { }; in
args@{ name, runScript ? "bash", extraBindMounts ? [], extraInstallCommands ? "", meta ? {}, passthru ? {}, ... }:
let
name = env.pname;
env = buildFHSEnv (removeAttrs args [ "runScript" "extraBindMounts" "extraInstallCommands" "meta" "passthru" ]);
# Sandboxing script
chroot-user = writeScriptBin "chroot-user" ''
chroot-user = writeScript "chroot-user" ''
#! ${ruby}/bin/ruby
${builtins.readFile ./chroot-user.rb}
'';
init = run: writeText "${name}-init" ''
source /etc/profile
# Make /tmp directory
mkdir -m 1777 /tmp
# Expose sockets in /tmp
for i in /host-tmp/.*-unix; do
ln -s "$i" "/tmp/$(basename "$i")"
init = run: writeScript "${name}-init" ''
#! ${stdenv.shell}
for i in ${env}/* /host/*; do
path="/''${i##*/}"
[ -e "$path" ] || ${coreutils}/bin/ln -s "$i" "$path"
done
[ -d "$1" ] && [ -r "$1" ] && cd "$1"
shift
source /etc/profile
exec ${run} "$@"
'';
@ -32,7 +33,7 @@ in runCommand name {
env = runCommand "${name}-shell-env" {
shellHook = ''
export CHROOTENV_EXTRA_BINDS="${lib.concatStringsSep ":" extraBindMounts}:$CHROOTENV_EXTRA_BINDS"
exec ${chroot-user}/bin/chroot-user ${env} bash ${init "bash"} "$(pwd)"
exec ${chroot-user} ${init "bash"} "$(pwd)"
'';
} ''
echo >&2 ""
@ -46,7 +47,7 @@ in runCommand name {
cat <<EOF >$out/bin/${name}
#! ${stdenv.shell}
export CHROOTENV_EXTRA_BINDS="${lib.concatStringsSep ":" extraBindMounts}:\$CHROOTENV_EXTRA_BINDS"
exec ${chroot-user}/bin/chroot-user ${env} bash ${init runScript} "\$(pwd)" "\$@"
exec ${chroot-user} ${init runScript} "\$(pwd)" "\$@"
EOF
chmod +x $out/bin/${name}
${extraInstallCommands}

View file

@ -1,7 +1,7 @@
{ nixpkgs, nixpkgs_i686, system
} :
{ stdenv, buildEnv, writeText, pkgs, pkgsi686Linux, system }:
{ name, profile ? ""
, pkgs ? null, targetPkgs ? pkgs: [], multiPkgs ? pkgs: []
, targetPkgs ? pkgs: [], multiPkgs ? pkgs: []
, extraBuildCommands ? "", extraBuildCommandsMulti ? ""
, extraOutputsToInstall ? []
}:
@ -22,37 +22,32 @@
# /lib will link to /lib32
let
isMultiBuild = pkgs == null && multiPkgs != null && system == "x86_64-linux";
is64Bit = system == "x86_64-linux";
isMultiBuild = multiPkgs != null && is64Bit;
isTargetBuild = !isMultiBuild;
# support deprecated "pkgs" option.
targetPkgs' =
if pkgs != null
then builtins.trace "buildFHSEnv: 'pkgs' option is deprecated, use 'targetPkgs'" (pkgs': pkgs)
else targetPkgs;
# list of packages (usually programs) which are only be installed for the
# host's architecture
targetPaths = targetPkgs' nixpkgs ++ (if multiPkgs == null then [] else multiPkgs nixpkgs);
targetPaths = targetPkgs pkgs ++ (if multiPkgs == null then [] else multiPkgs pkgs);
# list of packages which are installed for both x86 and x86_64 on x86_64
# systems
multiPaths = multiPkgs nixpkgs_i686;
multiPaths = multiPkgs pkgsi686Linux;
# base packages of the chroot
# these match the host's architecture, glibc_multi is used for multilib
# builds.
basePkgs = with nixpkgs;
basePkgs = with pkgs;
[ (if isMultiBuild then glibc_multi else glibc)
gcc.cc.lib bashInteractive coreutils less shadow su
(toString gcc.cc.lib) bashInteractive coreutils less shadow su
gawk diffutils findutils gnused gnugrep
gnutar gzip bzip2 xz glibcLocales
];
baseMultiPkgs = with nixpkgs_i686;
[ gcc.cc.lib
baseMultiPkgs = with pkgsi686Linux;
[ (toString gcc.cc.lib)
];
etcProfile = nixpkgs.writeText "profile" ''
etcProfile = writeText "profile" ''
export PS1='${name}-chrootenv:\u@\h:\w\$ '
export LOCALE_ARCHIVE='/usr/lib/locale/locale-archive'
export LD_LIBRARY_PATH='/run/opengl-driver/lib:/run/opengl-driver-32/lib:/usr/lib:/usr/lib32'
@ -67,7 +62,7 @@ let
'';
# Compose /etc for the chroot environment
etcPkg = nixpkgs.stdenv.mkDerivation {
etcPkg = stdenv.mkDerivation {
name = "${name}-chrootenv-etc";
buildCommand = ''
mkdir -p $out/etc
@ -77,38 +72,38 @@ let
ln -s ${etcProfile} profile
# compatibility with NixOS
ln -s /host-etc/static static
ln -s /host/etc/static static
# symlink some NSS stuff
ln -s /host-etc/passwd passwd
ln -s /host-etc/group group
ln -s /host-etc/shadow shadow
ln -s /host-etc/hosts hosts
ln -s /host-etc/resolv.conf resolv.conf
ln -s /host-etc/nsswitch.conf nsswitch.conf
ln -s /host/etc/passwd passwd
ln -s /host/etc/group group
ln -s /host/etc/shadow shadow
ln -s /host/etc/hosts hosts
ln -s /host/etc/resolv.conf resolv.conf
ln -s /host/etc/nsswitch.conf nsswitch.conf
# symlink sudo and su stuff
ln -s /host-etc/login.defs login.defs
ln -s /host-etc/sudoers sudoers
ln -s /host-etc/sudoers.d sudoers.d
ln -s /host/etc/login.defs login.defs
ln -s /host/etc/sudoers sudoers
ln -s /host/etc/sudoers.d sudoers.d
# symlink other core stuff
ln -s /host-etc/localtime localtime
ln -s /host-etc/machine-id machine-id
ln -s /host-etc/os-release os-release
ln -s /host/etc/localtime localtime
ln -s /host/etc/machine-id machine-id
ln -s /host/etc/os-release os-release
# symlink PAM stuff
ln -s /host-etc/pam.d pam.d
ln -s /host/etc/pam.d pam.d
# symlink fonts stuff
ln -s /host-etc/fonts fonts
ln -s /host/etc/fonts fonts
# symlink ALSA stuff
ln -s /host-etc/asound.conf asound.conf
ln -s /host/etc/asound.conf asound.conf
# symlink SSL certs
mkdir -p ssl
ln -s /host-etc/ssl/certs ssl/certs
ln -s /host/etc/ssl/certs ssl/certs
# symlink /etc/mtab -> /proc/mounts (compat for old userspace progs)
ln -s /proc/mounts mtab
@ -116,26 +111,25 @@ let
};
# Composes a /usr-like directory structure
staticUsrProfileTarget = nixpkgs.buildEnv {
staticUsrProfileTarget = buildEnv {
name = "${name}-usr-target";
paths = [ etcPkg ] ++ basePkgs ++ targetPaths;
extraOutputsToInstall = [ "lib" "out" ] ++ extraOutputsToInstall;
extraOutputsToInstall = [ "out" "lib" "bin" ] ++ extraOutputsToInstall;
ignoreCollisions = true;
};
staticUsrProfileMulti = nixpkgs.buildEnv {
staticUsrProfileMulti = buildEnv {
name = "${name}-usr-multi";
paths = baseMultiPkgs ++ multiPaths;
extraOutputsToInstall = [ "lib" "out" ] ++ extraOutputsToInstall;
extraOutputsToInstall = [ "out" "lib" ] ++ extraOutputsToInstall;
ignoreCollisions = true;
};
# setup library paths only for the targeted architecture
setupLibDirs_target = ''
mkdir -m0755 lib
# copy content of targetPaths
cp -rsHf ${staticUsrProfileTarget}/lib/* lib/
# link content of targetPaths
cp -rsHf ${staticUsrProfileTarget}/lib lib
ln -s lib lib${if is64Bit then "64" else "32"}
'';
# setup /lib, /lib32 and /lib64
@ -184,7 +178,7 @@ let
done
'';
in nixpkgs.stdenv.mkDerivation {
in stdenv.mkDerivation {
name = "${name}-fhs";
buildCommand = ''
mkdir -p $out
@ -196,7 +190,4 @@ in nixpkgs.stdenv.mkDerivation {
${if isMultiBuild then extraBuildCommandsMulti else ""}
'';
preferLocalBuild = true;
passthru = {
pname = name;
};
}

View file

@ -118,29 +118,7 @@ in
buildEnv = callPackage ../build-support/buildenv { }; # not actually a package
buildFHSEnv = callPackage ../build-support/build-fhs-chrootenv/env.nix {
nixpkgs = pkgs;
nixpkgs_i686 = pkgsi686Linux;
};
chrootFHSEnv = callPackage ../build-support/build-fhs-chrootenv { };
userFHSEnv = callPackage ../build-support/build-fhs-userenv {
ruby = ruby_2_1;
};
buildFHSChrootEnv = args: chrootFHSEnv {
env = buildFHSEnv (removeAttrs args [ "extraInstallCommands" ]);
extraInstallCommands = args.extraInstallCommands or "";
};
buildFHSUserEnv = args: userFHSEnv {
env = buildFHSEnv (removeAttrs args [ "runScript" "extraBindMounts" "extraInstallCommands" "meta" "passthru" ]);
runScript = args.runScript or "bash";
extraBindMounts = args.extraBindMounts or [];
extraInstallCommands = args.extraInstallCommands or "";
meta = args.meta or {};
passthru = args.passthru or {};
};
buildFHSUserEnv = callPackage ../build-support/build-fhs-userenv { };
buildMaven = callPackage ../build-support/build-maven.nix {};