aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/libstore/build/local-derivation-goal.cc103
-rw-r--r--src/libstore/build/local-derivation-goal.hh11
-rw-r--r--src/libstore/cgroup.cc87
-rw-r--r--src/libstore/cgroup.hh15
-rw-r--r--src/libstore/globals.hh21
-rw-r--r--src/libstore/local-store.cc77
-rw-r--r--src/libstore/local-store.hh15
-rw-r--r--src/libstore/lock.cc287
-rw-r--r--src/libstore/lock.hh46
-rw-r--r--src/libutil/experimental-features.cc2
-rw-r--r--src/libutil/experimental-features.hh2
-rw-r--r--src/nix-store/nix-store.cc6
12 files changed, 504 insertions, 168 deletions
diff --git a/src/libstore/build/local-derivation-goal.cc b/src/libstore/build/local-derivation-goal.cc
index 5cea3b590..64540d262 100644
--- a/src/libstore/build/local-derivation-goal.cc
+++ b/src/libstore/build/local-derivation-goal.cc
@@ -158,28 +158,21 @@ void LocalDerivationGoal::tryLocalBuild() {
return;
}
- /* If `build-users-group' is not empty, then we have to build as
- one of the members of that group. */
- if (settings.buildUsersGroup != "" && getuid() == 0) {
-#if defined(__linux__) || defined(__APPLE__)
- if (!buildUser) buildUser = std::make_unique<UserLock>();
-
- if (buildUser->findFreeUser()) {
- /* Make sure that no other processes are executing under this
- uid. */
- buildUser->kill();
- } else {
+ if (useBuildUsers()) {
+ if (!buildUser)
+ buildUser = acquireUserLock();
+
+ if (!buildUser) {
if (!actLock)
actLock = std::make_unique<Activity>(*logger, lvlWarn, actBuildWaiting,
fmt("waiting for UID to build '%s'", yellowtxt(worker.store.printStorePath(drvPath))));
worker.waitForAWhile(shared_from_this());
return;
}
-#else
- /* Don't know how to block the creation of setuid/setgid
- binaries on this platform. */
- throw Error("build users are not supported on this platform for security reasons");
-#endif
+
+ /* Make sure that no other processes are executing under this
+ uid. */
+ buildUser->kill();
}
actLock.reset();
@@ -502,6 +495,9 @@ void LocalDerivationGoal::startBuilder()
}
}
+ useUidRange = parsedDrv->getRequiredSystemFeatures().count("uid-range");
+ useSystemdCgroup = parsedDrv->getRequiredSystemFeatures().count("Systemd-cgroup");
+
if (useChroot) {
/* Allow a user-configurable set of directories from the
@@ -580,10 +576,11 @@ void LocalDerivationGoal::startBuilder()
printMsg(lvlChatty, format("setting up chroot environment in '%1%'") % chrootRootDir);
- if (mkdir(chrootRootDir.c_str(), 0750) == -1)
+ if (mkdir(chrootRootDir.c_str(), useUidRange ? 0755 : 0750) == -1)
throw SysError("cannot create '%1%'", chrootRootDir);
- if (buildUser && chown(chrootRootDir.c_str(), 0, buildUser->getGID()) == -1)
+ // FIXME: only make root writable for user namespace builds.
+ if (buildUser && chown(chrootRootDir.c_str(), buildUser->getUID(), buildUser->getGID()) == -1)
throw SysError("cannot change ownership of '%1%'", chrootRootDir);
/* Create a writable /tmp in the chroot. Many builders need
@@ -597,6 +594,10 @@ void LocalDerivationGoal::startBuilder()
nobody account. The latter is kind of a hack to support
Samba-in-QEMU. */
createDirs(chrootRootDir + "/etc");
+ chownToBuilder(chrootRootDir + "/etc");
+
+ if (useUidRange && (!buildUser || buildUser->getUIDCount() < 65536))
+ throw Error("feature 'uid-range' requires '%s' to be enabled", settings.autoAllocateUids.name);
/* Declare the build user's group so that programs get a consistent
view of the system (e.g., "id -gn"). */
@@ -647,12 +648,32 @@ void LocalDerivationGoal::startBuilder()
dirsInChroot.erase(worker.store.printStorePath(*i.second.second));
}
-#elif __APPLE__
- /* We don't really have any parent prep work to do (yet?)
- All work happens in the child, instead. */
+ if (useSystemdCgroup) {
+ settings.requireExperimentalFeature(Xp::SystemdCgroup);
+ std::optional<Path> cgroup;
+ if (!buildUser || !(cgroup = buildUser->getCgroup()))
+ throw Error("feature 'systemd-cgroup' requires 'auto-allocate-uids = true' in nix.conf");
+ chownToBuilder(*cgroup);
+ chownToBuilder(*cgroup + "/cgroup.procs");
+ }
+
#else
- throw Error("sandboxing builds is not supported on this platform");
+ if (useUidRange)
+ throw Error("feature 'uid-range' is not supported on this platform");
+ if (useSystemdCgroup)
+ throw Error("feature 'systemd-cgroup' is not supported on this platform");
+ #if __APPLE__
+ /* We don't really have any parent prep work to do (yet?)
+ All work happens in the child, instead. */
+ #else
+ throw Error("sandboxing builds is not supported on this platform");
+ #endif
#endif
+ } else {
+ if (useUidRange)
+ throw Error("feature 'uid-range' is only supported in sandboxed builds");
+ if (useSystemdCgroup)
+ throw Error("feature 'systemd-cgroup' is only supported in sandboxed builds");
}
if (needsHashRewrite() && pathExists(homeDir))
@@ -913,14 +934,16 @@ void LocalDerivationGoal::startBuilder()
the calling user (if build users are disabled). */
uid_t hostUid = buildUser ? buildUser->getUID() : getuid();
uid_t hostGid = buildUser ? buildUser->getGID() : getgid();
+ uint32_t nrIds = buildUser && useUidRange ? buildUser->getUIDCount() : 1;
writeFile("/proc/" + std::to_string(pid) + "/uid_map",
- fmt("%d %d 1", sandboxUid(), hostUid));
+ fmt("%d %d %d", sandboxUid(), hostUid, nrIds));
- writeFile("/proc/" + std::to_string(pid) + "/setgroups", "deny");
+ if (!useUidRange)
+ writeFile("/proc/" + std::to_string(pid) + "/setgroups", "deny");
writeFile("/proc/" + std::to_string(pid) + "/gid_map",
- fmt("%d %d 1", sandboxGid(), hostGid));
+ fmt("%d %d %d", sandboxGid(), hostGid, nrIds));
} else {
debug("note: not using a user namespace");
if (!buildUser)
@@ -947,6 +970,12 @@ void LocalDerivationGoal::startBuilder()
throw SysError("getting sandbox user namespace");
}
+ /* Move the child into its own cgroup. */
+ if (buildUser) {
+ if (auto cgroup = buildUser->getCgroup())
+ writeFile(*cgroup + "/cgroup.procs", fmt("%d", (pid_t) pid));
+ }
+
/* Signal the builder that we've updated its user namespace. */
writeFull(userNamespaceSync.writeSide.get(), "1");
@@ -1763,6 +1792,13 @@ void LocalDerivationGoal::runChild()
if (mount("none", (chrootRootDir + "/proc").c_str(), "proc", 0, 0) == -1)
throw SysError("mounting /proc");
+ /* Mount sysfs on /sys. */
+ if (useUidRange) {
+ createDirs(chrootRootDir + "/sys");
+ if (mount("none", (chrootRootDir + "/sys").c_str(), "sysfs", 0, 0) == -1)
+ throw SysError("mounting /sys");
+ }
+
/* Mount a new tmpfs on /dev/shm to ensure that whatever
the builder puts in /dev/shm is cleaned up automatically. */
if (pathExists("/dev/shm") && mount("none", (chrootRootDir + "/dev/shm").c_str(), "tmpfs", 0,
@@ -1805,6 +1841,12 @@ void LocalDerivationGoal::runChild()
if (unshare(CLONE_NEWNS) == -1)
throw SysError("unsharing mount namespace");
+ /* Unshare the cgroup namespace. This means
+ /proc/self/cgroup will show the child's cgroup as '/'
+ rather than whatever it is in the parent. */
+ if (useSystemdCgroup && unshare(CLONE_NEWCGROUP) == -1)
+ throw SysError("unsharing cgroup namespace");
+
/* Do the chroot(). */
if (chdir(chrootRootDir.c_str()) == -1)
throw SysError("cannot change directory to '%1%'", chrootRootDir);
@@ -2221,7 +2263,10 @@ DrvOutputs LocalDerivationGoal::registerOutputs()
/* Canonicalise first. This ensures that the path we're
rewriting doesn't contain a hard link to /etc/shadow or
something like that. */
- canonicalisePathMetaData(actualPath, buildUser ? buildUser->getUID() : -1, inodesSeen);
+ canonicalisePathMetaData(
+ actualPath,
+ buildUser ? std::optional(buildUser->getUIDRange()) : std::nullopt,
+ inodesSeen);
debug("scanning for references for output '%s' in temp location '%s'", outputName, actualPath);
@@ -2314,6 +2359,10 @@ DrvOutputs LocalDerivationGoal::registerOutputs()
sink.s = rewriteStrings(sink.s, outputRewrites);
StringSource source(sink.s);
restorePath(actualPath, source);
+
+ /* FIXME: set proper permissions in restorePath() so
+ we don't have to do another traversal. */
+ canonicalisePathMetaData(actualPath, {}, inodesSeen);
}
};
@@ -2476,7 +2525,7 @@ DrvOutputs LocalDerivationGoal::registerOutputs()
/* FIXME: set proper permissions in restorePath() so
we don't have to do another traversal. */
- canonicalisePathMetaData(actualPath, -1, inodesSeen);
+ canonicalisePathMetaData(actualPath, {}, inodesSeen);
/* Calculate where we'll move the output files. In the checking case we
will leave leave them where they are, for now, rather than move to
diff --git a/src/libstore/build/local-derivation-goal.hh b/src/libstore/build/local-derivation-goal.hh
index d456e9cae..e6700a383 100644
--- a/src/libstore/build/local-derivation-goal.hh
+++ b/src/libstore/build/local-derivation-goal.hh
@@ -41,6 +41,13 @@ struct LocalDerivationGoal : public DerivationGoal
Path chrootRootDir;
+ /* Whether to give the build more than 1 UID. */
+ bool useUidRange = false;
+
+ /* Whether to make the 'systemd' cgroup controller available to
+ the build. */
+ bool useSystemdCgroup = false;
+
/* RAII object to delete the chroot directory. */
std::shared_ptr<AutoDelete> autoDelChroot;
@@ -92,8 +99,8 @@ struct LocalDerivationGoal : public DerivationGoal
result. */
std::map<Path, ValidPathInfo> prevInfos;
- uid_t sandboxUid() { return usingUserNamespace ? 1000 : buildUser->getUID(); }
- gid_t sandboxGid() { return usingUserNamespace ? 100 : buildUser->getGID(); }
+ uid_t sandboxUid() { return usingUserNamespace ? (useUidRange ? 0 : 1000) : buildUser->getUID(); }
+ gid_t sandboxGid() { return usingUserNamespace ? (useUidRange ? 0 : 100) : buildUser->getGID(); }
const static Path homeDir;
diff --git a/src/libstore/cgroup.cc b/src/libstore/cgroup.cc
new file mode 100644
index 000000000..5d31609da
--- /dev/null
+++ b/src/libstore/cgroup.cc
@@ -0,0 +1,87 @@
+#if __linux__
+
+#include "cgroup.hh"
+#include "util.hh"
+
+#include <chrono>
+#include <cmath>
+#include <regex>
+#include <unordered_set>
+#include <thread>
+
+#include <dirent.h>
+
+namespace nix {
+
+std::map<std::string, std::string> getCgroups(const Path & cgroupFile)
+{
+ std::map<std::string, std::string> cgroups;
+
+ for (auto & line : tokenizeString<std::vector<std::string>>(readFile(cgroupFile), "\n")) {
+ static std::regex regex("([0-9]+):([^:]*):(.*)");
+ std::smatch match;
+ if (!std::regex_match(line, match, regex))
+ throw Error("invalid line '%s' in '%s'", line, cgroupFile);
+
+ std::string name = hasPrefix(std::string(match[2]), "name=") ? std::string(match[2], 5) : match[2];
+ cgroups.insert_or_assign(name, match[3]);
+ }
+
+ return cgroups;
+}
+
+void destroyCgroup(const Path & cgroup)
+{
+ if (!pathExists(cgroup)) return;
+
+ for (auto & entry : readDirectory(cgroup)) {
+ if (entry.type != DT_DIR) continue;
+ destroyCgroup(cgroup + "/" + entry.name);
+ }
+
+ int round = 1;
+
+ std::unordered_set<pid_t> pidsShown;
+
+ while (true) {
+ auto pids = tokenizeString<std::vector<std::string>>(readFile(cgroup + "/cgroup.procs"));
+
+ if (pids.empty()) break;
+
+ if (round > 20)
+ throw Error("cannot kill cgroup '%s'", cgroup);
+
+ for (auto & pid_s : pids) {
+ pid_t pid;
+ if (auto o = string2Int<pid_t>(pid_s))
+ pid = *o;
+ else
+ throw Error("invalid pid '%s'", pid);
+ if (pidsShown.insert(pid).second) {
+ try {
+ auto cmdline = readFile(fmt("/proc/%d/cmdline", pid));
+ using namespace std::string_literals;
+ warn("killing stray builder process %d (%s)...",
+ pid, trim(replaceStrings(cmdline, "\0"s, " ")));
+ } catch (SysError &) {
+ }
+ }
+ // FIXME: pid wraparound
+ if (kill(pid, SIGKILL) == -1 && errno != ESRCH)
+ throw SysError("killing member %d of cgroup '%s'", pid, cgroup);
+ }
+
+ auto sleep = std::chrono::milliseconds((int) std::pow(2.0, std::min(round, 10)));
+ if (sleep.count() > 100)
+ printError("waiting for %d ms for cgroup '%s' to become empty", sleep.count(), cgroup);
+ std::this_thread::sleep_for(sleep);
+ round++;
+ }
+
+ if (rmdir(cgroup.c_str()) == -1)
+ throw SysError("deleting cgroup '%s'", cgroup);
+}
+
+}
+
+#endif
diff --git a/src/libstore/cgroup.hh b/src/libstore/cgroup.hh
new file mode 100644
index 000000000..dc6758957
--- /dev/null
+++ b/src/libstore/cgroup.hh
@@ -0,0 +1,15 @@
+#pragma once
+
+#if __linux__
+
+#include "types.hh"
+
+namespace nix {
+
+std::map<std::string, std::string> getCgroups(const Path & cgroupFile);
+
+void destroyCgroup(const Path & cgroup);
+
+}
+
+#endif
diff --git a/src/libstore/globals.hh b/src/libstore/globals.hh
index 3dcf3d479..d3e86cc55 100644
--- a/src/libstore/globals.hh
+++ b/src/libstore/globals.hh
@@ -275,6 +275,19 @@ public:
multi-user settings with untrusted users.
)"};
+ #if __linux__
+ Setting<bool> autoAllocateUids{this, false, "auto-allocate-uids",
+ "Whether to allocate UIDs for builders automatically."};
+
+ const uint32_t idsPerBuild = 1 << 16;
+
+ Setting<uint32_t> startId{this, 872415232, "start-id",
+ "The first UID and GID to use for dynamic ID allocation."};
+
+ Setting<uint32_t> uidCount{this, idsPerBuild * 128, "id-count",
+ "The number of UIDs/GIDs to use for dynamic ID allocation."};
+ #endif
+
Setting<bool> impersonateLinux26{this, false, "impersonate-linux-26",
"Whether to impersonate a Linux 2.6 machine on newer kernels.",
{"build-impersonate-linux-26"}};
@@ -652,7 +665,7 @@ public:
is `root`.
> **Warning**
- >
+ >
> Adding a user to `trusted-users` is essentially equivalent to
> giving that user root access to the system. For example, the user
> can set `sandbox-paths` and thereby obtain read access to
@@ -742,13 +755,13 @@ public:
The program executes with no arguments. The program's environment
contains the following environment variables:
- - `DRV_PATH`
+ - `DRV_PATH`
The derivation for the built paths.
Example:
`/nix/store/5nihn1a7pa8b25l9zafqaqibznlvvp3f-bash-4.4-p23.drv`
- - `OUT_PATHS`
+ - `OUT_PATHS`
Output paths of the built derivation, separated by a space
character.
@@ -786,7 +799,7 @@ public:
documentation](https://ec.haxx.se/usingcurl-netrc.html).
> **Note**
- >
+ >
> This must be an absolute path, and `~` is not resolved. For
> example, `~/.netrc` won't resolve to your home directory's
> `.netrc`.
diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc
index d374d4558..04223d860 100644
--- a/src/libstore/local-store.cc
+++ b/src/libstore/local-store.cc
@@ -583,7 +583,10 @@ void canonicaliseTimestampAndPermissions(const Path & path)
}
-static void canonicalisePathMetaData_(const Path & path, uid_t fromUid, InodesSeen & inodesSeen)
+static void canonicalisePathMetaData_(
+ const Path & path,
+ std::optional<std::pair<uid_t, uid_t>> uidRange,
+ InodesSeen & inodesSeen)
{
checkInterrupt();
@@ -630,7 +633,7 @@ static void canonicalisePathMetaData_(const Path & path, uid_t fromUid, InodesSe
However, ignore files that we chown'ed ourselves previously to
ensure that we don't fail on hard links within the same build
(i.e. "touch $out/foo; ln $out/foo $out/bar"). */
- if (fromUid != (uid_t) -1 && st.st_uid != fromUid) {
+ if (uidRange && (st.st_uid < uidRange->first || st.st_uid > uidRange->second)) {
if (S_ISDIR(st.st_mode) || !inodesSeen.count(Inode(st.st_dev, st.st_ino)))
throw BuildError("invalid ownership on file '%1%'", path);
mode_t mode = st.st_mode & ~S_IFMT;
@@ -663,14 +666,17 @@ static void canonicalisePathMetaData_(const Path & path, uid_t fromUid, InodesSe
if (S_ISDIR(st.st_mode)) {
DirEntries entries = readDirectory(path);
for (auto & i : entries)
- canonicalisePathMetaData_(path + "/" + i.name, fromUid, inodesSeen);
+ canonicalisePathMetaData_(path + "/" + i.name, uidRange, inodesSeen);
}
}
-void canonicalisePathMetaData(const Path & path, uid_t fromUid, InodesSeen & inodesSeen)
+void canonicalisePathMetaData(
+ const Path & path,
+ std::optional<std::pair<uid_t, uid_t>> uidRange,
+ InodesSeen & inodesSeen)
{
- canonicalisePathMetaData_(path, fromUid, inodesSeen);
+ canonicalisePathMetaData_(path, uidRange, inodesSeen);
/* On platforms that don't have lchown(), the top-level path can't
be a symlink, since we can't change its ownership. */
@@ -683,10 +689,11 @@ void canonicalisePathMetaData(const Path & path, uid_t fromUid, InodesSeen & ino
}
-void canonicalisePathMetaData(const Path & path, uid_t fromUid)
+void canonicalisePathMetaData(const Path & path,
+ std::optional<std::pair<uid_t, uid_t>> uidRange)
{
InodesSeen inodesSeen;
- canonicalisePathMetaData(path, fromUid, inodesSeen);
+ canonicalisePathMetaData(path, uidRange, inodesSeen);
}
@@ -890,48 +897,48 @@ void LocalStore::queryPathInfoUncached(const StorePath & path,
std::shared_ptr<const ValidPathInfo> LocalStore::queryPathInfoInternal(State & state, const StorePath & path)
{
- /* Get the path info. */
+ /* Get the path info. */
auto useQueryPathInfo(state.stmts->QueryPathInfo.use()(printStorePath(path)));
- if (!useQueryPathInfo.next())
- return std::shared_ptr<ValidPathInfo>();
+ if (!useQueryPathInfo.next())
+ return std::shared_ptr<ValidPathInfo>();
- auto id = useQueryPathInfo.getInt(0);
+ auto id = useQueryPathInfo.getInt(0);
- auto narHash = Hash::dummy;
- try {
- narHash = Hash::parseAnyPrefixed(useQueryPathInfo.getStr(1));
- } catch (BadHash & e) {
- throw Error("invalid-path entry for '%s': %s", printStorePath(path), e.what());
- }
+ auto narHash = Hash::dummy;
+ try {
+ narHash = Hash::parseAnyPrefixed(useQueryPathInfo.getStr(1));
+ } catch (BadHash & e) {
+ throw Error("invalid-path entry for '%s': %s", printStorePath(path), e.what());
+ }
- auto info = std::make_shared<ValidPathInfo>(path, narHash);
+ auto info = std::make_shared<ValidPathInfo>(path, narHash);
- info->id = id;
+ info->id = id;
- info->registrationTime = useQueryPathInfo.getInt(2);
+ info->registrationTime = useQueryPathInfo.getInt(2);
auto s = (const char *) sqlite3_column_text(state.stmts->QueryPathInfo, 3);
- if (s) info->deriver = parseStorePath(s);
+ if (s) info->deriver = parseStorePath(s);
- /* Note that narSize = NULL yields 0. */
- info->narSize = useQueryPathInfo.getInt(4);
+ /* Note that narSize = NULL yields 0. */
+ info->narSize = useQueryPathInfo.getInt(4);
- info->ultimate = useQueryPathInfo.getInt(5) == 1;
+ info->ultimate = useQueryPathInfo.getInt(5) == 1;
s = (const char *) sqlite3_column_text(state.stmts->QueryPathInfo, 6);
- if (s) info->sigs = tokenizeString<StringSet>(s, " ");
+ if (s) info->sigs = tokenizeString<StringSet>(s, " ");
s = (const char *) sqlite3_column_text(state.stmts->QueryPathInfo, 7);
- if (s) info->ca = parseContentAddressOpt(s);
+ if (s) info->ca = parseContentAddressOpt(s);
- /* Get the references. */
+ /* Get the references. */
auto useQueryReferences(state.stmts->QueryReferences.use()(info->id));
- while (useQueryReferences.next())
- info->references.insert(parseStorePath(useQueryReferences.getStr(0)));
+ while (useQueryReferences.next())
+ info->references.insert(parseStorePath(useQueryReferences.getStr(0)));
- return info;
+ return info;
}
@@ -1034,9 +1041,9 @@ LocalStore::queryPartialDerivationOutputMap(const StorePath & path_)
auto path = path_;
auto outputs = retrySQLite<std::map<std::string, std::optional<StorePath>>>([&]() {
auto state(_state.lock());
- std::map<std::string, std::optional<StorePath>> outputs;
+ std::map<std::string, std::optional<StorePath>> outputs;
uint64_t drvId;
- drvId = queryValidPathId(*state, path);
+ drvId = queryValidPathId(*state, path);
auto use(state->stmts->QueryDerivationOutputs.use()(drvId));
while (use.next())
outputs.insert_or_assign(
@@ -1331,7 +1338,7 @@ void LocalStore::addToStore(const ValidPathInfo & info, Source & source,
autoGC();
- canonicalisePathMetaData(realPath, -1);
+ canonicalisePathMetaData(realPath, {});
optimisePath(realPath, repair); // FIXME: combine with hashPath()
@@ -1444,7 +1451,7 @@ StorePath LocalStore::addToStoreFromDump(Source & source0, std::string_view name
narHash = narSink.finish();
}
- canonicalisePathMetaData(realPath, -1); // FIXME: merge into restorePath
+ canonicalisePathMetaData(realPath, {}); // FIXME: merge into restorePath
optimisePath(realPath, repair);
@@ -1486,7 +1493,7 @@ StorePath LocalStore::addTextToStore(
writeFile(realPath, s);
- canonicalisePathMetaData(realPath, -1);
+ canonicalisePathMetaData(realPath, {});
StringSink sink;
dumpString(s, sink);
diff --git a/src/libstore/local-store.hh b/src/libstore/local-store.hh
index bd0ce1fe6..4579c2f62 100644
--- a/src/libstore/local-store.hh
+++ b/src/libstore/local-store.hh
@@ -310,9 +310,18 @@ typedef std::set<Inode> InodesSeen;
- the permissions are set of 444 or 555 (i.e., read-only with or
without execute permission; setuid bits etc. are cleared)
- the owner and group are set to the Nix user and group, if we're
- running as root. */
-void canonicalisePathMetaData(const Path & path, uid_t fromUid, InodesSeen & inodesSeen);
-void canonicalisePathMetaData(const Path & path, uid_t fromUid);
+ running as root.
+ If uidRange is not empty, this function will throw an error if it
+ encounters files owned by a user outside of the closed interval
+ [uidRange->first, uidRange->second].
+*/
+void canonicalisePathMetaData(
+ const Path & path,
+ std::optional<std::pair<uid_t, uid_t>> uidRange,
+ InodesSeen & inodesSeen);
+void canonicalisePathMetaData(
+ const Path & path,
+ std::optional<std::pair<uid_t, uid_t>> uidRange);
void canonicaliseTimestampAndPermissions(const Path & path);
diff --git a/src/libstore/lock.cc b/src/libstore/lock.cc
index fa718f55d..cc3977496 100644
--- a/src/libstore/lock.cc
+++ b/src/libstore/lock.cc
@@ -1,106 +1,245 @@
#include "lock.hh"
#include "globals.hh"
#include "pathlocks.hh"
+#include "cgroup.hh"
-#include <grp.h>
#include <pwd.h>
-
-#include <fcntl.h>
-#include <unistd.h>
+#include <grp.h>
namespace nix {
-UserLock::UserLock()
+struct SimpleUserLock : UserLock
{
- assert(settings.buildUsersGroup != "");
- createDirs(settings.nixStateDir + "/userpool");
-}
-
-bool UserLock::findFreeUser() {
- if (enabled()) return true;
-
- /* Get the members of the build-users-group. */
- struct group * gr = getgrnam(settings.buildUsersGroup.get().c_str());
- if (!gr)
- throw Error("the group '%1%' specified in 'build-users-group' does not exist",
- settings.buildUsersGroup);
- gid = gr->gr_gid;
-
- /* Copy the result of getgrnam. */
- Strings users;
- for (char * * p = gr->gr_mem; *p; ++p) {
- debug("found build user '%1%'", *p);
- users.push_back(*p);
+ AutoCloseFD fdUserLock;
+ uid_t uid;
+ gid_t gid;
+ std::vector<gid_t> supplementaryGIDs;
+
+ void kill() override
+ {
+ killUser(uid);
}
- if (users.empty())
- throw Error("the build users group '%1%' has no members",
- settings.buildUsersGroup);
+ std::pair<uid_t, uid_t> getUIDRange() override
+ {
+ assert(uid);
+ return {uid, uid};
+ }
- /* Find a user account that isn't currently in use for another
- build. */
- for (auto & i : users) {
- debug("trying user '%1%'", i);
+ gid_t getGID() override { assert(gid); return gid; }
- struct passwd * pw = getpwnam(i.c_str());
- if (!pw)
- throw Error("the user '%1%' in the group '%2%' does not exist",
- i, settings.buildUsersGroup);
+ std::vector<gid_t> getSupplementaryGIDs() override { return supplementaryGIDs; }
+ static std::unique_ptr<UserLock> acquire()
+ {
+ assert(settings.buildUsersGroup != "");
+ createDirs(settings.nixStateDir + "/userpool");
- fnUserLock = (format("%1%/userpool/%2%") % settings.nixStateDir % pw->pw_uid).str();
+ /* Get the members of the build-users-group. */
+ struct group * gr = getgrnam(settings.buildUsersGroup.get().c_str());
+ if (!gr)
+ throw Error("the group '%s' specified in 'build-users-group' does not exist", settings.buildUsersGroup);
- AutoCloseFD fd = open(fnUserLock.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0600);
- if (!fd)
- throw SysError("opening user lock '%1%'", fnUserLock);
+ /* Copy the result of getgrnam. */
+ Strings users;
+ for (char * * p = gr->gr_mem; *p; ++p) {
+ debug("found build user '%s'", *p);
+ users.push_back(*p);
+ }
- if (lockFile(fd.get(), ltWrite, false)) {
- fdUserLock = std::move(fd);
- user = i;
- uid = pw->pw_uid;
+ if (users.empty())
+ throw Error("the build users group '%s' has no members", settings.buildUsersGroup);
+
+ /* Find a user account that isn't currently in use for another
+ build. */
+ for (auto & i : users) {
+ debug("trying user '%s'", i);
+
+ struct passwd * pw = getpwnam(i.c_str());
+ if (!pw)
+ throw Error("the user '%s' in the group '%s' does not exist", i, settings.buildUsersGroup);
+
+ auto fnUserLock = fmt("%s/userpool/%s", settings.nixStateDir,pw->pw_uid);
+
+ AutoCloseFD fd = open(fnUserLock.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0600);
+ if (!fd)
+ throw SysError("opening user lock '%s'", fnUserLock);
+
+ if (lockFile(fd.get(), ltWrite, false)) {
+ auto lock = std::make_unique<SimpleUserLock>();
+
+ lock->fdUserLock = std::move(fd);
+ lock->uid = pw->pw_uid;
+ lock->gid = gr->gr_gid;
+
+ /* Sanity check... */
+ if (lock->uid == getuid() || lock->uid == geteuid())
+ throw Error("the Nix user should not be a member of '%s'", settings.buildUsersGroup);
+
+ #if __linux__
+ /* Get the list of supplementary groups of this build
+ user. This is usually either empty or contains a
+ group such as "kvm". */
+ int ngroups = 32; // arbitrary initial guess
+ lock->supplementaryGIDs.resize(ngroups);
+
+ int err = getgrouplist(
+ pw->pw_name, pw->pw_gid,
+ lock->supplementaryGIDs.data(),
+ &ngroups);
+
+ /* Our initial size of 32 wasn't sufficient, the
+ correct size has been stored in ngroups, so we try
+ again. */
+ if (err == -1) {
+ lock->supplementaryGIDs.resize(ngroups);
+ err = getgrouplist(
+ pw->pw_name, pw->pw_gid,
+ lock->supplementaryGIDs.data(),
+ &ngroups);
+ }
+
+ // If it failed once more, then something must be broken.
+ if (err == -1)
+ throw Error("failed to get list of supplementary groups for '%s'", pw->pw_name);
+
+ // Finally, trim back the GID list to its real size.
+ lock->supplementaryGIDs.resize(ngroups);
+ #endif
+
+ return lock;
+ }
+ }
- /* Sanity check... */
- if (uid == getuid() || uid == geteuid())
- throw Error("the Nix user should not be a member of '%1%'",
- settings.buildUsersGroup);
+ return nullptr;
+ }
+};
#if __linux__
- /* Get the list of supplementary groups of this build user. This
- is usually either empty or contains a group such as "kvm". */
- int ngroups = 32; // arbitrary initial guess
- supplementaryGIDs.resize(ngroups);
-
- int err = getgrouplist(pw->pw_name, pw->pw_gid, supplementaryGIDs.data(),
- &ngroups);
-
- // Our initial size of 32 wasn't sufficient, the correct size has
- // been stored in ngroups, so we try again.
- if (err == -1) {
- supplementaryGIDs.resize(ngroups);
- err = getgrouplist(pw->pw_name, pw->pw_gid, supplementaryGIDs.data(),
- &ngroups);
- }
+struct CgroupUserLock : UserLock
+{
+ AutoCloseFD fdUserLock;
+ uid_t uid;
+
+ void kill() override
+ {
+ if (cgroup) {
+ destroyCgroup(*cgroup);
+ cgroup.reset();
+ }
+ }
- // If it failed once more, then something must be broken.
- if (err == -1)
- throw Error("failed to get list of supplementary groups for '%1%'",
- pw->pw_name);
+ std::pair<uid_t, uid_t> getUIDRange() override
+ {
+ assert(uid);
+ return {uid, uid + settings.idsPerBuild - 1};
+ }
- // Finally, trim back the GID list to its real size
- supplementaryGIDs.resize(ngroups);
-#endif
+ gid_t getGID() override
+ {
+ // We use the same GID ranges as for the UIDs.
+ assert(uid);
+ return uid;
+ }
+
+ std::vector<gid_t> getSupplementaryGIDs() override { return {}; }
+
+ static std::unique_ptr<UserLock> acquire()
+ {
+ settings.requireExperimentalFeature(Xp::AutoAllocateUids);
+ assert(settings.startId > 0);
+ assert(settings.startId % settings.idsPerBuild == 0);
+ assert(settings.uidCount % settings.idsPerBuild == 0);
+ assert((uint64_t) settings.startId + (uint64_t) settings.uidCount <= std::numeric_limits<uid_t>::max());
+
+ // FIXME: check whether the id range overlaps any known users
+
+ createDirs(settings.nixStateDir + "/userpool2");
- isEnabled = true;
- return true;
+ size_t nrSlots = settings.uidCount / settings.idsPerBuild;
+
+ for (size_t i = 0; i < nrSlots; i++) {
+ debug("trying user slot '%d'", i);
+
+ createDirs(settings.nixStateDir + "/userpool2");
+
+ auto fnUserLock = fmt("%s/userpool2/slot-%d", settings.nixStateDir, i);
+
+ AutoCloseFD fd = open(fnUserLock.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0600);
+ if (!fd)
+ throw SysError("opening user lock '%s'", fnUserLock);
+
+ if (lockFile(fd.get(), ltWrite, false)) {
+ auto lock = std::make_unique<CgroupUserLock>();
+ lock->fdUserLock = std::move(fd);
+ lock->uid = settings.startId + i * settings.idsPerBuild;
+ auto s = drainFD(lock->fdUserLock.get());
+ if (s != "") lock->cgroup = s;
+ return lock;
+ }
}
+
+ return nullptr;
}
- return false;
+ std::optional<Path> cgroup;
+
+ std::optional<Path> getCgroup() override
+ {
+ if (!cgroup) {
+ /* Create a systemd cgroup since that's the minimum
+ required by systemd-nspawn. */
+ auto ourCgroups = getCgroups("/proc/self/cgroup");
+ auto systemdCgroup = ourCgroups["systemd"];
+ if (systemdCgroup == "")
+ throw Error("'systemd' cgroup does not exist");
+
+ auto hostCgroup = canonPath("/sys/fs/cgroup/systemd/" + systemdCgroup);
+
+ if (!pathExists(hostCgroup))
+ throw Error("expected cgroup directory '%s'", hostCgroup);
+
+ cgroup = fmt("%s/nix-%d", hostCgroup, uid);
+
+ destroyCgroup(*cgroup);
+
+ if (mkdir(cgroup->c_str(), 0755) == -1)
+ throw SysError("creating cgroup '%s'", *cgroup);
+
+ /* Record the cgroup in the lock file. This ensures that
+ if we subsequently get executed under a different parent
+ cgroup, we kill the previous cgroup first. */
+ if (ftruncate(fdUserLock.get(), 0) == -1)
+ throw Error("truncating user lock");
+ writeFull(fdUserLock.get(), *cgroup);
+ }
+
+ return cgroup;
+ };
+};
+#endif
+
+std::unique_ptr<UserLock> acquireUserLock()
+{
+ #if __linux__
+ if (settings.autoAllocateUids)
+ return CgroupUserLock::acquire();
+ else
+ #endif
+ return SimpleUserLock::acquire();
}
-void UserLock::kill()
+bool useBuildUsers()
{
- killUser(uid);
+ #if __linux__
+ static bool b = (settings.buildUsersGroup != "" || settings.startId.get() != 0) && getuid() == 0;
+ return b;
+ #elif __APPLE__
+ static bool b = settings.buildUsersGroup != "" && getuid() == 0;
+ return b;
+ #else
+ return false;
+ #endif
}
}
diff --git a/src/libstore/lock.hh b/src/libstore/lock.hh
index 3d29a7b5b..4b6d34069 100644
--- a/src/libstore/lock.hh
+++ b/src/libstore/lock.hh
@@ -1,37 +1,43 @@
#pragma once
-#include "sync.hh"
#include "types.hh"
-#include "util.hh"
+
+#include <optional>
namespace nix {
-class UserLock
+struct UserLock
{
-private:
- Path fnUserLock;
- AutoCloseFD fdUserLock;
+ virtual ~UserLock() { }
- bool isEnabled = false;
- std::string user;
- uid_t uid = 0;
- gid_t gid = 0;
- std::vector<gid_t> supplementaryGIDs;
+ /* Get the first and last UID. */
+ virtual std::pair<uid_t, uid_t> getUIDRange() = 0;
-public:
- UserLock();
+ /* Get the first UID. */
+ uid_t getUID()
+ {
+ return getUIDRange().first;
+ }
- void kill();
+ uid_t getUIDCount()
+ {
+ return getUIDRange().second - getUIDRange().first + 1;
+ }
- std::string getUser() { return user; }
- uid_t getUID() { assert(uid); return uid; }
- uid_t getGID() { assert(gid); return gid; }
- std::vector<gid_t> getSupplementaryGIDs() { return supplementaryGIDs; }
+ virtual gid_t getGID() = 0;
- bool findFreeUser();
+ virtual std::vector<gid_t> getSupplementaryGIDs() = 0;
- bool enabled() { return isEnabled; }
+ /* Kill any processes currently executing as this user. */
+ virtual void kill() = 0;
+ virtual std::optional<Path> getCgroup() { return {}; };
};
+/* Acquire a user lock. Note that this may return nullptr if no user
+ is available. */
+std::unique_ptr<UserLock> acquireUserLock();
+
+bool useBuildUsers();
+
}
diff --git a/src/libutil/experimental-features.cc b/src/libutil/experimental-features.cc
index fa79cca6b..30d071408 100644
--- a/src/libutil/experimental-features.cc
+++ b/src/libutil/experimental-features.cc
@@ -14,6 +14,8 @@ std::map<ExperimentalFeature, std::string> stringifiedXpFeatures = {
{ Xp::NoUrlLiterals, "no-url-literals" },
{ Xp::FetchClosure, "fetch-closure" },
{ Xp::ReplFlake, "repl-flake" },
+ { Xp::AutoAllocateUids, "auto-allocate-uids" },
+ { Xp::SystemdCgroup, "systemd-cgroup" },
};
const std::optional<ExperimentalFeature> parseExperimentalFeature(const std::string_view & name)
diff --git a/src/libutil/experimental-features.hh b/src/libutil/experimental-features.hh
index d09ab025c..c749d4767 100644
--- a/src/libutil/experimental-features.hh
+++ b/src/libutil/experimental-features.hh
@@ -23,6 +23,8 @@ enum struct ExperimentalFeature
NoUrlLiterals,
FetchClosure,
ReplFlake,
+ AutoAllocateUids,
+ SystemdCgroup,
};
/**
diff --git a/src/nix-store/nix-store.cc b/src/nix-store/nix-store.cc
index 23f2ad3cf..eb65e7dde 100644
--- a/src/nix-store/nix-store.cc
+++ b/src/nix-store/nix-store.cc
@@ -516,7 +516,7 @@ static void registerValidity(bool reregister, bool hashGiven, bool canonicalise)
if (!store->isValidPath(info->path) || reregister) {
/* !!! races */
if (canonicalise)
- canonicalisePathMetaData(store->printStorePath(info->path), -1);
+ canonicalisePathMetaData(store->printStorePath(info->path), {});
if (!hashGiven) {
HashResult hash = hashPath(htSHA256, store->printStorePath(info->path));
info->narHash = hash.first;
@@ -745,8 +745,8 @@ static void opVerifyPath(Strings opFlags, Strings opArgs)
auto current = sink.finish();
if (current.first != info->narHash) {
printError("path '%s' was modified! expected hash '%s', got '%s'",
- store->printStorePath(path),
- info->narHash.to_string(Base32, true),
+ store->printStorePath(path),
+ info->narHash.to_string(Base32, true),
current.first.to_string(Base32, true));
status = 1;
}