libstore: move some linux-specific child setup to platform code
best viewed with --color-moved --color-moved-ws=all Change-Id: I3738f07fd0b39498abf253967906270dc0b215f4
This commit is contained in:
@@ -62,7 +62,6 @@
|
||||
#include <sys/param.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/syscall.h>
|
||||
#define pivot_root(new_root, put_old) (syscall(SYS_pivot_root, new_root, put_old))
|
||||
#endif
|
||||
|
||||
#if __APPLE__
|
||||
@@ -1135,8 +1134,6 @@ void LocalDerivationGoal::runChild()
|
||||
|
||||
setupSyscallFilter();
|
||||
|
||||
bool setUser = true;
|
||||
|
||||
/* Make the contents of netrc and the CA certificate bundle
|
||||
available to builtin:fetchurl (which may run under a
|
||||
different uid and/or in a sandbox). */
|
||||
@@ -1156,337 +1153,7 @@ void LocalDerivationGoal::runChild()
|
||||
setupConfiguredCertificateAuthority();
|
||||
}
|
||||
|
||||
#if __linux__
|
||||
if (useChroot) {
|
||||
|
||||
userNamespaceSync.writeSide.reset();
|
||||
|
||||
if (drainFD(userNamespaceSync.readSide.get()) != "1") {
|
||||
throw Error("user namespace initialisation failed");
|
||||
}
|
||||
|
||||
userNamespaceSync.readSide.reset();
|
||||
|
||||
if (privateNetwork) {
|
||||
|
||||
/* Initialise the loopback interface. */
|
||||
AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP));
|
||||
if (!fd) {
|
||||
throw SysError("cannot open IP socket");
|
||||
}
|
||||
|
||||
struct ifreq ifr;
|
||||
strcpy(ifr.ifr_name, "lo");
|
||||
ifr.ifr_flags = IFF_UP | IFF_LOOPBACK | IFF_RUNNING;
|
||||
if (ioctl(fd.get(), SIOCSIFFLAGS, &ifr) == -1) {
|
||||
throw SysError("cannot set loopback interface flags");
|
||||
}
|
||||
}
|
||||
|
||||
/* Set the hostname etc. to fixed values. */
|
||||
char hostname[] = "localhost";
|
||||
if (sethostname(hostname, sizeof(hostname)) == -1) {
|
||||
throw SysError("cannot set host name");
|
||||
}
|
||||
char domainname[] = "(none)"; // kernel default
|
||||
if (setdomainname(domainname, sizeof(domainname)) == -1) {
|
||||
throw SysError("cannot set domain name");
|
||||
}
|
||||
|
||||
/* Make all filesystems private. This is necessary
|
||||
because subtrees may have been mounted as "shared"
|
||||
(MS_SHARED). (Systemd does this, for instance.) Even
|
||||
though we have a private mount namespace, mounting
|
||||
filesystems on top of a shared subtree still propagates
|
||||
outside of the namespace. Making a subtree private is
|
||||
local to the namespace, though, so setting MS_PRIVATE
|
||||
does not affect the outside world. */
|
||||
if (mount(0, "/", 0, MS_PRIVATE | MS_REC, 0) == -1) {
|
||||
throw SysError("unable to make '/' private");
|
||||
}
|
||||
|
||||
/* Bind-mount chroot directory to itself, to treat it as a
|
||||
different filesystem from /, as needed for pivot_root. */
|
||||
if (sys::mount(chrootRootDir, chrootRootDir, "", MS_BIND, 0) == -1) {
|
||||
throw SysError("unable to bind mount '%1%'", chrootRootDir);
|
||||
}
|
||||
|
||||
/* Bind-mount the sandbox's Nix store onto itself so that
|
||||
we can mark it as a "shared" subtree, allowing bind
|
||||
mounts made in *this* mount namespace to be propagated
|
||||
into the child namespace created by the
|
||||
unshare(CLONE_NEWNS) call below.
|
||||
|
||||
Marking chrootRootDir as MS_SHARED causes pivot_root()
|
||||
to fail with EINVAL. Don't know why. */
|
||||
Path chrootStoreDir = chrootRootDir + worker.store.config().storeDir;
|
||||
|
||||
if (sys::mount(chrootStoreDir, chrootStoreDir, "", MS_BIND, 0) == -1) {
|
||||
throw SysError("unable to bind mount the Nix store", chrootStoreDir);
|
||||
}
|
||||
|
||||
if (sys::mount("", chrootStoreDir, "", MS_SHARED, 0) == -1) {
|
||||
throw SysError("unable to make '%s' shared", chrootStoreDir);
|
||||
}
|
||||
|
||||
/* Set up a nearly empty /dev, unless the user asked to
|
||||
bind-mount the host /dev. */
|
||||
Strings ss;
|
||||
if (pathsInChroot.find("/dev") == pathsInChroot.end()) {
|
||||
createDirs(chrootRootDir + "/dev/shm");
|
||||
createDirs(chrootRootDir + "/dev/pts");
|
||||
ss.push_back("/dev/full");
|
||||
if (worker.store.config().systemFeatures.get().count("kvm") && pathExists("/dev/kvm")) {
|
||||
ss.push_back("/dev/kvm");
|
||||
}
|
||||
ss.push_back("/dev/null");
|
||||
ss.push_back("/dev/random");
|
||||
ss.push_back("/dev/tty");
|
||||
ss.push_back("/dev/urandom");
|
||||
ss.push_back("/dev/zero");
|
||||
createSymlink("/proc/self/fd", chrootRootDir + "/dev/fd");
|
||||
createSymlink("/proc/self/fd/0", chrootRootDir + "/dev/stdin");
|
||||
createSymlink("/proc/self/fd/1", chrootRootDir + "/dev/stdout");
|
||||
createSymlink("/proc/self/fd/2", chrootRootDir + "/dev/stderr");
|
||||
}
|
||||
|
||||
/* Fixed-output derivations typically need to access the
|
||||
network, so give them access to /etc/resolv.conf and so
|
||||
on. */
|
||||
if (!derivationType->isSandboxed()) {
|
||||
// Only use nss functions to resolve hosts and
|
||||
// services. Don’t use it for anything else that may
|
||||
// be configured for this system. This limits the
|
||||
// potential impurities introduced in fixed-outputs.
|
||||
writeFile(chrootRootDir + "/etc/nsswitch.conf", "hosts: files dns\nservices: files\n");
|
||||
|
||||
/* N.B. it is realistic that these paths might not exist. It
|
||||
happens when testing Nix building fixed-output derivations
|
||||
within a pure derivation. */
|
||||
for (auto & path : {"/etc/services", "/etc/hosts"}) {
|
||||
if (pathAccessible(path, true)) {
|
||||
// Copy the actual file, not the symlink, because we don't know where
|
||||
// the symlink is pointing, and we don't want to chase down the entire
|
||||
// chain.
|
||||
//
|
||||
// This means if your network config changes during a FOD build,
|
||||
// the DNS in the sandbox will be wrong. However, this is pretty unlikely
|
||||
// to actually be a problem, because FODs are generally pretty fast,
|
||||
// and machines with often-changing network configurations probably
|
||||
// want to run resolved or some other local resolver anyway.
|
||||
//
|
||||
// There's also just no simple way to do this correctly, you have to manually
|
||||
// inotify watch the files for changes on the outside and update the sandbox
|
||||
// while the build is running (or at least that's what Flatpak does).
|
||||
//
|
||||
// I also just generally feel icky about modifying sandbox state under a build,
|
||||
// even though it really shouldn't be a big deal. -K900
|
||||
copyFile(path, chrootRootDir + path, {.followSymlinks = true});
|
||||
} else if (pathExists(path)) {
|
||||
// The path exist but we were not able to access it. This is not a fatal
|
||||
// error, warn about this so the user can remediate.
|
||||
printTaggedWarning(
|
||||
"'%1%' exists but is inaccessible, it will not be copied in the "
|
||||
"sandbox",
|
||||
path
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (pathAccessible("/etc/resolv.conf", true)) {
|
||||
const auto resolvConf = rewriteResolvConf(readFile("/etc/resolv.conf"));
|
||||
writeFile(chrootRootDir + "/etc/resolv.conf", resolvConf);
|
||||
} else if (pathExists("/etc/resolv.conf")) {
|
||||
// The path exist but we were not able to access it. This is not a fatal error,
|
||||
// warn about this so the user can remediate.
|
||||
printTaggedWarning(
|
||||
"'/etc/resolv.conf' exists but is inaccessible, it will not be rewritten "
|
||||
"inside the sandbox; DNS operations inside the sandbox may be "
|
||||
"non-functional."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto & i : ss) {
|
||||
pathsInChroot.emplace(i, i);
|
||||
}
|
||||
|
||||
/* Bind-mount all the directories from the "host"
|
||||
filesystem that we want in the chroot
|
||||
environment. */
|
||||
for (auto & i : pathsInChroot) {
|
||||
if (i.second.source == "/proc") {
|
||||
continue; // backwards compatibility
|
||||
}
|
||||
|
||||
#if HAVE_EMBEDDED_SANDBOX_SHELL
|
||||
if (i.second.source == "__embedded_sandbox_shell__") {
|
||||
static unsigned char sh[] = {
|
||||
#include "embedded-sandbox-shell.gen.hh"
|
||||
};
|
||||
auto dst = chrootRootDir + i.first;
|
||||
createDirs(dirOf(dst));
|
||||
writeFile(dst, std::string_view((const char *) sh, sizeof(sh)));
|
||||
chmodPath(dst, 0555);
|
||||
} else
|
||||
#endif
|
||||
bindPath(i.second.source, chrootRootDir + i.first, i.second.optional);
|
||||
}
|
||||
|
||||
/* Bind a new instance of procfs on /proc. */
|
||||
createDirs(chrootRootDir + "/proc");
|
||||
if (sys::mount("none", chrootRootDir + "/proc", "proc", 0, 0) == -1) {
|
||||
throw SysError("mounting /proc");
|
||||
}
|
||||
|
||||
/* Mount sysfs on /sys. */
|
||||
if (buildUser && buildUser->getUIDCount() != 1) {
|
||||
createDirs(chrootRootDir + "/sys");
|
||||
if (sys::mount("none", chrootRootDir + "/sys", "sysfs", 0, 0) == -1) {
|
||||
throw SysError("mounting /sys");
|
||||
}
|
||||
}
|
||||
|
||||
/* Mount a new tmpfs on /dev/shm to ensure that whatever
|
||||
the builder puts in /dev/shm is cleaned up automatically. */
|
||||
if (pathExists("/dev/shm")
|
||||
&& sys::mount(
|
||||
"none",
|
||||
chrootRootDir + "/dev/shm",
|
||||
"tmpfs",
|
||||
0,
|
||||
fmt("size=%s", settings.sandboxShmSize).c_str()
|
||||
) == -1)
|
||||
{
|
||||
throw SysError("mounting /dev/shm");
|
||||
}
|
||||
|
||||
/* Mount a new devpts on /dev/pts. Note that this
|
||||
requires the kernel to be compiled with
|
||||
CONFIG_DEVPTS_MULTIPLE_INSTANCES=y (which is the case
|
||||
if /dev/ptx/ptmx exists). */
|
||||
if (pathExists("/dev/pts/ptmx") && !pathExists(chrootRootDir + "/dev/ptmx")
|
||||
&& !pathsInChroot.count("/dev/pts"))
|
||||
{
|
||||
if (sys::mount("none", (chrootRootDir + "/dev/pts"), "devpts", 0, "newinstance,mode=0620")
|
||||
== 0)
|
||||
{
|
||||
createSymlink("/dev/pts/ptmx", chrootRootDir + "/dev/ptmx");
|
||||
|
||||
/* Make sure /dev/pts/ptmx is world-writable. With some
|
||||
Linux versions, it is created with permissions 0. */
|
||||
chmodPath(chrootRootDir + "/dev/pts/ptmx", 0666);
|
||||
} else {
|
||||
if (errno != EINVAL) {
|
||||
throw SysError("mounting /dev/pts");
|
||||
}
|
||||
bindPath("/dev/pts", chrootRootDir + "/dev/pts");
|
||||
bindPath("/dev/ptmx", chrootRootDir + "/dev/ptmx");
|
||||
}
|
||||
}
|
||||
|
||||
/* Make /etc unwritable */
|
||||
if (!parsedDrv->useUidRange()) {
|
||||
chmodPath(chrootRootDir + "/etc", 0555);
|
||||
}
|
||||
|
||||
/* The comment below is now outdated. Recursive Nix has been removed.
|
||||
* So there's no need to make path appear in the sandbox.
|
||||
* TODO(Raito): cleanup before a merge.
|
||||
*/
|
||||
/* Unshare this mount namespace. This is necessary because
|
||||
pivot_root() below changes the root of the mount
|
||||
namespace. This means that the call to setns() in
|
||||
addDependency() would hide the host's filesystem,
|
||||
making it impossible to bind-mount paths from the host
|
||||
Nix store into the sandbox. Therefore, we save the
|
||||
pre-pivot_root namespace in
|
||||
sandboxMountNamespace. Since we made /nix/store a
|
||||
shared subtree above, this allows addDependency() to
|
||||
make paths appear in the sandbox. */
|
||||
if (unshare(CLONE_NEWNS) == -1) {
|
||||
throw SysError("unsharing mount namespace");
|
||||
}
|
||||
|
||||
/* Creating a new cgroup namespace is independent of whether we enabled the cgroup experimental
|
||||
* feature. We always create a new cgroup namespace from a sandboxing perspective. */
|
||||
/* Unshare the cgroup namespace. This means
|
||||
/proc/self/cgroup will show the child's cgroup as '/'
|
||||
rather than whatever it is in the parent. */
|
||||
if (unshare(CLONE_NEWCGROUP) == -1) {
|
||||
throw SysError("unsharing cgroup namespace");
|
||||
}
|
||||
|
||||
/* Do the chroot(). */
|
||||
if (sys::chdir(chrootRootDir) == -1) {
|
||||
throw SysError("cannot change directory to '%1%'", chrootRootDir);
|
||||
}
|
||||
|
||||
if (mkdir("real-root", 0) == -1) {
|
||||
throw SysError("cannot create real-root directory");
|
||||
}
|
||||
|
||||
if (pivot_root(".", "real-root") == -1) {
|
||||
throw SysError("cannot pivot old root directory onto '%1%'", (chrootRootDir + "/real-root"));
|
||||
}
|
||||
|
||||
if (chroot(".") == -1) {
|
||||
throw SysError("cannot change root directory to '%1%'", chrootRootDir);
|
||||
}
|
||||
|
||||
if (umount2("real-root", MNT_DETACH) == -1) {
|
||||
throw SysError("cannot unmount real root filesystem");
|
||||
}
|
||||
|
||||
if (rmdir("real-root") == -1) {
|
||||
throw SysError("cannot remove real-root directory");
|
||||
}
|
||||
|
||||
/* Switch to the sandbox uid/gid in the user namespace,
|
||||
which corresponds to the build user or calling user in
|
||||
the parent namespace. */
|
||||
if (setgid(sandboxGid()) == -1) {
|
||||
throw SysError("setgid failed");
|
||||
}
|
||||
if (setuid(sandboxUid()) == -1) {
|
||||
throw SysError("setuid failed");
|
||||
}
|
||||
|
||||
if (runPasta) {
|
||||
// wait for the pasta interface to appear. pasta can't signal us when
|
||||
// it's done setting up the namespace, so we have to wait for a while
|
||||
AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP));
|
||||
if (!fd) {
|
||||
throw SysError("cannot open IP socket");
|
||||
}
|
||||
|
||||
struct ifreq ifr;
|
||||
strcpy(ifr.ifr_name, LinuxLocalDerivationGoal::PASTA_NS_IFNAME);
|
||||
// wait two minutes for the interface to appear. if it does not do so
|
||||
// we are either grossly overloaded, or pasta startup failed somehow.
|
||||
static constexpr int SINGLE_WAIT_US = 1000;
|
||||
static constexpr int TOTAL_WAIT_US = 120'000'000;
|
||||
for (unsigned tries = 0;; tries++) {
|
||||
if (tries > TOTAL_WAIT_US / SINGLE_WAIT_US) {
|
||||
throw Error(
|
||||
"sandbox network setup timed out, please check daemon logs for "
|
||||
"possible error output."
|
||||
);
|
||||
} else if (ioctl(fd.get(), SIOCGIFFLAGS, &ifr) == 0) {
|
||||
if ((ifr.ifr_ifru.ifru_flags & IFF_UP) != 0) {
|
||||
break;
|
||||
}
|
||||
} else if (errno == ENODEV) {
|
||||
usleep(SINGLE_WAIT_US);
|
||||
} else {
|
||||
throw SysError("cannot get loopback interface flags");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setUser = false;
|
||||
}
|
||||
#endif
|
||||
const bool setUser = prepareChildSetup();
|
||||
|
||||
if (sys::chdir(tmpDirInSandbox) == -1) {
|
||||
throw SysError("changing into '%1%'", tmpDir);
|
||||
|
||||
@@ -59,11 +59,6 @@ struct LocalDerivationGoal : public DerivationGoal
|
||||
*/
|
||||
AutoCloseFD builderOutPTY;
|
||||
|
||||
/**
|
||||
* Pipe for synchronising updates to the builder namespaces.
|
||||
*/
|
||||
Pipe userNamespaceSync;
|
||||
|
||||
/**
|
||||
* Whether we're currently doing a chroot build.
|
||||
*/
|
||||
@@ -327,6 +322,18 @@ protected:
|
||||
*/
|
||||
virtual void setupSyscallFilter() {}
|
||||
|
||||
/**
|
||||
* Prepare the sandbox. Currently only used on linux to build the sandbox namespace,
|
||||
* write configuration files inside it, and to set up networking with pasta enabled.
|
||||
* Returns `true` if sandbox is running under the same credentials as the daemon, or
|
||||
* `false` if this step has changed our credentials to the build user/group already.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
virtual bool prepareChildSetup()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a special accessor that can access paths that were built within the sandbox's
|
||||
* chroot.
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "lix/libutil/file-system.hh"
|
||||
#include "lix/libutil/finally.hh"
|
||||
#include "lix/libstore/gc-store.hh"
|
||||
#include "lix/libutil/mount.hh"
|
||||
#include "lix/libutil/processes.hh"
|
||||
#include "lix/libutil/result.hh"
|
||||
#include "lix/libutil/signals.hh"
|
||||
@@ -16,8 +17,13 @@
|
||||
#include <cstdlib>
|
||||
#include <grp.h>
|
||||
#include <memory>
|
||||
#include <net/if.h>
|
||||
#include <netinet/in.h>
|
||||
#include <regex>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/prctl.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
#if __linux__
|
||||
#include <linux/capability.h>
|
||||
@@ -898,6 +904,334 @@ std::string LinuxLocalDerivationGoal::rewriteResolvConf(std::string fromHost)
|
||||
return std::regex_replace(fromHost, lineRegex, "") + nsInSandbox;
|
||||
}
|
||||
|
||||
bool LinuxLocalDerivationGoal::prepareChildSetup()
|
||||
{
|
||||
if (!useChroot) {
|
||||
return true;
|
||||
}
|
||||
|
||||
userNamespaceSync.writeSide.reset();
|
||||
|
||||
if (drainFD(userNamespaceSync.readSide.get()) != "1") {
|
||||
throw Error("user namespace initialisation failed");
|
||||
}
|
||||
|
||||
userNamespaceSync.readSide.reset();
|
||||
|
||||
if (privateNetwork) {
|
||||
|
||||
/* Initialise the loopback interface. */
|
||||
AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP));
|
||||
if (!fd) {
|
||||
throw SysError("cannot open IP socket");
|
||||
}
|
||||
|
||||
struct ifreq ifr;
|
||||
strcpy(ifr.ifr_name, "lo");
|
||||
ifr.ifr_flags = IFF_UP | IFF_LOOPBACK | IFF_RUNNING;
|
||||
if (ioctl(fd.get(), SIOCSIFFLAGS, &ifr) == -1) {
|
||||
throw SysError("cannot set loopback interface flags");
|
||||
}
|
||||
}
|
||||
|
||||
/* Set the hostname etc. to fixed values. */
|
||||
char hostname[] = "localhost";
|
||||
if (sethostname(hostname, sizeof(hostname)) == -1) {
|
||||
throw SysError("cannot set host name");
|
||||
}
|
||||
char domainname[] = "(none)"; // kernel default
|
||||
if (setdomainname(domainname, sizeof(domainname)) == -1) {
|
||||
throw SysError("cannot set domain name");
|
||||
}
|
||||
|
||||
/* Make all filesystems private. This is necessary
|
||||
because subtrees may have been mounted as "shared"
|
||||
(MS_SHARED). (Systemd does this, for instance.) Even
|
||||
though we have a private mount namespace, mounting
|
||||
filesystems on top of a shared subtree still propagates
|
||||
outside of the namespace. Making a subtree private is
|
||||
local to the namespace, though, so setting MS_PRIVATE
|
||||
does not affect the outside world. */
|
||||
if (mount(0, "/", 0, MS_PRIVATE | MS_REC, 0) == -1) {
|
||||
throw SysError("unable to make '/' private");
|
||||
}
|
||||
|
||||
/* Bind-mount chroot directory to itself, to treat it as a
|
||||
different filesystem from /, as needed for pivot_root. */
|
||||
if (sys::mount(chrootRootDir, chrootRootDir, "", MS_BIND, 0) == -1) {
|
||||
throw SysError("unable to bind mount '%1%'", chrootRootDir);
|
||||
}
|
||||
|
||||
/* Bind-mount the sandbox's Nix store onto itself so that
|
||||
we can mark it as a "shared" subtree, allowing bind
|
||||
mounts made in *this* mount namespace to be propagated
|
||||
into the child namespace created by the
|
||||
unshare(CLONE_NEWNS) call below.
|
||||
|
||||
Marking chrootRootDir as MS_SHARED causes pivot_root()
|
||||
to fail with EINVAL. Don't know why. */
|
||||
Path chrootStoreDir = chrootRootDir + worker.store.config().storeDir;
|
||||
|
||||
if (sys::mount(chrootStoreDir, chrootStoreDir, "", MS_BIND, 0) == -1) {
|
||||
throw SysError("unable to bind mount the Nix store", chrootStoreDir);
|
||||
}
|
||||
|
||||
if (sys::mount("", chrootStoreDir, "", MS_SHARED, 0) == -1) {
|
||||
throw SysError("unable to make '%s' shared", chrootStoreDir);
|
||||
}
|
||||
|
||||
/* Set up a nearly empty /dev, unless the user asked to
|
||||
bind-mount the host /dev. */
|
||||
Strings ss;
|
||||
if (pathsInChroot.find("/dev") == pathsInChroot.end()) {
|
||||
createDirs(chrootRootDir + "/dev/shm");
|
||||
createDirs(chrootRootDir + "/dev/pts");
|
||||
ss.push_back("/dev/full");
|
||||
if (worker.store.config().systemFeatures.get().count("kvm") && pathExists("/dev/kvm")) {
|
||||
ss.push_back("/dev/kvm");
|
||||
}
|
||||
ss.push_back("/dev/null");
|
||||
ss.push_back("/dev/random");
|
||||
ss.push_back("/dev/tty");
|
||||
ss.push_back("/dev/urandom");
|
||||
ss.push_back("/dev/zero");
|
||||
createSymlink("/proc/self/fd", chrootRootDir + "/dev/fd");
|
||||
createSymlink("/proc/self/fd/0", chrootRootDir + "/dev/stdin");
|
||||
createSymlink("/proc/self/fd/1", chrootRootDir + "/dev/stdout");
|
||||
createSymlink("/proc/self/fd/2", chrootRootDir + "/dev/stderr");
|
||||
}
|
||||
|
||||
/* Fixed-output derivations typically need to access the
|
||||
network, so give them access to /etc/resolv.conf and so
|
||||
on. */
|
||||
if (!derivationType->isSandboxed()) {
|
||||
// Only use nss functions to resolve hosts and
|
||||
// services. Don’t use it for anything else that may
|
||||
// be configured for this system. This limits the
|
||||
// potential impurities introduced in fixed-outputs.
|
||||
writeFile(chrootRootDir + "/etc/nsswitch.conf", "hosts: files dns\nservices: files\n");
|
||||
|
||||
/* N.B. it is realistic that these paths might not exist. It
|
||||
happens when testing Nix building fixed-output derivations
|
||||
within a pure derivation. */
|
||||
for (auto & path : {"/etc/services", "/etc/hosts"}) {
|
||||
if (pathAccessible(path, true)) {
|
||||
// Copy the actual file, not the symlink, because we don't know where
|
||||
// the symlink is pointing, and we don't want to chase down the entire
|
||||
// chain.
|
||||
//
|
||||
// This means if your network config changes during a FOD build,
|
||||
// the DNS in the sandbox will be wrong. However, this is pretty unlikely
|
||||
// to actually be a problem, because FODs are generally pretty fast,
|
||||
// and machines with often-changing network configurations probably
|
||||
// want to run resolved or some other local resolver anyway.
|
||||
//
|
||||
// There's also just no simple way to do this correctly, you have to manually
|
||||
// inotify watch the files for changes on the outside and update the sandbox
|
||||
// while the build is running (or at least that's what Flatpak does).
|
||||
//
|
||||
// I also just generally feel icky about modifying sandbox state under a build,
|
||||
// even though it really shouldn't be a big deal. -K900
|
||||
copyFile(path, chrootRootDir + path, {.followSymlinks = true});
|
||||
} else if (pathExists(path)) {
|
||||
// The path exist but we were not able to access it. This is not a fatal
|
||||
// error, warn about this so the user can remediate.
|
||||
printTaggedWarning(
|
||||
"'%1%' exists but is inaccessible, it will not be copied in the "
|
||||
"sandbox",
|
||||
path
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (pathAccessible("/etc/resolv.conf", true)) {
|
||||
const auto resolvConf = rewriteResolvConf(readFile("/etc/resolv.conf"));
|
||||
writeFile(chrootRootDir + "/etc/resolv.conf", resolvConf);
|
||||
} else if (pathExists("/etc/resolv.conf")) {
|
||||
// The path exist but we were not able to access it. This is not a fatal error,
|
||||
// warn about this so the user can remediate.
|
||||
printTaggedWarning(
|
||||
"'/etc/resolv.conf' exists but is inaccessible, it will not be rewritten "
|
||||
"inside the sandbox; DNS operations inside the sandbox may be "
|
||||
"non-functional."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto & i : ss) {
|
||||
pathsInChroot.emplace(i, i);
|
||||
}
|
||||
|
||||
/* Bind-mount all the directories from the "host"
|
||||
filesystem that we want in the chroot
|
||||
environment. */
|
||||
for (auto & i : pathsInChroot) {
|
||||
if (i.second.source == "/proc") {
|
||||
continue; // backwards compatibility
|
||||
}
|
||||
|
||||
#if HAVE_EMBEDDED_SANDBOX_SHELL
|
||||
if (i.second.source == "__embedded_sandbox_shell__") {
|
||||
static unsigned char sh[] = {
|
||||
#include "embedded-sandbox-shell.gen.hh"
|
||||
};
|
||||
auto dst = chrootRootDir + i.first;
|
||||
createDirs(dirOf(dst));
|
||||
writeFile(dst, std::string_view((const char *) sh, sizeof(sh)));
|
||||
chmodPath(dst, 0555);
|
||||
} else
|
||||
#endif
|
||||
bindPath(i.second.source, chrootRootDir + i.first, i.second.optional);
|
||||
}
|
||||
|
||||
/* Bind a new instance of procfs on /proc. */
|
||||
createDirs(chrootRootDir + "/proc");
|
||||
if (sys::mount("none", chrootRootDir + "/proc", "proc", 0, 0) == -1) {
|
||||
throw SysError("mounting /proc");
|
||||
}
|
||||
|
||||
/* Mount sysfs on /sys. */
|
||||
if (buildUser && buildUser->getUIDCount() != 1) {
|
||||
createDirs(chrootRootDir + "/sys");
|
||||
if (sys::mount("none", chrootRootDir + "/sys", "sysfs", 0, 0) == -1) {
|
||||
throw SysError("mounting /sys");
|
||||
}
|
||||
}
|
||||
|
||||
/* Mount a new tmpfs on /dev/shm to ensure that whatever
|
||||
the builder puts in /dev/shm is cleaned up automatically. */
|
||||
if (pathExists("/dev/shm")
|
||||
&& sys::mount(
|
||||
"none", chrootRootDir + "/dev/shm", "tmpfs", 0, fmt("size=%s", settings.sandboxShmSize).c_str()
|
||||
) == -1)
|
||||
{
|
||||
throw SysError("mounting /dev/shm");
|
||||
}
|
||||
|
||||
/* Mount a new devpts on /dev/pts. Note that this
|
||||
requires the kernel to be compiled with
|
||||
CONFIG_DEVPTS_MULTIPLE_INSTANCES=y (which is the case
|
||||
if /dev/ptx/ptmx exists). */
|
||||
if (pathExists("/dev/pts/ptmx") && !pathExists(chrootRootDir + "/dev/ptmx")
|
||||
&& !pathsInChroot.count("/dev/pts"))
|
||||
{
|
||||
if (sys::mount("none", (chrootRootDir + "/dev/pts"), "devpts", 0, "newinstance,mode=0620") == 0) {
|
||||
createSymlink("/dev/pts/ptmx", chrootRootDir + "/dev/ptmx");
|
||||
|
||||
/* Make sure /dev/pts/ptmx is world-writable. With some
|
||||
Linux versions, it is created with permissions 0. */
|
||||
chmodPath(chrootRootDir + "/dev/pts/ptmx", 0666);
|
||||
} else {
|
||||
if (errno != EINVAL) {
|
||||
throw SysError("mounting /dev/pts");
|
||||
}
|
||||
bindPath("/dev/pts", chrootRootDir + "/dev/pts");
|
||||
bindPath("/dev/ptmx", chrootRootDir + "/dev/ptmx");
|
||||
}
|
||||
}
|
||||
|
||||
/* Make /etc unwritable */
|
||||
if (!parsedDrv->useUidRange()) {
|
||||
chmodPath(chrootRootDir + "/etc", 0555);
|
||||
}
|
||||
|
||||
/* The comment below is now outdated. Recursive Nix has been removed.
|
||||
* So there's no need to make path appear in the sandbox.
|
||||
* TODO(Raito): cleanup before a merge.
|
||||
*/
|
||||
/* Unshare this mount namespace. This is necessary because
|
||||
pivot_root() below changes the root of the mount
|
||||
namespace. This means that the call to setns() in
|
||||
addDependency() would hide the host's filesystem,
|
||||
making it impossible to bind-mount paths from the host
|
||||
Nix store into the sandbox. Therefore, we save the
|
||||
pre-pivot_root namespace in
|
||||
sandboxMountNamespace. Since we made /nix/store a
|
||||
shared subtree above, this allows addDependency() to
|
||||
make paths appear in the sandbox. */
|
||||
if (unshare(CLONE_NEWNS) == -1) {
|
||||
throw SysError("unsharing mount namespace");
|
||||
}
|
||||
|
||||
/* Creating a new cgroup namespace is independent of whether we enabled the cgroup experimental feature.
|
||||
* We always create a new cgroup namespace from a sandboxing perspective. */
|
||||
/* Unshare the cgroup namespace. This means
|
||||
/proc/self/cgroup will show the child's cgroup as '/'
|
||||
rather than whatever it is in the parent. */
|
||||
if (unshare(CLONE_NEWCGROUP) == -1) {
|
||||
throw SysError("unsharing cgroup namespace");
|
||||
}
|
||||
|
||||
/* Do the chroot(). */
|
||||
if (sys::chdir(chrootRootDir) == -1) {
|
||||
throw SysError("cannot change directory to '%1%'", chrootRootDir);
|
||||
}
|
||||
|
||||
if (mkdir("real-root", 0) == -1) {
|
||||
throw SysError("cannot create real-root directory");
|
||||
}
|
||||
|
||||
if (syscall(SYS_pivot_root, ".", "real-root") == -1) {
|
||||
throw SysError("cannot pivot old root directory onto '%1%'", (chrootRootDir + "/real-root"));
|
||||
}
|
||||
|
||||
if (chroot(".") == -1) {
|
||||
throw SysError("cannot change root directory to '%1%'", chrootRootDir);
|
||||
}
|
||||
|
||||
if (umount2("real-root", MNT_DETACH) == -1) {
|
||||
throw SysError("cannot unmount real root filesystem");
|
||||
}
|
||||
|
||||
if (rmdir("real-root") == -1) {
|
||||
throw SysError("cannot remove real-root directory");
|
||||
}
|
||||
|
||||
/* Switch to the sandbox uid/gid in the user namespace,
|
||||
which corresponds to the build user or calling user in
|
||||
the parent namespace. */
|
||||
if (setgid(sandboxGid()) == -1) {
|
||||
throw SysError("setgid failed");
|
||||
}
|
||||
if (setuid(sandboxUid()) == -1) {
|
||||
throw SysError("setuid failed");
|
||||
}
|
||||
|
||||
if (runPasta) {
|
||||
// wait for the pasta interface to appear. pasta can't signal us when
|
||||
// it's done setting up the namespace, so we have to wait for a while
|
||||
AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP));
|
||||
if (!fd) {
|
||||
throw SysError("cannot open IP socket");
|
||||
}
|
||||
|
||||
struct ifreq ifr;
|
||||
strcpy(ifr.ifr_name, LinuxLocalDerivationGoal::PASTA_NS_IFNAME);
|
||||
// wait two minutes for the interface to appear. if it does not do so
|
||||
// we are either grossly overloaded, or pasta startup failed somehow.
|
||||
static constexpr int SINGLE_WAIT_US = 1000;
|
||||
static constexpr int TOTAL_WAIT_US = 120'000'000;
|
||||
for (unsigned tries = 0;; tries++) {
|
||||
if (tries > TOTAL_WAIT_US / SINGLE_WAIT_US) {
|
||||
throw Error(
|
||||
"sandbox network setup timed out, please check daemon logs for "
|
||||
"possible error output."
|
||||
);
|
||||
} else if (ioctl(fd.get(), SIOCGIFFLAGS, &ifr) == 0) {
|
||||
if ((ifr.ifr_ifru.ifru_flags & IFF_UP) != 0) {
|
||||
break;
|
||||
}
|
||||
} else if (errno == ENODEV) {
|
||||
usleep(SINGLE_WAIT_US);
|
||||
} else {
|
||||
throw SysError("cannot get loopback interface flags");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Pid LinuxLocalDerivationGoal::startChild(AutoCloseFD logPTY)
|
||||
{
|
||||
#if HAVE_SECCOMP
|
||||
|
||||
@@ -91,7 +91,14 @@ private:
|
||||
return true;
|
||||
}
|
||||
|
||||
bool prepareChildSetup() override;
|
||||
|
||||
std::string rewriteResolvConf(std::string fromHost) override;
|
||||
|
||||
/**
|
||||
* Pipe for synchronising updates to the builder namespaces.
|
||||
*/
|
||||
Pipe userNamespaceSync;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user