Merge pull request #8965 from Artturin/bindfilesinchroot

Bindmount files instead of hardlinking or copying to chroot

(cherry picked from commit 622191c2b53882a1675fed5066ff8090b4f01827)
Change-Id: I278ec1baacdfa9044992b58fdec8f14d6d7d09ce
This commit is contained in:
eldritch horrors 2024-03-04 05:17:31 +01:00
parent 8abb20390e
commit 622b0e9428
2 changed files with 57 additions and 88 deletions

View file

@ -386,27 +386,27 @@ void LocalDerivationGoal::cleanupPostOutputsRegisteredModeNonCheck()
cleanupPostOutputsRegisteredModeCheck(); cleanupPostOutputsRegisteredModeCheck();
} }
#if __linux__ #if __linux__
static void linkOrCopy(const Path & from, const Path & to) static void doBind(const Path & source, const Path & target, bool optional = false) {
{ debug("bind mounting '%1%' to '%2%'", source, target);
if (link(from.c_str(), to.c_str()) == -1) { struct stat st;
/* Hard-linking fails if we exceed the maximum link count on a if (stat(source.c_str(), &st) == -1) {
file (e.g. 32000 of ext3), which is quite possible after a if (optional && errno == ENOENT)
'nix-store --optimise'. FIXME: actually, why don't we just return;
bind-mount in this case? else
throw SysError("getting attributes of path '%1%'", source);
It can also fail with EPERM in BeegFS v7 and earlier versions
or fail with EXDEV in OpenAFS
which don't allow hard-links to other directories */
if (errno != EMLINK && errno != EPERM && errno != EXDEV)
throw SysError("linking '%s' to '%s'", to, from);
copyPath(from, to);
} }
} if (S_ISDIR(st.st_mode))
createDirs(target);
else {
createDirs(dirOf(target));
writeFile(target, "");
}
if (mount(source.c_str(), target.c_str(), "", MS_BIND | MS_REC, 0) == -1)
throw SysError("bind mount from '%1%' to '%2%' failed", source, target);
};
#endif #endif
void LocalDerivationGoal::startBuilder() void LocalDerivationGoal::startBuilder()
{ {
if ((buildUser && buildUser->getUIDCount() != 1) if ((buildUser && buildUser->getUIDCount() != 1)
@ -581,7 +581,7 @@ void LocalDerivationGoal::startBuilder()
/* Allow a user-configurable set of directories from the /* Allow a user-configurable set of directories from the
host file system. */ host file system. */
dirsInChroot.clear(); pathsInChroot.clear();
for (auto i : settings.sandboxPaths.get()) { for (auto i : settings.sandboxPaths.get()) {
if (i.empty()) continue; if (i.empty()) continue;
@ -592,19 +592,19 @@ void LocalDerivationGoal::startBuilder()
} }
size_t p = i.find('='); size_t p = i.find('=');
if (p == std::string::npos) if (p == std::string::npos)
dirsInChroot[i] = {i, optional}; pathsInChroot[i] = {i, optional};
else else
dirsInChroot[i.substr(0, p)] = {i.substr(p + 1), optional}; pathsInChroot[i.substr(0, p)] = {i.substr(p + 1), optional};
} }
if (hasPrefix(worker.store.storeDir, tmpDirInSandbox)) if (hasPrefix(worker.store.storeDir, tmpDirInSandbox))
{ {
throw Error("`sandbox-build-dir` must not contain the storeDir"); throw Error("`sandbox-build-dir` must not contain the storeDir");
} }
dirsInChroot[tmpDirInSandbox] = tmpDir; pathsInChroot[tmpDirInSandbox] = tmpDir;
/* Add the closure of store paths to the chroot. */ /* Add the closure of store paths to the chroot. */
StorePathSet closure; StorePathSet closure;
for (auto & i : dirsInChroot) for (auto & i : pathsInChroot)
try { try {
if (worker.store.isInStore(i.second.source)) if (worker.store.isInStore(i.second.source))
worker.store.computeFSClosure(worker.store.toStorePath(i.second.source).first, closure); worker.store.computeFSClosure(worker.store.toStorePath(i.second.source).first, closure);
@ -615,7 +615,7 @@ void LocalDerivationGoal::startBuilder()
} }
for (auto & i : closure) { for (auto & i : closure) {
auto p = worker.store.printStorePath(i); auto p = worker.store.printStorePath(i);
dirsInChroot.insert_or_assign(p, p); pathsInChroot.insert_or_assign(p, p);
} }
PathSet allowedPaths = settings.allowedImpureHostPrefixes; PathSet allowedPaths = settings.allowedImpureHostPrefixes;
@ -643,7 +643,7 @@ void LocalDerivationGoal::startBuilder()
/* Allow files in __impureHostDeps to be missing; e.g. /* Allow files in __impureHostDeps to be missing; e.g.
macOS 11+ has no /usr/lib/libSystem*.dylib */ macOS 11+ has no /usr/lib/libSystem*.dylib */
dirsInChroot[i] = {i, true}; pathsInChroot[i] = {i, true};
} }
#if __linux__ #if __linux__
@ -711,15 +711,12 @@ void LocalDerivationGoal::startBuilder()
for (auto & i : inputPaths) { for (auto & i : inputPaths) {
auto p = worker.store.printStorePath(i); auto p = worker.store.printStorePath(i);
Path r = worker.store.toRealPath(p); Path r = worker.store.toRealPath(p);
if (S_ISDIR(lstat(r).st_mode)) pathsInChroot.insert_or_assign(p, r);
dirsInChroot.insert_or_assign(p, r);
else
linkOrCopy(r, chrootRootDir + p);
} }
/* If we're repairing, checking or rebuilding part of a /* If we're repairing, checking or rebuilding part of a
multiple-outputs derivation, it's possible that we're multiple-outputs derivation, it's possible that we're
rebuilding a path that is in settings.dirsInChroot rebuilding a path that is in settings.sandbox-paths
(typically the dependencies of /bin/sh). Throw them (typically the dependencies of /bin/sh). Throw them
out. */ out. */
for (auto & i : drv->outputsAndOptPaths(worker.store)) { for (auto & i : drv->outputsAndOptPaths(worker.store)) {
@ -729,7 +726,7 @@ void LocalDerivationGoal::startBuilder()
is already in the sandbox, so we don't need to worry about is already in the sandbox, so we don't need to worry about
removing it. */ removing it. */
if (i.second.second) if (i.second.second)
dirsInChroot.erase(worker.store.printStorePath(*i.second.second)); pathsInChroot.erase(worker.store.printStorePath(*i.second.second));
} }
if (cgroup) { if (cgroup) {
@ -787,9 +784,9 @@ void LocalDerivationGoal::startBuilder()
} else { } else {
auto p = line.find('='); auto p = line.find('=');
if (p == std::string::npos) if (p == std::string::npos)
dirsInChroot[line] = line; pathsInChroot[line] = line;
else else
dirsInChroot[line.substr(0, p)] = line.substr(p + 1); pathsInChroot[line.substr(0, p)] = line.substr(p + 1);
} }
} }
} }
@ -1555,41 +1552,32 @@ void LocalDerivationGoal::addDependency(const StorePath & path)
Path source = worker.store.Store::toRealPath(path); Path source = worker.store.Store::toRealPath(path);
Path target = chrootRootDir + worker.store.printStorePath(path); Path target = chrootRootDir + worker.store.printStorePath(path);
debug("bind-mounting %s -> %s", target, source);
if (pathExists(target)) if (pathExists(target))
// There is a similar debug message in doBind, so only run it in this block to not have double messages.
debug("bind-mounting %s -> %s", target, source);
throw Error("store path '%s' already exists in the sandbox", worker.store.printStorePath(path)); throw Error("store path '%s' already exists in the sandbox", worker.store.printStorePath(path));
auto st = lstat(source); /* Bind-mount the path into the sandbox. This requires
entering its mount namespace, which is not possible
in multithreaded programs. So we do this in a
child process.*/
Pid child(startProcess([&]() {
if (S_ISDIR(st.st_mode)) { if (usingUserNamespace && (setns(sandboxUserNamespace.get(), 0) == -1))
throw SysError("entering sandbox user namespace");
/* Bind-mount the path into the sandbox. This requires if (setns(sandboxMountNamespace.get(), 0) == -1)
entering its mount namespace, which is not possible throw SysError("entering sandbox mount namespace");
in multithreaded programs. So we do this in a
child process.*/
Pid child(startProcess([&]() {
if (usingUserNamespace && (setns(sandboxUserNamespace.get(), 0) == -1)) doBind(source, target);
throw SysError("entering sandbox user namespace");
if (setns(sandboxMountNamespace.get(), 0) == -1) _exit(0);
throw SysError("entering sandbox mount namespace"); }));
createDirs(target); int status = child.wait();
if (status != 0)
if (mount(source.c_str(), target.c_str(), "", MS_BIND, 0) == -1) throw Error("could not add path '%s' to sandbox", worker.store.printStorePath(path));
throw SysError("bind mount from '%s' to '%s' failed", source, target);
_exit(0);
}));
int status = child.wait();
if (status != 0)
throw Error("could not add path '%s' to sandbox", worker.store.printStorePath(path));
} else
linkOrCopy(source, target);
#else #else
throw Error("don't know how to make path '%s' (produced by a recursive Nix call) appear in the sandbox", throw Error("don't know how to make path '%s' (produced by a recursive Nix call) appear in the sandbox",
@ -1779,7 +1767,7 @@ void LocalDerivationGoal::runChild()
/* Set up a nearly empty /dev, unless the user asked to /* Set up a nearly empty /dev, unless the user asked to
bind-mount the host /dev. */ bind-mount the host /dev. */
Strings ss; Strings ss;
if (dirsInChroot.find("/dev") == dirsInChroot.end()) { if (pathsInChroot.find("/dev") == pathsInChroot.end()) {
createDirs(chrootRootDir + "/dev/shm"); createDirs(chrootRootDir + "/dev/shm");
createDirs(chrootRootDir + "/dev/pts"); createDirs(chrootRootDir + "/dev/pts");
ss.push_back("/dev/full"); ss.push_back("/dev/full");
@ -1814,34 +1802,15 @@ void LocalDerivationGoal::runChild()
ss.push_back(path); ss.push_back(path);
if (settings.caFile != "") if (settings.caFile != "")
dirsInChroot.try_emplace("/etc/ssl/certs/ca-certificates.crt", settings.caFile, true); pathsInChroot.try_emplace("/etc/ssl/certs/ca-certificates.crt", settings.caFile, true);
} }
for (auto & i : ss) dirsInChroot.emplace(i, i); for (auto & i : ss) pathsInChroot.emplace(i, i);
/* Bind-mount all the directories from the "host" /* Bind-mount all the directories from the "host"
filesystem that we want in the chroot filesystem that we want in the chroot
environment. */ environment. */
auto doBind = [&](const Path & source, const Path & target, bool optional = false) { for (auto & i : pathsInChroot) {
debug("bind mounting '%1%' to '%2%'", source, target);
struct stat st;
if (stat(source.c_str(), &st) == -1) {
if (optional && errno == ENOENT)
return;
else
throw SysError("getting attributes of path '%1%'", source);
}
if (S_ISDIR(st.st_mode))
createDirs(target);
else {
createDirs(dirOf(target));
writeFile(target, "");
}
if (mount(source.c_str(), target.c_str(), "", MS_BIND | MS_REC, 0) == -1)
throw SysError("bind mount from '%1%' to '%2%' failed", source, target);
};
for (auto & i : dirsInChroot) {
if (i.second.source == "/proc") continue; // backwards compatibility if (i.second.source == "/proc") continue; // backwards compatibility
#if HAVE_EMBEDDED_SANDBOX_SHELL #if HAVE_EMBEDDED_SANDBOX_SHELL
@ -1882,7 +1851,7 @@ void LocalDerivationGoal::runChild()
if /dev/ptx/ptmx exists). */ if /dev/ptx/ptmx exists). */
if (pathExists("/dev/pts/ptmx") && if (pathExists("/dev/pts/ptmx") &&
!pathExists(chrootRootDir + "/dev/ptmx") !pathExists(chrootRootDir + "/dev/ptmx")
&& !dirsInChroot.count("/dev/pts")) && !pathsInChroot.count("/dev/pts"))
{ {
if (mount("none", (chrootRootDir + "/dev/pts").c_str(), "devpts", 0, "newinstance,mode=0620") == 0) if (mount("none", (chrootRootDir + "/dev/pts").c_str(), "devpts", 0, "newinstance,mode=0620") == 0)
{ {
@ -2017,7 +1986,7 @@ void LocalDerivationGoal::runChild()
/* We build the ancestry before adding all inputPaths to the store because we know they'll /* We build the ancestry before adding all inputPaths to the store because we know they'll
all have the same parents (the store), and there might be lots of inputs. This isn't all have the same parents (the store), and there might be lots of inputs. This isn't
particularly efficient... I doubt it'll be a bottleneck in practice */ particularly efficient... I doubt it'll be a bottleneck in practice */
for (auto & i : dirsInChroot) { for (auto & i : pathsInChroot) {
Path cur = i.first; Path cur = i.first;
while (cur.compare("/") != 0) { while (cur.compare("/") != 0) {
cur = dirOf(cur); cur = dirOf(cur);
@ -2025,7 +1994,7 @@ void LocalDerivationGoal::runChild()
} }
} }
/* And we want the store in there regardless of how empty dirsInChroot. We include the innermost /* And we want the store in there regardless of how empty pathsInChroot. We include the innermost
path component this time, since it's typically /nix/store and we care about that. */ path component this time, since it's typically /nix/store and we care about that. */
Path cur = worker.store.storeDir; Path cur = worker.store.storeDir;
while (cur.compare("/") != 0) { while (cur.compare("/") != 0) {
@ -2036,7 +2005,7 @@ void LocalDerivationGoal::runChild()
/* Add all our input paths to the chroot */ /* Add all our input paths to the chroot */
for (auto & i : inputPaths) { for (auto & i : inputPaths) {
auto p = worker.store.printStorePath(i); auto p = worker.store.printStorePath(i);
dirsInChroot[p] = p; pathsInChroot[p] = p;
} }
/* Violations will go to the syslog if you set this. Unfortunately the destination does not appear to be configurable */ /* Violations will go to the syslog if you set this. Unfortunately the destination does not appear to be configurable */
@ -2067,7 +2036,7 @@ void LocalDerivationGoal::runChild()
without file-write* allowed, access() incorrectly returns EPERM without file-write* allowed, access() incorrectly returns EPERM
*/ */
sandboxProfile += "(allow file-read* file-write* process-exec\n"; sandboxProfile += "(allow file-read* file-write* process-exec\n";
for (auto & i : dirsInChroot) { for (auto & i : pathsInChroot) {
if (i.first != i.second.source) if (i.first != i.second.source)
throw Error( throw Error(
"can't map '%1%' to '%2%': mismatched impure paths not supported on Darwin", "can't map '%1%' to '%2%': mismatched impure paths not supported on Darwin",

View file

@ -86,8 +86,8 @@ struct LocalDerivationGoal : public DerivationGoal
: source(source), optional(optional) : source(source), optional(optional)
{ } { }
}; };
typedef map<Path, ChrootPath> DirsInChroot; // maps target path to source path typedef map<Path, ChrootPath> PathsInChroot; // maps target path to source path
DirsInChroot dirsInChroot; PathsInChroot pathsInChroot;
typedef map<std::string, std::string> Environment; typedef map<std::string, std::string> Environment;
Environment env; Environment env;