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:
@@ -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;
|
||||
}
|
||||
@@ -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'),
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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. */
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user