mirror of
https://github.com/NixOS/nix.git
synced 2025-11-11 04:56:01 +01:00
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
4 changed files with 492 additions and 78 deletions
|
|
@ -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,
|
||||
bool trusted, unsigned int clientVersion,
|
||||
TrustedFlag trusted, RecursiveFlag recursive, unsigned int clientVersion,
|
||||
Source & from, BufferedSink & to, unsigned int op)
|
||||
{
|
||||
switch (op) {
|
||||
|
|
@ -464,70 +531,37 @@ static void performOp(TunnelLogger * logger, ref<Store> store,
|
|||
}
|
||||
|
||||
case wopSetOptions: {
|
||||
settings.keepFailed = readInt(from);
|
||||
settings.keepGoing = readInt(from);
|
||||
settings.tryFallback = readInt(from);
|
||||
verbosity = (Verbosity) readInt(from);
|
||||
settings.maxBuildJobs.assign(readInt(from));
|
||||
settings.maxSilentTime = readInt(from);
|
||||
|
||||
ClientSettings clientSettings;
|
||||
|
||||
clientSettings.keepFailed = readInt(from);
|
||||
clientSettings.keepGoing = readInt(from);
|
||||
clientSettings.tryFallback = readInt(from);
|
||||
clientSettings.verbosity = (Verbosity) readInt(from);
|
||||
clientSettings.maxBuildJobs = readInt(from);
|
||||
clientSettings.maxSilentTime = readInt(from);
|
||||
readInt(from); // obsolete useBuildHook
|
||||
settings.verboseBuild = lvlError == (Verbosity) readInt(from);
|
||||
clientSettings.verboseBuild = lvlError == (Verbosity) readInt(from);
|
||||
readInt(from); // obsolete logType
|
||||
readInt(from); // obsolete printBuildTrace
|
||||
settings.buildCores = readInt(from);
|
||||
settings.useSubstitutes = readInt(from);
|
||||
clientSettings.buildCores = readInt(from);
|
||||
clientSettings.useSubstitutes = readInt(from);
|
||||
|
||||
StringMap overrides;
|
||||
if (GET_PROTOCOL_MINOR(clientVersion) >= 12) {
|
||||
unsigned int n = readInt(from);
|
||||
for (unsigned int i = 0; i < n; i++) {
|
||||
string name = readString(from);
|
||||
string value = readString(from);
|
||||
overrides.emplace(name, value);
|
||||
clientSettings.overrides.emplace(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
logger->startWork();
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
// FIXME: use some setting in recursive mode. Will need to use
|
||||
// non-global variables.
|
||||
if (!recursive)
|
||||
clientSettings.apply(trusted);
|
||||
|
||||
logger->stopWork();
|
||||
break;
|
||||
|
|
@ -694,11 +728,12 @@ void processConnection(
|
|||
ref<Store> store,
|
||||
FdSource & from,
|
||||
FdSink & to,
|
||||
bool trusted,
|
||||
TrustedFlag trusted,
|
||||
RecursiveFlag recursive,
|
||||
const std::string & userName,
|
||||
uid_t userId)
|
||||
{
|
||||
MonitorFdHup monitor(from.fd);
|
||||
auto monitor = !recursive ? std::make_unique<MonitorFdHup>(from.fd) : nullptr;
|
||||
|
||||
/* Exchange the greeting. */
|
||||
unsigned int magic = readInt(from);
|
||||
|
|
@ -712,7 +747,9 @@ void processConnection(
|
|||
|
||||
auto tunnelLogger = new TunnelLogger(to, clientVersion);
|
||||
auto prevLogger = nix::logger;
|
||||
logger = tunnelLogger;
|
||||
// FIXME
|
||||
if (!recursive)
|
||||
logger = tunnelLogger;
|
||||
|
||||
unsigned int opCount = 0;
|
||||
|
||||
|
|
@ -721,8 +758,10 @@ void processConnection(
|
|||
prevLogger->log(lvlDebug, fmt("%d operations", opCount));
|
||||
});
|
||||
|
||||
if (GET_PROTOCOL_MINOR(clientVersion) >= 14 && readInt(from))
|
||||
setAffinityTo(readInt(from));
|
||||
if (GET_PROTOCOL_MINOR(clientVersion) >= 14 && readInt(from)) {
|
||||
auto affinity = readInt(from);
|
||||
setAffinityTo(affinity);
|
||||
}
|
||||
|
||||
readInt(from); // obsolete reserveSpace
|
||||
|
||||
|
|
@ -760,7 +799,7 @@ void processConnection(
|
|||
opCount++;
|
||||
|
||||
try {
|
||||
performOp(tunnelLogger, store, trusted, clientVersion, from, to, op);
|
||||
performOp(tunnelLogger, store, trusted, recursive, clientVersion, from, to, op);
|
||||
} catch (Error & e) {
|
||||
/* If we're not in a state where we can send replies, then
|
||||
something went wrong processing the input of the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue