forked from lix-project/lix
Recursive Nix support
This allows Nix builders to call Nix to build derivations, with some limitations. Example: let nixpkgs = fetchTarball channel:nixos-18.03; in with import <nixpkgs> {}; runCommand "foo" { buildInputs = [ nix jq ]; NIX_PATH = "nixpkgs=${nixpkgs}"; } '' hello=$(nix-build -E '(import <nixpkgs> {}).hello.overrideDerivation (args: { name = "hello-3.5"; })') $hello/bin/hello mkdir -p $out/bin ln -s $hello/bin/hello $out/bin/hello nix path-info -r --json $hello | jq . '' This derivation makes a recursive Nix call to build GNU Hello and symlinks it from its $out, i.e. # ll ./result/bin/ lrwxrwxrwx 1 root root 63 Jan 1 1970 hello -> /nix/store/s0awxrs71gickhaqdwxl506hzccb30y5-hello-3.5/bin/hello # nix-store -qR ./result /nix/store/hwwqshlmazzjzj7yhrkyjydxamvvkfd3-glibc-2.26-131 /nix/store/s0awxrs71gickhaqdwxl506hzccb30y5-hello-3.5 /nix/store/sgmvvyw8vhfqdqb619bxkcpfn9lvd8ss-foo This is implemented as follows: * Before running the outer builder, Nix creates a Unix domain socket '.nix-socket' in the builder's temporary directory and sets $NIX_REMOTE to point to it. It starts a thread to process connections to this socket. (Thus you don't need to have nix-daemon running.) * The daemon thread uses a wrapper store (RestrictedStore) to keep track of paths added through recursive Nix calls, to implement some restrictions (see below), and to do some censorship (e.g. for purity, queryPathInfo() won't return impure information such as signatures and timestamps). * After the build finishes, the output paths are scanned for references to the paths added through recursive Nix calls (in addition to the inputs closure). Thus, in the example above, $out has a reference to $hello. The main restriction on recursive Nix calls is that they cannot do arbitrary substitutions. For example, doing nix-store -r /nix/store/kmwd1hq55akdb9sc7l3finr175dajlby-hello-2.10 is forbidden unless /nix/store/kmwd... is in the inputs closure or previously built by a recursive Nix call. This is to prevent irreproducible derivations that have hidden dependencies on substituters or the current store contents. Building a derivation is fine, however, and Nix will use substitutes if available. In other words, the builder has to present proof that it knows how to build a desired store path from scratch by constructing a derivation graph for that path. Probably we should also disallow instantiating/building fixed-output derivations (specifically, those that access the network, but currently we have no way to mark fixed-output derivations that don't access the network). Otherwise sandboxed derivations can bypass sandbox restrictions and access the network. When sandboxing is enabled, we make paths appear in the sandbox of the builder by entering the mount namespace of the builder and bind-mounting each path. This is tricky because we do a pivot_root() in the builder to change the root directory of its mount namespace, and thus the host /nix/store is not visible in the mount namespace of the builder. To get around this, just before doing pivot_root(), we branch a second mount namespace that shares its /nix/store mountpoint with the parent. Recursive Nix currently doesn't work on macOS in sandboxed mode (because we can't change the sandbox policy of a running build) and in non-root mode (because setns() barfs).
This commit is contained in:
parent
b874272f7a
commit
c4d7c76b64
|
@ -13,6 +13,7 @@
|
||||||
#include "nar-info.hh"
|
#include "nar-info.hh"
|
||||||
#include "parsed-derivations.hh"
|
#include "parsed-derivations.hh"
|
||||||
#include "machines.hh"
|
#include "machines.hh"
|
||||||
|
#include "daemon.hh"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
@ -33,6 +34,7 @@
|
||||||
#include <sys/select.h>
|
#include <sys/select.h>
|
||||||
#include <sys/resource.h>
|
#include <sys/resource.h>
|
||||||
#include <sys/socket.h>
|
#include <sys/socket.h>
|
||||||
|
#include <sys/un.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <netdb.h>
|
#include <netdb.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
@ -765,9 +767,6 @@ private:
|
||||||
immediate input paths). */
|
immediate input paths). */
|
||||||
PathSet inputPaths;
|
PathSet inputPaths;
|
||||||
|
|
||||||
/* Referenceable paths (i.e., input and output paths). */
|
|
||||||
PathSet allPaths;
|
|
||||||
|
|
||||||
/* Outputs that are already valid. If we're repairing, these are
|
/* Outputs that are already valid. If we're repairing, these are
|
||||||
the outputs that are valid *and* not corrupt. */
|
the outputs that are valid *and* not corrupt. */
|
||||||
PathSet validPaths;
|
PathSet validPaths;
|
||||||
|
@ -805,9 +804,13 @@ private:
|
||||||
/* Pipe for the builder's standard output/error. */
|
/* Pipe for the builder's standard output/error. */
|
||||||
Pipe builderOut;
|
Pipe builderOut;
|
||||||
|
|
||||||
/* Pipe for synchronising updates to the builder user namespace. */
|
/* Pipe for synchronising updates to the builder namespaces. */
|
||||||
Pipe userNamespaceSync;
|
Pipe userNamespaceSync;
|
||||||
|
|
||||||
|
/* The mount namespace of the builder, used to add additional
|
||||||
|
paths to the sandbox as a result of recursive Nix calls. */
|
||||||
|
AutoCloseFD sandboxMountNamespace;
|
||||||
|
|
||||||
/* The build hook. */
|
/* The build hook. */
|
||||||
std::unique_ptr<HookInstance> hook;
|
std::unique_ptr<HookInstance> hook;
|
||||||
|
|
||||||
|
@ -886,6 +889,26 @@ private:
|
||||||
/* The remote machine on which we're building. */
|
/* The remote machine on which we're building. */
|
||||||
std::string machineName;
|
std::string machineName;
|
||||||
|
|
||||||
|
/* The recursive Nix daemon socket. */
|
||||||
|
AutoCloseFD daemonSocket;
|
||||||
|
|
||||||
|
/* The daemon main thread. */
|
||||||
|
std::thread daemonThread;
|
||||||
|
|
||||||
|
/* Paths that were added via recursive Nix calls. */
|
||||||
|
PathSet addedPaths;
|
||||||
|
|
||||||
|
/* Recursive Nix calls are only allowed to build or realize paths
|
||||||
|
in the original input closure or added via a recursive Nix call
|
||||||
|
(so e.g. you can't do 'nix-store -r /nix/store/<bla>' where
|
||||||
|
/nix/store/<bla> is some arbitrary path in a binary cache). */
|
||||||
|
bool isAllowed(const Path & path)
|
||||||
|
{
|
||||||
|
return inputPaths.count(path) || addedPaths.count(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
friend class RestrictedStore;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
DerivationGoal(const Path & drvPath, const StringSet & wantedOutputs,
|
DerivationGoal(const Path & drvPath, const StringSet & wantedOutputs,
|
||||||
Worker & worker, BuildMode buildMode = bmNormal);
|
Worker & worker, BuildMode buildMode = bmNormal);
|
||||||
|
@ -942,6 +965,14 @@ private:
|
||||||
/* Write a JSON file containing the derivation attributes. */
|
/* Write a JSON file containing the derivation attributes. */
|
||||||
void writeStructuredAttrs();
|
void writeStructuredAttrs();
|
||||||
|
|
||||||
|
void startDaemon();
|
||||||
|
|
||||||
|
void stopDaemon();
|
||||||
|
|
||||||
|
/* Add 'path' to the set of paths that may be referenced by the
|
||||||
|
outputs, and make it appear in the sandbox. */
|
||||||
|
void addDependency(const Path & path);
|
||||||
|
|
||||||
/* Make a file owned by the builder. */
|
/* Make a file owned by the builder. */
|
||||||
void chownToBuilder(const Path & path);
|
void chownToBuilder(const Path & path);
|
||||||
|
|
||||||
|
@ -1043,6 +1074,7 @@ DerivationGoal::~DerivationGoal()
|
||||||
/* Careful: we should never ever throw an exception from a
|
/* Careful: we should never ever throw an exception from a
|
||||||
destructor. */
|
destructor. */
|
||||||
try { killChild(); } catch (...) { ignoreException(); }
|
try { killChild(); } catch (...) { ignoreException(); }
|
||||||
|
try { stopDaemon(); } catch (...) { ignoreException(); }
|
||||||
try { deleteTmpDir(false); } catch (...) { ignoreException(); }
|
try { deleteTmpDir(false); } catch (...) { ignoreException(); }
|
||||||
try { closeLogFile(); } catch (...) { ignoreException(); }
|
try { closeLogFile(); } catch (...) { ignoreException(); }
|
||||||
}
|
}
|
||||||
|
@ -1332,12 +1364,6 @@ void DerivationGoal::inputsRealised()
|
||||||
/* Gather information necessary for computing the closure and/or
|
/* Gather information necessary for computing the closure and/or
|
||||||
running the build hook. */
|
running the build hook. */
|
||||||
|
|
||||||
/* The outputs are referenceable paths. */
|
|
||||||
for (auto & i : drv->outputs) {
|
|
||||||
debug(format("building path '%1%'") % i.second.path);
|
|
||||||
allPaths.insert(i.second.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Determine the full set of input paths. */
|
/* Determine the full set of input paths. */
|
||||||
|
|
||||||
/* First, the input derivations. */
|
/* First, the input derivations. */
|
||||||
|
@ -1362,8 +1388,6 @@ void DerivationGoal::inputsRealised()
|
||||||
|
|
||||||
debug(format("added input paths %1%") % showPaths(inputPaths));
|
debug(format("added input paths %1%") % showPaths(inputPaths));
|
||||||
|
|
||||||
allPaths.insert(inputPaths.begin(), inputPaths.end());
|
|
||||||
|
|
||||||
/* Is this a fixed-output derivation? */
|
/* Is this a fixed-output derivation? */
|
||||||
fixedOutput = drv->isFixedOutput();
|
fixedOutput = drv->isFixedOutput();
|
||||||
|
|
||||||
|
@ -1527,6 +1551,8 @@ void DerivationGoal::buildDone()
|
||||||
uid and then messing around with our output. */
|
uid and then messing around with our output. */
|
||||||
Finally releaseBuildUser([&]() { buildUser.reset(); });
|
Finally releaseBuildUser([&]() { buildUser.reset(); });
|
||||||
|
|
||||||
|
sandboxMountNamespace = -1;
|
||||||
|
|
||||||
/* Since we got an EOF on the logger pipe, the builder is presumed
|
/* Since we got an EOF on the logger pipe, the builder is presumed
|
||||||
to have terminated. In fact, the builder could also have
|
to have terminated. In fact, the builder could also have
|
||||||
simply have closed its end of the pipe, so just to be sure,
|
simply have closed its end of the pipe, so just to be sure,
|
||||||
|
@ -1558,6 +1584,9 @@ void DerivationGoal::buildDone()
|
||||||
root. */
|
root. */
|
||||||
if (buildUser) buildUser->kill();
|
if (buildUser) buildUser->kill();
|
||||||
|
|
||||||
|
/* Terminate the recursive Nix daemon. */
|
||||||
|
stopDaemon();
|
||||||
|
|
||||||
bool diskFull = false;
|
bool diskFull = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -2217,6 +2246,10 @@ void DerivationGoal::startBuilder()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Fire up a Nix daemon to process recursive Nix calls from the
|
||||||
|
builder. */
|
||||||
|
startDaemon();
|
||||||
|
|
||||||
/* Run the builder. */
|
/* Run the builder. */
|
||||||
printMsg(lvlChatty, format("executing builder '%1%'") % drv->builder);
|
printMsg(lvlChatty, format("executing builder '%1%'") % drv->builder);
|
||||||
|
|
||||||
|
@ -2391,6 +2424,12 @@ void DerivationGoal::startBuilder()
|
||||||
writeFile("/proc/" + std::to_string(pid) + "/gid_map",
|
writeFile("/proc/" + std::to_string(pid) + "/gid_map",
|
||||||
(format("%d %d 1") % sandboxGid % hostGid).str());
|
(format("%d %d 1") % sandboxGid % hostGid).str());
|
||||||
|
|
||||||
|
/* Save the mount namespace of the child. We have to do this
|
||||||
|
*before* the child does a chroot. */
|
||||||
|
sandboxMountNamespace = open(fmt("/proc/%d/ns/mnt", (pid_t) pid).c_str(), O_RDONLY);
|
||||||
|
if (sandboxMountNamespace.get() == -1)
|
||||||
|
throw SysError("getting sandbox mount namespace");
|
||||||
|
|
||||||
/* Signal the builder that we've updated its user namespace. */
|
/* Signal the builder that we've updated its user namespace. */
|
||||||
writeFull(userNamespaceSync.writeSide.get(), "1");
|
writeFull(userNamespaceSync.writeSide.get(), "1");
|
||||||
userNamespaceSync.writeSide = -1;
|
userNamespaceSync.writeSide = -1;
|
||||||
|
@ -2621,6 +2660,302 @@ void DerivationGoal::writeStructuredAttrs()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* A wrapper around LocalStore that only allows building/querying of
|
||||||
|
paths that are in the input closures of the build or were added via
|
||||||
|
recursive Nix calls. */
|
||||||
|
struct RestrictedStore : public LocalFSStore
|
||||||
|
{
|
||||||
|
ref<LocalStore> next;
|
||||||
|
|
||||||
|
DerivationGoal & goal;
|
||||||
|
|
||||||
|
RestrictedStore(const Params & params, ref<LocalStore> next, DerivationGoal & goal)
|
||||||
|
: Store(params), LocalFSStore(params), next(next), goal(goal)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
Path getRealStoreDir() override
|
||||||
|
{ return next->realStoreDir; }
|
||||||
|
|
||||||
|
std::string getUri() override
|
||||||
|
{ return next->getUri(); }
|
||||||
|
|
||||||
|
PathSet queryAllValidPaths() override
|
||||||
|
{
|
||||||
|
PathSet paths;
|
||||||
|
for (auto & p : goal.inputPaths) paths.insert(p);
|
||||||
|
for (auto & p : goal.addedPaths) paths.insert(p);
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
void queryPathInfoUncached(const Path & path,
|
||||||
|
Callback<std::shared_ptr<const ValidPathInfo>> callback) noexcept override
|
||||||
|
{
|
||||||
|
if (goal.isAllowed(path)) {
|
||||||
|
try {
|
||||||
|
/* Censor impure information. */
|
||||||
|
auto info = std::make_shared<ValidPathInfo>(*next->queryPathInfo(path));
|
||||||
|
info->deriver.clear();
|
||||||
|
info->registrationTime = 0;
|
||||||
|
info->ultimate = false;
|
||||||
|
info->sigs.clear();
|
||||||
|
callback(info);
|
||||||
|
} catch (InvalidPath &) {
|
||||||
|
callback(nullptr);
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
callback(nullptr);
|
||||||
|
};
|
||||||
|
|
||||||
|
void queryReferrers(const Path & path, PathSet & referrers) override
|
||||||
|
{ }
|
||||||
|
|
||||||
|
PathSet queryDerivationOutputs(const Path & path) override
|
||||||
|
{ throw Error("queryDerivationOutputs"); }
|
||||||
|
|
||||||
|
StringSet queryDerivationOutputNames(const Path & path) override
|
||||||
|
{ throw Error("queryDerivationOutputNames"); }
|
||||||
|
|
||||||
|
Path queryPathFromHashPart(const string & hashPart) override
|
||||||
|
{ throw Error("queryPathFromHashPart"); }
|
||||||
|
|
||||||
|
Path addToStore(const string & name, const Path & srcPath,
|
||||||
|
bool recursive = true, HashType hashAlgo = htSHA256,
|
||||||
|
PathFilter & filter = defaultPathFilter, RepairFlag repair = NoRepair) override
|
||||||
|
{ throw Error("addToStore");
|
||||||
|
}
|
||||||
|
|
||||||
|
Path addToStoreFromDump(const string & dump, const string & name,
|
||||||
|
bool recursive = true, HashType hashAlgo = htSHA256, RepairFlag repair = NoRepair) override
|
||||||
|
{
|
||||||
|
auto path = next->addToStoreFromDump(dump, name, recursive, hashAlgo, repair);
|
||||||
|
goal.addDependency(path);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
Path addTextToStore(const string & name, const string & s,
|
||||||
|
const PathSet & references, RepairFlag repair = NoRepair) override
|
||||||
|
{
|
||||||
|
auto path = next->addTextToStore(name, s, references, repair);
|
||||||
|
goal.addDependency(path);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
void narFromPath(const Path & path, Sink & sink) override
|
||||||
|
{
|
||||||
|
if (!goal.isAllowed(path))
|
||||||
|
throw InvalidPath("cannot dump unknown path '%s' in recursive Nix", path);
|
||||||
|
LocalFSStore::narFromPath(path, sink);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ensurePath(const Path & path) override
|
||||||
|
{
|
||||||
|
if (!goal.isAllowed(path))
|
||||||
|
throw InvalidPath("cannot substitute unknown path '%s' in recursive Nix", path);
|
||||||
|
/* Nothing to be done; 'path' must already be valid. */
|
||||||
|
}
|
||||||
|
|
||||||
|
void buildPaths(const PathSet & paths, BuildMode buildMode) override
|
||||||
|
{
|
||||||
|
if (buildMode != bmNormal) throw Error("unsupported build mode");
|
||||||
|
|
||||||
|
PathSet newPaths;
|
||||||
|
|
||||||
|
for (auto & path : paths) {
|
||||||
|
DrvPathWithOutputs i = parseDrvPathWithOutputs(path);
|
||||||
|
if (isDerivation(i.first)) {
|
||||||
|
if (!goal.isAllowed(i.first))
|
||||||
|
throw InvalidPath("cannot build unknown path '%s' in recursive Nix", i.first);
|
||||||
|
auto drv = derivationFromPath(i.first);
|
||||||
|
for (auto & output : drv.outputs)
|
||||||
|
if (wantOutput(output.first, i.second))
|
||||||
|
newPaths.insert(output.second.path);
|
||||||
|
} else if (!goal.isAllowed(path))
|
||||||
|
throw InvalidPath("cannot build unknown path '%s' in recursive Nix", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
next->buildPaths(paths, buildMode);
|
||||||
|
|
||||||
|
PathSet closure;
|
||||||
|
next->computeFSClosure(newPaths, closure);
|
||||||
|
for (auto & path : closure)
|
||||||
|
goal.addDependency(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildResult buildDerivation(const Path & drvPath, const BasicDerivation & drv,
|
||||||
|
BuildMode buildMode = bmNormal) override
|
||||||
|
{ unsupported("buildDerivation"); }
|
||||||
|
|
||||||
|
void addTempRoot(const Path & path)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
void addIndirectRoot(const Path & path)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
Roots findRoots()
|
||||||
|
{ return Roots(); }
|
||||||
|
|
||||||
|
void collectGarbage(const GCOptions & options, GCResults & results)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
void addSignatures(const Path & storePath, const StringSet & sigs)
|
||||||
|
{ unsupported("addSignatures"); }
|
||||||
|
|
||||||
|
void queryMissing(const PathSet & targets,
|
||||||
|
PathSet & willBuild, PathSet & willSubstitute, PathSet & unknown,
|
||||||
|
unsigned long long & downloadSize, unsigned long long & narSize)
|
||||||
|
{
|
||||||
|
/* This is slightly impure since it leaks information to the
|
||||||
|
client about what paths will be built/substituted or are
|
||||||
|
already present. Probably not a big deal. */
|
||||||
|
|
||||||
|
PathSet allowed;
|
||||||
|
for (auto & path : targets) {
|
||||||
|
DrvPathWithOutputs i = parseDrvPathWithOutputs(path);
|
||||||
|
if (goal.isAllowed(i.first))
|
||||||
|
allowed.insert(i.first);
|
||||||
|
else
|
||||||
|
unknown.insert(i.first);
|
||||||
|
}
|
||||||
|
|
||||||
|
next->queryMissing(allowed, willBuild, willSubstitute,
|
||||||
|
unknown, downloadSize, narSize);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
void DerivationGoal::startDaemon()
|
||||||
|
{
|
||||||
|
Store::Params params;
|
||||||
|
params["path-info-cache-size"] = "0";
|
||||||
|
params["store"] = worker.store.storeDir;
|
||||||
|
params["root"] = worker.store.rootDir;
|
||||||
|
params["state"] = "/no-such-path";
|
||||||
|
params["log"] = "/no-such-path";
|
||||||
|
auto store = make_ref<RestrictedStore>(params,
|
||||||
|
ref<LocalStore>(std::dynamic_pointer_cast<LocalStore>(worker.store.shared_from_this())),
|
||||||
|
*this);
|
||||||
|
|
||||||
|
addedPaths.clear();
|
||||||
|
|
||||||
|
auto socketName = ".nix-socket";
|
||||||
|
Path socketPath = tmpDir + "/" + socketName;
|
||||||
|
env["NIX_REMOTE"] = "unix://" + tmpDirInSandbox + "/" + socketName;
|
||||||
|
|
||||||
|
daemonSocket = createUnixDomainSocket(socketPath, 0600);
|
||||||
|
|
||||||
|
chownToBuilder(socketPath);
|
||||||
|
|
||||||
|
daemonThread = std::thread([this, store]() {
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
|
||||||
|
/* Accept a connection. */
|
||||||
|
struct sockaddr_un remoteAddr;
|
||||||
|
socklen_t remoteAddrLen = sizeof(remoteAddr);
|
||||||
|
|
||||||
|
AutoCloseFD remote = accept(daemonSocket.get(),
|
||||||
|
(struct sockaddr *) &remoteAddr, &remoteAddrLen);
|
||||||
|
if (!remote) {
|
||||||
|
if (errno == EINTR) continue;
|
||||||
|
if (errno == EINVAL) break;
|
||||||
|
throw SysError("accepting connection");
|
||||||
|
}
|
||||||
|
|
||||||
|
closeOnExec(remote.get());
|
||||||
|
|
||||||
|
debug("received daemon connection");
|
||||||
|
|
||||||
|
// FIXME: process on a separate thread.
|
||||||
|
FdSource from(remote.get());
|
||||||
|
FdSink to(remote.get());
|
||||||
|
try {
|
||||||
|
daemon::processConnection(store, from, to,
|
||||||
|
daemon::NotTrusted, daemon::Recursive, "nobody", 65535);
|
||||||
|
} catch (SysError &) {
|
||||||
|
ignoreException();
|
||||||
|
}
|
||||||
|
|
||||||
|
debug("terminated daemon connection");
|
||||||
|
}
|
||||||
|
|
||||||
|
debug("daemon shutting down");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void DerivationGoal::stopDaemon()
|
||||||
|
{
|
||||||
|
if (daemonSocket && shutdown(daemonSocket.get(), SHUT_RDWR) == -1)
|
||||||
|
throw SysError("shutting down daemon socket");
|
||||||
|
|
||||||
|
if (daemonThread.joinable())
|
||||||
|
daemonThread.join();
|
||||||
|
|
||||||
|
daemonSocket = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void DerivationGoal::addDependency(const Path & path)
|
||||||
|
{
|
||||||
|
worker.store.assertStorePath(path);
|
||||||
|
|
||||||
|
if (isAllowed(path)) return;
|
||||||
|
|
||||||
|
addedPaths.insert(path);
|
||||||
|
|
||||||
|
/* If we're doing a sandbox build, then we have to make the path
|
||||||
|
appear in the sandbox. */
|
||||||
|
if (useChroot) {
|
||||||
|
|
||||||
|
debug("materialising '%s' in the sandbox", path);
|
||||||
|
|
||||||
|
#if __linux__
|
||||||
|
|
||||||
|
Path source = worker.store.toRealPath(path);
|
||||||
|
Path target = chrootRootDir + path;
|
||||||
|
debug("bind-mounting %s -> %s", target, source);
|
||||||
|
|
||||||
|
if (pathExists(target))
|
||||||
|
throw Error("store path '%s' already exists in the sandbox", path);
|
||||||
|
|
||||||
|
struct stat st;
|
||||||
|
if (lstat(source.c_str(), &st))
|
||||||
|
throw SysError("getting attributes of path '%s'", source);
|
||||||
|
|
||||||
|
if (S_ISDIR(st.st_mode)) {
|
||||||
|
|
||||||
|
/* 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 (setns(sandboxMountNamespace.get(), 0) == -1)
|
||||||
|
throw SysError("entering sandbox mount namespace");
|
||||||
|
|
||||||
|
createDirs(target);
|
||||||
|
|
||||||
|
if (mount(source.c_str(), target.c_str(), "", MS_BIND, 0) == -1)
|
||||||
|
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", path);
|
||||||
|
|
||||||
|
} else
|
||||||
|
linkOrCopy(source, target);
|
||||||
|
|
||||||
|
#else
|
||||||
|
throw Error("don't know how to make path '%s' (produced by a recursive Nix call) appear in the sandbox", path);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void DerivationGoal::chownToBuilder(const Path & path)
|
void DerivationGoal::chownToBuilder(const Path & path)
|
||||||
{
|
{
|
||||||
if (!buildUser) return;
|
if (!buildUser) return;
|
||||||
|
@ -2756,15 +3091,30 @@ void DerivationGoal::runChild()
|
||||||
outside of the namespace. Making a subtree private is
|
outside of the namespace. Making a subtree private is
|
||||||
local to the namespace, though, so setting MS_PRIVATE
|
local to the namespace, though, so setting MS_PRIVATE
|
||||||
does not affect the outside world. */
|
does not affect the outside world. */
|
||||||
if (mount(0, "/", 0, MS_REC|MS_PRIVATE, 0) == -1) {
|
if (mount(0, "/", 0, MS_PRIVATE | MS_REC, 0) == -1)
|
||||||
throw SysError("unable to make '/' private mount");
|
throw SysError("unable to make '/' private");
|
||||||
}
|
|
||||||
|
|
||||||
/* Bind-mount chroot directory to itself, to treat it as a
|
/* Bind-mount chroot directory to itself, to treat it as a
|
||||||
different filesystem from /, as needed for pivot_root. */
|
different filesystem from /, as needed for pivot_root. */
|
||||||
if (mount(chrootRootDir.c_str(), chrootRootDir.c_str(), 0, MS_BIND, 0) == -1)
|
if (mount(chrootRootDir.c_str(), chrootRootDir.c_str(), 0, MS_BIND, 0) == -1)
|
||||||
throw SysError(format("unable to bind mount '%1%'") % chrootRootDir);
|
throw SysError(format("unable to bind mount '%1%'") % chrootRootDir);
|
||||||
|
|
||||||
|
/* Bind-mount the sandbox's Nix store onto itself so that
|
||||||
|
we can mark it as a "shared" subtree, allowing bind
|
||||||
|
mounts made in *this* mount namespace to be propagated
|
||||||
|
into the child namespace created by the
|
||||||
|
unshare(CLONE_NEWNS) call below.
|
||||||
|
|
||||||
|
Marking chrootRootDir as MS_SHARED causes pivot_root()
|
||||||
|
to fail with EINVAL. Don't know why. */
|
||||||
|
Path chrootStoreDir = chrootRootDir + worker.store.storeDir;
|
||||||
|
|
||||||
|
if (mount(chrootStoreDir.c_str(), chrootStoreDir.c_str(), 0, MS_BIND, 0) == -1)
|
||||||
|
throw SysError("unable to bind mount the Nix store", chrootStoreDir);
|
||||||
|
|
||||||
|
if (mount(0, chrootStoreDir.c_str(), 0, MS_SHARED, 0) == -1)
|
||||||
|
throw SysError("unable to make '%s' shared", chrootStoreDir);
|
||||||
|
|
||||||
/* 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;
|
||||||
|
@ -2866,6 +3216,19 @@ void DerivationGoal::runChild()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Unshare this mount namespace. This is necessary because
|
||||||
|
pivot_root() below changes the root of the mount
|
||||||
|
namespace. This means that the call to setns() in
|
||||||
|
addDependency() would hide the host's filesystem,
|
||||||
|
making it impossible to bind-mount paths from the host
|
||||||
|
Nix store into the sandbox. Therefore, we save the
|
||||||
|
pre-pivot_root namespace in
|
||||||
|
sandboxMountNamespace. Since we made /nix/store a
|
||||||
|
shared subtree above, this allows addDependency() to
|
||||||
|
make paths appear in the sandbox. */
|
||||||
|
if (unshare(CLONE_NEWNS) == -1)
|
||||||
|
throw SysError("unsharing mount namespace");
|
||||||
|
|
||||||
/* Do the chroot(). */
|
/* Do the chroot(). */
|
||||||
if (chdir(chrootRootDir.c_str()) == -1)
|
if (chdir(chrootRootDir.c_str()) == -1)
|
||||||
throw SysError(format("cannot change directory to '%1%'") % chrootRootDir);
|
throw SysError(format("cannot change directory to '%1%'") % chrootRootDir);
|
||||||
|
@ -3182,6 +3545,14 @@ void DerivationGoal::registerOutputs()
|
||||||
|
|
||||||
std::exception_ptr delayedException;
|
std::exception_ptr delayedException;
|
||||||
|
|
||||||
|
/* The paths that can be referenced are the input closures, the
|
||||||
|
output paths, and any paths that have been built via recursive
|
||||||
|
Nix calls. */
|
||||||
|
PathSet referenceablePaths;
|
||||||
|
for (auto & p : inputPaths) referenceablePaths.insert(p);
|
||||||
|
for (auto & i : drv->outputs) referenceablePaths.insert(i.second.path);
|
||||||
|
for (auto & p : addedPaths) referenceablePaths.insert(p);
|
||||||
|
|
||||||
/* Check whether the output paths were created, and grep each
|
/* Check whether the output paths were created, and grep each
|
||||||
output path to determine what other paths it references. Also make all
|
output path to determine what other paths it references. Also make all
|
||||||
output paths read-only. */
|
output paths read-only. */
|
||||||
|
@ -3317,7 +3688,7 @@ void DerivationGoal::registerOutputs()
|
||||||
verify later on whether nobody has messed with the store. */
|
verify later on whether nobody has messed with the store. */
|
||||||
debug("scanning for references inside '%1%'", path);
|
debug("scanning for references inside '%1%'", path);
|
||||||
HashResult hash;
|
HashResult hash;
|
||||||
PathSet references = scanForReferences(actualPath, allPaths, hash);
|
PathSet references = scanForReferences(actualPath, referenceablePaths, hash);
|
||||||
|
|
||||||
if (buildMode == bmCheck) {
|
if (buildMode == bmCheck) {
|
||||||
if (!worker.store.isValidPath(path)) continue;
|
if (!worker.store.isValidPath(path)) continue;
|
||||||
|
|
|
@ -186,8 +186,75 @@ struct RetrieveRegularNARSink : ParseSink
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct ClientSettings
|
||||||
|
{
|
||||||
|
bool keepFailed;
|
||||||
|
bool keepGoing;
|
||||||
|
bool tryFallback;
|
||||||
|
Verbosity verbosity;
|
||||||
|
unsigned int maxBuildJobs;
|
||||||
|
time_t maxSilentTime;
|
||||||
|
bool verboseBuild;
|
||||||
|
unsigned int buildCores;
|
||||||
|
bool useSubstitutes;
|
||||||
|
StringMap overrides;
|
||||||
|
|
||||||
|
void apply(TrustedFlag trusted)
|
||||||
|
{
|
||||||
|
settings.keepFailed = keepFailed;
|
||||||
|
settings.keepGoing = keepGoing;
|
||||||
|
settings.tryFallback = tryFallback;
|
||||||
|
nix::verbosity = verbosity;
|
||||||
|
settings.maxBuildJobs.assign(maxBuildJobs);
|
||||||
|
settings.maxSilentTime = maxSilentTime;
|
||||||
|
settings.verboseBuild = verboseBuild;
|
||||||
|
settings.buildCores = buildCores;
|
||||||
|
settings.useSubstitutes = useSubstitutes;
|
||||||
|
|
||||||
|
for (auto & i : overrides) {
|
||||||
|
auto & name(i.first);
|
||||||
|
auto & value(i.second);
|
||||||
|
|
||||||
|
auto setSubstituters = [&](Setting<Strings> & res) {
|
||||||
|
if (name != res.name && res.aliases.count(name) == 0)
|
||||||
|
return false;
|
||||||
|
StringSet trusted = settings.trustedSubstituters;
|
||||||
|
for (auto & s : settings.substituters.get())
|
||||||
|
trusted.insert(s);
|
||||||
|
Strings subs;
|
||||||
|
auto ss = tokenizeString<Strings>(value);
|
||||||
|
for (auto & s : ss)
|
||||||
|
if (trusted.count(s))
|
||||||
|
subs.push_back(s);
|
||||||
|
else
|
||||||
|
warn("ignoring untrusted substituter '%s'", s);
|
||||||
|
res = subs;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (name == "ssh-auth-sock") // obsolete
|
||||||
|
;
|
||||||
|
else if (trusted
|
||||||
|
|| name == settings.buildTimeout.name
|
||||||
|
|| name == "connect-timeout"
|
||||||
|
|| (name == "builders" && value == ""))
|
||||||
|
settings.set(name, value);
|
||||||
|
else if (setSubstituters(settings.substituters))
|
||||||
|
;
|
||||||
|
else if (setSubstituters(settings.extraSubstituters))
|
||||||
|
;
|
||||||
|
else
|
||||||
|
warn("ignoring the user-specified setting '%s', because it is a restricted setting and you are not a trusted user", name);
|
||||||
|
} catch (UsageError & e) {
|
||||||
|
warn(e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
static void performOp(TunnelLogger * logger, ref<Store> store,
|
static void performOp(TunnelLogger * logger, ref<Store> store,
|
||||||
bool trusted, unsigned int clientVersion,
|
TrustedFlag trusted, RecursiveFlag recursive, unsigned int clientVersion,
|
||||||
Source & from, BufferedSink & to, unsigned int op)
|
Source & from, BufferedSink & to, unsigned int op)
|
||||||
{
|
{
|
||||||
switch (op) {
|
switch (op) {
|
||||||
|
@ -464,70 +531,37 @@ static void performOp(TunnelLogger * logger, ref<Store> store,
|
||||||
}
|
}
|
||||||
|
|
||||||
case wopSetOptions: {
|
case wopSetOptions: {
|
||||||
settings.keepFailed = readInt(from);
|
|
||||||
settings.keepGoing = readInt(from);
|
ClientSettings clientSettings;
|
||||||
settings.tryFallback = readInt(from);
|
|
||||||
verbosity = (Verbosity) readInt(from);
|
clientSettings.keepFailed = readInt(from);
|
||||||
settings.maxBuildJobs.assign(readInt(from));
|
clientSettings.keepGoing = readInt(from);
|
||||||
settings.maxSilentTime = readInt(from);
|
clientSettings.tryFallback = readInt(from);
|
||||||
|
clientSettings.verbosity = (Verbosity) readInt(from);
|
||||||
|
clientSettings.maxBuildJobs = readInt(from);
|
||||||
|
clientSettings.maxSilentTime = readInt(from);
|
||||||
readInt(from); // obsolete useBuildHook
|
readInt(from); // obsolete useBuildHook
|
||||||
settings.verboseBuild = lvlError == (Verbosity) readInt(from);
|
clientSettings.verboseBuild = lvlError == (Verbosity) readInt(from);
|
||||||
readInt(from); // obsolete logType
|
readInt(from); // obsolete logType
|
||||||
readInt(from); // obsolete printBuildTrace
|
readInt(from); // obsolete printBuildTrace
|
||||||
settings.buildCores = readInt(from);
|
clientSettings.buildCores = readInt(from);
|
||||||
settings.useSubstitutes = readInt(from);
|
clientSettings.useSubstitutes = readInt(from);
|
||||||
|
|
||||||
StringMap overrides;
|
|
||||||
if (GET_PROTOCOL_MINOR(clientVersion) >= 12) {
|
if (GET_PROTOCOL_MINOR(clientVersion) >= 12) {
|
||||||
unsigned int n = readInt(from);
|
unsigned int n = readInt(from);
|
||||||
for (unsigned int i = 0; i < n; i++) {
|
for (unsigned int i = 0; i < n; i++) {
|
||||||
string name = readString(from);
|
string name = readString(from);
|
||||||
string value = readString(from);
|
string value = readString(from);
|
||||||
overrides.emplace(name, value);
|
clientSettings.overrides.emplace(name, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger->startWork();
|
logger->startWork();
|
||||||
|
|
||||||
for (auto & i : overrides) {
|
// FIXME: use some setting in recursive mode. Will need to use
|
||||||
auto & name(i.first);
|
// non-global variables.
|
||||||
auto & value(i.second);
|
if (!recursive)
|
||||||
|
clientSettings.apply(trusted);
|
||||||
auto setSubstituters = [&](Setting<Strings> & res) {
|
|
||||||
if (name != res.name && res.aliases.count(name) == 0)
|
|
||||||
return false;
|
|
||||||
StringSet trusted = settings.trustedSubstituters;
|
|
||||||
for (auto & s : settings.substituters.get())
|
|
||||||
trusted.insert(s);
|
|
||||||
Strings subs;
|
|
||||||
auto ss = tokenizeString<Strings>(value);
|
|
||||||
for (auto & s : ss)
|
|
||||||
if (trusted.count(s))
|
|
||||||
subs.push_back(s);
|
|
||||||
else
|
|
||||||
warn("ignoring untrusted substituter '%s'", s);
|
|
||||||
res = subs;
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (name == "ssh-auth-sock") // obsolete
|
|
||||||
;
|
|
||||||
else if (trusted
|
|
||||||
|| name == settings.buildTimeout.name
|
|
||||||
|| name == "connect-timeout"
|
|
||||||
|| (name == "builders" && value == ""))
|
|
||||||
settings.set(name, value);
|
|
||||||
else if (setSubstituters(settings.substituters))
|
|
||||||
;
|
|
||||||
else if (setSubstituters(settings.extraSubstituters))
|
|
||||||
;
|
|
||||||
else
|
|
||||||
warn("ignoring the user-specified setting '%s', because it is a restricted setting and you are not a trusted user", name);
|
|
||||||
} catch (UsageError & e) {
|
|
||||||
warn(e.what());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger->stopWork();
|
logger->stopWork();
|
||||||
break;
|
break;
|
||||||
|
@ -694,11 +728,12 @@ void processConnection(
|
||||||
ref<Store> store,
|
ref<Store> store,
|
||||||
FdSource & from,
|
FdSource & from,
|
||||||
FdSink & to,
|
FdSink & to,
|
||||||
bool trusted,
|
TrustedFlag trusted,
|
||||||
|
RecursiveFlag recursive,
|
||||||
const std::string & userName,
|
const std::string & userName,
|
||||||
uid_t userId)
|
uid_t userId)
|
||||||
{
|
{
|
||||||
MonitorFdHup monitor(from.fd);
|
auto monitor = !recursive ? std::make_unique<MonitorFdHup>(from.fd) : nullptr;
|
||||||
|
|
||||||
/* Exchange the greeting. */
|
/* Exchange the greeting. */
|
||||||
unsigned int magic = readInt(from);
|
unsigned int magic = readInt(from);
|
||||||
|
@ -712,7 +747,9 @@ void processConnection(
|
||||||
|
|
||||||
auto tunnelLogger = new TunnelLogger(to, clientVersion);
|
auto tunnelLogger = new TunnelLogger(to, clientVersion);
|
||||||
auto prevLogger = nix::logger;
|
auto prevLogger = nix::logger;
|
||||||
logger = tunnelLogger;
|
// FIXME
|
||||||
|
if (!recursive)
|
||||||
|
logger = tunnelLogger;
|
||||||
|
|
||||||
unsigned int opCount = 0;
|
unsigned int opCount = 0;
|
||||||
|
|
||||||
|
@ -721,8 +758,10 @@ void processConnection(
|
||||||
prevLogger->log(lvlDebug, fmt("%d operations", opCount));
|
prevLogger->log(lvlDebug, fmt("%d operations", opCount));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (GET_PROTOCOL_MINOR(clientVersion) >= 14 && readInt(from))
|
if (GET_PROTOCOL_MINOR(clientVersion) >= 14 && readInt(from)) {
|
||||||
setAffinityTo(readInt(from));
|
auto affinity = readInt(from);
|
||||||
|
setAffinityTo(affinity);
|
||||||
|
}
|
||||||
|
|
||||||
readInt(from); // obsolete reserveSpace
|
readInt(from); // obsolete reserveSpace
|
||||||
|
|
||||||
|
@ -760,7 +799,7 @@ void processConnection(
|
||||||
opCount++;
|
opCount++;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
performOp(tunnelLogger, store, trusted, clientVersion, from, to, op);
|
performOp(tunnelLogger, store, trusted, recursive, clientVersion, from, to, op);
|
||||||
} catch (Error & e) {
|
} catch (Error & e) {
|
||||||
/* If we're not in a state where we can send replies, then
|
/* If we're not in a state where we can send replies, then
|
||||||
something went wrong processing the input of the
|
something went wrong processing the input of the
|
||||||
|
|
|
@ -3,11 +3,15 @@
|
||||||
|
|
||||||
namespace nix::daemon {
|
namespace nix::daemon {
|
||||||
|
|
||||||
|
enum TrustedFlag : bool { NotTrusted = false, Trusted = true };
|
||||||
|
enum RecursiveFlag : bool { NotRecursive = false, Recursive = true };
|
||||||
|
|
||||||
void processConnection(
|
void processConnection(
|
||||||
ref<Store> store,
|
ref<Store> store,
|
||||||
FdSource & from,
|
FdSource & from,
|
||||||
FdSink & to,
|
FdSink & to,
|
||||||
bool trusted,
|
TrustedFlag trusted,
|
||||||
|
RecursiveFlag recursive,
|
||||||
const std::string & userName,
|
const std::string & userName,
|
||||||
uid_t userId);
|
uid_t userId);
|
||||||
|
|
||||||
|
|
|
@ -192,7 +192,7 @@ static void daemonLoop(char * * argv)
|
||||||
|
|
||||||
closeOnExec(remote.get());
|
closeOnExec(remote.get());
|
||||||
|
|
||||||
bool trusted = false;
|
TrustedFlag trusted = NotTrusted;
|
||||||
PeerInfo peer = getPeerInfo(remote.get());
|
PeerInfo peer = getPeerInfo(remote.get());
|
||||||
|
|
||||||
struct passwd * pw = peer.uidKnown ? getpwuid(peer.uid) : 0;
|
struct passwd * pw = peer.uidKnown ? getpwuid(peer.uid) : 0;
|
||||||
|
@ -205,7 +205,7 @@ static void daemonLoop(char * * argv)
|
||||||
Strings allowedUsers = settings.allowedUsers;
|
Strings allowedUsers = settings.allowedUsers;
|
||||||
|
|
||||||
if (matchUser(user, group, trustedUsers))
|
if (matchUser(user, group, trustedUsers))
|
||||||
trusted = true;
|
trusted = Trusted;
|
||||||
|
|
||||||
if ((!trusted && !matchUser(user, group, allowedUsers)) || group == settings.buildUsersGroup)
|
if ((!trusted && !matchUser(user, group, allowedUsers)) || group == settings.buildUsersGroup)
|
||||||
throw Error(format("user '%1%' is not allowed to connect to the Nix daemon") % user);
|
throw Error(format("user '%1%' is not allowed to connect to the Nix daemon") % user);
|
||||||
|
@ -239,7 +239,7 @@ static void daemonLoop(char * * argv)
|
||||||
/* Handle the connection. */
|
/* Handle the connection. */
|
||||||
FdSource from(remote.get());
|
FdSource from(remote.get());
|
||||||
FdSink to(remote.get());
|
FdSink to(remote.get());
|
||||||
processConnection(openUncachedStore(), from, to, trusted, user, peer.uid);
|
processConnection(openUncachedStore(), from, to, trusted, NotRecursive, user, peer.uid);
|
||||||
|
|
||||||
exit(0);
|
exit(0);
|
||||||
}, options);
|
}, options);
|
||||||
|
@ -321,7 +321,7 @@ static int _main(int argc, char * * argv)
|
||||||
} else {
|
} else {
|
||||||
FdSource from(STDIN_FILENO);
|
FdSource from(STDIN_FILENO);
|
||||||
FdSink to(STDOUT_FILENO);
|
FdSink to(STDOUT_FILENO);
|
||||||
processConnection(openUncachedStore(), from, to, true, "root", 0);
|
processConnection(openUncachedStore(), from, to, Trusted, NotRecursive, "root", 0);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
daemonLoop(argv);
|
daemonLoop(argv);
|
||||||
|
|
Loading…
Reference in a new issue