From 697a86c4af3365836334c50f643f6a419459d8e5 Mon Sep 17 00:00:00 2001 From: eldritch horrors Date: Tue, 20 Jan 2026 16:04:54 +0100 Subject: [PATCH] libutil/libstore: move namespace support checks to libexec this also requires moving namespace support information into Worker and out of function-scope static variables, otherwise we can't use async IO for the libexec helper output. we could set the fd to blocking for just one CL and extract the Worker changes into another that the reverts the blocking fd usage, but that seems not warranted for the scope of these. Change-Id: I6996fab1ae74693d50cefb6a6a9c21d61dada1d9 --- lix/libexec/check-namespace-support.cc | 86 +++++++++++++++++++++ lix/libexec/meson.build | 9 +++ lix/libstore/build/local-derivation-goal.cc | 16 +++- lix/libstore/build/local-derivation-goal.hh | 10 +-- lix/libstore/build/worker.cc | 4 +- lix/libstore/build/worker.hh | 8 +- lix/libstore/platform/linux.cc | 7 +- lix/libutil/namespaces.cc | 83 +++++++------------- lix/libutil/namespaces.hh | 14 ++-- 9 files changed, 159 insertions(+), 78 deletions(-) create mode 100644 lix/libexec/check-namespace-support.cc 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(); }