diff --git a/lix/libexec/check-namespace-support.cc b/lix/libexec/check-namespace-support.cc new file mode 100644 index 000000000..0d6da522e --- /dev/null +++ b/lix/libexec/check-namespace-support.cc @@ -0,0 +1,86 @@ +#include "common.hh" +#include +#include +#include +#include +#include +#include +#include +#include + +LIBEXEC_HELPER(0) + +static int waitFor(pid_t child) +{ + int status; + while (true) { + if (waitpid(child, &status, 0) == -1) { + if (errno != EINTR) { + DIE_UNLESS_SYS("waitpid()", -1); + } + } else if (WIFEXITED(status)) { + return WEXITSTATUS(status); + } else if (WIFSIGNALED(status)) { + die(std::format("child died with signal {}", WTERMSIG(status))); + } else { + die(std::format("child exited {}", status)); + } + } +} + +int helperMain(const char * name, std::span args) noexcept +{ + size_t stackSize = 1ul * 1024 * 1024; + auto stack = static_cast( + mmap(0, stackSize, PROT_WRITE | PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0) + ); + if (stack == MAP_FAILED) { + die(std::format("mmap(): {}", strerror(errno))); + } + + const bool haveUserNS = [&] { + auto child = clone([](void *) { return 0; }, stack + stackSize, CLONE_NEWUSER | SIGCHLD, nullptr); + if (child == -1) { + printf("user %s\n", strerror(errno)); + return false; + } else if (auto status = waitFor(child)) { + die(std::format("userns check child failed unexpectedly with status {}", status)); + } else { + printf("user\n"); + return true; + } + }(); + + { + auto child = clone( + [](void *) { + /* Make sure we don't remount the parent's /proc. */ + if (mount(0, "/", 0, MS_PRIVATE | MS_REC, 0) == -1) { + return 1; + } + + /* Test whether we can remount /proc. The kernel disallows + this if /proc is not fully visible, i.e. if there are + filesystems mounted on top of files inside /proc. See + https://lore.kernel.org/lkml/87tvsrjai0.fsf@xmission.com/T/. */ + if (mount("none", "/proc", "proc", 0, 0) == -1) { + return 2; + } + + return 0; + }, + stack + stackSize, + CLONE_NEWNS | CLONE_NEWPID | (haveUserNS ? CLONE_NEWUSER : 0) | SIGCHLD, + nullptr + ); + if (child == -1) { + printf("mount-pid %s\n", strerror(errno)); + } else if (waitFor(child) != 0) { + printf("mount-pid failed to remount /proc\n"); + } else { + printf("mount-pid\n"); + } + } + + return 0; +} diff --git a/lix/libexec/meson.build b/lix/libexec/meson.build index cef364819..8c458a51b 100644 --- a/lix/libexec/meson.build +++ b/lix/libexec/meson.build @@ -1,3 +1,12 @@ +if is_linux + check_namespace_support = executable( + 'check-namespace-support', + files('check-namespace-support.cc'), + install : true, + install_dir : libexecdir / 'lix', + ) +endif + kill_user = executable( 'kill-user', files('kill-user.cc'), diff --git a/lix/libstore/build/local-derivation-goal.cc b/lix/libstore/build/local-derivation-goal.cc index 019505f02..c790de9fe 100644 --- a/lix/libstore/build/local-derivation-goal.cc +++ b/lix/libstore/build/local-derivation-goal.cc @@ -176,6 +176,17 @@ void LocalDerivationGoal::killSandbox(bool getStats) } } +uid_t LocalDerivationGoal::sandboxUid() +{ + return worker.namespaces.user ? (!buildUser || buildUser->getUIDCount() == 1 ? 1000 : 0) + : buildUser->getUID(); +} + +gid_t LocalDerivationGoal::sandboxGid() +{ + return worker.namespaces.user ? (!buildUser || buildUser->getUIDCount() == 1 ? 100 : 0) + : buildUser->getGID(); +} kj::Promise> LocalDerivationGoal::tryLocalBuild() noexcept try { @@ -265,9 +276,8 @@ retry: // FIXME: should user namespaces being unsupported also require // sandbox-fallback to be allowed? I don't think so, since they aren't a // huge security win to have enabled. - usingUserNamespace = userNamespacesSupported(); - if (!mountAndPidNamespacesSupported()) { + if (!worker.namespaces.mountAndPid) { if (!settings.sandboxFallback) throw Error("this system does not support the kernel namespaces that are required for sandboxing; use '--no-sandbox' to disable sandboxing. Pass --debug for diagnostics on what is broken."); if (!sandboxFallbackAllowed) @@ -276,7 +286,7 @@ retry: useChroot = false; } - if (!usingUserNamespace && !buildUser) { + if (!worker.namespaces.user && !buildUser) { throw Error("cannot perform a sandboxed build because user namespaces are not available.\nIn this Lix's configuration, user namespaces are required due to either being non-root, or build-users-group being disabled without also enabling auto-allocate-uids"); } } diff --git a/lix/libstore/build/local-derivation-goal.hh b/lix/libstore/build/local-derivation-goal.hh index c1fac225f..a88eab8bf 100644 --- a/lix/libstore/build/local-derivation-goal.hh +++ b/lix/libstore/build/local-derivation-goal.hh @@ -64,12 +64,6 @@ struct LocalDerivationGoal : public DerivationGoal */ Pipe userNamespaceSync; - /** - * On Linux, whether we're doing the build in its own user - * namespace. - */ - bool usingUserNamespace = true; - /** * Whether we're currently doing a chroot build. */ @@ -139,8 +133,8 @@ struct LocalDerivationGoal : public DerivationGoal */ std::map prevInfos; - uid_t sandboxUid() { return usingUserNamespace ? (!buildUser || buildUser->getUIDCount() == 1 ? 1000 : 0) : buildUser->getUID(); } - gid_t sandboxGid() { return usingUserNamespace ? (!buildUser || buildUser->getUIDCount() == 1 ? 100 : 0) : buildUser->getGID(); } + uid_t sandboxUid(); + gid_t sandboxGid(); const static Path homeDir; diff --git a/lix/libstore/build/worker.cc b/lix/libstore/build/worker.cc index b27eb0299..672985c65 100644 --- a/lix/libstore/build/worker.cc +++ b/lix/libstore/build/worker.cc @@ -7,6 +7,7 @@ #include "lix/libstore/build/substitution-goal.hh" #include "lix/libstore/build/local-derivation-goal.hh" #include "lix/libutil/logging.hh" +#include "lix/libutil/namespaces.hh" #include "lix/libutil/signals.hh" #include "lix/libstore/build/hook-instance.hh" // IWYU pragma: keep #include @@ -25,7 +26,7 @@ struct ErrorHandler : kj::TaskSet::ErrorHandler } errorHandler; } -Worker::Worker(Store & store, Store & evalStore) +Worker::Worker(Store & store, Store & evalStore, AvailableNamespaces namespaces) : act(logger->startActivity(actRealise)) , actDerivations(logger->startActivity(actBuilds)) , actSubstitutions(logger->startActivity(actCopyPaths)) @@ -36,6 +37,7 @@ Worker::Worker(Store & store, Store & evalStore) , substitutions(std::max(1, settings.maxSubstitutionJobs)) , localBuilds(settings.maxBuildJobs) , children(errorHandler) + , namespaces(namespaces) { /* Debugging: prevent recursive workers. */ diff --git a/lix/libstore/build/worker.hh b/lix/libstore/build/worker.hh index 096f99316..cd3b27f61 100644 --- a/lix/libstore/build/worker.hh +++ b/lix/libstore/build/worker.hh @@ -4,6 +4,7 @@ #include "lix/libutil/async.hh" #include "lix/libutil/async-semaphore.hh" #include "lix/libutil/concepts.hh" +#include "lix/libutil/namespaces.hh" #include "lix/libutil/notifying-counter.hh" #include "lix/libutil/result.hh" #include "lix/libutil/cgroup.hh" @@ -233,8 +234,10 @@ public: NotifyingCounter expectedNarSize{[this] { updateStatisticsLater(); }}; NotifyingCounter doneNarSize{[this] { updateStatisticsLater(); }}; + const AvailableNamespaces namespaces; + private: - Worker(Store & store, Store & evalStore); + Worker(Store & store, Store & evalStore, AvailableNamespaces namespaces); ~Worker(); /** @@ -319,7 +322,8 @@ template kj::Promise> processGoals(Store & store, Store & evalStore, MkGoals && mkGoals) noexcept try { - co_return co_await Worker(store, evalStore).run(std::forward(mkGoals)); + co_return co_await Worker(store, evalStore, LIX_TRY_AWAIT(queryAvailableNamespaces())) + .run(std::forward(mkGoals)); } catch (...) { co_return result::current_exception(); } diff --git a/lix/libstore/platform/linux.cc b/lix/libstore/platform/linux.cc index 7822502de..e39c2e416 100644 --- a/lix/libstore/platform/linux.cc +++ b/lix/libstore/platform/linux.cc @@ -982,8 +982,9 @@ Pid LinuxLocalDerivationGoal::startChild(std::function openSlave) // we can't actually run it. not doing so hides bugs and impairs purity. if (settings.pastaPath != "" || privateNetwork) options.cloneFlags |= CLONE_NEWNET; - if (usingUserNamespace) + if (worker.namespaces.user) { options.cloneFlags |= CLONE_NEWUSER; + } pid_t child = startProcess([&]() { runChild(); }, options).release(); @@ -1008,7 +1009,7 @@ Pid LinuxLocalDerivationGoal::startChild(std::function openSlave) assert(ss.size() == 1); Pid pid = Pid{string2Int(ss[0]).value()}; - if (usingUserNamespace) { + if (worker.namespaces.user) { /* Set the UID/GID mapping of the builder's user namespace such that the sandbox user maps to the build user, or to the calling user (if build users are disabled). */ @@ -1079,7 +1080,7 @@ Pid LinuxLocalDerivationGoal::startChild(std::function openSlave) } AutoCloseFD userns; - if (usingUserNamespace) { + if (worker.namespaces.user) { userns = AutoCloseFD(sys::open(fmt("/proc/%i/ns/user", pid.get()), O_RDONLY | O_CLOEXEC)); if (!userns) { diff --git a/lix/libutil/namespaces.cc b/lix/libutil/namespaces.cc index 24def2736..7ccb3d5cf 100644 --- a/lix/libutil/namespaces.cc +++ b/lix/libutil/namespaces.cc @@ -6,6 +6,8 @@ #include "lix/libutil/processes.hh" #include "lix/libutil/strings.hh" +#include +#include #include #if __linux__ @@ -99,64 +101,39 @@ static void diagnoseUserNamespaces() } } -bool userNamespacesSupported() -{ - static auto res = [&]() -> bool - { - try { - Pid pid = startProcess([&]() { _exit(0); }, {.cloneFlags = CLONE_NEWUSER}); +kj::Promise> queryAvailableNamespaces() +try { + AvailableNamespaces result{}; - auto r = pid.wait(); - assert(!r); - } catch (SysError & e) { - printTaggedWarning("user namespaces do not work on this system: %s", e.msg()); + auto helper = runHelper("check-namespace-support", {.captureStdout = true}); + KJ_DEFER(helper.waitAndCheck()); + const auto results = tokenizeString(TRY_AWAIT(helper.getStdout()->drain()), "\n"); + + for (auto line : results) { + if (line == "user") { + result.user = true; + } else if (line.starts_with("user ")) { + printTaggedWarning("user namespaces do not work on this system: %s", line.substr(5)); diagnoseUserNamespaces(); - return false; + } else if (line == "mount-pid") { + result.mountAndPid = true; + } else if (line.starts_with("mount-pid")) { + debug("mount namespaces do not work on this system: %s", line.substr(9)); + } else { + throw Error("unexpected namespace check status: %s", line); } + } - return true; - }(); - return res; + co_return result; +} catch (...) { + co_return result::current_exception(); } - -bool mountAndPidNamespacesSupported() -{ - static auto res = [&]() -> bool - { - try { - - Pid pid = startProcess([&]() { - /* Make sure we don't remount the parent's /proc. */ - if (mount(0, "/", 0, MS_PRIVATE | MS_REC, 0) == -1) - _exit(1); - - /* Test whether we can remount /proc. The kernel disallows - this if /proc is not fully visible, i.e. if there are - filesystems mounted on top of files inside /proc. See - https://lore.kernel.org/lkml/87tvsrjai0.fsf@xmission.com/T/. */ - if (mount("none", "/proc", "proc", 0, 0) == -1) - _exit(2); - - _exit(0); - }, { - .cloneFlags = CLONE_NEWNS | CLONE_NEWPID | (userNamespacesSupported() ? CLONE_NEWUSER : 0) - }); - - if (pid.wait()) { - debug("PID namespaces do not work on this system: cannot remount /proc"); - return false; - } - - } catch (SysError & e) { - debug("mount namespaces do not work on this system: %s", e.msg()); - return false; - } - - return true; - }(); - return res; +#else +kj::Promise> queryAvailableNamespaces() +try { + return {AvailableNamespaces{}}; +} catch (...) { + return {result::current_exception()}; } - #endif - } diff --git a/lix/libutil/namespaces.hh b/lix/libutil/namespaces.hh index 3a920e665..6f086b4c0 100644 --- a/lix/libutil/namespaces.hh +++ b/lix/libutil/namespaces.hh @@ -22,13 +22,11 @@ void restoreMountNamespace(); */ void unshareFilesystem(); +struct AvailableNamespaces +{ + bool user = false; + bool mountAndPid = false; +}; -#if __linux__ - -bool userNamespacesSupported(); - -bool mountAndPidNamespacesSupported(); - -#endif - +kj::Promise> queryAvailableNamespaces(); }