diff --git a/lix/libstore/build/request.capnp b/lix/libstore/build/request.capnp index 44b7b6172..16cc75fb9 100644 --- a/lix/libstore/build/request.capnp +++ b/lix/libstore/build/request.capnp @@ -11,10 +11,40 @@ struct Request { supplementaryGroups @3 :List(UInt32); } + struct LinuxPlatform { + struct BoundPath { + source @0 :Data; + target @1 :Data; + optional @2 :Bool; + } + + struct Sandbox { + paths @0 :List(BoundPath); + privateNetwork @1 :Bool; + chrootRootDir @2 :Data; + storeDir @3 :Data; + wantsKvm @4 :Bool; + sandboxShmFlags @5 :Text; + useUidRange @6 :Bool; + uid @7 :UInt32; + gid @8 :UInt32; + waitForInterface @9 :Text; + } + + seccompFilters @0 :Data; + sandbox @1 :Sandbox; + platform @2 :Data; + parentPid @3 :Int32; + } + builder @0 :Data; args @1 :List(Data); environment @2 :List(Data); workingDir @3 :Data; enableCoreDumps @4 :Bool; credentials @5 :Credentials; + + platform :group { + linux @6 :LinuxPlatform; + } } diff --git a/lix/libstore/platform/linux.cc b/lix/libstore/platform/linux.cc index 33bdfb6dc..3f31594b5 100644 --- a/lix/libstore/platform/linux.cc +++ b/lix/libstore/platform/linux.cc @@ -1,4 +1,5 @@ #include "lix/libstore/build/personality.hh" +#include "lix/libstore/build/request.capnp.h" #include "lix/libstore/build/worker.hh" #include "lix/libutil/c-calls.hh" #include "lix/libutil/cgroup.hh" @@ -11,13 +12,17 @@ #include "lix/libstore/gc-store.hh" #include "lix/libutil/processes.hh" #include "lix/libutil/result.hh" +#include "lix/libutil/rpc.hh" #include "lix/libutil/signals.hh" #include "lix/libstore/platform/linux.hh" #include "lix/libutil/regex.hh" #include "lix/libutil/strings.hh" +#include "lix/libutil/types.hh" #include +#include #include +#include #include #include #include @@ -29,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +42,7 @@ #include #include #include +#include #if __linux__ #include @@ -1267,8 +1274,10 @@ static void bindPath(const Path & source, const Path & target, bool optional = f } } -bool LinuxLocalDerivationGoal::prepareChildSetup(build::Request::Reader request) +static bool prepareChildSetup_(build::Request::Reader request) { + auto config = request.getPlatform().getLinux(); + // Set the NO_NEW_PRIVS prctl flag. // This both makes loading seccomp filters work for unprivileged users, // and is an additional security measure in its own right. @@ -1276,26 +1285,34 @@ bool LinuxLocalDerivationGoal::prepareChildSetup(build::Request::Reader request) throw SysError("PR_SET_NO_NEW_PRIVS failed"); } #if HAVE_SECCOMP - const auto & seccompBPF = getSyscallFilter(); - assert(seccompBPF.size() <= std::numeric_limits::max()); - struct sock_fprog fprog = { - .len = static_cast(seccompBPF.size()), - // the kernel does not actually write to the filter - .filter = const_cast(seccompBPF.data()), - }; - if (syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER, 0, &fprog) != 0) { - throw SysError("unable to load seccomp BPF program"); + if (config.hasSeccompFilters()) { + const auto seccompBPF = config.getSeccompFilters(); + const auto entries = seccompBPF.size() / sizeof(struct sock_filter); + assert(entries <= std::numeric_limits::max()); + struct sock_fprog fprog = { + .len = static_cast(entries), + // the kernel does not actually write to the filter, and doesn't care about alignment + .filter = const_cast( + reinterpret_cast(seccompBPF.begin()) + ), + }; + if (syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER, 0, &fprog) != 0) { + throw SysError("unable to load seccomp BPF program"); + } } #endif - KJ_DEFER(setPersonality(drv->platform)); + KJ_DEFER(setPersonality(rpc::to(config.getPlatform()))); - if (!useChroot) { + if (!config.hasSandbox()) { return true; } - if (privateNetwork()) { + auto sandbox = config.getSandbox(); + const Path chrootRootDir{rpc::to(sandbox.getChrootRootDir())}; + + if (sandbox.getPrivateNetwork()) { /* Initialise the loopback interface. */ AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP)); if (!fd) { @@ -1346,7 +1363,7 @@ bool LinuxLocalDerivationGoal::prepareChildSetup(build::Request::Reader request) Marking chrootRootDir as MS_SHARED causes pivot_root() to fail with EINVAL. Don't know why. */ - Path chrootStoreDir = chrootRootDir + worker.store.config().storeDir; + Path chrootStoreDir = chrootRootDir + rpc::to(sandbox.getStoreDir()); if (sys::mount(chrootStoreDir, chrootStoreDir, "", MS_BIND, 0) == -1) { throw SysError("unable to bind mount the Nix store", chrootStoreDir); @@ -1362,25 +1379,27 @@ bool LinuxLocalDerivationGoal::prepareChildSetup(build::Request::Reader request) /* Bind-mount all the directories from the "host" filesystem that we want in the chroot environment. */ - for (auto & i : pathsInChroot) { - devMounted |= i.first == "/dev"; - devPtsMounted |= i.first == "/dev/pts"; - if (i.second.source == "/proc") { + for (auto path : sandbox.getPaths()) { + const std::string source{rpc::to(path.getSource())}; + const std::string target{rpc::to(path.getTarget())}; + devMounted |= target == "/dev"; + devPtsMounted |= target == "/dev/pts"; + if (source == "/proc") { continue; // backwards compatibility } #if HAVE_EMBEDDED_SANDBOX_SHELL - if (i.second.source == "__embedded_sandbox_shell__") { + if (source == "__embedded_sandbox_shell__") { static unsigned char sh[] = { #include "embedded-sandbox-shell.gen.hh" }; - auto dst = chrootRootDir + i.first; + auto dst = chrootRootDir + target; 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); + bindPath(source, chrootRootDir + target, path.getOptional()); } /* Set up a nearly empty /dev, unless the user asked to @@ -1391,7 +1410,7 @@ bool LinuxLocalDerivationGoal::prepareChildSetup(build::Request::Reader request) createDirs(chrootRootDir + "/dev/shm"); createDirs(chrootRootDir + "/dev/pts"); bind("/dev/full"); - if (settings.systemFeatures.get().count("kvm") && pathExists("/dev/kvm")) { + if (sandbox.getWantsKvm() && pathExists("/dev/kvm")) { bind("/dev/kvm"); } bind("/dev/null"); @@ -1412,7 +1431,7 @@ bool LinuxLocalDerivationGoal::prepareChildSetup(build::Request::Reader request) } /* Mount sysfs on /sys. */ - if (buildUser && buildUser->getUIDCount() != 1) { + if (request.hasCredentials() && request.getCredentials().getUidCount() != 1) { createDirs(chrootRootDir + "/sys"); if (sys::mount("none", chrootRootDir + "/sys", "sysfs", 0, 0) == -1) { throw SysError("mounting /sys"); @@ -1422,9 +1441,8 @@ bool LinuxLocalDerivationGoal::prepareChildSetup(build::Request::Reader request) /* 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) + && sys::mount("none", chrootRootDir + "/dev/shm", "tmpfs", 0, sandbox.getSandboxShmFlags().cStr()) + == -1) { throw SysError("mounting /dev/shm"); } @@ -1450,7 +1468,7 @@ bool LinuxLocalDerivationGoal::prepareChildSetup(build::Request::Reader request) } /* Make /etc unwritable */ - if (!parsedDrv->useUidRange()) { + if (!sandbox.getUseUidRange()) { chmodPath(chrootRootDir + "/etc", 0555); } @@ -1509,14 +1527,14 @@ bool LinuxLocalDerivationGoal::prepareChildSetup(build::Request::Reader request) /* 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) { + if (setgid(sandbox.getGid()) == -1) { throw SysError("setgid failed"); } - if (setuid(sandboxUid()) == -1) { + if (setuid(sandbox.getUid()) == -1) { throw SysError("setuid failed"); } - if (runPasta()) { + if (sandbox.hasWaitForInterface()) { // 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)); @@ -1525,7 +1543,8 @@ bool LinuxLocalDerivationGoal::prepareChildSetup(build::Request::Reader request) } struct ifreq ifr; - strcpy(ifr.ifr_name, LinuxLocalDerivationGoal::PASTA_NS_IFNAME); + // NOLINTNEXTLINE(lix-unsafe-c-calls): we receive this as a c string + strncpy(ifr.ifr_name, sandbox.getWaitForInterface().cStr(), sizeof(ifr.ifr_name)); // 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; @@ -1556,11 +1575,56 @@ void LinuxLocalDerivationGoal::finishChildSetup(build::Request::Reader request) if (prctl(PR_SET_PDEATHSIG, SIGKILL) == -1) { throw SysError("setting death signal"); } - if (getppid() != parentPid) { + if (getppid() != request.getPlatform().getLinux().getParentPid()) { raise(SIGKILL); } } +void LinuxLocalDerivationGoal::fillBuilderConfig(build::Request::Builder request) +{ + auto config = request.getPlatform().getLinux(); + +#if HAVE_SECCOMP + { + const auto & filters = std::span{getSyscallFilter()}; + auto builder = config.initSeccompFilters(filters.size_bytes()); + memcpy(builder.begin(), filters.data(), builder.size()); + } +#endif + + if (useChroot) { + auto sandbox = config.initSandbox(); + { + auto builder = sandbox.initPaths(pathsInChroot.size()); + for (const auto & [idx, path] : enumerate(pathsInChroot)) { + const auto & [target, binding] = path; + RPC_FILL(builder[idx], setTarget, target); + RPC_FILL(builder[idx], setSource, binding.source); + builder[idx].setOptional(binding.optional); + } + } + sandbox.setPrivateNetwork(privateNetwork()); + RPC_FILL(sandbox, setChrootRootDir, chrootRootDir); + RPC_FILL(sandbox, setStoreDir, worker.store.config().storeDir); + sandbox.setWantsKvm(settings.systemFeatures.get().contains("kvm")); + RPC_FILL(sandbox, setSandboxShmFlags, fmt("size=%s", settings.sandboxShmSize)); + sandbox.setUseUidRange(parsedDrv->useUidRange()); + sandbox.setUid(sandboxUid()); + sandbox.setGid(sandboxGid()); + if (runPasta()) { + sandbox.setWaitForInterface(LinuxLocalDerivationGoal::PASTA_NS_IFNAME); + } + } + + RPC_FILL(config, setPlatform, drv->platform); + config.setParentPid(getpid()); +} + +bool LinuxLocalDerivationGoal::prepareChildSetup(build::Request::Reader request) +{ + return prepareChildSetup_(request); +} + Pid LinuxLocalDerivationGoal::startChild(build::Request::Reader request, AutoCloseFD logPTY) { // If we're not sandboxing no need to faff about, use the fallback diff --git a/lix/libstore/platform/linux.hh b/lix/libstore/platform/linux.hh index 1f2f7b053..5acea7eaf 100644 --- a/lix/libstore/platform/linux.hh +++ b/lix/libstore/platform/linux.hh @@ -59,18 +59,14 @@ private: Pid pastaPid; - /** - * used to initialize the parent death signal of children without racing - * with the parent dying before we got around to setting a death signal. - */ - pid_t parentPid = getpid(); - /** * Create a special accessor that can access paths that were built within the sandbox's * chroot. */ std::optional> getChrootDirAwareFSAccessor() override; + void fillBuilderConfig(build::Request::Builder request) override; + /** * Create and populate chroot */