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
This commit is contained in:
eldritch horrors
2026-01-20 22:42:53 +00:00
parent be27e9696b
commit 697a86c4af
9 changed files with 159 additions and 78 deletions
+86
View File
@@ -0,0 +1,86 @@
#include "common.hh"
#include <csignal>
#include <cstdlib>
#include <cstring>
#include <format>
#include <sched.h>
#include <sys/mman.h>
#include <sys/mount.h>
#include <sys/wait.h>
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<char *> args) noexcept
{
size_t stackSize = 1ul * 1024 * 1024;
auto stack = static_cast<char *>(
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;
}
+9
View File
@@ -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'),
+13 -3
View File
@@ -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<Result<Goal::WorkResult>> 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");
}
}
+2 -8
View File
@@ -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<Path, ValidPathInfo> 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;
+3 -1
View File
@@ -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 <boost/outcome/try.hpp>
@@ -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<unsigned>(1, settings.maxSubstitutionJobs))
, localBuilds(settings.maxBuildJobs)
, children(errorHandler)
, namespaces(namespaces)
{
/* Debugging: prevent recursive workers. */
+6 -2
View File
@@ -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<uint64_t> expectedNarSize{[this] { updateStatisticsLater(); }};
NotifyingCounter<uint64_t> 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<typename MkGoals>
kj::Promise<Result<Worker::Results>>
processGoals(Store & store, Store & evalStore, MkGoals && mkGoals) noexcept
try {
co_return co_await Worker(store, evalStore).run(std::forward<MkGoals>(mkGoals));
co_return co_await Worker(store, evalStore, LIX_TRY_AWAIT(queryAvailableNamespaces()))
.run(std::forward<MkGoals>(mkGoals));
} catch (...) {
co_return result::current_exception();
}
+4 -3
View File
@@ -982,8 +982,9 @@ Pid LinuxLocalDerivationGoal::startChild(std::function<void()> 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<void()> openSlave)
assert(ss.size() == 1);
Pid pid = Pid{string2Int<pid_t>(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<void()> 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) {
+30 -53
View File
@@ -6,6 +6,8 @@
#include "lix/libutil/processes.hh"
#include "lix/libutil/strings.hh"
#include <kj/common.h>
#include <ranges>
#include <sys/mount.h>
#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<Result<AvailableNamespaces>> 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<Strings>(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<Result<AvailableNamespaces>> queryAvailableNamespaces()
try {
return {AvailableNamespaces{}};
} catch (...) {
return {result::current_exception()};
}
#endif
}
+6 -8
View File
@@ -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<Result<AvailableNamespaces>> queryAvailableNamespaces();
}