* Do chroot builds in a private namespace. This means that all the
bind-mounts we do are only visible to the builder process and its children. So accidentally doing "rm -rf" on the chroot directory won't wipe out /nix/store and other bind-mounted directories anymore. Also, the bind-mounts in the private namespace disappear automatically when the builder exits.
This commit is contained in:
parent
07cdfb09fb
commit
7c54f1603f
2 changed files with 46 additions and 134 deletions
|
@ -90,6 +90,7 @@ AC_LANG_POP(C++)
|
||||||
|
|
||||||
# Check for chroot support (requires chroot() and bind mounts).
|
# Check for chroot support (requires chroot() and bind mounts).
|
||||||
AC_CHECK_FUNCS([chroot])
|
AC_CHECK_FUNCS([chroot])
|
||||||
|
AC_CHECK_HEADERS([sched.h], [], [], [])
|
||||||
AC_CHECK_HEADERS([sys/param.h], [], [], [])
|
AC_CHECK_HEADERS([sys/param.h], [], [], [])
|
||||||
AC_CHECK_HEADERS([sys/mount.h], [], [],
|
AC_CHECK_HEADERS([sys/mount.h], [], [],
|
||||||
[#ifdef HAVE_SYS_PARAM_H
|
[#ifdef HAVE_SYS_PARAM_H
|
||||||
|
|
|
@ -33,8 +33,11 @@
|
||||||
#if HAVE_SYS_MOUNT_H
|
#if HAVE_SYS_MOUNT_H
|
||||||
#include <sys/mount.h>
|
#include <sys/mount.h>
|
||||||
#endif
|
#endif
|
||||||
|
#if HAVE_SCHED_H
|
||||||
|
#include <sched.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
#define CHROOT_ENABLED HAVE_CHROOT && HAVE_SYS_MOUNT_H && defined(MS_BIND)
|
#define CHROOT_ENABLED HAVE_CHROOT && HAVE_SYS_MOUNT_H && defined(MS_BIND) && defined(CLONE_NEWNS)
|
||||||
|
|
||||||
|
|
||||||
namespace nix {
|
namespace nix {
|
||||||
|
@ -603,89 +606,6 @@ void deletePathWrapped(const Path & path)
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
/* Helper RAII class for automatically unmounting bind-mounts in
|
|
||||||
chroots. */
|
|
||||||
struct BindMount
|
|
||||||
{
|
|
||||||
Path source, target;
|
|
||||||
Paths created;
|
|
||||||
|
|
||||||
BindMount()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
BindMount(const Path & source, const Path & target)
|
|
||||||
{
|
|
||||||
bind(source, target);
|
|
||||||
}
|
|
||||||
|
|
||||||
~BindMount()
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
unbind();
|
|
||||||
} catch (...) {
|
|
||||||
ignoreException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void bind(const Path & source, const Path & target)
|
|
||||||
{
|
|
||||||
#if CHROOT_ENABLED
|
|
||||||
debug(format("bind mounting `%1%' to `%2%'") % source % target);
|
|
||||||
|
|
||||||
this->source = source;
|
|
||||||
this->target = target;
|
|
||||||
|
|
||||||
created = createDirs(target);
|
|
||||||
|
|
||||||
if (mount(source.c_str(), target.c_str(), "", MS_BIND, 0) == -1)
|
|
||||||
throw SysError(format("bind mount from `%1%' to `%2%' failed") % source % target);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
void unbind()
|
|
||||||
{
|
|
||||||
#if CHROOT_ENABLED
|
|
||||||
if (source == "") return;
|
|
||||||
|
|
||||||
debug(format("unmount bind-mount `%1%'") % target);
|
|
||||||
|
|
||||||
/* Urgh. Unmount sometimes doesn't succeed right away because
|
|
||||||
the mount point is still busy. It shouldn't be, because
|
|
||||||
we've killed all the build processes by now (at least when
|
|
||||||
using a build user; see the check in killUser()). But
|
|
||||||
maybe this is because those processes are still zombies and
|
|
||||||
are keeping some kernel structures busy (open files,
|
|
||||||
current directories, etc.). So retry a few times
|
|
||||||
(actually, a 1 second sleep is almost certainly enough for
|
|
||||||
the zombies to be cleaned up). */
|
|
||||||
unsigned int tries = 0;
|
|
||||||
while (umount(target.c_str()) == -1) {
|
|
||||||
if (errno == EBUSY && ++tries < 10) {
|
|
||||||
printMsg(lvlError, format("unmounting `%1%' failed, retrying after 1 second...") % target);
|
|
||||||
sleep(1);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
throw SysError(format("unmounting bind-mount `%1%' failed") % target);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Get rid of the directories for the mount point created in
|
|
||||||
bind(). */
|
|
||||||
for (Paths::reverse_iterator i = created.rbegin(); i != created.rend(); ++i) {
|
|
||||||
debug(format("deleting `%1%'") % *i);
|
|
||||||
if (remove(i->c_str()) == -1)
|
|
||||||
throw SysError(format("cannot unlink `%1%'") % *i);
|
|
||||||
}
|
|
||||||
|
|
||||||
source = "";
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
|
|
||||||
class DerivationGoal : public Goal
|
class DerivationGoal : public Goal
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
|
@ -729,16 +649,8 @@ private:
|
||||||
/* Whether we're currently doing a chroot build. */
|
/* Whether we're currently doing a chroot build. */
|
||||||
bool useChroot;
|
bool useChroot;
|
||||||
|
|
||||||
/* RAII objects to delete the chroot directory and its /tmp
|
/* RAII object to delete the chroot directory. */
|
||||||
directory. Don't change the order: autoDelChrootTmp has to be
|
boost::shared_ptr<AutoDelete> autoDelChroot;
|
||||||
deleted before autoDelChrootRoot. */
|
|
||||||
boost::shared_ptr<AutoDelete> autoDelChrootRoot;
|
|
||||||
boost::shared_ptr<AutoDelete> autoDelChrootTmp;
|
|
||||||
|
|
||||||
/* In chroot builds, the list of bind mounts currently active.
|
|
||||||
The destructor of BindMount will cause the binds to be
|
|
||||||
unmounted. Keep this *below* autoDelChroot* just to be safe! */
|
|
||||||
list<boost::shared_ptr<BindMount> > bindMounts;
|
|
||||||
|
|
||||||
typedef void (DerivationGoal::*GoalState)();
|
typedef void (DerivationGoal::*GoalState)();
|
||||||
GoalState state;
|
GoalState state;
|
||||||
|
@ -1183,8 +1095,8 @@ void DerivationGoal::buildDone()
|
||||||
|
|
||||||
deleteTmpDir(true);
|
deleteTmpDir(true);
|
||||||
|
|
||||||
/* In chroot builds, unmount the bind mounts ASAP. */
|
/* Delete the chroot (if we were using one). */
|
||||||
bindMounts.clear(); /* the destructors will do the rest */
|
autoDelChroot.reset(); /* this runs the destructor */
|
||||||
|
|
||||||
/* Compute the FS closure of the outputs and register them as
|
/* Compute the FS closure of the outputs and register them as
|
||||||
being valid. */
|
being valid. */
|
||||||
|
@ -1737,29 +1649,18 @@ void DerivationGoal::startBuilder()
|
||||||
is somewhat pointless anyway. */
|
is somewhat pointless anyway. */
|
||||||
useChroot = queryBoolSetting("build-use-chroot", false);
|
useChroot = queryBoolSetting("build-use-chroot", false);
|
||||||
Path chrootRootDir;
|
Path chrootRootDir;
|
||||||
|
Paths dirsInChroot;
|
||||||
|
|
||||||
if (fixedOutput) useChroot = false;
|
if (fixedOutput) useChroot = false;
|
||||||
|
|
||||||
if (useChroot) {
|
if (useChroot) {
|
||||||
#if CHROOT_ENABLED
|
#if CHROOT_ENABLED
|
||||||
/* Create a temporary directory in which we set up the chroot
|
/* Create a temporary directory in which we set up the chroot
|
||||||
environment using bind-mounts.
|
environment using bind-mounts. */
|
||||||
|
|
||||||
!!! Bind mounts are potentially dangerous: if the user
|
|
||||||
cleans up his system by doing "rm -rf
|
|
||||||
/nix/var/nix/chroots/*", this will recurse into /nix/store
|
|
||||||
via the bind mounts (and potentially other parts of the
|
|
||||||
filesystem, depending on the setting of the
|
|
||||||
`build-chroot-dirs' option). */
|
|
||||||
chrootRootDir = createTempDir(nixChrootsDir, "chroot-nix");
|
chrootRootDir = createTempDir(nixChrootsDir, "chroot-nix");
|
||||||
|
|
||||||
/* Clean up the chroot directory automatically, but don't
|
/* Clean up the chroot directory automatically. */
|
||||||
recurse; that would be very very bad if the unmount of a
|
autoDelChroot = boost::shared_ptr<AutoDelete>(new AutoDelete(chrootRootDir));
|
||||||
bind-mount fails. Instead BindMount::unbind() unmounts and
|
|
||||||
deletes exactly those directories that it created to
|
|
||||||
produce the mount point, so that after all the BindMount
|
|
||||||
destructors have run, chrootRootDir should be empty. */
|
|
||||||
autoDelChrootRoot = boost::shared_ptr<AutoDelete>(new AutoDelete(chrootRootDir, false));
|
|
||||||
|
|
||||||
printMsg(lvlChatty, format("setting up chroot environment in `%1%'") % chrootRootDir);
|
printMsg(lvlChatty, format("setting up chroot environment in `%1%'") % chrootRootDir);
|
||||||
|
|
||||||
|
@ -1772,12 +1673,6 @@ void DerivationGoal::startBuilder()
|
||||||
if (chmod(chrootTmpDir.c_str(), 01777) == -1)
|
if (chmod(chrootTmpDir.c_str(), 01777) == -1)
|
||||||
throw SysError("creating /tmp in the chroot");
|
throw SysError("creating /tmp in the chroot");
|
||||||
|
|
||||||
/* When deleting this, do recurse (the builder might have left
|
|
||||||
crap there). As long as nothing important is bind-mounted
|
|
||||||
under /tmp it's okay (and the bind-mounts are unmounted
|
|
||||||
before autoDelChrootTmp's destructor runs, anyway). */
|
|
||||||
autoDelChrootTmp = boost::shared_ptr<AutoDelete>(new AutoDelete(chrootTmpDir));
|
|
||||||
|
|
||||||
/* Bind-mount a user-configurable set of directories from the
|
/* Bind-mount a user-configurable set of directories from the
|
||||||
host file system. The `/dev/pts' directory must be mounted
|
host file system. The `/dev/pts' directory must be mounted
|
||||||
separately so that newly-created pseudo-terminals show
|
separately so that newly-created pseudo-terminals show
|
||||||
|
@ -1787,17 +1682,10 @@ void DerivationGoal::startBuilder()
|
||||||
defaultDirs.push_back("/dev/pts");
|
defaultDirs.push_back("/dev/pts");
|
||||||
defaultDirs.push_back("/proc");
|
defaultDirs.push_back("/proc");
|
||||||
|
|
||||||
Paths dirsInChroot = querySetting("build-chroot-dirs", defaultDirs);
|
dirsInChroot = querySetting("build-chroot-dirs", defaultDirs);
|
||||||
|
|
||||||
dirsInChroot.push_front(nixStore);
|
dirsInChroot.push_front(nixStore);
|
||||||
dirsInChroot.push_front(tmpDir);
|
dirsInChroot.push_front(tmpDir);
|
||||||
|
|
||||||
/* Push BindMounts at the front of the list so that they get
|
|
||||||
unmounted in LIFO order. (!!! Does the C++ standard
|
|
||||||
guarantee that list elements are destroyed in order?) */
|
|
||||||
for (Paths::iterator i = dirsInChroot.begin(); i != dirsInChroot.end(); ++i)
|
|
||||||
bindMounts.push_front(boost::shared_ptr<BindMount>(new BindMount(*i, chrootRootDir + *i)));
|
|
||||||
|
|
||||||
#else
|
#else
|
||||||
throw Error("chroot builds are not supported on this platform");
|
throw Error("chroot builds are not supported on this platform");
|
||||||
#endif
|
#endif
|
||||||
|
@ -1829,14 +1717,37 @@ void DerivationGoal::startBuilder()
|
||||||
try { /* child */
|
try { /* child */
|
||||||
|
|
||||||
#if CHROOT_ENABLED
|
#if CHROOT_ENABLED
|
||||||
/* If building in a chroot, do the chroot right away.
|
if (useChroot) {
|
||||||
initChild() will do a chdir() to the temporary build
|
/* Create our own mount namespace. This means that
|
||||||
directory to make sure the current directory is in the
|
all the bind mounts we do will only show up in this
|
||||||
chroot. (Actually the order doesn't matter, since due
|
process and its children, and will disappear
|
||||||
to the bind mount tmpDir and tmpRootDit/tmpDir are the
|
automatically when we're done. */
|
||||||
same directories.) */
|
if (unshare(CLONE_NEWNS) == -1)
|
||||||
if (useChroot && chroot(chrootRootDir.c_str()) == -1)
|
throw SysError(format("cannot set up a private mount namespace"));
|
||||||
throw SysError(format("cannot change root directory to `%1%'") % chrootRootDir);
|
|
||||||
|
/* Bind-mount all the directories from the "host"
|
||||||
|
filesystem that we want in the chroot
|
||||||
|
environment. */
|
||||||
|
foreach (Paths::iterator, i, dirsInChroot) {
|
||||||
|
Path source = *i;
|
||||||
|
Path target = chrootRootDir + source;
|
||||||
|
printMsg(lvlError, format("bind mounting `%1%' to `%2%'") % source % target);
|
||||||
|
|
||||||
|
createDirs(target);
|
||||||
|
|
||||||
|
if (mount(source.c_str(), target.c_str(), "", MS_BIND, 0) == -1)
|
||||||
|
throw SysError(format("bind mount from `%1%' to `%2%' failed") % source % target);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Do the chroot(). initChild() will do a chdir() to
|
||||||
|
the temporary build directory to make sure the
|
||||||
|
current directory is in the chroot. (Actually the
|
||||||
|
order doesn't matter, since due to the bind mount
|
||||||
|
tmpDir and tmpRootDit/tmpDir are the same
|
||||||
|
directories.) */
|
||||||
|
if (chroot(chrootRootDir.c_str()) == -1)
|
||||||
|
throw SysError(format("cannot change root directory to `%1%'") % chrootRootDir);
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
initChild();
|
initChild();
|
||||||
|
|
Loading…
Reference in a new issue