Files
lix/lix/libexec/launch-builder.cc
T
eldritch horrors 6ddc59de47 launch-builder: don't limit sandbox setup data size
the channel we're using is fully trusted, we don't need the traversal limit.

fixes #1121

Change-Id: I2833a7459cec1e2392e8740d8cab213685d3e2b3
2026-03-02 12:10:12 +00:00

226 lines
7.4 KiB
C++

#include "launch-builder.hh"
#include "lix/libstore/build/request.capnp.h"
#include "lix/libutil/rpc.hh"
#include <capnp/message.h>
#include <capnp/serialize.h>
#include <csignal>
#include <cstdint>
#include <exception>
#include <fcntl.h>
#include <filesystem>
#include <grp.h>
#include <limits>
#include <sys/resource.h>
#include <unistd.h>
#include <vector>
namespace nix {
bool printDebugLogs = false;
static void requireCString(const char * context, const std::string & s)
{
if (s.contains('\0')) {
std::string p{s};
for (auto pos = p.find('\0'); pos != p.npos; pos = p.find('\0')) {
p.replace(pos, 1, "␀");
}
// NOLINTNEXTLINE(lix-foreign-exceptions)
throw std::runtime_error(std::format("derivation {} {} contains NUL bytes", context, p));
}
}
ExecRequest::ExecRequest(build::Request::Reader request)
{
const auto fill = [](auto context, auto & strings, auto & pointers, auto from) {
strings.reserve(from.size());
for (auto arg : from) {
strings.push_back(rpc::to<std::string>(arg));
requireCString(context, strings.back());
pointers.push_back(strings.back().data());
}
pointers.push_back(nullptr);
};
builder = rpc::to<std::string>(request.getBuilder());
requireCString("derivation builder", builder);
fill("derivation argument", argsStorage, args, request.getArgs());
fill("derivation environment entry", envsStorage, envs, request.getEnvironment());
}
void writeFull(int fd, std::string_view data)
{
while (!data.empty()) {
const auto wrote = ::write(fd, data.data(), data.size());
if (wrote < 0) {
throw SysError("write()");
} else {
data.remove_prefix(size_t(wrote));
}
}
}
static void closeExtraFDs()
{
constexpr int MAX_KEPT_FD = 2;
static_assert(std::max({STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO}) == MAX_KEPT_FD);
// Both Linux and FreeBSD support close_range.
#if __linux__ || __FreeBSD__
auto closeRange = [](unsigned int first, unsigned int last, int flags) -> int {
// musl does not have close_range as of 2024-08-10
// patch: https://www.openwall.com/lists/musl/2024/08/01/9
#if HAVE_CLOSE_RANGE
return close_range(first, last, flags);
#else
return syscall(SYS_close_range, first, last, flags);
#endif
};
// first try to close_range everything we don't care about. if this
// returns an error with these parameters we're running on a kernel
// that does not implement close_range (i.e. pre 5.9) and fall back
// to the old method. we should remove that though, in some future.
if (closeRange(3, ~0U, 0) == 0) {
return;
}
#endif
#if __linux__
try {
for (auto & s : std::filesystem::directory_iterator("/proc/self/fd")) {
auto fd = std::stoi(s.path().filename().c_str());
if (fd > MAX_KEPT_FD) {
debug("closing leaked FD %d", fd);
close(fd);
}
}
return;
} catch (std::exception &) { // NOLINT(lix-foreign-exceptions): that's what std::filesystem throws
}
#endif
int maxFD = 0;
maxFD = sysconf(_SC_OPEN_MAX);
for (int fd = MAX_KEPT_FD + 1; fd < maxFD; ++fd) {
close(fd); /* ignore result */
}
}
}
int main(int argc, char * argv[])
{
using namespace nix;
if (argc < 1) {
return 255;
}
bool sendException = true;
try {
capnp::MallocMessageBuilder buf;
capnp::readMessageCopyFromFd(
STDIN_FILENO, buf, {.traversalLimitInWords = std::numeric_limits<uint64_t>::max()}
);
auto request = buf.getRoot<build::Request>().asReader();
printDebugLogs = request.getDebug();
{
sigset_t set;
sigemptyset(&set);
if (sigprocmask(SIG_SETMASK, &set, nullptr)) {
throw SysError("failed to unmask signals");
}
}
/* Put the child in a separate session (and thus a separate
process group) so that it has no controlling terminal (meaning
that e.g. ssh cannot open /dev/tty) and it doesn't receive
terminal signals. */
if (setsid() == -1) {
throw SysError("creating a new session");
}
/* Dup stderr to stdout. */
if (dup2(STDERR_FILENO, STDOUT_FILENO) == -1) {
throw SysError("cannot dup stderr into stdout");
}
const bool setUser = prepareChildSetup(request);
// NOLINTNEXTLINE(lix-unsafe-c-calls): we trust the parent here
if (chdir(rpc::to<std::string>(request.getWorkingDir()).c_str()) == -1) {
throw SysError("changing into %s", rpc::to<std::string>(request.getWorkingDir()));
}
/* Disable core dumps by default. */
struct rlimit limit = {0, RLIM_INFINITY};
if (request.getEnableCoreDumps()) {
limit.rlim_cur = RLIM_INFINITY;
}
setrlimit(RLIMIT_CORE, &limit);
// FIXME: set other limits to deterministic values?
/* If we are running in `build-users' mode, then switch to the
user we allocated above. Make sure that we drop all root
privileges. Note that above we have closed all file
descriptors except std*, so that's safe. Also note that
setuid() when run as root sets the real, effective and
saved UIDs. */
if (setUser && request.hasCredentials()) {
auto creds = request.getCredentials();
/* Preserve supplementary groups of the build user, to allow
admins to specify groups such as "kvm". */
std::vector<gid_t> gids;
std::copy(
creds.getSupplementaryGroups().begin(),
creds.getSupplementaryGroups().end(),
std::back_inserter(gids)
);
if (setgroups(gids.size(), gids.data()) == -1) {
throw SysError("cannot set supplementary groups of build user");
}
if (setgid(creds.getGid()) == -1 || getgid() != creds.getGid() || getegid() != creds.getGid()) {
throw SysError("setgid failed");
}
if (setuid(creds.getUid()) == -1 || getuid() != creds.getUid() || geteuid() != creds.getUid()) {
throw SysError("setuid failed");
}
}
finishChildSetup(request);
/* Close all other file descriptors. */
closeExtraFDs();
// Reroute stdin to /dev/null. closing the setup socket fd also signals
// successful setup of the builder, all other errors must go to stderr.
kj::AutoCloseFd fdDevNull{open("/dev/null", O_RDWR | O_CLOEXEC)};
if (fdDevNull == nullptr) {
throw SysError("cannot open /dev/null");
}
if (dup2(fdDevNull.get(), STDIN_FILENO) == -1) {
throw SysError("cannot dup null device into stdin");
}
sendException = false;
execBuilder(request);
} catch (std::exception & e) { // NOLINT(lix-foreign-exceptions)
if (sendException) {
capnp::MallocMessageBuilder builder;
auto error = builder.getRoot<build::SetupResponse>();
RPC_FILL(error, setFatalError, e.what());
capnp::writeMessageToFd(STDIN_FILENO, builder);
} else {
writeFull(STDERR_FILENO, e.what());
}
return 1;
}
}