diff --git a/lix/libstore/build/local-derivation-goal.cc b/lix/libstore/build/local-derivation-goal.cc index 4bf79434a..85c410cb8 100644 --- a/lix/libstore/build/local-derivation-goal.cc +++ b/lix/libstore/build/local-derivation-goal.cc @@ -62,7 +62,6 @@ #include #include #include -#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); diff --git a/lix/libstore/build/local-derivation-goal.hh b/lix/libstore/build/local-derivation-goal.hh index 5cbe96c48..76fbfc239 100644 --- a/lix/libstore/build/local-derivation-goal.hh +++ b/lix/libstore/build/local-derivation-goal.hh @@ -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. diff --git a/lix/libstore/platform/linux.cc b/lix/libstore/platform/linux.cc index cfb75fb66..792a46216 100644 --- a/lix/libstore/platform/linux.cc +++ b/lix/libstore/platform/linux.cc @@ -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 #include #include +#include +#include #include +#include +#include #include +#include #if __linux__ #include @@ -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 diff --git a/lix/libstore/platform/linux.hh b/lix/libstore/platform/linux.hh index 5908f82f5..00dc7206c 100644 --- a/lix/libstore/platform/linux.hh +++ b/lix/libstore/platform/linux.hh @@ -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; }; }