From b110da68e22d924b75650b2a6dbda72f5d1da7f1 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Sun, 16 Mar 2025 16:48:41 -0400 Subject: [PATCH 1/5] Make `appendLogTailErrorMsg` as class method after all The other parameters it took were somewhat implementation-specific. --- src/libstore/build/derivation-goal.cc | 10 +++------- .../include/nix/store/build/derivation-goal.hh | 9 ++------- src/libstore/unix/build/local-derivation-goal.cc | 2 +- 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/src/libstore/build/derivation-goal.cc b/src/libstore/build/derivation-goal.cc index 76456dac5..0e3163b93 100644 --- a/src/libstore/build/derivation-goal.cc +++ b/src/libstore/build/derivation-goal.cc @@ -871,11 +871,7 @@ void runPostBuildHook( } -void appendLogTailErrorMsg( - const Store & store, - const StorePath & drvPath, - const std::list & logTail, - std::string & msg) +void DerivationGoal::appendLogTailErrorMsg(std::string & msg) { if (!logger->isVerbose() && !logTail.empty()) { msg += fmt(";\nlast %d log lines:\n", logTail.size()); @@ -892,7 +888,7 @@ void appendLogTailErrorMsg( // command will not put it at the start of the line unfortunately. msg += fmt("For full logs, run:\n " ANSI_BOLD "%s %s" ANSI_NORMAL, nixLogCommand, - store.printStorePath(drvPath)); + worker.store.printStorePath(drvPath)); } } @@ -939,7 +935,7 @@ Goal::Co DerivationGoal::hookDone() Magenta(worker.store.printStorePath(drvPath)), statusToString(status)); - appendLogTailErrorMsg(worker.store, drvPath, logTail, msg); + appendLogTailErrorMsg(msg); outputLocks.unlock(); diff --git a/src/libstore/include/nix/store/build/derivation-goal.hh b/src/libstore/include/nix/store/build/derivation-goal.hh index 3baf4babf..390c60668 100644 --- a/src/libstore/include/nix/store/build/derivation-goal.hh +++ b/src/libstore/include/nix/store/build/derivation-goal.hh @@ -62,13 +62,6 @@ void runPostBuildHook( const StorePath & drvPath, const StorePathSet & outputPaths); -/** Used internally */ -void appendLogTailErrorMsg( - const Store & store, - const StorePath & drvPath, - const std::list & logTail, - std::string & msg); - /** * A goal for building some or all of the outputs of a derivation. */ @@ -305,6 +298,8 @@ struct DerivationGoal : public Goal SingleDrvOutputs builtOutputs = {}, std::optional ex = {}); + void appendLogTailErrorMsg(std::string & msg); + StorePathSet exportReferences(const StorePathSet & storePaths); JobCategory jobCategory() const override { diff --git a/src/libstore/unix/build/local-derivation-goal.cc b/src/libstore/unix/build/local-derivation-goal.cc index 8adc001f0..00df5db5e 100644 --- a/src/libstore/unix/build/local-derivation-goal.cc +++ b/src/libstore/unix/build/local-derivation-goal.cc @@ -624,7 +624,7 @@ Goal::Co LocalDerivationGoal::tryLocalBuild() Magenta(worker.store.printStorePath(drvPath)), statusToString(status)); - appendLogTailErrorMsg(worker.store, drvPath, logTail, msg); + appendLogTailErrorMsg(msg); if (diskFull) msg += "\nnote: build failure may have been caused by lack of free disk space"; From fe915ad9a15625d9b1035445814b67c61e1a3241 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Fri, 14 Mar 2025 15:37:59 -0400 Subject: [PATCH 2/5] Gut `LocalDerivationGoal::tryLocalBuild` Now, most of it is in two new functions: `LocalDerivationGoal::{,un}repareBuild`. This might seems like a step backwards from coroutines --- now we have more functions, and are stuck with class vars --- but I don't think it needs to be. There's a few options here: - (Re)introduce coroutines for the isolated building logic. We could use the same coroutines types, or simpler ones specialized to this use-case. The `tryLocalBuild` caller can still use `Goal::Co`, and just will manually "pump" this inner coroutine. - Return closures from each step. This is sort of like coroutines by hand, but it still allows us to stop writing down the local variables in each type. Being able to fully-use RAII again would be very nice! - Keep top-level first-order functions like now, but make more functional. Instead of having one state object (`DerivationBuilder`) for all steps (setup, run, teardown), we can have separate structs for the live variables at each point we consume and return. This at least avoids "are these variables active at this time?" questions, but doesn't give us the full benefit of RAII as we must manually ensure FIFO create/destroy orders still. One thing to note is that by keeping the `outputLock` unlocking in `tryLocalBuild`, we are arguably uncovering a rebuild scheduling vs building distinction, as the output locks are pretty squarely a scheduling concern. It's nice that the builder doesn't need to know about them at all. --- .../unix/build/local-derivation-goal.cc | 109 ++++++++++++------ 1 file changed, 76 insertions(+), 33 deletions(-) diff --git a/src/libstore/unix/build/local-derivation-goal.cc b/src/libstore/unix/build/local-derivation-goal.cc index 00df5db5e..d9a4eeede 100644 --- a/src/libstore/unix/build/local-derivation-goal.cc +++ b/src/libstore/unix/build/local-derivation-goal.cc @@ -253,11 +253,32 @@ struct LocalDerivationGoal : DerivationGoal, RestrictionContext */ Goal::Co tryLocalBuild() override; + /** + * Set up build environment / sandbox, acquiring resources (e.g. + * locks as needed). After this is run, the builder should be + * started. + * + * @returns true if successful, false if we could not acquire a build + * user. In that case, the caller must wait and then try again. + */ + bool prepareBuild(); + /** * Start building a derivation. */ void startBuilder(); + /** + * Tear down build environment after the builder exits (either on + * its own or if it is killed). + * + * @returns The first case indicates failure during output + * processing. A status code and exception are returned, providing + * more information. The second case indicates success, and + * realisations for each output of the derivation are returned. + */ + std::variant, SingleDrvOutputs> unprepareBuild(); + /** * Fill in the environment for the builder. */ @@ -489,6 +510,53 @@ Goal::Co LocalDerivationGoal::tryLocalBuild() co_return tryToBuild(); } + if (!prepareBuild()) { + if (!actLock) + actLock = std::make_unique(*logger, lvlWarn, actBuildWaiting, + fmt("waiting for a free build user ID for '%s'", Magenta(worker.store.printStorePath(drvPath)))); + co_await waitForAWhile(); + co_return tryLocalBuild(); + } + + actLock.reset(); + + try { + + /* Okay, we have to build. */ + startBuilder(); + + } catch (BuildError & e) { + outputLocks.unlock(); + buildUser.reset(); + worker.permanentFailure = true; + co_return done(BuildResult::InputRejected, {}, std::move(e)); + } + + started(); + co_await Suspend{}; + + trace("build done"); + + auto res = unprepareBuild(); + // N.B. cannot use `std::visit` with co-routine return + if (auto * ste = std::get_if<0>(&res)) { + outputLocks.unlock(); + co_return done(std::move(ste->first), {}, std::move(ste->second)); + } else if (auto * builtOutputs = std::get_if<1>(&res)) { + /* It is now safe to delete the lock files, since all future + lockers will see that the output paths are valid; they will + not create new lock files with the same names as the old + (unlinked) lock files. */ + outputLocks.setDeletion(true); + outputLocks.unlock(); + co_return done(BuildResult::Built, std::move(*builtOutputs)); + } else { + unreachable(); + } +} + +bool LocalDerivationGoal::prepareBuild() +{ /* Cache this */ derivationType = drv->type(); @@ -536,33 +604,16 @@ Goal::Co LocalDerivationGoal::tryLocalBuild() buildUser = acquireUserLock(drvOptions->useUidRange(*drv) ? 65536 : 1, useChroot); if (!buildUser) { - if (!actLock) - actLock = std::make_unique(*logger, lvlWarn, actBuildWaiting, - fmt("waiting for a free build user ID for '%s'", Magenta(worker.store.printStorePath(drvPath)))); - co_await waitForAWhile(); - co_return tryLocalBuild(); + return false; } } - actLock.reset(); + return true; +} - try { - - /* Okay, we have to build. */ - startBuilder(); - - } catch (BuildError & e) { - outputLocks.unlock(); - buildUser.reset(); - worker.permanentFailure = true; - co_return done(BuildResult::InputRejected, {}, std::move(e)); - } - - started(); - co_await Suspend{}; - - trace("build done"); +std::variant, SingleDrvOutputs> LocalDerivationGoal::unprepareBuild() +{ Finally releaseBuildUser([&](){ /* Release the build user at the end of this function. We don't do it right away because we don't want another build grabbing this @@ -655,18 +706,9 @@ Goal::Co LocalDerivationGoal::tryLocalBuild() deleteTmpDir(true); - /* It is now safe to delete the lock files, since all future - lockers will see that the output paths are valid; they will - not create new lock files with the same names as the old - (unlinked) lock files. */ - outputLocks.setDeletion(true); - outputLocks.unlock(); - - co_return done(BuildResult::Built, std::move(builtOutputs)); + return std::move(builtOutputs); } catch (BuildError & e) { - outputLocks.unlock(); - assert(derivationType); BuildResult::Status st = dynamic_cast(&e) ? BuildResult::NotDeterministic : @@ -674,10 +716,11 @@ Goal::Co LocalDerivationGoal::tryLocalBuild() !derivationType->isSandboxed() || diskFull ? BuildResult::TransientFailure : BuildResult::PermanentFailure; - co_return done(st, {}, std::move(e)); + return std::pair{std::move(st), std::move(e)}; } } + static void chmod_(const Path & path, mode_t mode) { if (chmod(path.c_str(), mode) == -1) From c1bae909e2ca82921ff29e268d041a31d59d22f0 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Fri, 14 Mar 2025 04:40:02 -0400 Subject: [PATCH 3/5] Start separating scheduling from building We have a new `DerivationBuilder` struct, and `DerivationBuilderParams` `DerivationBuilderCallbacks` supporting it. `LocalDerivationGoal` doesn't subclass any of these, so we are ready to now move them out to a new file! --- .../unix/build/local-derivation-goal.cc | 551 +++++++++++++----- 1 file changed, 399 insertions(+), 152 deletions(-) diff --git a/src/libstore/unix/build/local-derivation-goal.cc b/src/libstore/unix/build/local-derivation-goal.cc index d9a4eeede..93154c7dc 100644 --- a/src/libstore/unix/build/local-derivation-goal.cc +++ b/src/libstore/unix/build/local-derivation-goal.cc @@ -81,9 +81,138 @@ extern "C" int sandbox_init_with_parameters(const char *profile, uint64_t flags, namespace nix { -struct LocalDerivationGoal : DerivationGoal, RestrictionContext +/** + * Parameters by (mostly) `const` reference for `DerivationBuilder`. + */ +struct DerivationBuilderParams { - LocalStore & getLocalStore(); + /** The path of the derivation. */ + const StorePath & drvPath; + + BuildResult & buildResult; + + /** + * The derivation stored at drvPath. + * + * @todo Remove double indirection by delaying when this is + * initialized. + */ + const std::unique_ptr & drv; + + const std::unique_ptr & parsedDrv; + const std::unique_ptr & drvOptions; + + /** + * The remainder is state held during the build. + */ + + /** + * All input paths (that is, the union of FS closures of the + * immediate input paths). + */ + const StorePathSet & inputPaths; + + /** + * @note we do in fact mutate this + */ + std::map & initialOutputs; + + const BuildMode & buildMode; + + DerivationBuilderParams( + const StorePath & drvPath, + const BuildMode & buildMode, + BuildResult & buildResult, + const std::unique_ptr & drv, + const std::unique_ptr & parsedDrv, + const std::unique_ptr & drvOptions, + const StorePathSet & inputPaths, + std::map & initialOutputs) + : drvPath{drvPath} + , buildResult{buildResult} + , drv{drv} + , parsedDrv{parsedDrv} + , drvOptions{drvOptions} + , inputPaths{inputPaths} + , initialOutputs{initialOutputs} + , buildMode{buildMode} + { } + + DerivationBuilderParams(DerivationBuilderParams &&) = default; +}; + +/** + * Callbacks that `DerivationBuilder` needs. + */ +struct DerivationBuilderCallbacks +{ + /** + * Open a log file and a pipe to it. + */ + virtual Path openLogFile() = 0; + + /** + * Close the log file. + */ + virtual void closeLogFile() = 0; + + /** + * Aborts if any output is not valid or corrupt, and otherwise + * returns a 'SingleDrvOutputs' structure containing all outputs. + * + * @todo Probably should just be in `DerivationGoal`. + */ + virtual SingleDrvOutputs assertPathValidity() = 0; + + virtual void appendLogTailErrorMsg(std::string & msg) = 0; + + /** + * Hook up `builderOut` to some mechanism to ingest the log + * + * @todo this should be reworked + */ + virtual void childStarted() = 0; + + /** + * @todo this should be reworked + */ + virtual void childTerminated() = 0; + + virtual void noteHashMismatch(void) = 0; + virtual void noteCheckMismatch(void) = 0; + + virtual void markContentsGood(const StorePath & path) = 0; +}; + +/** + * This class represents the state for building locally. + * + * @todo Ideally, it would not be a class, but a single function. + * However, besides the main entry point, there are a few more methods + * which are externally called, and need to be gotten rid of. There are + * also some virtual methods (either directly here or inherited from + * `DerivationBuilderCallbacks`, a stop-gap) that represent outgoing + * rather than incoming call edges that either should be removed, or + * become (higher order) function parameters. + */ +class DerivationBuilder : public RestrictionContext, DerivationBuilderParams +{ + Store & store; + + DerivationBuilderCallbacks & miscMethods; + +public: + + DerivationBuilder( + Store & store, + DerivationBuilderCallbacks & miscMethods, + DerivationBuilderParams params) + : DerivationBuilderParams{std::move(params)} + , store{store} + , miscMethods{miscMethods} + { } + + LocalStore & getLocalStore(); /** * User selected for running the builder. @@ -95,6 +224,8 @@ struct LocalDerivationGoal : DerivationGoal, RestrictionContext */ Pid pid; +private: + /** * The cgroup of the builder, if any. */ @@ -116,12 +247,16 @@ struct LocalDerivationGoal : DerivationGoal, RestrictionContext */ Path tmpDirInSandbox; +public: + /** * Master side of the pseudoterminal used for the builder's * standard output/error. */ AutoCloseFD builderOut; +private: + /** * Pipe for synchronising updates to the builder namespaces. */ @@ -239,19 +374,12 @@ struct LocalDerivationGoal : DerivationGoal, RestrictionContext friend struct RestrictedStore; - using DerivationGoal::DerivationGoal; - - virtual ~LocalDerivationGoal() override; - /** * Whether we need to perform hash rewriting if there are valid output paths. */ bool needsHashRewrite(); - /** - * The additional states. - */ - Goal::Co tryLocalBuild() override; +public: /** * Set up build environment / sandbox, acquiring resources (e.g. @@ -279,6 +407,8 @@ struct LocalDerivationGoal : DerivationGoal, RestrictionContext */ std::variant, SingleDrvOutputs> unprepareBuild(); +private: + /** * Fill in the environment for the builder. */ @@ -304,12 +434,16 @@ struct LocalDerivationGoal : DerivationGoal, RestrictionContext */ void startDaemon(); +public: + /** * Stop the in-process nix daemon thread. * @see startDaemon */ void stopDaemon(); +private: + void addDependency(const StorePath & path) override; /** @@ -335,26 +469,21 @@ struct LocalDerivationGoal : DerivationGoal, RestrictionContext */ void checkOutputs(const std::map & outputs); - bool isReadDesc(int fd) override; +public: /** * Delete the temporary directory, if we have one. */ void deleteTmpDir(bool force); - /** - * Forcibly kill the child process, if any. - * - * Called by destructor, can't be overridden - */ - void killChild() override final; - /** * Kill any processes running under the build user UID or in the * cgroup of the build. */ void killSandbox(bool getStats); +private: + bool cleanupDecideWhetherDiskFull(); /** @@ -374,6 +503,98 @@ struct LocalDerivationGoal : DerivationGoal, RestrictionContext StorePath makeFallbackPath(OutputNameView outputName); }; +/** + * This hooks up `DerivationBuilder` to the scheduler / goal machinary. + * + * @todo Eventually, this shouldn't exist, because `DerivationGoal` can + * just choose to use `DerivationBuilder` or its remote-building + * equalivalent directly, at the "value level" rather than "class + * inheritance hierarchy" level. + */ +struct LocalDerivationGoal : DerivationGoal, DerivationBuilderCallbacks +{ + DerivationBuilder builder; + + LocalDerivationGoal(const StorePath & drvPath, + const OutputsSpec & wantedOutputs, Worker & worker, + BuildMode buildMode) + : DerivationGoal{drvPath, wantedOutputs, worker, buildMode} + , builder{ + worker.store, + static_cast(*this), + DerivationBuilderParams { + DerivationGoal::drvPath, + DerivationGoal::buildMode, + DerivationGoal::buildResult, + DerivationGoal::drv, + DerivationGoal::parsedDrv, + DerivationGoal::drvOptions, + DerivationGoal::inputPaths, + DerivationGoal::initialOutputs, + }, + } + {} + + LocalDerivationGoal(const StorePath & drvPath, const BasicDerivation & drv, + const OutputsSpec & wantedOutputs, Worker & worker, + BuildMode buildMode = bmNormal) + : DerivationGoal{drvPath, drv, wantedOutputs, worker, buildMode} + , builder{ + worker.store, + static_cast(*this), + DerivationBuilderParams { + DerivationGoal::drvPath, + DerivationGoal::buildMode, + DerivationGoal::buildResult, + DerivationGoal::drv, + DerivationGoal::parsedDrv, + DerivationGoal::drvOptions, + DerivationGoal::inputPaths, + DerivationGoal::initialOutputs, + }, + } + {} + + virtual ~LocalDerivationGoal() override; + + /** + * The additional states. + */ + Goal::Co tryLocalBuild() override; + + bool isReadDesc(int fd) override; + + /** + * Forcibly kill the child process, if any. + * + * Called by destructor, can't be overridden + */ + void killChild() override final; + + void childStarted() override; + void childTerminated() override; + + void noteHashMismatch(void) override; + void noteCheckMismatch(void) override; + + void markContentsGood(const StorePath &) override; + + // Fake overrides to isntantiate identically-named virtual methods + + Path openLogFile() override { + return DerivationGoal::openLogFile(); + } + void closeLogFile() override { + DerivationGoal::closeLogFile(); + } + SingleDrvOutputs assertPathValidity() override { + return DerivationGoal::assertPathValidity(); + } + void appendLogTailErrorMsg(std::string & msg) override { + DerivationGoal::appendLogTailErrorMsg(msg); + } +}; + std::shared_ptr makeLocalDerivationGoal( const StorePath & drvPath, const OutputsSpec & wantedOutputs, Worker & worker, @@ -424,20 +645,20 @@ void handleDiffHook( } } -const Path LocalDerivationGoal::homeDir = "/homeless-shelter"; +const Path DerivationBuilder::homeDir = "/homeless-shelter"; LocalDerivationGoal::~LocalDerivationGoal() { /* Careful: we should never ever throw an exception from a destructor. */ - try { deleteTmpDir(false); } catch (...) { ignoreExceptionInDestructor(); } + try { builder.deleteTmpDir(false); } catch (...) { ignoreExceptionInDestructor(); } try { killChild(); } catch (...) { ignoreExceptionInDestructor(); } - try { stopDaemon(); } catch (...) { ignoreExceptionInDestructor(); } + try { builder.stopDaemon(); } catch (...) { ignoreExceptionInDestructor(); } } -inline bool LocalDerivationGoal::needsHashRewrite() +inline bool DerivationBuilder::needsHashRewrite() { #ifdef __linux__ return !useChroot; @@ -448,9 +669,9 @@ inline bool LocalDerivationGoal::needsHashRewrite() } -LocalStore & LocalDerivationGoal::getLocalStore() +LocalStore & DerivationBuilder::getLocalStore() { - auto p = dynamic_cast(&worker.store); + auto p = dynamic_cast(&store); assert(p); return *p; } @@ -458,7 +679,7 @@ LocalStore & LocalDerivationGoal::getLocalStore() void LocalDerivationGoal::killChild() { - if (pid != -1) { + if (builder.pid != -1) { worker.childTerminated(this); /* If we're using a build user, then there is a tricky race @@ -466,18 +687,18 @@ void LocalDerivationGoal::killChild() done its setuid() to the build user uid, then it won't be killed, and we'll potentially lock up in pid.wait(). So also send a conventional kill to the child. */ - ::kill(-pid, SIGKILL); /* ignore the result */ + ::kill(-builder.pid, SIGKILL); /* ignore the result */ - killSandbox(true); + builder.killSandbox(true); - pid.wait(); + builder.pid.wait(); } DerivationGoal::killChild(); } -void LocalDerivationGoal::killSandbox(bool getStats) +void DerivationBuilder::killSandbox(bool getStats) { if (cgroup) { #ifdef __linux__ @@ -499,6 +720,34 @@ void LocalDerivationGoal::killSandbox(bool getStats) } +void LocalDerivationGoal::childStarted() +{ + worker.childStarted(shared_from_this(), {builder.builderOut.get()}, true, true); +} + +void LocalDerivationGoal::childTerminated() +{ + worker.childTerminated(this); +} + +void LocalDerivationGoal::noteHashMismatch() +{ + worker.hashMismatch = true; +} + + +void LocalDerivationGoal::noteCheckMismatch() +{ + worker.checkMismatch = true; +} + + +void LocalDerivationGoal::markContentsGood(const StorePath & path) +{ + worker.markContentsGood(path); +} + + Goal::Co LocalDerivationGoal::tryLocalBuild() { assert(!hook); @@ -510,7 +759,7 @@ Goal::Co LocalDerivationGoal::tryLocalBuild() co_return tryToBuild(); } - if (!prepareBuild()) { + if (!builder.prepareBuild()) { if (!actLock) actLock = std::make_unique(*logger, lvlWarn, actBuildWaiting, fmt("waiting for a free build user ID for '%s'", Magenta(worker.store.printStorePath(drvPath)))); @@ -523,11 +772,11 @@ Goal::Co LocalDerivationGoal::tryLocalBuild() try { /* Okay, we have to build. */ - startBuilder(); + builder.startBuilder(); } catch (BuildError & e) { outputLocks.unlock(); - buildUser.reset(); + builder.buildUser.reset(); worker.permanentFailure = true; co_return done(BuildResult::InputRejected, {}, std::move(e)); } @@ -537,7 +786,7 @@ Goal::Co LocalDerivationGoal::tryLocalBuild() trace("build done"); - auto res = unprepareBuild(); + auto res = builder.unprepareBuild(); // N.B. cannot use `std::visit` with co-routine return if (auto * ste = std::get_if<0>(&res)) { outputLocks.unlock(); @@ -555,7 +804,7 @@ Goal::Co LocalDerivationGoal::tryLocalBuild() } } -bool LocalDerivationGoal::prepareBuild() +bool DerivationBuilder::prepareBuild() { /* Cache this */ derivationType = drv->type(); @@ -565,7 +814,7 @@ bool LocalDerivationGoal::prepareBuild() if (settings.sandboxMode == smEnabled) { if (drvOptions->noChroot) throw Error("derivation '%s' has '__noChroot' set, " - "but that's not allowed when 'sandbox' is 'true'", worker.store.printStorePath(drvPath)); + "but that's not allowed when 'sandbox' is 'true'", store.printStorePath(drvPath)); #ifdef __APPLE__ if (drvOptions->additionalSandboxProfile != "") throw Error("derivation '%s' specifies a sandbox profile, " @@ -612,7 +861,7 @@ bool LocalDerivationGoal::prepareBuild() } -std::variant, SingleDrvOutputs> LocalDerivationGoal::unprepareBuild() +std::variant, SingleDrvOutputs> DerivationBuilder::unprepareBuild() { Finally releaseBuildUser([&](){ /* Release the build user at the end of this function. We don't do @@ -630,19 +879,19 @@ std::variant, SingleDrvOutputs> LocalDeriv kill it. */ int status = pid.kill(); - debug("builder process for '%s' finished", worker.store.printStorePath(drvPath)); + debug("builder process for '%s' finished", store.printStorePath(drvPath)); buildResult.timesBuilt++; buildResult.stopTime = time(0); /* So the child is gone now. */ - worker.childTerminated(this); + miscMethods.childTerminated(); /* Close the read side of the logger pipe. */ builderOut.close(); /* Close the log file. */ - closeLogFile(); + miscMethods.closeLogFile(); /* When running under a build user, make sure that all processes running under that uid are gone. This is to prevent a @@ -656,7 +905,7 @@ std::variant, SingleDrvOutputs> LocalDeriv if (buildResult.cpuUser && buildResult.cpuSystem) { debug("builder for '%s' terminated with status %d, user CPU %.3fs, system CPU %.3fs", - worker.store.printStorePath(drvPath), + store.printStorePath(drvPath), status, ((double) buildResult.cpuUser->count()) / 1000000, ((double) buildResult.cpuSystem->count()) / 1000000); @@ -672,10 +921,10 @@ std::variant, SingleDrvOutputs> LocalDeriv diskFull |= cleanupDecideWhetherDiskFull(); auto msg = fmt("builder for '%s' %s", - Magenta(worker.store.printStorePath(drvPath)), + Magenta(store.printStorePath(drvPath)), statusToString(status)); - appendLogTailErrorMsg(msg); + miscMethods.appendLogTailErrorMsg(msg); if (diskFull) msg += "\nnote: build failure may have been caused by lack of free disk space"; @@ -691,7 +940,7 @@ std::variant, SingleDrvOutputs> LocalDeriv for (auto & [_, output] : builtOutputs) outputPaths.insert(output.outPath); runPostBuildHook( - worker.store, + store, *logger, drvPath, outputPaths @@ -699,7 +948,7 @@ std::variant, SingleDrvOutputs> LocalDeriv /* Delete unused redirected outputs (when doing hash rewriting). */ for (auto & i : redirectedOutputs) - deletePath(worker.store.Store::toRealPath(i.second)); + deletePath(store.Store::toRealPath(i.second)); /* Delete the chroot (if we were using one). */ autoDelChroot.reset(); /* this runs the destructor */ @@ -750,7 +999,7 @@ static void movePath(const Path & src, const Path & dst) extern void replaceValidPath(const Path & storePath, const Path & tmpPath); -bool LocalDerivationGoal::cleanupDecideWhetherDiskFull() +bool DerivationBuilder::cleanupDecideWhetherDiskFull() { bool diskFull = false; @@ -781,7 +1030,7 @@ bool LocalDerivationGoal::cleanupDecideWhetherDiskFull() for (auto & [_, status] : initialOutputs) { if (!status.known) continue; if (buildMode != bmCheck && status.known->isValid()) continue; - auto p = worker.store.toRealPath(status.known->path); + auto p = store.toRealPath(status.known->path); if (pathExists(chrootRootDir + p)) std::filesystem::rename((chrootRootDir + p), p); } @@ -843,7 +1092,7 @@ static void rethrowExceptionAsError() /** * Send the current exception to the parent in the format expected by - * `LocalDerivationGoal::processSandboxSetupMessages()`. + * `DerivationBuilder::processSandboxSetupMessages()`. */ static void handleChildException(bool sendException) { @@ -860,7 +1109,7 @@ static void handleChildException(bool sendException) } } -void LocalDerivationGoal::startBuilder() +void DerivationBuilder::startBuilder() { if ((buildUser && buildUser->getUIDCount() != 1) #ifdef __linux__ @@ -918,7 +1167,7 @@ void LocalDerivationGoal::startBuilder() killSandbox(false); /* Right platform? */ - if (!drvOptions->canBuildLocally(worker.store, *drv)) { + if (!drvOptions->canBuildLocally(store, *drv)) { // since aarch64-darwin has Rosetta 2, this user can actually run x86_64-darwin on their hardware - we should tell them to run the command to install Darwin 2 if (drv->platform == "x86_64-darwin" && settings.thisSystem == "aarch64-darwin") { throw Error("run `/usr/sbin/softwareupdate --install-rosetta` to enable your %s to run programs for %s", settings.thisSystem, drv->platform); @@ -926,9 +1175,9 @@ void LocalDerivationGoal::startBuilder() throw Error("a '%s' with features {%s} is required to build '%s', but I am a '%s' with features {%s}", drv->platform, concatStringsSep(", ", drvOptions->getRequiredSystemFeatures(*drv)), - worker.store.printStorePath(drvPath), + store.printStorePath(drvPath), settings.thisSystem, - concatStringsSep(", ", worker.store.systemFeatures)); + concatStringsSep(", ", store.systemFeatures)); } } @@ -986,7 +1235,7 @@ void LocalDerivationGoal::startBuilder() /* Substitute output placeholders with the scratch output paths. We'll use during the build. */ - inputRewrites[hashPlaceholder(outputName)] = worker.store.printStorePath(scratchPath); + inputRewrites[hashPlaceholder(outputName)] = store.printStorePath(scratchPath); /* Additional tasks if we know the final path a priori. */ if (!status.known) continue; @@ -997,7 +1246,7 @@ void LocalDerivationGoal::startBuilder() if (fixedFinalPath == scratchPath) continue; /* Ensure scratch path is ours to use. */ - deletePath(worker.store.printStorePath(scratchPath)); + deletePath(store.printStorePath(scratchPath)); /* Rewrite and unrewrite paths */ { @@ -1034,14 +1283,14 @@ void LocalDerivationGoal::startBuilder() throw Error("invalid file name '%s' in 'exportReferencesGraph'", fileName); auto storePathS = *i++; - if (!worker.store.isInStore(storePathS)) + if (!store.isInStore(storePathS)) throw BuildError("'exportReferencesGraph' contains a non-store path '%1%'", storePathS); - auto storePath = worker.store.toStorePath(storePathS).first; + auto storePath = store.toStorePath(storePathS).first; /* Write closure info to . */ writeFile(tmpDir + "/" + fileName, - worker.store.makeValidityRegistration( - worker.store.exportReferences({storePath}, inputPaths), false, false)); + store.makeValidityRegistration( + store.exportReferences({storePath}, inputPaths), false, false)); } } @@ -1064,7 +1313,7 @@ void LocalDerivationGoal::startBuilder() else pathsInChroot[i.substr(0, p)] = {i.substr(p + 1), optional}; } - if (hasPrefix(worker.store.storeDir, tmpDirInSandbox)) + if (hasPrefix(store.storeDir, tmpDirInSandbox)) { throw Error("`sandbox-build-dir` must not contain the storeDir"); } @@ -1074,15 +1323,15 @@ void LocalDerivationGoal::startBuilder() StorePathSet closure; for (auto & i : pathsInChroot) try { - if (worker.store.isInStore(i.second.source)) - worker.store.computeFSClosure(worker.store.toStorePath(i.second.source).first, closure); + if (store.isInStore(i.second.source)) + store.computeFSClosure(store.toStorePath(i.second.source).first, closure); } catch (InvalidPath & e) { } catch (Error & e) { e.addTrace({}, "while processing 'sandbox-paths'"); throw; } for (auto & i : closure) { - auto p = worker.store.printStorePath(i); + auto p = store.printStorePath(i); pathsInChroot.insert_or_assign(p, p); } @@ -1107,7 +1356,7 @@ void LocalDerivationGoal::startBuilder() } if (!found) throw Error("derivation '%s' requested impure path '%s', but it was not in allowed-impure-host-deps", - worker.store.printStorePath(drvPath), i); + store.printStorePath(drvPath), i); /* Allow files in drvOptions->impureHostDeps to be missing; e.g. macOS 11+ has no /usr/lib/libSystem*.dylib */ @@ -1119,7 +1368,7 @@ void LocalDerivationGoal::startBuilder() environment using bind-mounts. We put it in the Nix store so that the build outputs can be moved efficiently from the chroot to their final location. */ - auto chrootParentDir = worker.store.Store::toRealPath(drvPath) + ".chroot"; + auto chrootParentDir = store.Store::toRealPath(drvPath) + ".chroot"; deletePath(chrootParentDir); /* Clean up the chroot directory automatically. */ @@ -1173,7 +1422,7 @@ void LocalDerivationGoal::startBuilder() can be bind-mounted). !!! As an extra security precaution, make the fake Nix store only writable by the build user. */ - Path chrootStoreDir = chrootRootDir + worker.store.storeDir; + Path chrootStoreDir = chrootRootDir + store.storeDir; createDirs(chrootStoreDir); chmod_(chrootStoreDir, 01775); @@ -1181,8 +1430,8 @@ void LocalDerivationGoal::startBuilder() throw SysError("cannot change ownership of '%1%'", chrootStoreDir); for (auto & i : inputPaths) { - auto p = worker.store.printStorePath(i); - Path r = worker.store.toRealPath(p); + auto p = store.printStorePath(i); + Path r = store.toRealPath(p); pathsInChroot.insert_or_assign(p, r); } @@ -1191,14 +1440,14 @@ void LocalDerivationGoal::startBuilder() rebuilding a path that is in settings.sandbox-paths (typically the dependencies of /bin/sh). Throw them out. */ - for (auto & i : drv->outputsAndOptPaths(worker.store)) { + for (auto & i : drv->outputsAndOptPaths(store)) { /* If the name isn't known a priori (i.e. floating content-addressing derivation), the temporary location we use should be fresh. Freshness means it is impossible that the path is already in the sandbox, so we don't need to worry about removing it. */ if (i.second.second) - pathsInChroot.erase(worker.store.printStorePath(*i.second.second)); + pathsInChroot.erase(store.printStorePath(*i.second.second)); } if (cgroup) { @@ -1230,8 +1479,8 @@ void LocalDerivationGoal::startBuilder() if (useChroot && settings.preBuildHook != "" && dynamic_cast(drv.get())) { printMsg(lvlChatty, "executing pre-build hook '%1%'", settings.preBuildHook); - auto args = useChroot ? Strings({worker.store.printStorePath(drvPath), chrootRootDir}) : - Strings({ worker.store.printStorePath(drvPath) }); + auto args = useChroot ? Strings({store.printStorePath(drvPath), chrootRootDir}) : + Strings({ store.printStorePath(drvPath) }); enum BuildHookState { stBegin, stExtraChrootDirs @@ -1276,7 +1525,7 @@ void LocalDerivationGoal::startBuilder() printMsg(lvlVomit, "setting builder env variable '%1%'='%2%'", i.first, i.second); /* Create the log file. */ - [[maybe_unused]] Path logFile = openLogFile(); + [[maybe_unused]] Path logFile = miscMethods.openLogFile(); /* Create a pseudoterminal to get the output of the builder. */ builderOut = posix_openpt(O_RDWR | O_NOCTTY); @@ -1486,13 +1735,13 @@ void LocalDerivationGoal::startBuilder() /* parent */ pid.setSeparatePG(true); - worker.childStarted(shared_from_this(), {builderOut.get()}, true, true); + miscMethods.childStarted(); processSandboxSetupMessages(); } -void LocalDerivationGoal::processSandboxSetupMessages() +void DerivationBuilder::processSandboxSetupMessages() { std::vector msgs; while (true) { @@ -1502,7 +1751,7 @@ void LocalDerivationGoal::processSandboxSetupMessages() } catch (Error & e) { auto status = pid.wait(); e.addTrace({}, "while waiting for the build environment for '%s' to initialize (%s, previous messages: %s)", - worker.store.printStorePath(drvPath), + store.printStorePath(drvPath), statusToString(status), concatStringsSep("|", msgs)); throw; @@ -1521,7 +1770,7 @@ void LocalDerivationGoal::processSandboxSetupMessages() } -void LocalDerivationGoal::initTmpDir() +void DerivationBuilder::initTmpDir() { /* In a sandbox, for determinism, always use the same temporary directory. */ @@ -1565,7 +1814,7 @@ void LocalDerivationGoal::initTmpDir() } -void LocalDerivationGoal::initEnv() +void DerivationBuilder::initEnv() { env.clear(); @@ -1586,7 +1835,7 @@ void LocalDerivationGoal::initEnv() shouldn't care, but this is useful for purity checking (e.g., the compiler or linker might only want to accept paths to files in the store or in the build directory). */ - env["NIX_STORE"] = worker.store.storeDir; + env["NIX_STORE"] = store.storeDir; /* The maximum number of cores to utilize for parallel building. */ env["NIX_BUILD_CORES"] = fmt("%d", settings.buildCores); @@ -1633,9 +1882,9 @@ void LocalDerivationGoal::initEnv() } -void LocalDerivationGoal::writeStructuredAttrs() +void DerivationBuilder::writeStructuredAttrs() { - if (auto structAttrsJson = parsedDrv->prepareStructuredAttrs(worker.store, inputPaths)) { + if (auto structAttrsJson = parsedDrv->prepareStructuredAttrs(store, inputPaths)) { auto json = structAttrsJson.value(); nlohmann::json rewritten; for (auto & [i, v] : json["outputs"].get()) { @@ -1658,19 +1907,19 @@ void LocalDerivationGoal::writeStructuredAttrs() } -void LocalDerivationGoal::startDaemon() +void DerivationBuilder::startDaemon() { experimentalFeatureSettings.require(Xp::RecursiveNix); Store::Params params; params["path-info-cache-size"] = "0"; - params["store"] = worker.store.storeDir; + params["store"] = store.storeDir; if (auto & optRoot = getLocalStore().rootDir.get()) params["root"] = *optRoot; params["state"] = "/no-such-path"; params["log"] = "/no-such-path"; auto store = makeRestrictedStore(params, - ref(std::dynamic_pointer_cast(worker.store.shared_from_this())), + ref(std::dynamic_pointer_cast(this->store.shared_from_this())), *this); addedPaths.clear(); @@ -1726,7 +1975,7 @@ void LocalDerivationGoal::startDaemon() } -void LocalDerivationGoal::stopDaemon() +void DerivationBuilder::stopDaemon() { if (daemonSocket && shutdown(daemonSocket.get(), SHUT_RDWR) == -1) { // According to the POSIX standard, the 'shutdown' function should @@ -1759,7 +2008,7 @@ void LocalDerivationGoal::stopDaemon() } -void LocalDerivationGoal::addDependency(const StorePath & path) +void DerivationBuilder::addDependency(const StorePath & path) { if (isAllowed(path)) return; @@ -1769,17 +2018,17 @@ void LocalDerivationGoal::addDependency(const StorePath & path) appear in the sandbox. */ if (useChroot) { - debug("materialising '%s' in the sandbox", worker.store.printStorePath(path)); + debug("materialising '%s' in the sandbox", store.printStorePath(path)); #ifdef __linux__ - Path source = worker.store.Store::toRealPath(path); - Path target = chrootRootDir + worker.store.printStorePath(path); + Path source = store.Store::toRealPath(path); + Path target = chrootRootDir + store.printStorePath(path); 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", store.printStorePath(path)); } /* Bind-mount the path into the sandbox. This requires @@ -1801,7 +2050,7 @@ void LocalDerivationGoal::addDependency(const StorePath & path) int status = child.wait(); if (status != 0) - throw Error("could not add path '%s' to sandbox", worker.store.printStorePath(path)); + throw Error("could not add path '%s' to sandbox", store.printStorePath(path)); #else throw Error("don't know how to make path '%s' (produced by a recursive Nix call) appear in the sandbox", @@ -1811,7 +2060,7 @@ void LocalDerivationGoal::addDependency(const StorePath & path) } } -void LocalDerivationGoal::chownToBuilder(const Path & path) +void DerivationBuilder::chownToBuilder(const Path & path) { if (!buildUser) return; if (chown(path.c_str(), buildUser->getUID(), buildUser->getGID()) == -1) @@ -1907,7 +2156,7 @@ void setupSeccomp() } -void LocalDerivationGoal::runChild() +void DerivationBuilder::runChild() { /* Warning: in the child we should absolutely not make any SQLite calls! */ @@ -1996,7 +2245,7 @@ void LocalDerivationGoal::runChild() Marking chrootRootDir as MS_SHARED causes pivot_root() to fail with EINVAL. Don't know why. */ - Path chrootStoreDir = chrootRootDir + worker.store.storeDir; + Path chrootStoreDir = chrootRootDir + 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); @@ -2011,7 +2260,7 @@ void LocalDerivationGoal::runChild() createDirs(chrootRootDir + "/dev/shm"); createDirs(chrootRootDir + "/dev/pts"); ss.push_back("/dev/full"); - if (worker.store.systemFeatures.get().count("kvm") && pathExists("/dev/kvm")) + if (store.systemFeatures.get().count("kvm") && pathExists("/dev/kvm")) ss.push_back("/dev/kvm"); ss.push_back("/dev/null"); ss.push_back("/dev/random"); @@ -2236,7 +2485,7 @@ void LocalDerivationGoal::runChild() /* 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 cur = worker.store.storeDir; + Path cur = store.storeDir; while (cur.compare("/") != 0) { ancestry.insert(cur); cur = dirOf(cur); @@ -2244,7 +2493,7 @@ void LocalDerivationGoal::runChild() /* Add all our input paths to the chroot */ for (auto & i : inputPaths) { - auto p = worker.store.printStorePath(i); + auto p = store.printStorePath(i); pathsInChroot[p] = p; } @@ -2267,7 +2516,7 @@ void LocalDerivationGoal::runChild() /* Add the output paths we'll use at build-time to the chroot */ sandboxProfile += "(allow file-read* file-write* process-exec\n"; for (auto & [_, path] : scratchOutputs) - sandboxProfile += fmt("\t(subpath \"%s\")\n", worker.store.printStorePath(path)); + sandboxProfile += fmt("\t(subpath \"%s\")\n", store.printStorePath(path)); sandboxProfile += ")\n"; @@ -2360,7 +2609,7 @@ void LocalDerivationGoal::runChild() std::map outputs; for (auto & e : drv->outputs) outputs.insert_or_assign(e.first, - worker.store.printStorePath(scratchOutputs.at(e.first))); + store.printStorePath(scratchOutputs.at(e.first))); if (drv->builder == "builtin:fetchurl") builtinFetchurl(*drv, outputs, netrcData, caFileData); @@ -2420,10 +2669,8 @@ void LocalDerivationGoal::runChild() } -SingleDrvOutputs LocalDerivationGoal::registerOutputs() +SingleDrvOutputs DerivationBuilder::registerOutputs() { - assert(!hook); - std::map infos; /* Set of inodes seen during calls to canonicalisePathMetaData() @@ -2448,7 +2695,7 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() auto toRealPathChroot = [&](const Path & p) -> Path { return useChroot && !needsHashRewrite() ? chrootRootDir + p - : worker.store.toRealPath(p); + : store.toRealPath(p); }; /* Check whether the output paths were created, and make all @@ -2466,8 +2713,8 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() if (!scratchOutput) throw BuildError( "builder for '%s' has no scratch output for '%s'", - worker.store.printStorePath(drvPath), outputName); - auto actualPath = toRealPathChroot(worker.store.printStorePath(*scratchOutput)); + store.printStorePath(drvPath), outputName); + auto actualPath = toRealPathChroot(store.printStorePath(*scratchOutput)); outputsToSort.insert(outputName); @@ -2476,7 +2723,7 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() if (!initialOutput) throw BuildError( "builder for '%s' has no initial output for '%s'", - worker.store.printStorePath(drvPath), outputName); + store.printStorePath(drvPath), outputName); auto & initialInfo = *initialOutput; /* Don't register if already valid, and not checking */ @@ -2493,7 +2740,7 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() if (!optSt) throw BuildError( "builder for '%s' failed to produce output path for output '%s' at '%s'", - worker.store.printStorePath(drvPath), outputName, actualPath); + store.printStorePath(drvPath), outputName, actualPath); struct stat & st = *optSt; #ifndef __CYGWIN__ @@ -2544,7 +2791,7 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() if (!orifu) throw BuildError( "no output reference for '%s' in build of '%s'", - name, worker.store.printStorePath(drvPath)); + name, store.printStorePath(drvPath)); return std::visit(overloaded { /* Since we'll use the already installed versions of these, we can treat them as leaves and ignore any references they @@ -2565,7 +2812,7 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() // TODO with more -vvvv also show the temporary paths for manual inspection. return BuildError( "cycle detected in build of '%s' in the references of output '%s' from output '%s'", - worker.store.printStorePath(drvPath), path, parent); + store.printStorePath(drvPath), path, parent); }}); std::reverse(sortedOutputNames.begin(), sortedOutputNames.end()); @@ -2576,7 +2823,7 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() auto output = get(drv->outputs, outputName); auto scratchPath = get(scratchOutputs, outputName); assert(output && scratchPath); - auto actualPath = toRealPathChroot(worker.store.printStorePath(*scratchPath)); + auto actualPath = toRealPathChroot(store.printStorePath(*scratchPath)); auto finish = [&](StorePath finalStorePath) { /* Store the final path */ @@ -2694,7 +2941,7 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() }(); ValidPathInfo newInfo0 { - worker.store, + store, outputPathName(drv->name, outputName), ContentAddressWithReferences::fromParts( outputHash.method, @@ -2772,10 +3019,10 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() if (wanted != got) { /* Throw an error after registering the path as valid. */ - worker.hashMismatch = true; + miscMethods.noteHashMismatch(); delayedException = std::make_exception_ptr( BuildError("hash mismatch in fixed-output derivation '%s':\n specified: %s\n got: %s", - worker.store.printStorePath(drvPath), + store.printStorePath(drvPath), wanted.to_string(HashFormat::SRI, true), got.to_string(HashFormat::SRI, true))); } @@ -2783,9 +3030,9 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() auto numViolations = newInfo.references.size(); delayedException = std::make_exception_ptr( BuildError("fixed-output derivations must not reference store paths: '%s' references %d distinct paths, e.g. '%s'", - worker.store.printStorePath(drvPath), + store.printStorePath(drvPath), numViolations, - worker.store.printStorePath(*newInfo.references.begin()))); + store.printStorePath(*newInfo.references.begin()))); } return newInfo0; @@ -2817,36 +3064,36 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() /* 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 their usual "final destination" */ - auto finalDestPath = worker.store.printStorePath(newInfo.path); + auto finalDestPath = store.printStorePath(newInfo.path); /* Lock final output path, if not already locked. This happens with floating CA derivations and hash-mismatching fixed-output derivations. */ PathLocks dynamicOutputLock; dynamicOutputLock.setDeletion(true); - auto optFixedPath = output->path(worker.store, drv->name, outputName); + auto optFixedPath = output->path(store, drv->name, outputName); if (!optFixedPath || - worker.store.printStorePath(*optFixedPath) != finalDestPath) + store.printStorePath(*optFixedPath) != finalDestPath) { assert(newInfo.ca); - dynamicOutputLock.lockPaths({worker.store.toRealPath(finalDestPath)}); + dynamicOutputLock.lockPaths({store.toRealPath(finalDestPath)}); } /* Move files, if needed */ - if (worker.store.toRealPath(finalDestPath) != actualPath) { + if (store.toRealPath(finalDestPath) != actualPath) { if (buildMode == bmRepair) { /* Path already exists, need to replace it */ - replaceValidPath(worker.store.toRealPath(finalDestPath), actualPath); - actualPath = worker.store.toRealPath(finalDestPath); + replaceValidPath(store.toRealPath(finalDestPath), actualPath); + actualPath = store.toRealPath(finalDestPath); } else if (buildMode == bmCheck) { /* Path already exists, and we want to compare, so we leave out new path in place. */ - } else if (worker.store.isValidPath(newInfo.path)) { + } else if (store.isValidPath(newInfo.path)) { /* Path already exists because CA path produced by something else. No moving needed. */ assert(newInfo.ca); } else { - auto destPath = worker.store.toRealPath(finalDestPath); + auto destPath = store.toRealPath(finalDestPath); deletePath(destPath); movePath(actualPath, destPath); actualPath = destPath; @@ -2857,25 +3104,25 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() if (buildMode == bmCheck) { - if (!worker.store.isValidPath(newInfo.path)) continue; - ValidPathInfo oldInfo(*worker.store.queryPathInfo(newInfo.path)); + if (!store.isValidPath(newInfo.path)) continue; + ValidPathInfo oldInfo(*store.queryPathInfo(newInfo.path)); if (newInfo.narHash != oldInfo.narHash) { - worker.checkMismatch = true; + miscMethods.noteCheckMismatch(); if (settings.runDiffHook || settings.keepFailed) { - auto dst = worker.store.toRealPath(finalDestPath + checkSuffix); + auto dst = store.toRealPath(finalDestPath + checkSuffix); deletePath(dst); movePath(actualPath, dst); handleDiffHook( buildUser ? buildUser->getUID() : getuid(), buildUser ? buildUser->getGID() : getgid(), - finalDestPath, dst, worker.store.printStorePath(drvPath), tmpDir); + finalDestPath, dst, store.printStorePath(drvPath), tmpDir); throw NotDeterministic("derivation '%s' may not be deterministic: output '%s' differs from '%s'", - worker.store.printStorePath(drvPath), worker.store.toRealPath(finalDestPath), dst); + store.printStorePath(drvPath), store.toRealPath(finalDestPath), dst); } else throw NotDeterministic("derivation '%s' may not be deterministic: output '%s' differs", - worker.store.printStorePath(drvPath), worker.store.toRealPath(finalDestPath)); + store.printStorePath(drvPath), store.toRealPath(finalDestPath)); } /* Since we verified the build, it's now ultimately trusted. */ @@ -2891,13 +3138,13 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() /* For debugging, print out the referenced and unreferenced paths. */ for (auto & i : inputPaths) { if (references.count(i)) - debug("referenced input: '%1%'", worker.store.printStorePath(i)); + debug("referenced input: '%1%'", store.printStorePath(i)); else - debug("unreferenced input: '%1%'", worker.store.printStorePath(i)); + debug("unreferenced input: '%1%'", store.printStorePath(i)); } localStore.optimisePath(actualPath, NoRepair); // FIXME: combine with scanForReferences() - worker.markContentsGood(newInfo.path); + miscMethods.markContentsGood(newInfo.path); newInfo.deriver = drvPath; newInfo.ultimate = true; @@ -2920,7 +3167,7 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() also a source for non-determinism. */ if (delayedException) std::rethrow_exception(delayedException); - return assertPathValidity(); + return miscMethods.assertPathValidity(); } /* Apply output checks. */ @@ -2964,8 +3211,8 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() if (experimentalFeatureSettings.isEnabled(Xp::CaDerivations) && !drv->type().isImpure()) { - worker.store.signRealisation(thisRealisation); - worker.store.registerDrvOutput(thisRealisation); + store.signRealisation(thisRealisation); + store.registerDrvOutput(thisRealisation); } builtOutputs.emplace(outputName, thisRealisation); } @@ -2974,11 +3221,11 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() } -void LocalDerivationGoal::checkOutputs(const std::map & outputs) +void DerivationBuilder::checkOutputs(const std::map & outputs) { std::map outputsByPath; for (auto & output : outputs) - outputsByPath.emplace(worker.store.printStorePath(output.second.path), output.second); + outputsByPath.emplace(store.printStorePath(output.second.path), output.second); for (auto & output : outputs) { auto & outputName = output.first; @@ -2999,13 +3246,13 @@ void LocalDerivationGoal::checkOutputs(const std::mapsecond.narSize; for (auto & ref : i->second.references) pathsLeft.push(ref); } else { - auto info = worker.store.queryPathInfo(path); + auto info = store.queryPathInfo(path); closureSize += info->narSize; for (auto & ref : info->references) pathsLeft.push(ref); @@ -3019,13 +3266,13 @@ void LocalDerivationGoal::checkOutputs(const std::map *checks.maxSize) throw BuildError("path '%s' is too large at %d bytes; limit is %d bytes", - worker.store.printStorePath(info.path), info.narSize, *checks.maxSize); + store.printStorePath(info.path), info.narSize, *checks.maxSize); if (checks.maxClosureSize) { uint64_t closureSize = getClosure(info.path).second; if (closureSize > *checks.maxClosureSize) throw BuildError("closure of path '%s' is too large at %d bytes; limit is %d bytes", - worker.store.printStorePath(info.path), closureSize, *checks.maxClosureSize); + store.printStorePath(info.path), closureSize, *checks.maxClosureSize); } auto checkRefs = [&](const StringSet & value, bool allowed, bool recursive) @@ -3035,15 +3282,15 @@ void LocalDerivationGoal::checkOutputs(const std::mappath); else { std::string outputsListing = concatMapStringsSep(", ", outputs, [](auto & o) { return o.first; }); throw BuildError("derivation '%s' output check for '%s' contains an illegal reference specifier '%s'," " expected store path or output name (one of [%s])", - worker.store.printStorePath(drvPath), outputName, i, outputsListing); + store.printStorePath(drvPath), outputName, i, outputsListing); } } @@ -3069,10 +3316,10 @@ void LocalDerivationGoal::checkOutputs(const std::mapname, outputName)); } -StorePath LocalDerivationGoal::makeFallbackPath(const StorePath & path) +StorePath DerivationBuilder::makeFallbackPath(const StorePath & path) { // This is a bogus path type, constructed this way to ensure that it doesn't collide with any other store path // See doc/manual/source/protocols/store-path.md for details auto pathType = "rewrite:" + std::string(drvPath.to_string()) + ":" + std::string(path.to_string()); - return worker.store.makeStorePath( + return store.makeStorePath( pathType, // pass an all-zeroes hash Hash(HashAlgorithm::SHA256), path.name()); From 3f6d2b1c5c551a98f8dd6f1c92613103e0e437ee Mon Sep 17 00:00:00 2001 From: John Ericson Date: Thu, 20 Mar 2025 19:03:21 -0400 Subject: [PATCH 4/5] Copy `local-derivation-goal.cc` to `derivation-builder.{cc,hh}` This is done to prior to splitting, just like 05cc5a858717c092e1835e2b0fec4c4b1a7fc97e for 68f4c728eca33f115f90e3f924c9081a4cd59896. --- src/libstore/unix/build/derivation-builder.cc | 3409 +++++++++++++++++ .../nix/store/build/derivation-builder.hh | 3409 +++++++++++++++++ 2 files changed, 6818 insertions(+) create mode 100644 src/libstore/unix/build/derivation-builder.cc create mode 100644 src/libstore/unix/include/nix/store/build/derivation-builder.hh diff --git a/src/libstore/unix/build/derivation-builder.cc b/src/libstore/unix/build/derivation-builder.cc new file mode 100644 index 000000000..93154c7dc --- /dev/null +++ b/src/libstore/unix/build/derivation-builder.cc @@ -0,0 +1,3409 @@ +#include "nix/store/build/local-derivation-goal.hh" +#include "nix/store/local-store.hh" +#include "nix/util/processes.hh" +#include "nix/store/indirect-root-store.hh" +#include "nix/store/build/hook-instance.hh" +#include "nix/store/build/worker.hh" +#include "nix/store/builtins.hh" +#include "nix/store/builtins/buildenv.hh" +#include "nix/store/path-references.hh" +#include "nix/util/finally.hh" +#include "nix/util/util.hh" +#include "nix/util/archive.hh" +#include "nix/util/git.hh" +#include "nix/util/compression.hh" +#include "nix/store/daemon.hh" +#include "nix/util/topo-sort.hh" +#include "nix/util/callback.hh" +#include "nix/util/json-utils.hh" +#include "nix/util/current-process.hh" +#include "nix/store/build/child.hh" +#include "nix/util/unix-domain-socket.hh" +#include "nix/store/posix-fs-canonicalise.hh" +#include "nix/util/posix-source-accessor.hh" +#include "nix/store/restricted-store.hh" +#include "nix/store/config.hh" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "store-config-private.hh" + +#if HAVE_STATVFS +#include +#endif + +/* Includes required for chroot support. */ +#ifdef __linux__ +# include "linux/fchmodat2-compat.hh" +# include +# include +# include +# include +# include +# include +# include +# include +# include "nix/util/namespaces.hh" +# if HAVE_SECCOMP +# include +# endif +# define pivot_root(new_root, put_old) (syscall(SYS_pivot_root, new_root, put_old)) +# include "nix/util/cgroup.hh" +# include "nix/store/personality.hh" +#endif + +#ifdef __APPLE__ +#include +#include +#include + +/* This definition is undocumented but depended upon by all major browsers. */ +extern "C" int sandbox_init_with_parameters(const char *profile, uint64_t flags, const char *const parameters[], char **errorbuf); +#endif + +#include +#include +#include + +#include "nix/util/strings.hh" +#include "nix/util/signals.hh" + +#include "store-config-private.hh" + +namespace nix { + +/** + * Parameters by (mostly) `const` reference for `DerivationBuilder`. + */ +struct DerivationBuilderParams +{ + /** The path of the derivation. */ + const StorePath & drvPath; + + BuildResult & buildResult; + + /** + * The derivation stored at drvPath. + * + * @todo Remove double indirection by delaying when this is + * initialized. + */ + const std::unique_ptr & drv; + + const std::unique_ptr & parsedDrv; + const std::unique_ptr & drvOptions; + + /** + * The remainder is state held during the build. + */ + + /** + * All input paths (that is, the union of FS closures of the + * immediate input paths). + */ + const StorePathSet & inputPaths; + + /** + * @note we do in fact mutate this + */ + std::map & initialOutputs; + + const BuildMode & buildMode; + + DerivationBuilderParams( + const StorePath & drvPath, + const BuildMode & buildMode, + BuildResult & buildResult, + const std::unique_ptr & drv, + const std::unique_ptr & parsedDrv, + const std::unique_ptr & drvOptions, + const StorePathSet & inputPaths, + std::map & initialOutputs) + : drvPath{drvPath} + , buildResult{buildResult} + , drv{drv} + , parsedDrv{parsedDrv} + , drvOptions{drvOptions} + , inputPaths{inputPaths} + , initialOutputs{initialOutputs} + , buildMode{buildMode} + { } + + DerivationBuilderParams(DerivationBuilderParams &&) = default; +}; + +/** + * Callbacks that `DerivationBuilder` needs. + */ +struct DerivationBuilderCallbacks +{ + /** + * Open a log file and a pipe to it. + */ + virtual Path openLogFile() = 0; + + /** + * Close the log file. + */ + virtual void closeLogFile() = 0; + + /** + * Aborts if any output is not valid or corrupt, and otherwise + * returns a 'SingleDrvOutputs' structure containing all outputs. + * + * @todo Probably should just be in `DerivationGoal`. + */ + virtual SingleDrvOutputs assertPathValidity() = 0; + + virtual void appendLogTailErrorMsg(std::string & msg) = 0; + + /** + * Hook up `builderOut` to some mechanism to ingest the log + * + * @todo this should be reworked + */ + virtual void childStarted() = 0; + + /** + * @todo this should be reworked + */ + virtual void childTerminated() = 0; + + virtual void noteHashMismatch(void) = 0; + virtual void noteCheckMismatch(void) = 0; + + virtual void markContentsGood(const StorePath & path) = 0; +}; + +/** + * This class represents the state for building locally. + * + * @todo Ideally, it would not be a class, but a single function. + * However, besides the main entry point, there are a few more methods + * which are externally called, and need to be gotten rid of. There are + * also some virtual methods (either directly here or inherited from + * `DerivationBuilderCallbacks`, a stop-gap) that represent outgoing + * rather than incoming call edges that either should be removed, or + * become (higher order) function parameters. + */ +class DerivationBuilder : public RestrictionContext, DerivationBuilderParams +{ + Store & store; + + DerivationBuilderCallbacks & miscMethods; + +public: + + DerivationBuilder( + Store & store, + DerivationBuilderCallbacks & miscMethods, + DerivationBuilderParams params) + : DerivationBuilderParams{std::move(params)} + , store{store} + , miscMethods{miscMethods} + { } + + LocalStore & getLocalStore(); + + /** + * User selected for running the builder. + */ + std::unique_ptr buildUser; + + /** + * The process ID of the builder. + */ + Pid pid; + +private: + + /** + * The cgroup of the builder, if any. + */ + std::optional cgroup; + + /** + * The temporary directory used for the build. + */ + Path tmpDir; + + /** + * The top-level temporary directory. `tmpDir` is either equal to + * or a child of this directory. + */ + Path topTmpDir; + + /** + * The path of the temporary directory in the sandbox. + */ + Path tmpDirInSandbox; + +public: + + /** + * Master side of the pseudoterminal used for the builder's + * standard output/error. + */ + AutoCloseFD builderOut; + +private: + + /** + * Pipe for synchronising updates to the builder namespaces. + */ + Pipe userNamespaceSync; + + /** + * The mount namespace and user namespace of the builder, used to add additional + * paths to the sandbox as a result of recursive Nix calls. + */ + AutoCloseFD sandboxMountNamespace; + AutoCloseFD sandboxUserNamespace; + + /** + * On Linux, whether we're doing the build in its own user + * namespace. + */ + bool usingUserNamespace = true; + + /** + * Whether we're currently doing a chroot build. + */ + bool useChroot = false; + + /** + * The root of the chroot environment. + */ + Path chrootRootDir; + + /** + * RAII object to delete the chroot directory. + */ + std::shared_ptr autoDelChroot; + + /** + * The sort of derivation we are building. + * + * Just a cached value, can be recomputed from `drv`. + */ + std::optional derivationType; + + /** + * Stuff we need to pass to initChild(). + */ + struct ChrootPath { + Path source; + bool optional; + ChrootPath(Path source = "", bool optional = false) + : source(source), optional(optional) + { } + }; + typedef map PathsInChroot; // maps target path to source path + PathsInChroot pathsInChroot; + + typedef map Environment; + Environment env; + + /** + * Hash rewriting. + */ + StringMap inputRewrites, outputRewrites; + typedef map RedirectedOutputs; + RedirectedOutputs redirectedOutputs; + + /** + * The output paths used during the build. + * + * - Input-addressed derivations or fixed content-addressed outputs are + * sometimes built when some of their outputs already exist, and can not + * be hidden via sandboxing. We use temporary locations instead and + * rewrite after the build. Otherwise the regular predetermined paths are + * put here. + * + * - Floating content-addressing derivations do not know their final build + * output paths until the outputs are hashed, so random locations are + * used, and then renamed. The randomness helps guard against hidden + * self-references. + */ + OutputPathMap scratchOutputs; + + uid_t sandboxUid() { return usingUserNamespace ? (!buildUser || buildUser->getUIDCount() == 1 ? 1000 : 0) : buildUser->getUID(); } + gid_t sandboxGid() { return usingUserNamespace ? (!buildUser || buildUser->getUIDCount() == 1 ? 100 : 0) : buildUser->getGID(); } + + const static Path homeDir; + + /** + * The recursive Nix daemon socket. + */ + AutoCloseFD daemonSocket; + + /** + * The daemon main thread. + */ + std::thread daemonThread; + + /** + * The daemon worker threads. + */ + std::vector daemonWorkerThreads; + + const StorePathSet & originalPaths() override + { + return inputPaths; + } + + bool isAllowed(const StorePath & path) override + { + return inputPaths.count(path) || addedPaths.count(path); + } + bool isAllowed(const DrvOutput & id) override + { + return addedDrvOutputs.count(id); + } + + bool isAllowed(const DerivedPath & req); + + friend struct RestrictedStore; + + /** + * Whether we need to perform hash rewriting if there are valid output paths. + */ + bool needsHashRewrite(); + +public: + + /** + * Set up build environment / sandbox, acquiring resources (e.g. + * locks as needed). After this is run, the builder should be + * started. + * + * @returns true if successful, false if we could not acquire a build + * user. In that case, the caller must wait and then try again. + */ + bool prepareBuild(); + + /** + * Start building a derivation. + */ + void startBuilder(); + + /** + * Tear down build environment after the builder exits (either on + * its own or if it is killed). + * + * @returns The first case indicates failure during output + * processing. A status code and exception are returned, providing + * more information. The second case indicates success, and + * realisations for each output of the derivation are returned. + */ + std::variant, SingleDrvOutputs> unprepareBuild(); + +private: + + /** + * Fill in the environment for the builder. + */ + void initEnv(); + + /** + * Process messages send by the sandbox initialization. + */ + void processSandboxSetupMessages(); + + /** + * Setup tmp dir location. + */ + void initTmpDir(); + + /** + * Write a JSON file containing the derivation attributes. + */ + void writeStructuredAttrs(); + + /** + * Start an in-process nix daemon thread for recursive-nix. + */ + void startDaemon(); + +public: + + /** + * Stop the in-process nix daemon thread. + * @see startDaemon + */ + void stopDaemon(); + +private: + + void addDependency(const StorePath & path) override; + + /** + * Make a file owned by the builder. + */ + void chownToBuilder(const Path & path); + + /** + * Run the builder's process. + */ + void runChild(); + + /** + * Check that the derivation outputs all exist and register them + * as valid. + */ + SingleDrvOutputs registerOutputs(); + + /** + * Check that an output meets the requirements specified by the + * 'outputChecks' attribute (or the legacy + * '{allowed,disallowed}{References,Requisites}' attributes). + */ + void checkOutputs(const std::map & outputs); + +public: + + /** + * Delete the temporary directory, if we have one. + */ + void deleteTmpDir(bool force); + + /** + * Kill any processes running under the build user UID or in the + * cgroup of the build. + */ + void killSandbox(bool getStats); + +private: + + bool cleanupDecideWhetherDiskFull(); + + /** + * Create alternative path calculated from but distinct from the + * input, so we can avoid overwriting outputs (or other store paths) + * that already exist. + */ + StorePath makeFallbackPath(const StorePath & path); + + /** + * Make a path to another based on the output name along with the + * derivation hash. + * + * @todo Add option to randomize, so we can audit whether our + * rewrites caught everything + */ + StorePath makeFallbackPath(OutputNameView outputName); +}; + +/** + * This hooks up `DerivationBuilder` to the scheduler / goal machinary. + * + * @todo Eventually, this shouldn't exist, because `DerivationGoal` can + * just choose to use `DerivationBuilder` or its remote-building + * equalivalent directly, at the "value level" rather than "class + * inheritance hierarchy" level. + */ +struct LocalDerivationGoal : DerivationGoal, DerivationBuilderCallbacks +{ + DerivationBuilder builder; + + LocalDerivationGoal(const StorePath & drvPath, + const OutputsSpec & wantedOutputs, Worker & worker, + BuildMode buildMode) + : DerivationGoal{drvPath, wantedOutputs, worker, buildMode} + , builder{ + worker.store, + static_cast(*this), + DerivationBuilderParams { + DerivationGoal::drvPath, + DerivationGoal::buildMode, + DerivationGoal::buildResult, + DerivationGoal::drv, + DerivationGoal::parsedDrv, + DerivationGoal::drvOptions, + DerivationGoal::inputPaths, + DerivationGoal::initialOutputs, + }, + } + {} + + LocalDerivationGoal(const StorePath & drvPath, const BasicDerivation & drv, + const OutputsSpec & wantedOutputs, Worker & worker, + BuildMode buildMode = bmNormal) + : DerivationGoal{drvPath, drv, wantedOutputs, worker, buildMode} + , builder{ + worker.store, + static_cast(*this), + DerivationBuilderParams { + DerivationGoal::drvPath, + DerivationGoal::buildMode, + DerivationGoal::buildResult, + DerivationGoal::drv, + DerivationGoal::parsedDrv, + DerivationGoal::drvOptions, + DerivationGoal::inputPaths, + DerivationGoal::initialOutputs, + }, + } + {} + + virtual ~LocalDerivationGoal() override; + + /** + * The additional states. + */ + Goal::Co tryLocalBuild() override; + + bool isReadDesc(int fd) override; + + /** + * Forcibly kill the child process, if any. + * + * Called by destructor, can't be overridden + */ + void killChild() override final; + + void childStarted() override; + void childTerminated() override; + + void noteHashMismatch(void) override; + void noteCheckMismatch(void) override; + + void markContentsGood(const StorePath &) override; + + // Fake overrides to isntantiate identically-named virtual methods + + Path openLogFile() override { + return DerivationGoal::openLogFile(); + } + void closeLogFile() override { + DerivationGoal::closeLogFile(); + } + SingleDrvOutputs assertPathValidity() override { + return DerivationGoal::assertPathValidity(); + } + void appendLogTailErrorMsg(std::string & msg) override { + DerivationGoal::appendLogTailErrorMsg(msg); + } +}; + +std::shared_ptr makeLocalDerivationGoal( + const StorePath & drvPath, + const OutputsSpec & wantedOutputs, Worker & worker, + BuildMode buildMode) +{ + return std::make_shared(drvPath, wantedOutputs, worker, buildMode); +} + +std::shared_ptr makeLocalDerivationGoal( + const StorePath & drvPath, const BasicDerivation & drv, + const OutputsSpec & wantedOutputs, Worker & worker, + BuildMode buildMode) +{ + return std::make_shared(drvPath, drv, wantedOutputs, worker, buildMode); +} + +void handleDiffHook( + uid_t uid, uid_t gid, + const Path & tryA, const Path & tryB, + const Path & drvPath, const Path & tmpDir) +{ + auto & diffHookOpt = settings.diffHook.get(); + if (diffHookOpt && settings.runDiffHook) { + auto & diffHook = *diffHookOpt; + try { + auto diffRes = runProgram(RunOptions { + .program = diffHook, + .lookupPath = true, + .args = {tryA, tryB, drvPath, tmpDir}, + .uid = uid, + .gid = gid, + .chdir = "/" + }); + if (!statusOk(diffRes.first)) + throw ExecError(diffRes.first, + "diff-hook program '%1%' %2%", + diffHook, + statusToString(diffRes.first)); + + if (diffRes.second != "") + printError(chomp(diffRes.second)); + } catch (Error & error) { + ErrorInfo ei = error.info(); + // FIXME: wrap errors. + ei.msg = HintFmt("diff hook execution failed: %s", ei.msg.str()); + logError(ei); + } + } +} + +const Path DerivationBuilder::homeDir = "/homeless-shelter"; + + +LocalDerivationGoal::~LocalDerivationGoal() +{ + /* Careful: we should never ever throw an exception from a + destructor. */ + try { builder.deleteTmpDir(false); } catch (...) { ignoreExceptionInDestructor(); } + try { killChild(); } catch (...) { ignoreExceptionInDestructor(); } + try { builder.stopDaemon(); } catch (...) { ignoreExceptionInDestructor(); } +} + + +inline bool DerivationBuilder::needsHashRewrite() +{ +#ifdef __linux__ + return !useChroot; +#else + /* Darwin requires hash rewriting even when sandboxing is enabled. */ + return true; +#endif +} + + +LocalStore & DerivationBuilder::getLocalStore() +{ + auto p = dynamic_cast(&store); + assert(p); + return *p; +} + + +void LocalDerivationGoal::killChild() +{ + if (builder.pid != -1) { + worker.childTerminated(this); + + /* If we're using a build user, then there is a tricky race + condition: if we kill the build user before the child has + done its setuid() to the build user uid, then it won't be + killed, and we'll potentially lock up in pid.wait(). So + also send a conventional kill to the child. */ + ::kill(-builder.pid, SIGKILL); /* ignore the result */ + + builder.killSandbox(true); + + builder.pid.wait(); + } + + DerivationGoal::killChild(); +} + + +void DerivationBuilder::killSandbox(bool getStats) +{ + if (cgroup) { + #ifdef __linux__ + auto stats = destroyCgroup(*cgroup); + if (getStats) { + buildResult.cpuUser = stats.cpuUser; + buildResult.cpuSystem = stats.cpuSystem; + } + #else + unreachable(); + #endif + } + + else if (buildUser) { + auto uid = buildUser->getUID(); + assert(uid != 0); + killUser(uid); + } +} + + +void LocalDerivationGoal::childStarted() +{ + worker.childStarted(shared_from_this(), {builder.builderOut.get()}, true, true); +} + +void LocalDerivationGoal::childTerminated() +{ + worker.childTerminated(this); +} + +void LocalDerivationGoal::noteHashMismatch() +{ + worker.hashMismatch = true; +} + + +void LocalDerivationGoal::noteCheckMismatch() +{ + worker.checkMismatch = true; +} + + +void LocalDerivationGoal::markContentsGood(const StorePath & path) +{ + worker.markContentsGood(path); +} + + +Goal::Co LocalDerivationGoal::tryLocalBuild() +{ + assert(!hook); + + unsigned int curBuilds = worker.getNrLocalBuilds(); + if (curBuilds >= settings.maxBuildJobs) { + outputLocks.unlock(); + co_await waitForBuildSlot(); + co_return tryToBuild(); + } + + if (!builder.prepareBuild()) { + if (!actLock) + actLock = std::make_unique(*logger, lvlWarn, actBuildWaiting, + fmt("waiting for a free build user ID for '%s'", Magenta(worker.store.printStorePath(drvPath)))); + co_await waitForAWhile(); + co_return tryLocalBuild(); + } + + actLock.reset(); + + try { + + /* Okay, we have to build. */ + builder.startBuilder(); + + } catch (BuildError & e) { + outputLocks.unlock(); + builder.buildUser.reset(); + worker.permanentFailure = true; + co_return done(BuildResult::InputRejected, {}, std::move(e)); + } + + started(); + co_await Suspend{}; + + trace("build done"); + + auto res = builder.unprepareBuild(); + // N.B. cannot use `std::visit` with co-routine return + if (auto * ste = std::get_if<0>(&res)) { + outputLocks.unlock(); + co_return done(std::move(ste->first), {}, std::move(ste->second)); + } else if (auto * builtOutputs = std::get_if<1>(&res)) { + /* It is now safe to delete the lock files, since all future + lockers will see that the output paths are valid; they will + not create new lock files with the same names as the old + (unlinked) lock files. */ + outputLocks.setDeletion(true); + outputLocks.unlock(); + co_return done(BuildResult::Built, std::move(*builtOutputs)); + } else { + unreachable(); + } +} + +bool DerivationBuilder::prepareBuild() +{ + /* Cache this */ + derivationType = drv->type(); + + /* Are we doing a chroot build? */ + { + if (settings.sandboxMode == smEnabled) { + if (drvOptions->noChroot) + throw Error("derivation '%s' has '__noChroot' set, " + "but that's not allowed when 'sandbox' is 'true'", store.printStorePath(drvPath)); +#ifdef __APPLE__ + if (drvOptions->additionalSandboxProfile != "") + throw Error("derivation '%s' specifies a sandbox profile, " + "but this is only allowed when 'sandbox' is 'relaxed'", worker.store.printStorePath(drvPath)); +#endif + useChroot = true; + } + else if (settings.sandboxMode == smDisabled) + useChroot = false; + else if (settings.sandboxMode == smRelaxed) + useChroot = derivationType->isSandboxed() && !drvOptions->noChroot; + } + + auto & localStore = getLocalStore(); + if (localStore.storeDir != localStore.realStoreDir.get()) { + #ifdef __linux__ + useChroot = true; + #else + throw Error("building using a diverted store is not supported on this platform"); + #endif + } + + #ifdef __linux__ + if (useChroot) { + if (!mountAndPidNamespacesSupported()) { + if (!settings.sandboxFallback) + throw Error("this system does not support the kernel namespaces that are required for sandboxing; use '--no-sandbox' to disable sandboxing"); + debug("auto-disabling sandboxing because the prerequisite namespaces are not available"); + useChroot = false; + } + } + #endif + + if (useBuildUsers()) { + if (!buildUser) + buildUser = acquireUserLock(drvOptions->useUidRange(*drv) ? 65536 : 1, useChroot); + + if (!buildUser) { + return false; + } + } + + return true; +} + + +std::variant, SingleDrvOutputs> DerivationBuilder::unprepareBuild() +{ + Finally releaseBuildUser([&](){ + /* Release the build user at the end of this function. We don't do + it right away because we don't want another build grabbing this + uid and then messing around with our output. */ + buildUser.reset(); + }); + + sandboxMountNamespace = -1; + sandboxUserNamespace = -1; + + /* Since we got an EOF on the logger pipe, the builder is presumed + to have terminated. In fact, the builder could also have + simply have closed its end of the pipe, so just to be sure, + kill it. */ + int status = pid.kill(); + + debug("builder process for '%s' finished", store.printStorePath(drvPath)); + + buildResult.timesBuilt++; + buildResult.stopTime = time(0); + + /* So the child is gone now. */ + miscMethods.childTerminated(); + + /* Close the read side of the logger pipe. */ + builderOut.close(); + + /* Close the log file. */ + miscMethods.closeLogFile(); + + /* When running under a build user, make sure that all processes + running under that uid are gone. This is to prevent a + malicious user from leaving behind a process that keeps files + open and modifies them after they have been chown'ed to + root. */ + killSandbox(true); + + /* Terminate the recursive Nix daemon. */ + stopDaemon(); + + if (buildResult.cpuUser && buildResult.cpuSystem) { + debug("builder for '%s' terminated with status %d, user CPU %.3fs, system CPU %.3fs", + store.printStorePath(drvPath), + status, + ((double) buildResult.cpuUser->count()) / 1000000, + ((double) buildResult.cpuSystem->count()) / 1000000); + } + + bool diskFull = false; + + try { + + /* Check the exit status. */ + if (!statusOk(status)) { + + diskFull |= cleanupDecideWhetherDiskFull(); + + auto msg = fmt("builder for '%s' %s", + Magenta(store.printStorePath(drvPath)), + statusToString(status)); + + miscMethods.appendLogTailErrorMsg(msg); + + if (diskFull) + msg += "\nnote: build failure may have been caused by lack of free disk space"; + + throw BuildError(msg); + } + + /* Compute the FS closure of the outputs and register them as + being valid. */ + auto builtOutputs = registerOutputs(); + + StorePathSet outputPaths; + for (auto & [_, output] : builtOutputs) + outputPaths.insert(output.outPath); + runPostBuildHook( + store, + *logger, + drvPath, + outputPaths + ); + + /* Delete unused redirected outputs (when doing hash rewriting). */ + for (auto & i : redirectedOutputs) + deletePath(store.Store::toRealPath(i.second)); + + /* Delete the chroot (if we were using one). */ + autoDelChroot.reset(); /* this runs the destructor */ + + deleteTmpDir(true); + + return std::move(builtOutputs); + + } catch (BuildError & e) { + assert(derivationType); + BuildResult::Status st = + dynamic_cast(&e) ? BuildResult::NotDeterministic : + statusOk(status) ? BuildResult::OutputRejected : + !derivationType->isSandboxed() || diskFull ? BuildResult::TransientFailure : + BuildResult::PermanentFailure; + + return std::pair{std::move(st), std::move(e)}; + } +} + + +static void chmod_(const Path & path, mode_t mode) +{ + if (chmod(path.c_str(), mode) == -1) + throw SysError("setting permissions on '%s'", path); +} + + +/* Move/rename path 'src' to 'dst'. Temporarily make 'src' writable if + it's a directory and we're not root (to be able to update the + directory's parent link ".."). */ +static void movePath(const Path & src, const Path & dst) +{ + auto st = lstat(src); + + bool changePerm = (geteuid() && S_ISDIR(st.st_mode) && !(st.st_mode & S_IWUSR)); + + if (changePerm) + chmod_(src, st.st_mode | S_IWUSR); + + std::filesystem::rename(src, dst); + + if (changePerm) + chmod_(dst, st.st_mode); +} + + +extern void replaceValidPath(const Path & storePath, const Path & tmpPath); + + +bool DerivationBuilder::cleanupDecideWhetherDiskFull() +{ + bool diskFull = false; + + /* Heuristically check whether the build failure may have + been caused by a disk full condition. We have no way + of knowing whether the build actually got an ENOSPC. + So instead, check if the disk is (nearly) full now. If + so, we don't mark this build as a permanent failure. */ +#if HAVE_STATVFS + { + auto & localStore = getLocalStore(); + uint64_t required = 8ULL * 1024 * 1024; // FIXME: make configurable + struct statvfs st; + if (statvfs(localStore.realStoreDir.get().c_str(), &st) == 0 && + (uint64_t) st.f_bavail * st.f_bsize < required) + diskFull = true; + if (statvfs(tmpDir.c_str(), &st) == 0 && + (uint64_t) st.f_bavail * st.f_bsize < required) + diskFull = true; + } +#endif + + deleteTmpDir(false); + + /* Move paths out of the chroot for easier debugging of + build failures. */ + if (useChroot && buildMode == bmNormal) + for (auto & [_, status] : initialOutputs) { + if (!status.known) continue; + if (buildMode != bmCheck && status.known->isValid()) continue; + auto p = store.toRealPath(status.known->path); + if (pathExists(chrootRootDir + p)) + std::filesystem::rename((chrootRootDir + p), p); + } + + return diskFull; +} + + +#ifdef __linux__ +static void doBind(const Path & source, const Path & target, bool optional = false) { + debug("bind mounting '%1%' to '%2%'", source, target); + + auto bindMount = [&]() { + 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); + }; + + auto maybeSt = maybeLstat(source); + if (!maybeSt) { + if (optional) + return; + else + throw SysError("getting attributes of path '%1%'", source); + } + auto st = *maybeSt; + + if (S_ISDIR(st.st_mode)) { + createDirs(target); + bindMount(); + } else if (S_ISLNK(st.st_mode)) { + // Symlinks can (apparently) not be bind-mounted, so just copy it + createDirs(dirOf(target)); + copyFile( + std::filesystem::path(source), + std::filesystem::path(target), false); + } else { + createDirs(dirOf(target)); + writeFile(target, ""); + bindMount(); + } +}; +#endif + +/** + * Rethrow the current exception as a subclass of `Error`. + */ +static void rethrowExceptionAsError() +{ + try { + throw; + } catch (Error &) { + throw; + } catch (std::exception & e) { + throw Error(e.what()); + } catch (...) { + throw Error("unknown exception"); + } +} + +/** + * Send the current exception to the parent in the format expected by + * `DerivationBuilder::processSandboxSetupMessages()`. + */ +static void handleChildException(bool sendException) +{ + try { + rethrowExceptionAsError(); + } catch (Error & e) { + if (sendException) { + writeFull(STDERR_FILENO, "\1\n"); + FdSink sink(STDERR_FILENO); + sink << e; + sink.flush(); + } else + std::cerr << e.msg(); + } +} + +void DerivationBuilder::startBuilder() +{ + if ((buildUser && buildUser->getUIDCount() != 1) + #ifdef __linux__ + || settings.useCgroups + #endif + ) + { + #ifdef __linux__ + experimentalFeatureSettings.require(Xp::Cgroups); + + /* If we're running from the daemon, then this will return the + root cgroup of the service. Otherwise, it will return the + current cgroup. */ + auto rootCgroup = getRootCgroup(); + auto cgroupFS = getCgroupFS(); + if (!cgroupFS) + throw Error("cannot determine the cgroups file system"); + auto rootCgroupPath = canonPath(*cgroupFS + "/" + rootCgroup); + if (!pathExists(rootCgroupPath)) + throw Error("expected cgroup directory '%s'", rootCgroupPath); + + static std::atomic counter{0}; + + cgroup = buildUser + ? fmt("%s/nix-build-uid-%d", rootCgroupPath, buildUser->getUID()) + : fmt("%s/nix-build-pid-%d-%d", rootCgroupPath, getpid(), counter++); + + debug("using cgroup '%s'", *cgroup); + + /* When using a build user, record the cgroup we used for that + user so that if we got interrupted previously, we can kill + any left-over cgroup first. */ + if (buildUser) { + auto cgroupsDir = settings.nixStateDir + "/cgroups"; + createDirs(cgroupsDir); + + auto cgroupFile = fmt("%s/%d", cgroupsDir, buildUser->getUID()); + + if (pathExists(cgroupFile)) { + auto prevCgroup = readFile(cgroupFile); + destroyCgroup(prevCgroup); + } + + writeFile(cgroupFile, *cgroup); + } + + #else + throw Error("cgroups are not supported on this platform"); + #endif + } + + /* Make sure that no other processes are executing under the + sandbox uids. This must be done before any chownToBuilder() + calls. */ + killSandbox(false); + + /* Right platform? */ + if (!drvOptions->canBuildLocally(store, *drv)) { + // since aarch64-darwin has Rosetta 2, this user can actually run x86_64-darwin on their hardware - we should tell them to run the command to install Darwin 2 + if (drv->platform == "x86_64-darwin" && settings.thisSystem == "aarch64-darwin") { + throw Error("run `/usr/sbin/softwareupdate --install-rosetta` to enable your %s to run programs for %s", settings.thisSystem, drv->platform); + } else { + throw Error("a '%s' with features {%s} is required to build '%s', but I am a '%s' with features {%s}", + drv->platform, + concatStringsSep(", ", drvOptions->getRequiredSystemFeatures(*drv)), + store.printStorePath(drvPath), + settings.thisSystem, + concatStringsSep(", ", store.systemFeatures)); + } + } + + /* Create a temporary directory where the build will take + place. */ + topTmpDir = createTempDir(settings.buildDir.get().value_or(""), "nix-build-" + std::string(drvPath.name()), false, false, 0700); +#ifdef __APPLE__ + if (false) { +#else + if (useChroot) { +#endif + /* If sandboxing is enabled, put the actual TMPDIR underneath + an inaccessible root-owned directory, to prevent outside + access. + + On macOS, we don't use an actual chroot, so this isn't + possible. Any mitigation along these lines would have to be + done directly in the sandbox profile. */ + tmpDir = topTmpDir + "/build"; + createDir(tmpDir, 0700); + } else { + tmpDir = topTmpDir; + } + chownToBuilder(tmpDir); + + for (auto & [outputName, status] : initialOutputs) { + /* Set scratch path we'll actually use during the build. + + If we're not doing a chroot build, but we have some valid + output paths. Since we can't just overwrite or delete + them, we have to do hash rewriting: i.e. in the + environment/arguments passed to the build, we replace the + hashes of the valid outputs with unique dummy strings; + after the build, we discard the redirected outputs + corresponding to the valid outputs, and rewrite the + contents of the new outputs to replace the dummy strings + with the actual hashes. */ + auto scratchPath = + !status.known + ? makeFallbackPath(outputName) + : !needsHashRewrite() + /* Can always use original path in sandbox */ + ? status.known->path + : !status.known->isPresent() + /* If path doesn't yet exist can just use it */ + ? status.known->path + : buildMode != bmRepair && !status.known->isValid() + /* If we aren't repairing we'll delete a corrupted path, so we + can use original path */ + ? status.known->path + : /* If we are repairing or the path is totally valid, we'll need + to use a temporary path */ + makeFallbackPath(status.known->path); + scratchOutputs.insert_or_assign(outputName, scratchPath); + + /* Substitute output placeholders with the scratch output paths. + We'll use during the build. */ + inputRewrites[hashPlaceholder(outputName)] = store.printStorePath(scratchPath); + + /* Additional tasks if we know the final path a priori. */ + if (!status.known) continue; + auto fixedFinalPath = status.known->path; + + /* Additional tasks if the final and scratch are both known and + differ. */ + if (fixedFinalPath == scratchPath) continue; + + /* Ensure scratch path is ours to use. */ + deletePath(store.printStorePath(scratchPath)); + + /* Rewrite and unrewrite paths */ + { + std::string h1 { fixedFinalPath.hashPart() }; + std::string h2 { scratchPath.hashPart() }; + inputRewrites[h1] = h2; + } + + redirectedOutputs.insert_or_assign(std::move(fixedFinalPath), std::move(scratchPath)); + } + + /* Construct the environment passed to the builder. */ + initEnv(); + + writeStructuredAttrs(); + + /* Handle exportReferencesGraph(), if set. */ + if (!parsedDrv->hasStructuredAttrs()) { + /* The `exportReferencesGraph' feature allows the references graph + to be passed to a builder. This attribute should be a list of + pairs [name1 path1 name2 path2 ...]. The references graph of + each `pathN' will be stored in a text file `nameN' in the + temporary build directory. The text files have the format used + by `nix-store --register-validity'. However, the deriver + fields are left empty. */ + auto s = getOr(drv->env, "exportReferencesGraph", ""); + Strings ss = tokenizeString(s); + if (ss.size() % 2 != 0) + throw BuildError("odd number of tokens in 'exportReferencesGraph': '%1%'", s); + for (Strings::iterator i = ss.begin(); i != ss.end(); ) { + auto fileName = *i++; + static std::regex regex("[A-Za-z_][A-Za-z0-9_.-]*"); + if (!std::regex_match(fileName, regex)) + throw Error("invalid file name '%s' in 'exportReferencesGraph'", fileName); + + auto storePathS = *i++; + if (!store.isInStore(storePathS)) + throw BuildError("'exportReferencesGraph' contains a non-store path '%1%'", storePathS); + auto storePath = store.toStorePath(storePathS).first; + + /* Write closure info to . */ + writeFile(tmpDir + "/" + fileName, + store.makeValidityRegistration( + store.exportReferences({storePath}, inputPaths), false, false)); + } + } + + if (useChroot) { + + /* Allow a user-configurable set of directories from the + host file system. */ + pathsInChroot.clear(); + + for (auto i : settings.sandboxPaths.get()) { + if (i.empty()) continue; + bool optional = false; + if (i[i.size() - 1] == '?') { + optional = true; + i.pop_back(); + } + size_t p = i.find('='); + if (p == std::string::npos) + pathsInChroot[i] = {i, optional}; + else + pathsInChroot[i.substr(0, p)] = {i.substr(p + 1), optional}; + } + if (hasPrefix(store.storeDir, tmpDirInSandbox)) + { + throw Error("`sandbox-build-dir` must not contain the storeDir"); + } + pathsInChroot[tmpDirInSandbox] = tmpDir; + + /* Add the closure of store paths to the chroot. */ + StorePathSet closure; + for (auto & i : pathsInChroot) + try { + if (store.isInStore(i.second.source)) + store.computeFSClosure(store.toStorePath(i.second.source).first, closure); + } catch (InvalidPath & e) { + } catch (Error & e) { + e.addTrace({}, "while processing 'sandbox-paths'"); + throw; + } + for (auto & i : closure) { + auto p = store.printStorePath(i); + pathsInChroot.insert_or_assign(p, p); + } + + PathSet allowedPaths = settings.allowedImpureHostPrefixes; + + /* This works like the above, except on a per-derivation level */ + auto impurePaths = drvOptions->impureHostDeps; + + for (auto & i : impurePaths) { + bool found = false; + /* Note: we're not resolving symlinks here to prevent + giving a non-root user info about inaccessible + files. */ + Path canonI = canonPath(i); + /* If only we had a trie to do this more efficiently :) luckily, these are generally going to be pretty small */ + for (auto & a : allowedPaths) { + Path canonA = canonPath(a); + if (isDirOrInDir(canonI, canonA)) { + found = true; + break; + } + } + if (!found) + throw Error("derivation '%s' requested impure path '%s', but it was not in allowed-impure-host-deps", + store.printStorePath(drvPath), i); + + /* Allow files in drvOptions->impureHostDeps to be missing; e.g. + macOS 11+ has no /usr/lib/libSystem*.dylib */ + pathsInChroot[i] = {i, true}; + } + +#ifdef __linux__ + /* Create a temporary directory in which we set up the chroot + environment using bind-mounts. We put it in the Nix store + so that the build outputs can be moved efficiently from the + chroot to their final location. */ + auto chrootParentDir = store.Store::toRealPath(drvPath) + ".chroot"; + deletePath(chrootParentDir); + + /* Clean up the chroot directory automatically. */ + autoDelChroot = std::make_shared(chrootParentDir); + + printMsg(lvlChatty, "setting up chroot environment in '%1%'", chrootParentDir); + + if (mkdir(chrootParentDir.c_str(), 0700) == -1) + throw SysError("cannot create '%s'", chrootRootDir); + + chrootRootDir = chrootParentDir + "/root"; + + if (mkdir(chrootRootDir.c_str(), buildUser && buildUser->getUIDCount() != 1 ? 0755 : 0750) == -1) + throw SysError("cannot create '%1%'", chrootRootDir); + + if (buildUser && chown(chrootRootDir.c_str(), buildUser->getUIDCount() != 1 ? buildUser->getUID() : 0, buildUser->getGID()) == -1) + throw SysError("cannot change ownership of '%1%'", chrootRootDir); + + /* Create a writable /tmp in the chroot. Many builders need + this. (Of course they should really respect $TMPDIR + instead.) */ + Path chrootTmpDir = chrootRootDir + "/tmp"; + createDirs(chrootTmpDir); + chmod_(chrootTmpDir, 01777); + + /* Create a /etc/passwd with entries for the build user and the + nobody account. The latter is kind of a hack to support + Samba-in-QEMU. */ + createDirs(chrootRootDir + "/etc"); + if (drvOptions->useUidRange(*drv)) + chownToBuilder(chrootRootDir + "/etc"); + + if (drvOptions->useUidRange(*drv) && (!buildUser || buildUser->getUIDCount() < 65536)) + throw Error("feature 'uid-range' requires the setting '%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"). */ + writeFile(chrootRootDir + "/etc/group", + fmt("root:x:0:\n" + "nixbld:!:%1%:\n" + "nogroup:x:65534:\n", sandboxGid())); + + /* Create /etc/hosts with localhost entry. */ + if (derivationType->isSandboxed()) + writeFile(chrootRootDir + "/etc/hosts", "127.0.0.1 localhost\n::1 localhost\n"); + + /* Make the closure of the inputs available in the chroot, + rather than the whole Nix store. This prevents any access + to undeclared dependencies. Directories are bind-mounted, + while other inputs are hard-linked (since only directories + can be bind-mounted). !!! As an extra security + precaution, make the fake Nix store only writable by the + build user. */ + Path chrootStoreDir = chrootRootDir + store.storeDir; + createDirs(chrootStoreDir); + chmod_(chrootStoreDir, 01775); + + if (buildUser && chown(chrootStoreDir.c_str(), 0, buildUser->getGID()) == -1) + throw SysError("cannot change ownership of '%1%'", chrootStoreDir); + + for (auto & i : inputPaths) { + auto p = store.printStorePath(i); + Path r = store.toRealPath(p); + pathsInChroot.insert_or_assign(p, r); + } + + /* If we're repairing, checking or rebuilding part of a + multiple-outputs derivation, it's possible that we're + rebuilding a path that is in settings.sandbox-paths + (typically the dependencies of /bin/sh). Throw them + out. */ + for (auto & i : drv->outputsAndOptPaths(store)) { + /* If the name isn't known a priori (i.e. floating + content-addressing derivation), the temporary location we use + should be fresh. Freshness means it is impossible that the path + is already in the sandbox, so we don't need to worry about + removing it. */ + if (i.second.second) + pathsInChroot.erase(store.printStorePath(*i.second.second)); + } + + if (cgroup) { + if (mkdir(cgroup->c_str(), 0755) != 0) + throw SysError("creating cgroup '%s'", *cgroup); + chownToBuilder(*cgroup); + chownToBuilder(*cgroup + "/cgroup.procs"); + chownToBuilder(*cgroup + "/cgroup.threads"); + //chownToBuilder(*cgroup + "/cgroup.subtree_control"); + } + +#else + if (drvOptions->useUidRange(*drv)) + throw Error("feature 'uid-range' is not supported on this platform"); + #ifdef __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 (drvOptions->useUidRange(*drv)) + throw Error("feature 'uid-range' is only supported in sandboxed builds"); + } + + if (needsHashRewrite() && pathExists(homeDir)) + throw Error("home directory '%1%' exists; please remove it to assure purity of builds without sandboxing", homeDir); + + if (useChroot && settings.preBuildHook != "" && dynamic_cast(drv.get())) { + printMsg(lvlChatty, "executing pre-build hook '%1%'", settings.preBuildHook); + auto args = useChroot ? Strings({store.printStorePath(drvPath), chrootRootDir}) : + Strings({ store.printStorePath(drvPath) }); + enum BuildHookState { + stBegin, + stExtraChrootDirs + }; + auto state = stBegin; + auto lines = runProgram(settings.preBuildHook, false, args); + auto lastPos = std::string::size_type{0}; + for (auto nlPos = lines.find('\n'); nlPos != std::string::npos; + nlPos = lines.find('\n', lastPos)) + { + auto line = lines.substr(lastPos, nlPos - lastPos); + lastPos = nlPos + 1; + if (state == stBegin) { + if (line == "extra-sandbox-paths" || line == "extra-chroot-dirs") { + state = stExtraChrootDirs; + } else { + throw Error("unknown pre-build hook command '%1%'", line); + } + } else if (state == stExtraChrootDirs) { + if (line == "") { + state = stBegin; + } else { + auto p = line.find('='); + if (p == std::string::npos) + pathsInChroot[line] = line; + else + pathsInChroot[line.substr(0, p)] = line.substr(p + 1); + } + } + } + } + + /* Fire up a Nix daemon to process recursive Nix calls from the + builder. */ + if (drvOptions->getRequiredSystemFeatures(*drv).count("recursive-nix")) + startDaemon(); + + /* Run the builder. */ + printMsg(lvlChatty, "executing builder '%1%'", drv->builder); + printMsg(lvlChatty, "using builder args '%1%'", concatStringsSep(" ", drv->args)); + for (auto & i : drv->env) + printMsg(lvlVomit, "setting builder env variable '%1%'='%2%'", i.first, i.second); + + /* Create the log file. */ + [[maybe_unused]] Path logFile = miscMethods.openLogFile(); + + /* Create a pseudoterminal to get the output of the builder. */ + builderOut = posix_openpt(O_RDWR | O_NOCTTY); + if (!builderOut) + throw SysError("opening pseudoterminal master"); + + // FIXME: not thread-safe, use ptsname_r + std::string slaveName = ptsname(builderOut.get()); + + if (buildUser) { + if (chmod(slaveName.c_str(), 0600)) + throw SysError("changing mode of pseudoterminal slave"); + + if (chown(slaveName.c_str(), buildUser->getUID(), 0)) + throw SysError("changing owner of pseudoterminal slave"); + } +#ifdef __APPLE__ + else { + if (grantpt(builderOut.get())) + throw SysError("granting access to pseudoterminal slave"); + } +#endif + + if (unlockpt(builderOut.get())) + throw SysError("unlocking pseudoterminal"); + + /* Open the slave side of the pseudoterminal and use it as stderr. */ + auto openSlave = [&]() + { + AutoCloseFD builderOut = open(slaveName.c_str(), O_RDWR | O_NOCTTY); + if (!builderOut) + throw SysError("opening pseudoterminal slave"); + + // Put the pt into raw mode to prevent \n -> \r\n translation. + struct termios term; + if (tcgetattr(builderOut.get(), &term)) + throw SysError("getting pseudoterminal attributes"); + + cfmakeraw(&term); + + if (tcsetattr(builderOut.get(), TCSANOW, &term)) + throw SysError("putting pseudoterminal into raw mode"); + + if (dup2(builderOut.get(), STDERR_FILENO) == -1) + throw SysError("cannot pipe standard error into log file"); + }; + + buildResult.startTime = time(0); + + /* Fork a child to build the package. */ + +#ifdef __linux__ + if (useChroot) { + /* Set up private namespaces for the build: + + - The PID namespace causes the build to start as PID 1. + Processes outside of the chroot are not visible to those + on the inside, but processes inside the chroot are + visible from the outside (though with different PIDs). + + - The private mount namespace ensures that all the bind + mounts we do will only show up in this process and its + children, and will disappear automatically when we're + done. + + - The private network namespace ensures that the builder + cannot talk to the outside world (or vice versa). It + only has a private loopback interface. (Fixed-output + derivations are not run in a private network namespace + to allow functions like fetchurl to work.) + + - The IPC namespace prevents the builder from communicating + with outside processes using SysV IPC mechanisms (shared + memory, message queues, semaphores). It also ensures + that all IPC objects are destroyed when the builder + exits. + + - The UTS namespace ensures that builders see a hostname of + localhost rather than the actual hostname. + + We use a helper process to do the clone() to work around + clone() being broken in multi-threaded programs due to + at-fork handlers not being run. Note that we use + CLONE_PARENT to ensure that the real builder is parented to + us. + */ + + userNamespaceSync.create(); + + usingUserNamespace = userNamespacesSupported(); + + Pipe sendPid; + sendPid.create(); + + Pid helper = startProcess([&]() { + sendPid.readSide.close(); + + /* We need to open the slave early, before + CLONE_NEWUSER. Otherwise we get EPERM when running as + root. */ + openSlave(); + + try { + /* Drop additional groups here because we can't do it + after we've created the new user namespace. */ + if (setgroups(0, 0) == -1) { + if (errno != EPERM) + throw SysError("setgroups failed"); + if (settings.requireDropSupplementaryGroups) + throw Error("setgroups failed. Set the require-drop-supplementary-groups option to false to skip this step."); + } + + ProcessOptions options; + options.cloneFlags = CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWIPC | CLONE_NEWUTS | CLONE_PARENT | SIGCHLD; + if (derivationType->isSandboxed()) + options.cloneFlags |= CLONE_NEWNET; + if (usingUserNamespace) + options.cloneFlags |= CLONE_NEWUSER; + + pid_t child = startProcess([&]() { runChild(); }, options); + + writeFull(sendPid.writeSide.get(), fmt("%d\n", child)); + _exit(0); + } catch (...) { + handleChildException(true); + _exit(1); + } + }); + + sendPid.writeSide.close(); + + if (helper.wait() != 0) { + processSandboxSetupMessages(); + // Only reached if the child process didn't send an exception. + throw Error("unable to start build process"); + } + + userNamespaceSync.readSide = -1; + + /* Close the write side to prevent runChild() from hanging + reading from this. */ + Finally cleanup([&]() { + userNamespaceSync.writeSide = -1; + }); + + auto ss = tokenizeString>(readLine(sendPid.readSide.get())); + assert(ss.size() == 1); + pid = string2Int(ss[0]).value(); + + if (usingUserNamespace) { + /* Set the UID/GID mapping of the builder's user namespace + such that the sandbox user maps to the build user, or to + the calling user (if build users are disabled). */ + uid_t hostUid = buildUser ? buildUser->getUID() : getuid(); + uid_t hostGid = buildUser ? buildUser->getGID() : getgid(); + uid_t nrIds = buildUser ? buildUser->getUIDCount() : 1; + + writeFile("/proc/" + std::to_string(pid) + "/uid_map", + fmt("%d %d %d", sandboxUid(), hostUid, nrIds)); + + if (!buildUser || buildUser->getUIDCount() == 1) + writeFile("/proc/" + std::to_string(pid) + "/setgroups", "deny"); + + writeFile("/proc/" + std::to_string(pid) + "/gid_map", + fmt("%d %d %d", sandboxGid(), hostGid, nrIds)); + } else { + debug("note: not using a user namespace"); + if (!buildUser) + throw Error("cannot perform a sandboxed build because user namespaces are not enabled; check /proc/sys/user/max_user_namespaces"); + } + + /* Now that we now the sandbox uid, we can write + /etc/passwd. */ + writeFile(chrootRootDir + "/etc/passwd", fmt( + "root:x:0:0:Nix build user:%3%:/noshell\n" + "nixbld:x:%1%:%2%:Nix build user:%3%:/noshell\n" + "nobody:x:65534:65534:Nobody:/:/noshell\n", + sandboxUid(), sandboxGid(), settings.sandboxBuildDir)); + + /* Save the mount- and user 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"); + + if (usingUserNamespace) { + sandboxUserNamespace = open(fmt("/proc/%d/ns/user", (pid_t) pid).c_str(), O_RDONLY); + if (sandboxUserNamespace.get() == -1) + throw SysError("getting sandbox user namespace"); + } + + /* Move the child into its own cgroup. */ + if (cgroup) + writeFile(*cgroup + "/cgroup.procs", fmt("%d", (pid_t) pid)); + + /* Signal the builder that we've updated its user namespace. */ + writeFull(userNamespaceSync.writeSide.get(), "1"); + + } else +#endif + { + pid = startProcess([&]() { + openSlave(); + runChild(); + }); + } + + /* parent */ + pid.setSeparatePG(true); + miscMethods.childStarted(); + + processSandboxSetupMessages(); +} + + +void DerivationBuilder::processSandboxSetupMessages() +{ + std::vector msgs; + while (true) { + std::string msg = [&]() { + try { + return readLine(builderOut.get()); + } catch (Error & e) { + auto status = pid.wait(); + e.addTrace({}, "while waiting for the build environment for '%s' to initialize (%s, previous messages: %s)", + store.printStorePath(drvPath), + statusToString(status), + concatStringsSep("|", msgs)); + throw; + } + }(); + if (msg.substr(0, 1) == "\2") break; + if (msg.substr(0, 1) == "\1") { + FdSource source(builderOut.get()); + auto ex = readError(source); + ex.addTrace({}, "while setting up the build environment"); + throw ex; + } + debug("sandbox setup: " + msg); + msgs.push_back(std::move(msg)); + } +} + + +void DerivationBuilder::initTmpDir() +{ + /* In a sandbox, for determinism, always use the same temporary + directory. */ +#ifdef __linux__ + tmpDirInSandbox = useChroot ? settings.sandboxBuildDir : tmpDir; +#else + tmpDirInSandbox = tmpDir; +#endif + + /* In non-structured mode, set all bindings either directory in the + environment or via a file, as specified by + `DerivationOptions::passAsFile`. */ + if (!parsedDrv->hasStructuredAttrs()) { + for (auto & i : drv->env) { + if (drvOptions->passAsFile.find(i.first) == drvOptions->passAsFile.end()) { + env[i.first] = i.second; + } else { + auto hash = hashString(HashAlgorithm::SHA256, i.first); + std::string fn = ".attr-" + hash.to_string(HashFormat::Nix32, false); + Path p = tmpDir + "/" + fn; + writeFile(p, rewriteStrings(i.second, inputRewrites)); + chownToBuilder(p); + env[i.first + "Path"] = tmpDirInSandbox + "/" + fn; + } + } + + } + + /* For convenience, set an environment pointing to the top build + directory. */ + env["NIX_BUILD_TOP"] = tmpDirInSandbox; + + /* Also set TMPDIR and variants to point to this directory. */ + env["TMPDIR"] = env["TEMPDIR"] = env["TMP"] = env["TEMP"] = tmpDirInSandbox; + + /* Explicitly set PWD to prevent problems with chroot builds. In + particular, dietlibc cannot figure out the cwd because the + inode of the current directory doesn't appear in .. (because + getdents returns the inode of the mount point). */ + env["PWD"] = tmpDirInSandbox; +} + + +void DerivationBuilder::initEnv() +{ + env.clear(); + + /* Most shells initialise PATH to some default (/bin:/usr/bin:...) when + PATH is not set. We don't want this, so we fill it in with some dummy + value. */ + env["PATH"] = "/path-not-set"; + + /* Set HOME to a non-existing path to prevent certain programs from using + /etc/passwd (or NIS, or whatever) to locate the home directory (for + example, wget looks for ~/.wgetrc). I.e., these tools use /etc/passwd + if HOME is not set, but they will just assume that the settings file + they are looking for does not exist if HOME is set but points to some + non-existing path. */ + env["HOME"] = homeDir; + + /* Tell the builder where the Nix store is. Usually they + shouldn't care, but this is useful for purity checking (e.g., + the compiler or linker might only want to accept paths to files + in the store or in the build directory). */ + env["NIX_STORE"] = store.storeDir; + + /* The maximum number of cores to utilize for parallel building. */ + env["NIX_BUILD_CORES"] = fmt("%d", settings.buildCores); + + initTmpDir(); + + /* Compatibility hack with Nix <= 0.7: if this is a fixed-output + derivation, tell the builder, so that for instance `fetchurl' + can skip checking the output. On older Nixes, this environment + variable won't be set, so `fetchurl' will do the check. */ + if (derivationType->isFixed()) env["NIX_OUTPUT_CHECKED"] = "1"; + + /* *Only* if this is a fixed-output derivation, propagate the + values of the environment variables specified in the + `impureEnvVars' attribute to the builder. This allows for + instance environment variables for proxy configuration such as + `http_proxy' to be easily passed to downloaders like + `fetchurl'. Passing such environment variables from the caller + to the builder is generally impure, but the output of + fixed-output derivations is by definition pure (since we + already know the cryptographic hash of the output). */ + if (!derivationType->isSandboxed()) { + auto & impureEnv = settings.impureEnv.get(); + if (!impureEnv.empty()) + experimentalFeatureSettings.require(Xp::ConfigurableImpureEnv); + + for (auto & i : drvOptions->impureEnvVars){ + auto envVar = impureEnv.find(i); + if (envVar != impureEnv.end()) { + env[i] = envVar->second; + } else { + env[i] = getEnv(i).value_or(""); + } + } + } + + /* Currently structured log messages piggyback on stderr, but we + may change that in the future. So tell the builder which file + descriptor to use for that. */ + env["NIX_LOG_FD"] = "2"; + + /* Trigger colored output in various tools. */ + env["TERM"] = "xterm-256color"; +} + + +void DerivationBuilder::writeStructuredAttrs() +{ + if (auto structAttrsJson = parsedDrv->prepareStructuredAttrs(store, inputPaths)) { + auto json = structAttrsJson.value(); + nlohmann::json rewritten; + for (auto & [i, v] : json["outputs"].get()) { + /* The placeholder must have a rewrite, so we use it to cover both the + cases where we know or don't know the output path ahead of time. */ + rewritten[i] = rewriteStrings((std::string) v, inputRewrites); + } + + json["outputs"] = rewritten; + + auto jsonSh = writeStructuredAttrsShell(json); + + writeFile(tmpDir + "/.attrs.sh", rewriteStrings(jsonSh, inputRewrites)); + chownToBuilder(tmpDir + "/.attrs.sh"); + env["NIX_ATTRS_SH_FILE"] = tmpDirInSandbox + "/.attrs.sh"; + writeFile(tmpDir + "/.attrs.json", rewriteStrings(json.dump(), inputRewrites)); + chownToBuilder(tmpDir + "/.attrs.json"); + env["NIX_ATTRS_JSON_FILE"] = tmpDirInSandbox + "/.attrs.json"; + } +} + + +void DerivationBuilder::startDaemon() +{ + experimentalFeatureSettings.require(Xp::RecursiveNix); + + Store::Params params; + params["path-info-cache-size"] = "0"; + params["store"] = store.storeDir; + if (auto & optRoot = getLocalStore().rootDir.get()) + params["root"] = *optRoot; + params["state"] = "/no-such-path"; + params["log"] = "/no-such-path"; + auto store = makeRestrictedStore(params, + ref(std::dynamic_pointer_cast(this->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 || errno == EAGAIN) continue; + if (errno == EINVAL || errno == ECONNABORTED) break; + throw SysError("accepting connection"); + } + + unix::closeOnExec(remote.get()); + + debug("received daemon connection"); + + auto workerThread = std::thread([store, remote{std::move(remote)}]() { + try { + daemon::processConnection( + store, + FdSource(remote.get()), + FdSink(remote.get()), + NotTrusted, daemon::Recursive); + debug("terminated daemon connection"); + } catch (const Interrupted &) { + debug("interrupted daemon connection"); + } catch (SystemError &) { + ignoreExceptionExceptInterrupt(); + } + }); + + daemonWorkerThreads.push_back(std::move(workerThread)); + } + + debug("daemon shutting down"); + }); +} + + +void DerivationBuilder::stopDaemon() +{ + if (daemonSocket && shutdown(daemonSocket.get(), SHUT_RDWR) == -1) { + // According to the POSIX standard, the 'shutdown' function should + // return an ENOTCONN error when attempting to shut down a socket that + // hasn't been connected yet. This situation occurs when the 'accept' + // function is called on a socket without any accepted connections, + // leaving the socket unconnected. While Linux doesn't seem to produce + // an error for sockets that have only been accepted, more + // POSIX-compliant operating systems like OpenBSD, macOS, and others do + // return the ENOTCONN error. Therefore, we handle this error here to + // avoid raising an exception for compliant behaviour. + if (errno == ENOTCONN) { + daemonSocket.close(); + } else { + throw SysError("shutting down daemon socket"); + } + } + + if (daemonThread.joinable()) + daemonThread.join(); + + // FIXME: should prune worker threads more quickly. + // FIXME: shutdown the client socket to speed up worker termination. + for (auto & thread : daemonWorkerThreads) + thread.join(); + daemonWorkerThreads.clear(); + + // release the socket. + daemonSocket.close(); +} + + +void DerivationBuilder::addDependency(const StorePath & 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", store.printStorePath(path)); + + #ifdef __linux__ + + Path source = store.Store::toRealPath(path); + Path target = chrootRootDir + store.printStorePath(path); + + 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", store.printStorePath(path)); + } + + /* 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 (usingUserNamespace && (setns(sandboxUserNamespace.get(), 0) == -1)) + throw SysError("entering sandbox user namespace"); + + if (setns(sandboxMountNamespace.get(), 0) == -1) + throw SysError("entering sandbox mount namespace"); + + doBind(source, target); + + _exit(0); + })); + + int status = child.wait(); + if (status != 0) + throw Error("could not add path '%s' to sandbox", store.printStorePath(path)); + + #else + throw Error("don't know how to make path '%s' (produced by a recursive Nix call) appear in the sandbox", + worker.store.printStorePath(path)); + #endif + + } +} + +void DerivationBuilder::chownToBuilder(const Path & path) +{ + if (!buildUser) return; + if (chown(path.c_str(), buildUser->getUID(), buildUser->getGID()) == -1) + throw SysError("cannot change ownership of '%1%'", path); +} + + +void setupSeccomp() +{ +#ifdef __linux__ + if (!settings.filterSyscalls) return; +#if HAVE_SECCOMP + scmp_filter_ctx ctx; + + if (!(ctx = seccomp_init(SCMP_ACT_ALLOW))) + throw SysError("unable to initialize seccomp mode 2"); + + Finally cleanup([&]() { + seccomp_release(ctx); + }); + + constexpr std::string_view nativeSystem = NIX_LOCAL_SYSTEM; + + if (nativeSystem == "x86_64-linux" && + seccomp_arch_add(ctx, SCMP_ARCH_X86) != 0) + throw SysError("unable to add 32-bit seccomp architecture"); + + if (nativeSystem == "x86_64-linux" && + seccomp_arch_add(ctx, SCMP_ARCH_X32) != 0) + throw SysError("unable to add X32 seccomp architecture"); + + if (nativeSystem == "aarch64-linux" && + seccomp_arch_add(ctx, SCMP_ARCH_ARM) != 0) + printError("unable to add ARM seccomp architecture; this may result in spurious build failures if running 32-bit ARM processes"); + + if (nativeSystem == "mips64-linux" && + seccomp_arch_add(ctx, SCMP_ARCH_MIPS) != 0) + printError("unable to add mips seccomp architecture"); + + if (nativeSystem == "mips64-linux" && + seccomp_arch_add(ctx, SCMP_ARCH_MIPS64N32) != 0) + printError("unable to add mips64-*abin32 seccomp architecture"); + + if (nativeSystem == "mips64el-linux" && + seccomp_arch_add(ctx, SCMP_ARCH_MIPSEL) != 0) + printError("unable to add mipsel seccomp architecture"); + + if (nativeSystem == "mips64el-linux" && + seccomp_arch_add(ctx, SCMP_ARCH_MIPSEL64N32) != 0) + printError("unable to add mips64el-*abin32 seccomp architecture"); + + /* Prevent builders from creating setuid/setgid binaries. */ + for (int perm : { S_ISUID, S_ISGID }) { + if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(chmod), 1, + SCMP_A1(SCMP_CMP_MASKED_EQ, (scmp_datum_t) perm, (scmp_datum_t) perm)) != 0) + throw SysError("unable to add seccomp rule"); + + if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(fchmod), 1, + SCMP_A1(SCMP_CMP_MASKED_EQ, (scmp_datum_t) perm, (scmp_datum_t) perm)) != 0) + throw SysError("unable to add seccomp rule"); + + if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(fchmodat), 1, + SCMP_A2(SCMP_CMP_MASKED_EQ, (scmp_datum_t) perm, (scmp_datum_t) perm)) != 0) + throw SysError("unable to add seccomp rule"); + + if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), NIX_SYSCALL_FCHMODAT2, 1, + SCMP_A2(SCMP_CMP_MASKED_EQ, (scmp_datum_t) perm, (scmp_datum_t) perm)) != 0) + throw SysError("unable to add seccomp rule"); + } + + /* Prevent builders from using EAs or ACLs. Not all filesystems + support these, and they're not allowed in the Nix store because + they're not representable in the NAR serialisation. */ + if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(getxattr), 0) != 0 || + seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(lgetxattr), 0) != 0 || + seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(fgetxattr), 0) != 0 || + seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(setxattr), 0) != 0 || + seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(lsetxattr), 0) != 0 || + seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(fsetxattr), 0) != 0) + throw SysError("unable to add seccomp rule"); + + if (seccomp_attr_set(ctx, SCMP_FLTATR_CTL_NNP, settings.allowNewPrivileges ? 0 : 1) != 0) + throw SysError("unable to set 'no new privileges' seccomp attribute"); + + if (seccomp_load(ctx) != 0) + throw SysError("unable to load seccomp BPF program"); +#else + throw Error( + "seccomp is not supported on this platform; " + "you can bypass this error by setting the option 'filter-syscalls' to false, but note that untrusted builds can then create setuid binaries!"); +#endif +#endif +} + + +void DerivationBuilder::runChild() +{ + /* Warning: in the child we should absolutely not make any SQLite + calls! */ + + bool sendException = true; + + try { /* child */ + + commonChildInit(); + + try { + setupSeccomp(); + } catch (...) { + if (buildUser) throw; + } + + bool setUser = true; + + /* Make the contents of netrc and the CA certificate bundle + available to builtin:fetchurl (which may run under a + different uid and/or in a sandbox). */ + std::string netrcData; + std::string caFileData; + if (drv->isBuiltin() && drv->builder == "builtin:fetchurl") { + try { + netrcData = readFile(settings.netrcFile); + } catch (SystemError &) { } + + try { + caFileData = readFile(settings.caFile); + } catch (SystemError &) { } + } + +#ifdef __linux__ + if (useChroot) { + + userNamespaceSync.writeSide = -1; + + if (drainFD(userNamespaceSync.readSide.get()) != "1") + throw Error("user namespace initialisation failed"); + + userNamespaceSync.readSide = -1; + + if (derivationType->isSandboxed()) { + + /* Initialise the loopback interface. */ + AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP)); + if (!fd) throw SysError("cannot open IP socket"); + + struct ifreq ifr; + strcpy(ifr.ifr_name, "lo"); + ifr.ifr_flags = IFF_UP | IFF_LOOPBACK | IFF_RUNNING; + if (ioctl(fd.get(), SIOCSIFFLAGS, &ifr) == -1) + throw SysError("cannot set loopback interface flags"); + } + + /* Set the hostname etc. to fixed values. */ + char hostname[] = "localhost"; + if (sethostname(hostname, sizeof(hostname)) == -1) + throw SysError("cannot set host name"); + char domainname[] = "(none)"; // kernel default + if (setdomainname(domainname, sizeof(domainname)) == -1) + throw SysError("cannot set domain name"); + + /* Make all filesystems private. This is necessary + because subtrees may have been mounted as "shared" + (MS_SHARED). (Systemd does this, for instance.) Even + though we have a private mount namespace, mounting + filesystems on top of a shared subtree still propagates + outside of the namespace. Making a subtree private is + local to the namespace, though, so setting MS_PRIVATE + does not affect the outside world. */ + if (mount(0, "/", 0, MS_PRIVATE | MS_REC, 0) == -1) + throw SysError("unable to make '/' private"); + + /* Bind-mount chroot directory to itself, to treat it as a + different filesystem from /, as needed for pivot_root. */ + if (mount(chrootRootDir.c_str(), chrootRootDir.c_str(), 0, MS_BIND, 0) == -1) + throw SysError("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 + 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 + bind-mount the host /dev. */ + Strings ss; + if (pathsInChroot.find("/dev") == pathsInChroot.end()) { + createDirs(chrootRootDir + "/dev/shm"); + createDirs(chrootRootDir + "/dev/pts"); + ss.push_back("/dev/full"); + if (store.systemFeatures.get().count("kvm") && pathExists("/dev/kvm")) + ss.push_back("/dev/kvm"); + ss.push_back("/dev/null"); + ss.push_back("/dev/random"); + ss.push_back("/dev/tty"); + ss.push_back("/dev/urandom"); + ss.push_back("/dev/zero"); + createSymlink("/proc/self/fd", chrootRootDir + "/dev/fd"); + createSymlink("/proc/self/fd/0", chrootRootDir + "/dev/stdin"); + createSymlink("/proc/self/fd/1", chrootRootDir + "/dev/stdout"); + createSymlink("/proc/self/fd/2", chrootRootDir + "/dev/stderr"); + } + + /* Fixed-output derivations typically need to access the + network, so give them access to /etc/resolv.conf and so + on. */ + if (!derivationType->isSandboxed()) { + // Only use nss functions to resolve hosts and + // services. Don’t use it for anything else that may + // be configured for this system. This limits the + // potential impurities introduced in fixed-outputs. + writeFile(chrootRootDir + "/etc/nsswitch.conf", "hosts: files dns\nservices: files\n"); + + /* N.B. it is realistic that these paths might not exist. It + happens when testing Nix building fixed-output derivations + within a pure derivation. */ + for (auto & path : { "/etc/resolv.conf", "/etc/services", "/etc/hosts" }) + if (pathExists(path)) + ss.push_back(path); + + if (settings.caFile != "" && pathExists(settings.caFile)) { + Path caFile = settings.caFile; + pathsInChroot.try_emplace("/etc/ssl/certs/ca-certificates.crt", canonPath(caFile, true), true); + } + } + + for (auto & i : ss) { + // For backwards-compatibiliy, resolve all the symlinks in the + // chroot paths + auto canonicalPath = canonPath(i, true); + pathsInChroot.emplace(i, canonicalPath); + } + + /* Bind-mount all the directories from the "host" + filesystem that we want in the chroot + environment. */ + for (auto & i : pathsInChroot) { + if (i.second.source == "/proc") continue; // backwards compatibility + + #if HAVE_EMBEDDED_SANDBOX_SHELL + if (i.second.source == "__embedded_sandbox_shell__") { + static unsigned char sh[] = { + #include "embedded-sandbox-shell.gen.hh" + }; + auto dst = chrootRootDir + i.first; + createDirs(dirOf(dst)); + writeFile(dst, std::string_view((const char *) sh, sizeof(sh))); + chmod_(dst, 0555); + } else + #endif + doBind(i.second.source, chrootRootDir + i.first, i.second.optional); + } + + /* Bind a new instance of procfs on /proc. */ + createDirs(chrootRootDir + "/proc"); + if (mount("none", (chrootRootDir + "/proc").c_str(), "proc", 0, 0) == -1) + throw SysError("mounting /proc"); + + /* Mount sysfs on /sys. */ + if (buildUser && buildUser->getUIDCount() != 1) { + 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, + fmt("size=%s", settings.sandboxShmSize).c_str()) == -1) + throw SysError("mounting /dev/shm"); + + /* Mount a new devpts on /dev/pts. Note that this + requires the kernel to be compiled with + CONFIG_DEVPTS_MULTIPLE_INSTANCES=y (which is the case + if /dev/ptx/ptmx exists). */ + if (pathExists("/dev/pts/ptmx") && + !pathExists(chrootRootDir + "/dev/ptmx") + && !pathsInChroot.count("/dev/pts")) + { + if (mount("none", (chrootRootDir + "/dev/pts").c_str(), "devpts", 0, "newinstance,mode=0620") == 0) + { + createSymlink("/dev/pts/ptmx", chrootRootDir + "/dev/ptmx"); + + /* Make sure /dev/pts/ptmx is world-writable. With some + Linux versions, it is created with permissions 0. */ + chmod_(chrootRootDir + "/dev/pts/ptmx", 0666); + } else { + if (errno != EINVAL) + throw SysError("mounting /dev/pts"); + doBind("/dev/pts", chrootRootDir + "/dev/pts"); + doBind("/dev/ptmx", chrootRootDir + "/dev/ptmx"); + } + } + + /* Make /etc unwritable */ + if (!drvOptions->useUidRange(*drv)) + chmod_(chrootRootDir + "/etc", 0555); + + /* 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"); + + /* 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 (cgroup && 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); + + if (mkdir("real-root", 0500) == -1) + throw SysError("cannot create real-root directory"); + + if (pivot_root(".", "real-root") == -1) + throw SysError("cannot pivot old root directory onto '%1%'", (chrootRootDir + "/real-root")); + + if (chroot(".") == -1) + throw SysError("cannot change root directory to '%1%'", chrootRootDir); + + if (umount2("real-root", MNT_DETACH) == -1) + throw SysError("cannot unmount real root filesystem"); + + if (rmdir("real-root") == -1) + throw SysError("cannot remove real-root directory"); + + /* Switch to the sandbox uid/gid in the user namespace, + which corresponds to the build user or calling user in + the parent namespace. */ + if (setgid(sandboxGid()) == -1) + throw SysError("setgid failed"); + if (setuid(sandboxUid()) == -1) + throw SysError("setuid failed"); + + setUser = false; + } +#endif + + if (chdir(tmpDirInSandbox.c_str()) == -1) + throw SysError("changing into '%1%'", tmpDir); + + /* Close all other file descriptors. */ + unix::closeExtraFDs(); + +#ifdef __linux__ + linux::setPersonality(drv->platform); +#endif + + /* Disable core dumps by default. */ + struct rlimit limit = { 0, RLIM_INFINITY }; + setrlimit(RLIMIT_CORE, &limit); + + // FIXME: set other limits to deterministic values? + + /* Fill in the environment. */ + Strings envStrs; + for (auto & i : env) + envStrs.push_back(rewriteStrings(i.first + "=" + i.second, inputRewrites)); + + /* If we are running in `build-users' mode, then switch to the + user we allocated above. Make sure that we drop all root + privileges. Note that above we have closed all file + descriptors except std*, so that's safe. Also note that + setuid() when run as root sets the real, effective and + saved UIDs. */ + if (setUser && buildUser) { + /* Preserve supplementary groups of the build user, to allow + admins to specify groups such as "kvm". */ + auto gids = buildUser->getSupplementaryGIDs(); + if (setgroups(gids.size(), gids.data()) == -1) + throw SysError("cannot set supplementary groups of build user"); + + if (setgid(buildUser->getGID()) == -1 || + getgid() != buildUser->getGID() || + getegid() != buildUser->getGID()) + throw SysError("setgid failed"); + + if (setuid(buildUser->getUID()) == -1 || + getuid() != buildUser->getUID() || + geteuid() != buildUser->getUID()) + throw SysError("setuid failed"); + } + +#ifdef __APPLE__ + /* This has to appear before import statements. */ + std::string sandboxProfile = "(version 1)\n"; + + if (useChroot) { + + /* Lots and lots and lots of file functions freak out if they can't stat their full ancestry */ + PathSet ancestry; + + /* 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 + particularly efficient... I doubt it'll be a bottleneck in practice */ + for (auto & i : pathsInChroot) { + Path cur = i.first; + while (cur.compare("/") != 0) { + cur = dirOf(cur); + ancestry.insert(cur); + } + } + + /* 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 cur = store.storeDir; + while (cur.compare("/") != 0) { + ancestry.insert(cur); + cur = dirOf(cur); + } + + /* Add all our input paths to the chroot */ + for (auto & i : inputPaths) { + auto p = store.printStorePath(i); + pathsInChroot[p] = p; + } + + /* Violations will go to the syslog if you set this. Unfortunately the destination does not appear to be configurable */ + if (settings.darwinLogSandboxViolations) { + sandboxProfile += "(deny default)\n"; + } else { + sandboxProfile += "(deny default (with no-log))\n"; + } + + sandboxProfile += + #include "sandbox-defaults.sb" + ; + + if (!derivationType->isSandboxed()) + sandboxProfile += + #include "sandbox-network.sb" + ; + + /* Add the output paths we'll use at build-time to the chroot */ + sandboxProfile += "(allow file-read* file-write* process-exec\n"; + for (auto & [_, path] : scratchOutputs) + sandboxProfile += fmt("\t(subpath \"%s\")\n", store.printStorePath(path)); + + sandboxProfile += ")\n"; + + /* Our inputs (transitive dependencies and any impurities computed above) + + without file-write* allowed, access() incorrectly returns EPERM + */ + sandboxProfile += "(allow file-read* file-write* process-exec\n"; + + // We create multiple allow lists, to avoid exceeding a limit in the darwin sandbox interpreter. + // See https://github.com/NixOS/nix/issues/4119 + // We split our allow groups approximately at half the actual limit, 1 << 16 + const size_t breakpoint = sandboxProfile.length() + (1 << 14); + for (auto & i : pathsInChroot) { + + if (sandboxProfile.length() >= breakpoint) { + debug("Sandbox break: %d %d", sandboxProfile.length(), breakpoint); + sandboxProfile += ")\n(allow file-read* file-write* process-exec\n"; + } + + if (i.first != i.second.source) + throw Error( + "can't map '%1%' to '%2%': mismatched impure paths not supported on Darwin", + i.first, i.second.source); + + std::string path = i.first; + auto optSt = maybeLstat(path.c_str()); + if (!optSt) { + if (i.second.optional) + continue; + throw SysError("getting attributes of required path '%s", path); + } + if (S_ISDIR(optSt->st_mode)) + sandboxProfile += fmt("\t(subpath \"%s\")\n", path); + else + sandboxProfile += fmt("\t(literal \"%s\")\n", path); + } + sandboxProfile += ")\n"; + + /* Allow file-read* on full directory hierarchy to self. Allows realpath() */ + sandboxProfile += "(allow file-read*\n"; + for (auto & i : ancestry) { + sandboxProfile += fmt("\t(literal \"%s\")\n", i); + } + sandboxProfile += ")\n"; + + sandboxProfile += drvOptions->additionalSandboxProfile; + } else + sandboxProfile += + #include "sandbox-minimal.sb" + ; + + debug("Generated sandbox profile:"); + debug(sandboxProfile); + + /* The tmpDir in scope points at the temporary build directory for our derivation. Some packages try different mechanisms + to find temporary directories, so we want to open up a broader place for them to put their files, if needed. */ + Path globalTmpDir = canonPath(defaultTempDir(), true); + + /* They don't like trailing slashes on subpath directives */ + while (!globalTmpDir.empty() && globalTmpDir.back() == '/') + globalTmpDir.pop_back(); + + if (getEnv("_NIX_TEST_NO_SANDBOX") != "1") { + Strings sandboxArgs; + sandboxArgs.push_back("_GLOBAL_TMP_DIR"); + sandboxArgs.push_back(globalTmpDir); + if (drvOptions->allowLocalNetworking) { + sandboxArgs.push_back("_ALLOW_LOCAL_NETWORKING"); + sandboxArgs.push_back("1"); + } + char * sandbox_errbuf = nullptr; + if (sandbox_init_with_parameters(sandboxProfile.c_str(), 0, stringsToCharPtrs(sandboxArgs).data(), &sandbox_errbuf)) { + writeFull(STDERR_FILENO, fmt("failed to configure sandbox: %s\n", sandbox_errbuf ? sandbox_errbuf : "(null)")); + _exit(1); + } + } +#endif + + /* Indicate that we managed to set up the build environment. */ + writeFull(STDERR_FILENO, std::string("\2\n")); + + sendException = false; + + /* Execute the program. This should not return. */ + if (drv->isBuiltin()) { + try { + logger = makeJSONLogger(getStandardError()); + + std::map outputs; + for (auto & e : drv->outputs) + outputs.insert_or_assign(e.first, + store.printStorePath(scratchOutputs.at(e.first))); + + if (drv->builder == "builtin:fetchurl") + builtinFetchurl(*drv, outputs, netrcData, caFileData); + else if (drv->builder == "builtin:buildenv") + builtinBuildenv(*drv, outputs); + else if (drv->builder == "builtin:unpack-channel") + builtinUnpackChannel(*drv, outputs); + else + throw Error("unsupported builtin builder '%1%'", drv->builder.substr(8)); + _exit(0); + } catch (std::exception & e) { + writeFull(STDERR_FILENO, e.what() + std::string("\n")); + _exit(1); + } + } + + // Now builder is not builtin + + Strings args; + args.push_back(std::string(baseNameOf(drv->builder))); + + for (auto & i : drv->args) + args.push_back(rewriteStrings(i, inputRewrites)); + +#ifdef __APPLE__ + posix_spawnattr_t attrp; + + if (posix_spawnattr_init(&attrp)) + throw SysError("failed to initialize builder"); + + if (posix_spawnattr_setflags(&attrp, POSIX_SPAWN_SETEXEC)) + throw SysError("failed to initialize builder"); + + if (drv->platform == "aarch64-darwin") { + // Unset kern.curproc_arch_affinity so we can escape Rosetta + int affinity = 0; + sysctlbyname("kern.curproc_arch_affinity", NULL, NULL, &affinity, sizeof(affinity)); + + cpu_type_t cpu = CPU_TYPE_ARM64; + posix_spawnattr_setbinpref_np(&attrp, 1, &cpu, NULL); + } else if (drv->platform == "x86_64-darwin") { + cpu_type_t cpu = CPU_TYPE_X86_64; + posix_spawnattr_setbinpref_np(&attrp, 1, &cpu, NULL); + } + + posix_spawn(NULL, drv->builder.c_str(), NULL, &attrp, stringsToCharPtrs(args).data(), stringsToCharPtrs(envStrs).data()); +#else + execve(drv->builder.c_str(), stringsToCharPtrs(args).data(), stringsToCharPtrs(envStrs).data()); +#endif + + throw SysError("executing '%1%'", drv->builder); + + } catch (...) { + handleChildException(sendException); + _exit(1); + } +} + + +SingleDrvOutputs DerivationBuilder::registerOutputs() +{ + std::map infos; + + /* Set of inodes seen during calls to canonicalisePathMetaData() + for this build's outputs. This needs to be shared between + outputs to allow hard links between outputs. */ + InodesSeen inodesSeen; + + Path checkSuffix = ".check"; + + 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. */ + StorePathSet referenceablePaths; + for (auto & p : inputPaths) referenceablePaths.insert(p); + for (auto & i : scratchOutputs) referenceablePaths.insert(i.second); + for (auto & p : addedPaths) referenceablePaths.insert(p); + + /* FIXME `needsHashRewrite` should probably be removed and we get to the + real reason why we aren't using the chroot dir */ + auto toRealPathChroot = [&](const Path & p) -> Path { + return useChroot && !needsHashRewrite() + ? chrootRootDir + p + : store.toRealPath(p); + }; + + /* Check whether the output paths were created, and make all + output paths read-only. Then get the references of each output (that we + might need to register), so we can topologically sort them. For the ones + that are most definitely already installed, we just store their final + name so we can also use it in rewrites. */ + StringSet outputsToSort; + struct AlreadyRegistered { StorePath path; }; + struct PerhapsNeedToRegister { StorePathSet refs; }; + std::map> outputReferencesIfUnregistered; + std::map outputStats; + for (auto & [outputName, _] : drv->outputs) { + auto scratchOutput = get(scratchOutputs, outputName); + if (!scratchOutput) + throw BuildError( + "builder for '%s' has no scratch output for '%s'", + store.printStorePath(drvPath), outputName); + auto actualPath = toRealPathChroot(store.printStorePath(*scratchOutput)); + + outputsToSort.insert(outputName); + + /* Updated wanted info to remove the outputs we definitely don't need to register */ + auto initialOutput = get(initialOutputs, outputName); + if (!initialOutput) + throw BuildError( + "builder for '%s' has no initial output for '%s'", + store.printStorePath(drvPath), outputName); + auto & initialInfo = *initialOutput; + + /* Don't register if already valid, and not checking */ + initialInfo.wanted = buildMode == bmCheck + || !(initialInfo.known && initialInfo.known->isValid()); + if (!initialInfo.wanted) { + outputReferencesIfUnregistered.insert_or_assign( + outputName, + AlreadyRegistered { .path = initialInfo.known->path }); + continue; + } + + auto optSt = maybeLstat(actualPath.c_str()); + if (!optSt) + throw BuildError( + "builder for '%s' failed to produce output path for output '%s' at '%s'", + store.printStorePath(drvPath), outputName, actualPath); + struct stat & st = *optSt; + +#ifndef __CYGWIN__ + /* Check that the output is not group or world writable, as + that means that someone else can have interfered with the + build. Also, the output should be owned by the build + user. */ + if ((!S_ISLNK(st.st_mode) && (st.st_mode & (S_IWGRP | S_IWOTH))) || + (buildUser && st.st_uid != buildUser->getUID())) + throw BuildError( + "suspicious ownership or permission on '%s' for output '%s'; rejecting this build output", + actualPath, outputName); +#endif + + /* 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 ? std::optional(buildUser->getUIDRange()) : std::nullopt, + inodesSeen); + + bool discardReferences = false; + if (auto udr = get(drvOptions->unsafeDiscardReferences, outputName)) { + discardReferences = *udr; + } + + StorePathSet references; + if (discardReferences) + debug("discarding references of output '%s'", outputName); + else { + debug("scanning for references for output '%s' in temp location '%s'", outputName, actualPath); + + /* Pass blank Sink as we are not ready to hash data at this stage. */ + NullSink blank; + references = scanForReferences(blank, actualPath, referenceablePaths); + } + + outputReferencesIfUnregistered.insert_or_assign( + outputName, + PerhapsNeedToRegister { .refs = references }); + outputStats.insert_or_assign(outputName, std::move(st)); + } + + auto sortedOutputNames = topoSort(outputsToSort, + {[&](const std::string & name) { + auto orifu = get(outputReferencesIfUnregistered, name); + if (!orifu) + throw BuildError( + "no output reference for '%s' in build of '%s'", + name, store.printStorePath(drvPath)); + return std::visit(overloaded { + /* Since we'll use the already installed versions of these, we + can treat them as leaves and ignore any references they + have. */ + [&](const AlreadyRegistered &) { return StringSet {}; }, + [&](const PerhapsNeedToRegister & refs) { + StringSet referencedOutputs; + /* FIXME build inverted map up front so no quadratic waste here */ + for (auto & r : refs.refs) + for (auto & [o, p] : scratchOutputs) + if (r == p) + referencedOutputs.insert(o); + return referencedOutputs; + }, + }, *orifu); + }}, + {[&](const std::string & path, const std::string & parent) { + // TODO with more -vvvv also show the temporary paths for manual inspection. + return BuildError( + "cycle detected in build of '%s' in the references of output '%s' from output '%s'", + store.printStorePath(drvPath), path, parent); + }}); + + std::reverse(sortedOutputNames.begin(), sortedOutputNames.end()); + + OutputPathMap finalOutputs; + + for (auto & outputName : sortedOutputNames) { + auto output = get(drv->outputs, outputName); + auto scratchPath = get(scratchOutputs, outputName); + assert(output && scratchPath); + auto actualPath = toRealPathChroot(store.printStorePath(*scratchPath)); + + auto finish = [&](StorePath finalStorePath) { + /* Store the final path */ + finalOutputs.insert_or_assign(outputName, finalStorePath); + /* The rewrite rule will be used in downstream outputs that refer to + use. This is why the topological sort is essential to do first + before this for loop. */ + if (*scratchPath != finalStorePath) + outputRewrites[std::string { scratchPath->hashPart() }] = std::string { finalStorePath.hashPart() }; + }; + + auto orifu = get(outputReferencesIfUnregistered, outputName); + assert(orifu); + + std::optional referencesOpt = std::visit(overloaded { + [&](const AlreadyRegistered & skippedFinalPath) -> std::optional { + finish(skippedFinalPath.path); + return std::nullopt; + }, + [&](const PerhapsNeedToRegister & r) -> std::optional { + return r.refs; + }, + }, *orifu); + + if (!referencesOpt) + continue; + auto references = *referencesOpt; + + auto rewriteOutput = [&](const StringMap & rewrites) { + /* Apply hash rewriting if necessary. */ + if (!rewrites.empty()) { + debug("rewriting hashes in '%1%'; cross fingers", actualPath); + + /* FIXME: Is this actually streaming? */ + auto source = sinkToSource([&](Sink & nextSink) { + RewritingSink rsink(rewrites, nextSink); + dumpPath(actualPath, rsink); + rsink.flush(); + }); + Path tmpPath = actualPath + ".tmp"; + restorePath(tmpPath, *source); + deletePath(actualPath); + movePath(tmpPath, actualPath); + + /* FIXME: set proper permissions in restorePath() so + we don't have to do another traversal. */ + canonicalisePathMetaData(actualPath, {}, inodesSeen); + } + }; + + auto rewriteRefs = [&]() -> StoreReferences { + /* In the CA case, we need the rewritten refs to calculate the + final path, therefore we look for a *non-rewritten + self-reference, and use a bool rather try to solve the + computationally intractable fixed point. */ + StoreReferences res { + .self = false, + }; + for (auto & r : references) { + auto name = r.name(); + auto origHash = std::string { r.hashPart() }; + if (r == *scratchPath) { + res.self = true; + } else if (auto outputRewrite = get(outputRewrites, origHash)) { + std::string newRef = *outputRewrite; + newRef += '-'; + newRef += name; + res.others.insert(StorePath { newRef }); + } else { + res.others.insert(r); + } + } + return res; + }; + + auto newInfoFromCA = [&](const DerivationOutput::CAFloating outputHash) -> ValidPathInfo { + auto st = get(outputStats, outputName); + if (!st) + throw BuildError( + "output path %1% without valid stats info", + actualPath); + if (outputHash.method.getFileIngestionMethod() == FileIngestionMethod::Flat) + { + /* The output path should be a regular file without execute permission. */ + if (!S_ISREG(st->st_mode) || (st->st_mode & S_IXUSR) != 0) + throw BuildError( + "output path '%1%' should be a non-executable regular file " + "since recursive hashing is not enabled (one of outputHashMode={flat,text} is true)", + actualPath); + } + rewriteOutput(outputRewrites); + /* FIXME optimize and deduplicate with addToStore */ + std::string oldHashPart { scratchPath->hashPart() }; + auto got = [&]{ + auto fim = outputHash.method.getFileIngestionMethod(); + switch (fim) { + case FileIngestionMethod::Flat: + case FileIngestionMethod::NixArchive: + { + HashModuloSink caSink { outputHash.hashAlgo, oldHashPart }; + auto fim = outputHash.method.getFileIngestionMethod(); + dumpPath( + {getFSSourceAccessor(), CanonPath(actualPath)}, + caSink, + (FileSerialisationMethod) fim); + return caSink.finish().first; + } + case FileIngestionMethod::Git: { + return git::dumpHash( + outputHash.hashAlgo, + {getFSSourceAccessor(), CanonPath(actualPath)}).hash; + } + } + assert(false); + }(); + + ValidPathInfo newInfo0 { + store, + outputPathName(drv->name, outputName), + ContentAddressWithReferences::fromParts( + outputHash.method, + std::move(got), + rewriteRefs()), + Hash::dummy, + }; + if (*scratchPath != newInfo0.path) { + // If the path has some self-references, we need to rewrite + // them. + // (note that this doesn't invalidate the ca hash we calculated + // above because it's computed *modulo the self-references*, so + // it already takes this rewrite into account). + rewriteOutput( + StringMap{{oldHashPart, + std::string(newInfo0.path.hashPart())}}); + } + + { + HashResult narHashAndSize = hashPath( + {getFSSourceAccessor(), CanonPath(actualPath)}, + FileSerialisationMethod::NixArchive, HashAlgorithm::SHA256); + newInfo0.narHash = narHashAndSize.first; + newInfo0.narSize = narHashAndSize.second; + } + + assert(newInfo0.ca); + return newInfo0; + }; + + ValidPathInfo newInfo = std::visit(overloaded { + + [&](const DerivationOutput::InputAddressed & output) { + /* input-addressed case */ + auto requiredFinalPath = output.path; + /* Preemptively add rewrite rule for final hash, as that is + what the NAR hash will use rather than normalized-self references */ + if (*scratchPath != requiredFinalPath) + outputRewrites.insert_or_assign( + std::string { scratchPath->hashPart() }, + std::string { requiredFinalPath.hashPart() }); + rewriteOutput(outputRewrites); + HashResult narHashAndSize = hashPath( + {getFSSourceAccessor(), CanonPath(actualPath)}, + FileSerialisationMethod::NixArchive, HashAlgorithm::SHA256); + ValidPathInfo newInfo0 { requiredFinalPath, narHashAndSize.first }; + newInfo0.narSize = narHashAndSize.second; + auto refs = rewriteRefs(); + newInfo0.references = std::move(refs.others); + if (refs.self) + newInfo0.references.insert(newInfo0.path); + return newInfo0; + }, + + [&](const DerivationOutput::CAFixed & dof) { + auto & wanted = dof.ca.hash; + + // Replace the output by a fresh copy of itself to make sure + // that there's no stale file descriptor pointing to it + Path tmpOutput = actualPath + ".tmp"; + copyFile( + std::filesystem::path(actualPath), + std::filesystem::path(tmpOutput), true); + + std::filesystem::rename(tmpOutput, actualPath); + + auto newInfo0 = newInfoFromCA(DerivationOutput::CAFloating { + .method = dof.ca.method, + .hashAlgo = wanted.algo, + }); + + /* Check wanted hash */ + assert(newInfo0.ca); + auto & got = newInfo0.ca->hash; + if (wanted != got) { + /* Throw an error after registering the path as + valid. */ + miscMethods.noteHashMismatch(); + delayedException = std::make_exception_ptr( + BuildError("hash mismatch in fixed-output derivation '%s':\n specified: %s\n got: %s", + store.printStorePath(drvPath), + wanted.to_string(HashFormat::SRI, true), + got.to_string(HashFormat::SRI, true))); + } + if (!newInfo0.references.empty()) { + auto numViolations = newInfo.references.size(); + delayedException = std::make_exception_ptr( + BuildError("fixed-output derivations must not reference store paths: '%s' references %d distinct paths, e.g. '%s'", + store.printStorePath(drvPath), + numViolations, + store.printStorePath(*newInfo.references.begin()))); + } + + return newInfo0; + }, + + [&](const DerivationOutput::CAFloating & dof) { + return newInfoFromCA(dof); + }, + + [&](const DerivationOutput::Deferred &) -> ValidPathInfo { + // No derivation should reach that point without having been + // rewritten first + assert(false); + }, + + [&](const DerivationOutput::Impure & doi) { + return newInfoFromCA(DerivationOutput::CAFloating { + .method = doi.method, + .hashAlgo = doi.hashAlgo, + }); + }, + + }, output->raw); + + /* FIXME: set proper permissions in restorePath() so + we don't have to do another traversal. */ + 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 + their usual "final destination" */ + auto finalDestPath = store.printStorePath(newInfo.path); + + /* Lock final output path, if not already locked. This happens with + floating CA derivations and hash-mismatching fixed-output + derivations. */ + PathLocks dynamicOutputLock; + dynamicOutputLock.setDeletion(true); + auto optFixedPath = output->path(store, drv->name, outputName); + if (!optFixedPath || + store.printStorePath(*optFixedPath) != finalDestPath) + { + assert(newInfo.ca); + dynamicOutputLock.lockPaths({store.toRealPath(finalDestPath)}); + } + + /* Move files, if needed */ + if (store.toRealPath(finalDestPath) != actualPath) { + if (buildMode == bmRepair) { + /* Path already exists, need to replace it */ + replaceValidPath(store.toRealPath(finalDestPath), actualPath); + actualPath = store.toRealPath(finalDestPath); + } else if (buildMode == bmCheck) { + /* Path already exists, and we want to compare, so we leave out + new path in place. */ + } else if (store.isValidPath(newInfo.path)) { + /* Path already exists because CA path produced by something + else. No moving needed. */ + assert(newInfo.ca); + } else { + auto destPath = store.toRealPath(finalDestPath); + deletePath(destPath); + movePath(actualPath, destPath); + actualPath = destPath; + } + } + + auto & localStore = getLocalStore(); + + if (buildMode == bmCheck) { + + if (!store.isValidPath(newInfo.path)) continue; + ValidPathInfo oldInfo(*store.queryPathInfo(newInfo.path)); + if (newInfo.narHash != oldInfo.narHash) { + miscMethods.noteCheckMismatch(); + if (settings.runDiffHook || settings.keepFailed) { + auto dst = store.toRealPath(finalDestPath + checkSuffix); + deletePath(dst); + movePath(actualPath, dst); + + handleDiffHook( + buildUser ? buildUser->getUID() : getuid(), + buildUser ? buildUser->getGID() : getgid(), + finalDestPath, dst, store.printStorePath(drvPath), tmpDir); + + throw NotDeterministic("derivation '%s' may not be deterministic: output '%s' differs from '%s'", + store.printStorePath(drvPath), store.toRealPath(finalDestPath), dst); + } else + throw NotDeterministic("derivation '%s' may not be deterministic: output '%s' differs", + store.printStorePath(drvPath), store.toRealPath(finalDestPath)); + } + + /* Since we verified the build, it's now ultimately trusted. */ + if (!oldInfo.ultimate) { + oldInfo.ultimate = true; + localStore.signPathInfo(oldInfo); + localStore.registerValidPaths({{oldInfo.path, oldInfo}}); + } + + continue; + } + + /* For debugging, print out the referenced and unreferenced paths. */ + for (auto & i : inputPaths) { + if (references.count(i)) + debug("referenced input: '%1%'", store.printStorePath(i)); + else + debug("unreferenced input: '%1%'", store.printStorePath(i)); + } + + localStore.optimisePath(actualPath, NoRepair); // FIXME: combine with scanForReferences() + miscMethods.markContentsGood(newInfo.path); + + newInfo.deriver = drvPath; + newInfo.ultimate = true; + localStore.signPathInfo(newInfo); + + finish(newInfo.path); + + /* If it's a CA path, register it right away. This is necessary if it + isn't statically known so that we can safely unlock the path before + the next iteration */ + if (newInfo.ca) + localStore.registerValidPaths({{newInfo.path, newInfo}}); + + infos.emplace(outputName, std::move(newInfo)); + } + + if (buildMode == bmCheck) { + /* In case of fixed-output derivations, if there are + mismatches on `--check` an error must be thrown as this is + also a source for non-determinism. */ + if (delayedException) + std::rethrow_exception(delayedException); + return miscMethods.assertPathValidity(); + } + + /* Apply output checks. */ + checkOutputs(infos); + + /* Register each output path as valid, and register the sets of + paths referenced by each of them. If there are cycles in the + outputs, this will fail. */ + { + auto & localStore = getLocalStore(); + + ValidPathInfos infos2; + for (auto & [outputName, newInfo] : infos) { + infos2.insert_or_assign(newInfo.path, newInfo); + } + localStore.registerValidPaths(infos2); + } + + /* In case of a fixed-output derivation hash mismatch, throw an + exception now that we have registered the output as valid. */ + if (delayedException) + std::rethrow_exception(delayedException); + + /* If we made it this far, we are sure the output matches the derivation + (since the delayedException would be a fixed output CA mismatch). That + means it's safe to link the derivation to the output hash. We must do + that for floating CA derivations, which otherwise couldn't be cached, + but it's fine to do in all cases. */ + SingleDrvOutputs builtOutputs; + + for (auto & [outputName, newInfo] : infos) { + auto oldinfo = get(initialOutputs, outputName); + assert(oldinfo); + auto thisRealisation = Realisation { + .id = DrvOutput { + oldinfo->outputHash, + outputName + }, + .outPath = newInfo.path + }; + if (experimentalFeatureSettings.isEnabled(Xp::CaDerivations) + && !drv->type().isImpure()) + { + store.signRealisation(thisRealisation); + store.registerDrvOutput(thisRealisation); + } + builtOutputs.emplace(outputName, thisRealisation); + } + + return builtOutputs; +} + + +void DerivationBuilder::checkOutputs(const std::map & outputs) +{ + std::map outputsByPath; + for (auto & output : outputs) + outputsByPath.emplace(store.printStorePath(output.second.path), output.second); + + for (auto & output : outputs) { + auto & outputName = output.first; + auto & info = output.second; + + /* Compute the closure and closure size of some output. This + is slightly tricky because some of its references (namely + other outputs) may not be valid yet. */ + auto getClosure = [&](const StorePath & path) + { + uint64_t closureSize = 0; + StorePathSet pathsDone; + std::queue pathsLeft; + pathsLeft.push(path); + + while (!pathsLeft.empty()) { + auto path = pathsLeft.front(); + pathsLeft.pop(); + if (!pathsDone.insert(path).second) continue; + + auto i = outputsByPath.find(store.printStorePath(path)); + if (i != outputsByPath.end()) { + closureSize += i->second.narSize; + for (auto & ref : i->second.references) + pathsLeft.push(ref); + } else { + auto info = store.queryPathInfo(path); + closureSize += info->narSize; + for (auto & ref : info->references) + pathsLeft.push(ref); + } + } + + return std::make_pair(std::move(pathsDone), closureSize); + }; + + auto applyChecks = [&](const DerivationOptions::OutputChecks & checks) + { + if (checks.maxSize && info.narSize > *checks.maxSize) + throw BuildError("path '%s' is too large at %d bytes; limit is %d bytes", + store.printStorePath(info.path), info.narSize, *checks.maxSize); + + if (checks.maxClosureSize) { + uint64_t closureSize = getClosure(info.path).second; + if (closureSize > *checks.maxClosureSize) + throw BuildError("closure of path '%s' is too large at %d bytes; limit is %d bytes", + store.printStorePath(info.path), closureSize, *checks.maxClosureSize); + } + + auto checkRefs = [&](const StringSet & value, bool allowed, bool recursive) + { + /* Parse a list of reference specifiers. Each element must + either be a store path, or the symbolic name of the output + of the derivation (such as `out'). */ + StorePathSet spec; + for (auto & i : value) { + if (store.isStorePath(i)) + spec.insert(store.parseStorePath(i)); + else if (auto output = get(outputs, i)) + spec.insert(output->path); + else { + std::string outputsListing = concatMapStringsSep(", ", outputs, [](auto & o) { return o.first; }); + throw BuildError("derivation '%s' output check for '%s' contains an illegal reference specifier '%s'," + " expected store path or output name (one of [%s])", + store.printStorePath(drvPath), outputName, i, outputsListing); + } + } + + auto used = recursive + ? getClosure(info.path).first + : info.references; + + if (recursive && checks.ignoreSelfRefs) + used.erase(info.path); + + StorePathSet badPaths; + + for (auto & i : used) + if (allowed) { + if (!spec.count(i)) + badPaths.insert(i); + } else { + if (spec.count(i)) + badPaths.insert(i); + } + + if (!badPaths.empty()) { + std::string badPathsStr; + for (auto & i : badPaths) { + badPathsStr += "\n "; + badPathsStr += store.printStorePath(i); + } + throw BuildError("output '%s' is not allowed to refer to the following paths:%s", + store.printStorePath(info.path), badPathsStr); + } + }; + + /* Mandatory check: absent whitelist, and present but empty + whitelist mean very different things. */ + if (auto & refs = checks.allowedReferences) { + checkRefs(*refs, true, false); + } + if (auto & refs = checks.allowedRequisites) { + checkRefs(*refs, true, true); + } + + /* Optimization: don't need to do anything when + disallowed and empty set. */ + if (!checks.disallowedReferences.empty()) { + checkRefs(checks.disallowedReferences, false, false); + } + if (!checks.disallowedRequisites.empty()) { + checkRefs(checks.disallowedRequisites, false, true); + } + }; + + std::visit(overloaded{ + [&](const DerivationOptions::OutputChecks & checks) { + applyChecks(checks); + }, + [&](const std::map & checksPerOutput) { + if (auto outputChecks = get(checksPerOutput, outputName)) + + applyChecks(*outputChecks); + }, + }, drvOptions->outputChecks); + } +} + + +void DerivationBuilder::deleteTmpDir(bool force) +{ + if (topTmpDir != "") { + /* Don't keep temporary directories for builtins because they + might have privileged stuff (like a copy of netrc). */ + if (settings.keepFailed && !force && !drv->isBuiltin()) { + printError("note: keeping build directory '%s'", tmpDir); + chmod(topTmpDir.c_str(), 0755); + chmod(tmpDir.c_str(), 0755); + } + else + deletePath(topTmpDir); + topTmpDir = ""; + tmpDir = ""; + } +} + + +bool LocalDerivationGoal::isReadDesc(int fd) +{ + return (hook && DerivationGoal::isReadDesc(fd)) || + (!hook && fd == builder.builderOut.get()); +} + + +StorePath DerivationBuilder::makeFallbackPath(OutputNameView outputName) +{ + // This is a bogus path type, constructed this way to ensure that it doesn't collide with any other store path + // See doc/manual/source/protocols/store-path.md for details + // TODO: We may want to separate the responsibilities of constructing the path fingerprint and of actually doing the hashing + auto pathType = "rewrite:" + std::string(drvPath.to_string()) + ":name:" + std::string(outputName); + return store.makeStorePath( + pathType, + // pass an all-zeroes hash + Hash(HashAlgorithm::SHA256), outputPathName(drv->name, outputName)); +} + + +StorePath DerivationBuilder::makeFallbackPath(const StorePath & path) +{ + // This is a bogus path type, constructed this way to ensure that it doesn't collide with any other store path + // See doc/manual/source/protocols/store-path.md for details + auto pathType = "rewrite:" + std::string(drvPath.to_string()) + ":" + std::string(path.to_string()); + return store.makeStorePath( + pathType, + // pass an all-zeroes hash + Hash(HashAlgorithm::SHA256), path.name()); +} + + +} diff --git a/src/libstore/unix/include/nix/store/build/derivation-builder.hh b/src/libstore/unix/include/nix/store/build/derivation-builder.hh new file mode 100644 index 000000000..93154c7dc --- /dev/null +++ b/src/libstore/unix/include/nix/store/build/derivation-builder.hh @@ -0,0 +1,3409 @@ +#include "nix/store/build/local-derivation-goal.hh" +#include "nix/store/local-store.hh" +#include "nix/util/processes.hh" +#include "nix/store/indirect-root-store.hh" +#include "nix/store/build/hook-instance.hh" +#include "nix/store/build/worker.hh" +#include "nix/store/builtins.hh" +#include "nix/store/builtins/buildenv.hh" +#include "nix/store/path-references.hh" +#include "nix/util/finally.hh" +#include "nix/util/util.hh" +#include "nix/util/archive.hh" +#include "nix/util/git.hh" +#include "nix/util/compression.hh" +#include "nix/store/daemon.hh" +#include "nix/util/topo-sort.hh" +#include "nix/util/callback.hh" +#include "nix/util/json-utils.hh" +#include "nix/util/current-process.hh" +#include "nix/store/build/child.hh" +#include "nix/util/unix-domain-socket.hh" +#include "nix/store/posix-fs-canonicalise.hh" +#include "nix/util/posix-source-accessor.hh" +#include "nix/store/restricted-store.hh" +#include "nix/store/config.hh" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "store-config-private.hh" + +#if HAVE_STATVFS +#include +#endif + +/* Includes required for chroot support. */ +#ifdef __linux__ +# include "linux/fchmodat2-compat.hh" +# include +# include +# include +# include +# include +# include +# include +# include +# include "nix/util/namespaces.hh" +# if HAVE_SECCOMP +# include +# endif +# define pivot_root(new_root, put_old) (syscall(SYS_pivot_root, new_root, put_old)) +# include "nix/util/cgroup.hh" +# include "nix/store/personality.hh" +#endif + +#ifdef __APPLE__ +#include +#include +#include + +/* This definition is undocumented but depended upon by all major browsers. */ +extern "C" int sandbox_init_with_parameters(const char *profile, uint64_t flags, const char *const parameters[], char **errorbuf); +#endif + +#include +#include +#include + +#include "nix/util/strings.hh" +#include "nix/util/signals.hh" + +#include "store-config-private.hh" + +namespace nix { + +/** + * Parameters by (mostly) `const` reference for `DerivationBuilder`. + */ +struct DerivationBuilderParams +{ + /** The path of the derivation. */ + const StorePath & drvPath; + + BuildResult & buildResult; + + /** + * The derivation stored at drvPath. + * + * @todo Remove double indirection by delaying when this is + * initialized. + */ + const std::unique_ptr & drv; + + const std::unique_ptr & parsedDrv; + const std::unique_ptr & drvOptions; + + /** + * The remainder is state held during the build. + */ + + /** + * All input paths (that is, the union of FS closures of the + * immediate input paths). + */ + const StorePathSet & inputPaths; + + /** + * @note we do in fact mutate this + */ + std::map & initialOutputs; + + const BuildMode & buildMode; + + DerivationBuilderParams( + const StorePath & drvPath, + const BuildMode & buildMode, + BuildResult & buildResult, + const std::unique_ptr & drv, + const std::unique_ptr & parsedDrv, + const std::unique_ptr & drvOptions, + const StorePathSet & inputPaths, + std::map & initialOutputs) + : drvPath{drvPath} + , buildResult{buildResult} + , drv{drv} + , parsedDrv{parsedDrv} + , drvOptions{drvOptions} + , inputPaths{inputPaths} + , initialOutputs{initialOutputs} + , buildMode{buildMode} + { } + + DerivationBuilderParams(DerivationBuilderParams &&) = default; +}; + +/** + * Callbacks that `DerivationBuilder` needs. + */ +struct DerivationBuilderCallbacks +{ + /** + * Open a log file and a pipe to it. + */ + virtual Path openLogFile() = 0; + + /** + * Close the log file. + */ + virtual void closeLogFile() = 0; + + /** + * Aborts if any output is not valid or corrupt, and otherwise + * returns a 'SingleDrvOutputs' structure containing all outputs. + * + * @todo Probably should just be in `DerivationGoal`. + */ + virtual SingleDrvOutputs assertPathValidity() = 0; + + virtual void appendLogTailErrorMsg(std::string & msg) = 0; + + /** + * Hook up `builderOut` to some mechanism to ingest the log + * + * @todo this should be reworked + */ + virtual void childStarted() = 0; + + /** + * @todo this should be reworked + */ + virtual void childTerminated() = 0; + + virtual void noteHashMismatch(void) = 0; + virtual void noteCheckMismatch(void) = 0; + + virtual void markContentsGood(const StorePath & path) = 0; +}; + +/** + * This class represents the state for building locally. + * + * @todo Ideally, it would not be a class, but a single function. + * However, besides the main entry point, there are a few more methods + * which are externally called, and need to be gotten rid of. There are + * also some virtual methods (either directly here or inherited from + * `DerivationBuilderCallbacks`, a stop-gap) that represent outgoing + * rather than incoming call edges that either should be removed, or + * become (higher order) function parameters. + */ +class DerivationBuilder : public RestrictionContext, DerivationBuilderParams +{ + Store & store; + + DerivationBuilderCallbacks & miscMethods; + +public: + + DerivationBuilder( + Store & store, + DerivationBuilderCallbacks & miscMethods, + DerivationBuilderParams params) + : DerivationBuilderParams{std::move(params)} + , store{store} + , miscMethods{miscMethods} + { } + + LocalStore & getLocalStore(); + + /** + * User selected for running the builder. + */ + std::unique_ptr buildUser; + + /** + * The process ID of the builder. + */ + Pid pid; + +private: + + /** + * The cgroup of the builder, if any. + */ + std::optional cgroup; + + /** + * The temporary directory used for the build. + */ + Path tmpDir; + + /** + * The top-level temporary directory. `tmpDir` is either equal to + * or a child of this directory. + */ + Path topTmpDir; + + /** + * The path of the temporary directory in the sandbox. + */ + Path tmpDirInSandbox; + +public: + + /** + * Master side of the pseudoterminal used for the builder's + * standard output/error. + */ + AutoCloseFD builderOut; + +private: + + /** + * Pipe for synchronising updates to the builder namespaces. + */ + Pipe userNamespaceSync; + + /** + * The mount namespace and user namespace of the builder, used to add additional + * paths to the sandbox as a result of recursive Nix calls. + */ + AutoCloseFD sandboxMountNamespace; + AutoCloseFD sandboxUserNamespace; + + /** + * On Linux, whether we're doing the build in its own user + * namespace. + */ + bool usingUserNamespace = true; + + /** + * Whether we're currently doing a chroot build. + */ + bool useChroot = false; + + /** + * The root of the chroot environment. + */ + Path chrootRootDir; + + /** + * RAII object to delete the chroot directory. + */ + std::shared_ptr autoDelChroot; + + /** + * The sort of derivation we are building. + * + * Just a cached value, can be recomputed from `drv`. + */ + std::optional derivationType; + + /** + * Stuff we need to pass to initChild(). + */ + struct ChrootPath { + Path source; + bool optional; + ChrootPath(Path source = "", bool optional = false) + : source(source), optional(optional) + { } + }; + typedef map PathsInChroot; // maps target path to source path + PathsInChroot pathsInChroot; + + typedef map Environment; + Environment env; + + /** + * Hash rewriting. + */ + StringMap inputRewrites, outputRewrites; + typedef map RedirectedOutputs; + RedirectedOutputs redirectedOutputs; + + /** + * The output paths used during the build. + * + * - Input-addressed derivations or fixed content-addressed outputs are + * sometimes built when some of their outputs already exist, and can not + * be hidden via sandboxing. We use temporary locations instead and + * rewrite after the build. Otherwise the regular predetermined paths are + * put here. + * + * - Floating content-addressing derivations do not know their final build + * output paths until the outputs are hashed, so random locations are + * used, and then renamed. The randomness helps guard against hidden + * self-references. + */ + OutputPathMap scratchOutputs; + + uid_t sandboxUid() { return usingUserNamespace ? (!buildUser || buildUser->getUIDCount() == 1 ? 1000 : 0) : buildUser->getUID(); } + gid_t sandboxGid() { return usingUserNamespace ? (!buildUser || buildUser->getUIDCount() == 1 ? 100 : 0) : buildUser->getGID(); } + + const static Path homeDir; + + /** + * The recursive Nix daemon socket. + */ + AutoCloseFD daemonSocket; + + /** + * The daemon main thread. + */ + std::thread daemonThread; + + /** + * The daemon worker threads. + */ + std::vector daemonWorkerThreads; + + const StorePathSet & originalPaths() override + { + return inputPaths; + } + + bool isAllowed(const StorePath & path) override + { + return inputPaths.count(path) || addedPaths.count(path); + } + bool isAllowed(const DrvOutput & id) override + { + return addedDrvOutputs.count(id); + } + + bool isAllowed(const DerivedPath & req); + + friend struct RestrictedStore; + + /** + * Whether we need to perform hash rewriting if there are valid output paths. + */ + bool needsHashRewrite(); + +public: + + /** + * Set up build environment / sandbox, acquiring resources (e.g. + * locks as needed). After this is run, the builder should be + * started. + * + * @returns true if successful, false if we could not acquire a build + * user. In that case, the caller must wait and then try again. + */ + bool prepareBuild(); + + /** + * Start building a derivation. + */ + void startBuilder(); + + /** + * Tear down build environment after the builder exits (either on + * its own or if it is killed). + * + * @returns The first case indicates failure during output + * processing. A status code and exception are returned, providing + * more information. The second case indicates success, and + * realisations for each output of the derivation are returned. + */ + std::variant, SingleDrvOutputs> unprepareBuild(); + +private: + + /** + * Fill in the environment for the builder. + */ + void initEnv(); + + /** + * Process messages send by the sandbox initialization. + */ + void processSandboxSetupMessages(); + + /** + * Setup tmp dir location. + */ + void initTmpDir(); + + /** + * Write a JSON file containing the derivation attributes. + */ + void writeStructuredAttrs(); + + /** + * Start an in-process nix daemon thread for recursive-nix. + */ + void startDaemon(); + +public: + + /** + * Stop the in-process nix daemon thread. + * @see startDaemon + */ + void stopDaemon(); + +private: + + void addDependency(const StorePath & path) override; + + /** + * Make a file owned by the builder. + */ + void chownToBuilder(const Path & path); + + /** + * Run the builder's process. + */ + void runChild(); + + /** + * Check that the derivation outputs all exist and register them + * as valid. + */ + SingleDrvOutputs registerOutputs(); + + /** + * Check that an output meets the requirements specified by the + * 'outputChecks' attribute (or the legacy + * '{allowed,disallowed}{References,Requisites}' attributes). + */ + void checkOutputs(const std::map & outputs); + +public: + + /** + * Delete the temporary directory, if we have one. + */ + void deleteTmpDir(bool force); + + /** + * Kill any processes running under the build user UID or in the + * cgroup of the build. + */ + void killSandbox(bool getStats); + +private: + + bool cleanupDecideWhetherDiskFull(); + + /** + * Create alternative path calculated from but distinct from the + * input, so we can avoid overwriting outputs (or other store paths) + * that already exist. + */ + StorePath makeFallbackPath(const StorePath & path); + + /** + * Make a path to another based on the output name along with the + * derivation hash. + * + * @todo Add option to randomize, so we can audit whether our + * rewrites caught everything + */ + StorePath makeFallbackPath(OutputNameView outputName); +}; + +/** + * This hooks up `DerivationBuilder` to the scheduler / goal machinary. + * + * @todo Eventually, this shouldn't exist, because `DerivationGoal` can + * just choose to use `DerivationBuilder` or its remote-building + * equalivalent directly, at the "value level" rather than "class + * inheritance hierarchy" level. + */ +struct LocalDerivationGoal : DerivationGoal, DerivationBuilderCallbacks +{ + DerivationBuilder builder; + + LocalDerivationGoal(const StorePath & drvPath, + const OutputsSpec & wantedOutputs, Worker & worker, + BuildMode buildMode) + : DerivationGoal{drvPath, wantedOutputs, worker, buildMode} + , builder{ + worker.store, + static_cast(*this), + DerivationBuilderParams { + DerivationGoal::drvPath, + DerivationGoal::buildMode, + DerivationGoal::buildResult, + DerivationGoal::drv, + DerivationGoal::parsedDrv, + DerivationGoal::drvOptions, + DerivationGoal::inputPaths, + DerivationGoal::initialOutputs, + }, + } + {} + + LocalDerivationGoal(const StorePath & drvPath, const BasicDerivation & drv, + const OutputsSpec & wantedOutputs, Worker & worker, + BuildMode buildMode = bmNormal) + : DerivationGoal{drvPath, drv, wantedOutputs, worker, buildMode} + , builder{ + worker.store, + static_cast(*this), + DerivationBuilderParams { + DerivationGoal::drvPath, + DerivationGoal::buildMode, + DerivationGoal::buildResult, + DerivationGoal::drv, + DerivationGoal::parsedDrv, + DerivationGoal::drvOptions, + DerivationGoal::inputPaths, + DerivationGoal::initialOutputs, + }, + } + {} + + virtual ~LocalDerivationGoal() override; + + /** + * The additional states. + */ + Goal::Co tryLocalBuild() override; + + bool isReadDesc(int fd) override; + + /** + * Forcibly kill the child process, if any. + * + * Called by destructor, can't be overridden + */ + void killChild() override final; + + void childStarted() override; + void childTerminated() override; + + void noteHashMismatch(void) override; + void noteCheckMismatch(void) override; + + void markContentsGood(const StorePath &) override; + + // Fake overrides to isntantiate identically-named virtual methods + + Path openLogFile() override { + return DerivationGoal::openLogFile(); + } + void closeLogFile() override { + DerivationGoal::closeLogFile(); + } + SingleDrvOutputs assertPathValidity() override { + return DerivationGoal::assertPathValidity(); + } + void appendLogTailErrorMsg(std::string & msg) override { + DerivationGoal::appendLogTailErrorMsg(msg); + } +}; + +std::shared_ptr makeLocalDerivationGoal( + const StorePath & drvPath, + const OutputsSpec & wantedOutputs, Worker & worker, + BuildMode buildMode) +{ + return std::make_shared(drvPath, wantedOutputs, worker, buildMode); +} + +std::shared_ptr makeLocalDerivationGoal( + const StorePath & drvPath, const BasicDerivation & drv, + const OutputsSpec & wantedOutputs, Worker & worker, + BuildMode buildMode) +{ + return std::make_shared(drvPath, drv, wantedOutputs, worker, buildMode); +} + +void handleDiffHook( + uid_t uid, uid_t gid, + const Path & tryA, const Path & tryB, + const Path & drvPath, const Path & tmpDir) +{ + auto & diffHookOpt = settings.diffHook.get(); + if (diffHookOpt && settings.runDiffHook) { + auto & diffHook = *diffHookOpt; + try { + auto diffRes = runProgram(RunOptions { + .program = diffHook, + .lookupPath = true, + .args = {tryA, tryB, drvPath, tmpDir}, + .uid = uid, + .gid = gid, + .chdir = "/" + }); + if (!statusOk(diffRes.first)) + throw ExecError(diffRes.first, + "diff-hook program '%1%' %2%", + diffHook, + statusToString(diffRes.first)); + + if (diffRes.second != "") + printError(chomp(diffRes.second)); + } catch (Error & error) { + ErrorInfo ei = error.info(); + // FIXME: wrap errors. + ei.msg = HintFmt("diff hook execution failed: %s", ei.msg.str()); + logError(ei); + } + } +} + +const Path DerivationBuilder::homeDir = "/homeless-shelter"; + + +LocalDerivationGoal::~LocalDerivationGoal() +{ + /* Careful: we should never ever throw an exception from a + destructor. */ + try { builder.deleteTmpDir(false); } catch (...) { ignoreExceptionInDestructor(); } + try { killChild(); } catch (...) { ignoreExceptionInDestructor(); } + try { builder.stopDaemon(); } catch (...) { ignoreExceptionInDestructor(); } +} + + +inline bool DerivationBuilder::needsHashRewrite() +{ +#ifdef __linux__ + return !useChroot; +#else + /* Darwin requires hash rewriting even when sandboxing is enabled. */ + return true; +#endif +} + + +LocalStore & DerivationBuilder::getLocalStore() +{ + auto p = dynamic_cast(&store); + assert(p); + return *p; +} + + +void LocalDerivationGoal::killChild() +{ + if (builder.pid != -1) { + worker.childTerminated(this); + + /* If we're using a build user, then there is a tricky race + condition: if we kill the build user before the child has + done its setuid() to the build user uid, then it won't be + killed, and we'll potentially lock up in pid.wait(). So + also send a conventional kill to the child. */ + ::kill(-builder.pid, SIGKILL); /* ignore the result */ + + builder.killSandbox(true); + + builder.pid.wait(); + } + + DerivationGoal::killChild(); +} + + +void DerivationBuilder::killSandbox(bool getStats) +{ + if (cgroup) { + #ifdef __linux__ + auto stats = destroyCgroup(*cgroup); + if (getStats) { + buildResult.cpuUser = stats.cpuUser; + buildResult.cpuSystem = stats.cpuSystem; + } + #else + unreachable(); + #endif + } + + else if (buildUser) { + auto uid = buildUser->getUID(); + assert(uid != 0); + killUser(uid); + } +} + + +void LocalDerivationGoal::childStarted() +{ + worker.childStarted(shared_from_this(), {builder.builderOut.get()}, true, true); +} + +void LocalDerivationGoal::childTerminated() +{ + worker.childTerminated(this); +} + +void LocalDerivationGoal::noteHashMismatch() +{ + worker.hashMismatch = true; +} + + +void LocalDerivationGoal::noteCheckMismatch() +{ + worker.checkMismatch = true; +} + + +void LocalDerivationGoal::markContentsGood(const StorePath & path) +{ + worker.markContentsGood(path); +} + + +Goal::Co LocalDerivationGoal::tryLocalBuild() +{ + assert(!hook); + + unsigned int curBuilds = worker.getNrLocalBuilds(); + if (curBuilds >= settings.maxBuildJobs) { + outputLocks.unlock(); + co_await waitForBuildSlot(); + co_return tryToBuild(); + } + + if (!builder.prepareBuild()) { + if (!actLock) + actLock = std::make_unique(*logger, lvlWarn, actBuildWaiting, + fmt("waiting for a free build user ID for '%s'", Magenta(worker.store.printStorePath(drvPath)))); + co_await waitForAWhile(); + co_return tryLocalBuild(); + } + + actLock.reset(); + + try { + + /* Okay, we have to build. */ + builder.startBuilder(); + + } catch (BuildError & e) { + outputLocks.unlock(); + builder.buildUser.reset(); + worker.permanentFailure = true; + co_return done(BuildResult::InputRejected, {}, std::move(e)); + } + + started(); + co_await Suspend{}; + + trace("build done"); + + auto res = builder.unprepareBuild(); + // N.B. cannot use `std::visit` with co-routine return + if (auto * ste = std::get_if<0>(&res)) { + outputLocks.unlock(); + co_return done(std::move(ste->first), {}, std::move(ste->second)); + } else if (auto * builtOutputs = std::get_if<1>(&res)) { + /* It is now safe to delete the lock files, since all future + lockers will see that the output paths are valid; they will + not create new lock files with the same names as the old + (unlinked) lock files. */ + outputLocks.setDeletion(true); + outputLocks.unlock(); + co_return done(BuildResult::Built, std::move(*builtOutputs)); + } else { + unreachable(); + } +} + +bool DerivationBuilder::prepareBuild() +{ + /* Cache this */ + derivationType = drv->type(); + + /* Are we doing a chroot build? */ + { + if (settings.sandboxMode == smEnabled) { + if (drvOptions->noChroot) + throw Error("derivation '%s' has '__noChroot' set, " + "but that's not allowed when 'sandbox' is 'true'", store.printStorePath(drvPath)); +#ifdef __APPLE__ + if (drvOptions->additionalSandboxProfile != "") + throw Error("derivation '%s' specifies a sandbox profile, " + "but this is only allowed when 'sandbox' is 'relaxed'", worker.store.printStorePath(drvPath)); +#endif + useChroot = true; + } + else if (settings.sandboxMode == smDisabled) + useChroot = false; + else if (settings.sandboxMode == smRelaxed) + useChroot = derivationType->isSandboxed() && !drvOptions->noChroot; + } + + auto & localStore = getLocalStore(); + if (localStore.storeDir != localStore.realStoreDir.get()) { + #ifdef __linux__ + useChroot = true; + #else + throw Error("building using a diverted store is not supported on this platform"); + #endif + } + + #ifdef __linux__ + if (useChroot) { + if (!mountAndPidNamespacesSupported()) { + if (!settings.sandboxFallback) + throw Error("this system does not support the kernel namespaces that are required for sandboxing; use '--no-sandbox' to disable sandboxing"); + debug("auto-disabling sandboxing because the prerequisite namespaces are not available"); + useChroot = false; + } + } + #endif + + if (useBuildUsers()) { + if (!buildUser) + buildUser = acquireUserLock(drvOptions->useUidRange(*drv) ? 65536 : 1, useChroot); + + if (!buildUser) { + return false; + } + } + + return true; +} + + +std::variant, SingleDrvOutputs> DerivationBuilder::unprepareBuild() +{ + Finally releaseBuildUser([&](){ + /* Release the build user at the end of this function. We don't do + it right away because we don't want another build grabbing this + uid and then messing around with our output. */ + buildUser.reset(); + }); + + sandboxMountNamespace = -1; + sandboxUserNamespace = -1; + + /* Since we got an EOF on the logger pipe, the builder is presumed + to have terminated. In fact, the builder could also have + simply have closed its end of the pipe, so just to be sure, + kill it. */ + int status = pid.kill(); + + debug("builder process for '%s' finished", store.printStorePath(drvPath)); + + buildResult.timesBuilt++; + buildResult.stopTime = time(0); + + /* So the child is gone now. */ + miscMethods.childTerminated(); + + /* Close the read side of the logger pipe. */ + builderOut.close(); + + /* Close the log file. */ + miscMethods.closeLogFile(); + + /* When running under a build user, make sure that all processes + running under that uid are gone. This is to prevent a + malicious user from leaving behind a process that keeps files + open and modifies them after they have been chown'ed to + root. */ + killSandbox(true); + + /* Terminate the recursive Nix daemon. */ + stopDaemon(); + + if (buildResult.cpuUser && buildResult.cpuSystem) { + debug("builder for '%s' terminated with status %d, user CPU %.3fs, system CPU %.3fs", + store.printStorePath(drvPath), + status, + ((double) buildResult.cpuUser->count()) / 1000000, + ((double) buildResult.cpuSystem->count()) / 1000000); + } + + bool diskFull = false; + + try { + + /* Check the exit status. */ + if (!statusOk(status)) { + + diskFull |= cleanupDecideWhetherDiskFull(); + + auto msg = fmt("builder for '%s' %s", + Magenta(store.printStorePath(drvPath)), + statusToString(status)); + + miscMethods.appendLogTailErrorMsg(msg); + + if (diskFull) + msg += "\nnote: build failure may have been caused by lack of free disk space"; + + throw BuildError(msg); + } + + /* Compute the FS closure of the outputs and register them as + being valid. */ + auto builtOutputs = registerOutputs(); + + StorePathSet outputPaths; + for (auto & [_, output] : builtOutputs) + outputPaths.insert(output.outPath); + runPostBuildHook( + store, + *logger, + drvPath, + outputPaths + ); + + /* Delete unused redirected outputs (when doing hash rewriting). */ + for (auto & i : redirectedOutputs) + deletePath(store.Store::toRealPath(i.second)); + + /* Delete the chroot (if we were using one). */ + autoDelChroot.reset(); /* this runs the destructor */ + + deleteTmpDir(true); + + return std::move(builtOutputs); + + } catch (BuildError & e) { + assert(derivationType); + BuildResult::Status st = + dynamic_cast(&e) ? BuildResult::NotDeterministic : + statusOk(status) ? BuildResult::OutputRejected : + !derivationType->isSandboxed() || diskFull ? BuildResult::TransientFailure : + BuildResult::PermanentFailure; + + return std::pair{std::move(st), std::move(e)}; + } +} + + +static void chmod_(const Path & path, mode_t mode) +{ + if (chmod(path.c_str(), mode) == -1) + throw SysError("setting permissions on '%s'", path); +} + + +/* Move/rename path 'src' to 'dst'. Temporarily make 'src' writable if + it's a directory and we're not root (to be able to update the + directory's parent link ".."). */ +static void movePath(const Path & src, const Path & dst) +{ + auto st = lstat(src); + + bool changePerm = (geteuid() && S_ISDIR(st.st_mode) && !(st.st_mode & S_IWUSR)); + + if (changePerm) + chmod_(src, st.st_mode | S_IWUSR); + + std::filesystem::rename(src, dst); + + if (changePerm) + chmod_(dst, st.st_mode); +} + + +extern void replaceValidPath(const Path & storePath, const Path & tmpPath); + + +bool DerivationBuilder::cleanupDecideWhetherDiskFull() +{ + bool diskFull = false; + + /* Heuristically check whether the build failure may have + been caused by a disk full condition. We have no way + of knowing whether the build actually got an ENOSPC. + So instead, check if the disk is (nearly) full now. If + so, we don't mark this build as a permanent failure. */ +#if HAVE_STATVFS + { + auto & localStore = getLocalStore(); + uint64_t required = 8ULL * 1024 * 1024; // FIXME: make configurable + struct statvfs st; + if (statvfs(localStore.realStoreDir.get().c_str(), &st) == 0 && + (uint64_t) st.f_bavail * st.f_bsize < required) + diskFull = true; + if (statvfs(tmpDir.c_str(), &st) == 0 && + (uint64_t) st.f_bavail * st.f_bsize < required) + diskFull = true; + } +#endif + + deleteTmpDir(false); + + /* Move paths out of the chroot for easier debugging of + build failures. */ + if (useChroot && buildMode == bmNormal) + for (auto & [_, status] : initialOutputs) { + if (!status.known) continue; + if (buildMode != bmCheck && status.known->isValid()) continue; + auto p = store.toRealPath(status.known->path); + if (pathExists(chrootRootDir + p)) + std::filesystem::rename((chrootRootDir + p), p); + } + + return diskFull; +} + + +#ifdef __linux__ +static void doBind(const Path & source, const Path & target, bool optional = false) { + debug("bind mounting '%1%' to '%2%'", source, target); + + auto bindMount = [&]() { + 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); + }; + + auto maybeSt = maybeLstat(source); + if (!maybeSt) { + if (optional) + return; + else + throw SysError("getting attributes of path '%1%'", source); + } + auto st = *maybeSt; + + if (S_ISDIR(st.st_mode)) { + createDirs(target); + bindMount(); + } else if (S_ISLNK(st.st_mode)) { + // Symlinks can (apparently) not be bind-mounted, so just copy it + createDirs(dirOf(target)); + copyFile( + std::filesystem::path(source), + std::filesystem::path(target), false); + } else { + createDirs(dirOf(target)); + writeFile(target, ""); + bindMount(); + } +}; +#endif + +/** + * Rethrow the current exception as a subclass of `Error`. + */ +static void rethrowExceptionAsError() +{ + try { + throw; + } catch (Error &) { + throw; + } catch (std::exception & e) { + throw Error(e.what()); + } catch (...) { + throw Error("unknown exception"); + } +} + +/** + * Send the current exception to the parent in the format expected by + * `DerivationBuilder::processSandboxSetupMessages()`. + */ +static void handleChildException(bool sendException) +{ + try { + rethrowExceptionAsError(); + } catch (Error & e) { + if (sendException) { + writeFull(STDERR_FILENO, "\1\n"); + FdSink sink(STDERR_FILENO); + sink << e; + sink.flush(); + } else + std::cerr << e.msg(); + } +} + +void DerivationBuilder::startBuilder() +{ + if ((buildUser && buildUser->getUIDCount() != 1) + #ifdef __linux__ + || settings.useCgroups + #endif + ) + { + #ifdef __linux__ + experimentalFeatureSettings.require(Xp::Cgroups); + + /* If we're running from the daemon, then this will return the + root cgroup of the service. Otherwise, it will return the + current cgroup. */ + auto rootCgroup = getRootCgroup(); + auto cgroupFS = getCgroupFS(); + if (!cgroupFS) + throw Error("cannot determine the cgroups file system"); + auto rootCgroupPath = canonPath(*cgroupFS + "/" + rootCgroup); + if (!pathExists(rootCgroupPath)) + throw Error("expected cgroup directory '%s'", rootCgroupPath); + + static std::atomic counter{0}; + + cgroup = buildUser + ? fmt("%s/nix-build-uid-%d", rootCgroupPath, buildUser->getUID()) + : fmt("%s/nix-build-pid-%d-%d", rootCgroupPath, getpid(), counter++); + + debug("using cgroup '%s'", *cgroup); + + /* When using a build user, record the cgroup we used for that + user so that if we got interrupted previously, we can kill + any left-over cgroup first. */ + if (buildUser) { + auto cgroupsDir = settings.nixStateDir + "/cgroups"; + createDirs(cgroupsDir); + + auto cgroupFile = fmt("%s/%d", cgroupsDir, buildUser->getUID()); + + if (pathExists(cgroupFile)) { + auto prevCgroup = readFile(cgroupFile); + destroyCgroup(prevCgroup); + } + + writeFile(cgroupFile, *cgroup); + } + + #else + throw Error("cgroups are not supported on this platform"); + #endif + } + + /* Make sure that no other processes are executing under the + sandbox uids. This must be done before any chownToBuilder() + calls. */ + killSandbox(false); + + /* Right platform? */ + if (!drvOptions->canBuildLocally(store, *drv)) { + // since aarch64-darwin has Rosetta 2, this user can actually run x86_64-darwin on their hardware - we should tell them to run the command to install Darwin 2 + if (drv->platform == "x86_64-darwin" && settings.thisSystem == "aarch64-darwin") { + throw Error("run `/usr/sbin/softwareupdate --install-rosetta` to enable your %s to run programs for %s", settings.thisSystem, drv->platform); + } else { + throw Error("a '%s' with features {%s} is required to build '%s', but I am a '%s' with features {%s}", + drv->platform, + concatStringsSep(", ", drvOptions->getRequiredSystemFeatures(*drv)), + store.printStorePath(drvPath), + settings.thisSystem, + concatStringsSep(", ", store.systemFeatures)); + } + } + + /* Create a temporary directory where the build will take + place. */ + topTmpDir = createTempDir(settings.buildDir.get().value_or(""), "nix-build-" + std::string(drvPath.name()), false, false, 0700); +#ifdef __APPLE__ + if (false) { +#else + if (useChroot) { +#endif + /* If sandboxing is enabled, put the actual TMPDIR underneath + an inaccessible root-owned directory, to prevent outside + access. + + On macOS, we don't use an actual chroot, so this isn't + possible. Any mitigation along these lines would have to be + done directly in the sandbox profile. */ + tmpDir = topTmpDir + "/build"; + createDir(tmpDir, 0700); + } else { + tmpDir = topTmpDir; + } + chownToBuilder(tmpDir); + + for (auto & [outputName, status] : initialOutputs) { + /* Set scratch path we'll actually use during the build. + + If we're not doing a chroot build, but we have some valid + output paths. Since we can't just overwrite or delete + them, we have to do hash rewriting: i.e. in the + environment/arguments passed to the build, we replace the + hashes of the valid outputs with unique dummy strings; + after the build, we discard the redirected outputs + corresponding to the valid outputs, and rewrite the + contents of the new outputs to replace the dummy strings + with the actual hashes. */ + auto scratchPath = + !status.known + ? makeFallbackPath(outputName) + : !needsHashRewrite() + /* Can always use original path in sandbox */ + ? status.known->path + : !status.known->isPresent() + /* If path doesn't yet exist can just use it */ + ? status.known->path + : buildMode != bmRepair && !status.known->isValid() + /* If we aren't repairing we'll delete a corrupted path, so we + can use original path */ + ? status.known->path + : /* If we are repairing or the path is totally valid, we'll need + to use a temporary path */ + makeFallbackPath(status.known->path); + scratchOutputs.insert_or_assign(outputName, scratchPath); + + /* Substitute output placeholders with the scratch output paths. + We'll use during the build. */ + inputRewrites[hashPlaceholder(outputName)] = store.printStorePath(scratchPath); + + /* Additional tasks if we know the final path a priori. */ + if (!status.known) continue; + auto fixedFinalPath = status.known->path; + + /* Additional tasks if the final and scratch are both known and + differ. */ + if (fixedFinalPath == scratchPath) continue; + + /* Ensure scratch path is ours to use. */ + deletePath(store.printStorePath(scratchPath)); + + /* Rewrite and unrewrite paths */ + { + std::string h1 { fixedFinalPath.hashPart() }; + std::string h2 { scratchPath.hashPart() }; + inputRewrites[h1] = h2; + } + + redirectedOutputs.insert_or_assign(std::move(fixedFinalPath), std::move(scratchPath)); + } + + /* Construct the environment passed to the builder. */ + initEnv(); + + writeStructuredAttrs(); + + /* Handle exportReferencesGraph(), if set. */ + if (!parsedDrv->hasStructuredAttrs()) { + /* The `exportReferencesGraph' feature allows the references graph + to be passed to a builder. This attribute should be a list of + pairs [name1 path1 name2 path2 ...]. The references graph of + each `pathN' will be stored in a text file `nameN' in the + temporary build directory. The text files have the format used + by `nix-store --register-validity'. However, the deriver + fields are left empty. */ + auto s = getOr(drv->env, "exportReferencesGraph", ""); + Strings ss = tokenizeString(s); + if (ss.size() % 2 != 0) + throw BuildError("odd number of tokens in 'exportReferencesGraph': '%1%'", s); + for (Strings::iterator i = ss.begin(); i != ss.end(); ) { + auto fileName = *i++; + static std::regex regex("[A-Za-z_][A-Za-z0-9_.-]*"); + if (!std::regex_match(fileName, regex)) + throw Error("invalid file name '%s' in 'exportReferencesGraph'", fileName); + + auto storePathS = *i++; + if (!store.isInStore(storePathS)) + throw BuildError("'exportReferencesGraph' contains a non-store path '%1%'", storePathS); + auto storePath = store.toStorePath(storePathS).first; + + /* Write closure info to . */ + writeFile(tmpDir + "/" + fileName, + store.makeValidityRegistration( + store.exportReferences({storePath}, inputPaths), false, false)); + } + } + + if (useChroot) { + + /* Allow a user-configurable set of directories from the + host file system. */ + pathsInChroot.clear(); + + for (auto i : settings.sandboxPaths.get()) { + if (i.empty()) continue; + bool optional = false; + if (i[i.size() - 1] == '?') { + optional = true; + i.pop_back(); + } + size_t p = i.find('='); + if (p == std::string::npos) + pathsInChroot[i] = {i, optional}; + else + pathsInChroot[i.substr(0, p)] = {i.substr(p + 1), optional}; + } + if (hasPrefix(store.storeDir, tmpDirInSandbox)) + { + throw Error("`sandbox-build-dir` must not contain the storeDir"); + } + pathsInChroot[tmpDirInSandbox] = tmpDir; + + /* Add the closure of store paths to the chroot. */ + StorePathSet closure; + for (auto & i : pathsInChroot) + try { + if (store.isInStore(i.second.source)) + store.computeFSClosure(store.toStorePath(i.second.source).first, closure); + } catch (InvalidPath & e) { + } catch (Error & e) { + e.addTrace({}, "while processing 'sandbox-paths'"); + throw; + } + for (auto & i : closure) { + auto p = store.printStorePath(i); + pathsInChroot.insert_or_assign(p, p); + } + + PathSet allowedPaths = settings.allowedImpureHostPrefixes; + + /* This works like the above, except on a per-derivation level */ + auto impurePaths = drvOptions->impureHostDeps; + + for (auto & i : impurePaths) { + bool found = false; + /* Note: we're not resolving symlinks here to prevent + giving a non-root user info about inaccessible + files. */ + Path canonI = canonPath(i); + /* If only we had a trie to do this more efficiently :) luckily, these are generally going to be pretty small */ + for (auto & a : allowedPaths) { + Path canonA = canonPath(a); + if (isDirOrInDir(canonI, canonA)) { + found = true; + break; + } + } + if (!found) + throw Error("derivation '%s' requested impure path '%s', but it was not in allowed-impure-host-deps", + store.printStorePath(drvPath), i); + + /* Allow files in drvOptions->impureHostDeps to be missing; e.g. + macOS 11+ has no /usr/lib/libSystem*.dylib */ + pathsInChroot[i] = {i, true}; + } + +#ifdef __linux__ + /* Create a temporary directory in which we set up the chroot + environment using bind-mounts. We put it in the Nix store + so that the build outputs can be moved efficiently from the + chroot to their final location. */ + auto chrootParentDir = store.Store::toRealPath(drvPath) + ".chroot"; + deletePath(chrootParentDir); + + /* Clean up the chroot directory automatically. */ + autoDelChroot = std::make_shared(chrootParentDir); + + printMsg(lvlChatty, "setting up chroot environment in '%1%'", chrootParentDir); + + if (mkdir(chrootParentDir.c_str(), 0700) == -1) + throw SysError("cannot create '%s'", chrootRootDir); + + chrootRootDir = chrootParentDir + "/root"; + + if (mkdir(chrootRootDir.c_str(), buildUser && buildUser->getUIDCount() != 1 ? 0755 : 0750) == -1) + throw SysError("cannot create '%1%'", chrootRootDir); + + if (buildUser && chown(chrootRootDir.c_str(), buildUser->getUIDCount() != 1 ? buildUser->getUID() : 0, buildUser->getGID()) == -1) + throw SysError("cannot change ownership of '%1%'", chrootRootDir); + + /* Create a writable /tmp in the chroot. Many builders need + this. (Of course they should really respect $TMPDIR + instead.) */ + Path chrootTmpDir = chrootRootDir + "/tmp"; + createDirs(chrootTmpDir); + chmod_(chrootTmpDir, 01777); + + /* Create a /etc/passwd with entries for the build user and the + nobody account. The latter is kind of a hack to support + Samba-in-QEMU. */ + createDirs(chrootRootDir + "/etc"); + if (drvOptions->useUidRange(*drv)) + chownToBuilder(chrootRootDir + "/etc"); + + if (drvOptions->useUidRange(*drv) && (!buildUser || buildUser->getUIDCount() < 65536)) + throw Error("feature 'uid-range' requires the setting '%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"). */ + writeFile(chrootRootDir + "/etc/group", + fmt("root:x:0:\n" + "nixbld:!:%1%:\n" + "nogroup:x:65534:\n", sandboxGid())); + + /* Create /etc/hosts with localhost entry. */ + if (derivationType->isSandboxed()) + writeFile(chrootRootDir + "/etc/hosts", "127.0.0.1 localhost\n::1 localhost\n"); + + /* Make the closure of the inputs available in the chroot, + rather than the whole Nix store. This prevents any access + to undeclared dependencies. Directories are bind-mounted, + while other inputs are hard-linked (since only directories + can be bind-mounted). !!! As an extra security + precaution, make the fake Nix store only writable by the + build user. */ + Path chrootStoreDir = chrootRootDir + store.storeDir; + createDirs(chrootStoreDir); + chmod_(chrootStoreDir, 01775); + + if (buildUser && chown(chrootStoreDir.c_str(), 0, buildUser->getGID()) == -1) + throw SysError("cannot change ownership of '%1%'", chrootStoreDir); + + for (auto & i : inputPaths) { + auto p = store.printStorePath(i); + Path r = store.toRealPath(p); + pathsInChroot.insert_or_assign(p, r); + } + + /* If we're repairing, checking or rebuilding part of a + multiple-outputs derivation, it's possible that we're + rebuilding a path that is in settings.sandbox-paths + (typically the dependencies of /bin/sh). Throw them + out. */ + for (auto & i : drv->outputsAndOptPaths(store)) { + /* If the name isn't known a priori (i.e. floating + content-addressing derivation), the temporary location we use + should be fresh. Freshness means it is impossible that the path + is already in the sandbox, so we don't need to worry about + removing it. */ + if (i.second.second) + pathsInChroot.erase(store.printStorePath(*i.second.second)); + } + + if (cgroup) { + if (mkdir(cgroup->c_str(), 0755) != 0) + throw SysError("creating cgroup '%s'", *cgroup); + chownToBuilder(*cgroup); + chownToBuilder(*cgroup + "/cgroup.procs"); + chownToBuilder(*cgroup + "/cgroup.threads"); + //chownToBuilder(*cgroup + "/cgroup.subtree_control"); + } + +#else + if (drvOptions->useUidRange(*drv)) + throw Error("feature 'uid-range' is not supported on this platform"); + #ifdef __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 (drvOptions->useUidRange(*drv)) + throw Error("feature 'uid-range' is only supported in sandboxed builds"); + } + + if (needsHashRewrite() && pathExists(homeDir)) + throw Error("home directory '%1%' exists; please remove it to assure purity of builds without sandboxing", homeDir); + + if (useChroot && settings.preBuildHook != "" && dynamic_cast(drv.get())) { + printMsg(lvlChatty, "executing pre-build hook '%1%'", settings.preBuildHook); + auto args = useChroot ? Strings({store.printStorePath(drvPath), chrootRootDir}) : + Strings({ store.printStorePath(drvPath) }); + enum BuildHookState { + stBegin, + stExtraChrootDirs + }; + auto state = stBegin; + auto lines = runProgram(settings.preBuildHook, false, args); + auto lastPos = std::string::size_type{0}; + for (auto nlPos = lines.find('\n'); nlPos != std::string::npos; + nlPos = lines.find('\n', lastPos)) + { + auto line = lines.substr(lastPos, nlPos - lastPos); + lastPos = nlPos + 1; + if (state == stBegin) { + if (line == "extra-sandbox-paths" || line == "extra-chroot-dirs") { + state = stExtraChrootDirs; + } else { + throw Error("unknown pre-build hook command '%1%'", line); + } + } else if (state == stExtraChrootDirs) { + if (line == "") { + state = stBegin; + } else { + auto p = line.find('='); + if (p == std::string::npos) + pathsInChroot[line] = line; + else + pathsInChroot[line.substr(0, p)] = line.substr(p + 1); + } + } + } + } + + /* Fire up a Nix daemon to process recursive Nix calls from the + builder. */ + if (drvOptions->getRequiredSystemFeatures(*drv).count("recursive-nix")) + startDaemon(); + + /* Run the builder. */ + printMsg(lvlChatty, "executing builder '%1%'", drv->builder); + printMsg(lvlChatty, "using builder args '%1%'", concatStringsSep(" ", drv->args)); + for (auto & i : drv->env) + printMsg(lvlVomit, "setting builder env variable '%1%'='%2%'", i.first, i.second); + + /* Create the log file. */ + [[maybe_unused]] Path logFile = miscMethods.openLogFile(); + + /* Create a pseudoterminal to get the output of the builder. */ + builderOut = posix_openpt(O_RDWR | O_NOCTTY); + if (!builderOut) + throw SysError("opening pseudoterminal master"); + + // FIXME: not thread-safe, use ptsname_r + std::string slaveName = ptsname(builderOut.get()); + + if (buildUser) { + if (chmod(slaveName.c_str(), 0600)) + throw SysError("changing mode of pseudoterminal slave"); + + if (chown(slaveName.c_str(), buildUser->getUID(), 0)) + throw SysError("changing owner of pseudoterminal slave"); + } +#ifdef __APPLE__ + else { + if (grantpt(builderOut.get())) + throw SysError("granting access to pseudoterminal slave"); + } +#endif + + if (unlockpt(builderOut.get())) + throw SysError("unlocking pseudoterminal"); + + /* Open the slave side of the pseudoterminal and use it as stderr. */ + auto openSlave = [&]() + { + AutoCloseFD builderOut = open(slaveName.c_str(), O_RDWR | O_NOCTTY); + if (!builderOut) + throw SysError("opening pseudoterminal slave"); + + // Put the pt into raw mode to prevent \n -> \r\n translation. + struct termios term; + if (tcgetattr(builderOut.get(), &term)) + throw SysError("getting pseudoterminal attributes"); + + cfmakeraw(&term); + + if (tcsetattr(builderOut.get(), TCSANOW, &term)) + throw SysError("putting pseudoterminal into raw mode"); + + if (dup2(builderOut.get(), STDERR_FILENO) == -1) + throw SysError("cannot pipe standard error into log file"); + }; + + buildResult.startTime = time(0); + + /* Fork a child to build the package. */ + +#ifdef __linux__ + if (useChroot) { + /* Set up private namespaces for the build: + + - The PID namespace causes the build to start as PID 1. + Processes outside of the chroot are not visible to those + on the inside, but processes inside the chroot are + visible from the outside (though with different PIDs). + + - The private mount namespace ensures that all the bind + mounts we do will only show up in this process and its + children, and will disappear automatically when we're + done. + + - The private network namespace ensures that the builder + cannot talk to the outside world (or vice versa). It + only has a private loopback interface. (Fixed-output + derivations are not run in a private network namespace + to allow functions like fetchurl to work.) + + - The IPC namespace prevents the builder from communicating + with outside processes using SysV IPC mechanisms (shared + memory, message queues, semaphores). It also ensures + that all IPC objects are destroyed when the builder + exits. + + - The UTS namespace ensures that builders see a hostname of + localhost rather than the actual hostname. + + We use a helper process to do the clone() to work around + clone() being broken in multi-threaded programs due to + at-fork handlers not being run. Note that we use + CLONE_PARENT to ensure that the real builder is parented to + us. + */ + + userNamespaceSync.create(); + + usingUserNamespace = userNamespacesSupported(); + + Pipe sendPid; + sendPid.create(); + + Pid helper = startProcess([&]() { + sendPid.readSide.close(); + + /* We need to open the slave early, before + CLONE_NEWUSER. Otherwise we get EPERM when running as + root. */ + openSlave(); + + try { + /* Drop additional groups here because we can't do it + after we've created the new user namespace. */ + if (setgroups(0, 0) == -1) { + if (errno != EPERM) + throw SysError("setgroups failed"); + if (settings.requireDropSupplementaryGroups) + throw Error("setgroups failed. Set the require-drop-supplementary-groups option to false to skip this step."); + } + + ProcessOptions options; + options.cloneFlags = CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWIPC | CLONE_NEWUTS | CLONE_PARENT | SIGCHLD; + if (derivationType->isSandboxed()) + options.cloneFlags |= CLONE_NEWNET; + if (usingUserNamespace) + options.cloneFlags |= CLONE_NEWUSER; + + pid_t child = startProcess([&]() { runChild(); }, options); + + writeFull(sendPid.writeSide.get(), fmt("%d\n", child)); + _exit(0); + } catch (...) { + handleChildException(true); + _exit(1); + } + }); + + sendPid.writeSide.close(); + + if (helper.wait() != 0) { + processSandboxSetupMessages(); + // Only reached if the child process didn't send an exception. + throw Error("unable to start build process"); + } + + userNamespaceSync.readSide = -1; + + /* Close the write side to prevent runChild() from hanging + reading from this. */ + Finally cleanup([&]() { + userNamespaceSync.writeSide = -1; + }); + + auto ss = tokenizeString>(readLine(sendPid.readSide.get())); + assert(ss.size() == 1); + pid = string2Int(ss[0]).value(); + + if (usingUserNamespace) { + /* Set the UID/GID mapping of the builder's user namespace + such that the sandbox user maps to the build user, or to + the calling user (if build users are disabled). */ + uid_t hostUid = buildUser ? buildUser->getUID() : getuid(); + uid_t hostGid = buildUser ? buildUser->getGID() : getgid(); + uid_t nrIds = buildUser ? buildUser->getUIDCount() : 1; + + writeFile("/proc/" + std::to_string(pid) + "/uid_map", + fmt("%d %d %d", sandboxUid(), hostUid, nrIds)); + + if (!buildUser || buildUser->getUIDCount() == 1) + writeFile("/proc/" + std::to_string(pid) + "/setgroups", "deny"); + + writeFile("/proc/" + std::to_string(pid) + "/gid_map", + fmt("%d %d %d", sandboxGid(), hostGid, nrIds)); + } else { + debug("note: not using a user namespace"); + if (!buildUser) + throw Error("cannot perform a sandboxed build because user namespaces are not enabled; check /proc/sys/user/max_user_namespaces"); + } + + /* Now that we now the sandbox uid, we can write + /etc/passwd. */ + writeFile(chrootRootDir + "/etc/passwd", fmt( + "root:x:0:0:Nix build user:%3%:/noshell\n" + "nixbld:x:%1%:%2%:Nix build user:%3%:/noshell\n" + "nobody:x:65534:65534:Nobody:/:/noshell\n", + sandboxUid(), sandboxGid(), settings.sandboxBuildDir)); + + /* Save the mount- and user 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"); + + if (usingUserNamespace) { + sandboxUserNamespace = open(fmt("/proc/%d/ns/user", (pid_t) pid).c_str(), O_RDONLY); + if (sandboxUserNamespace.get() == -1) + throw SysError("getting sandbox user namespace"); + } + + /* Move the child into its own cgroup. */ + if (cgroup) + writeFile(*cgroup + "/cgroup.procs", fmt("%d", (pid_t) pid)); + + /* Signal the builder that we've updated its user namespace. */ + writeFull(userNamespaceSync.writeSide.get(), "1"); + + } else +#endif + { + pid = startProcess([&]() { + openSlave(); + runChild(); + }); + } + + /* parent */ + pid.setSeparatePG(true); + miscMethods.childStarted(); + + processSandboxSetupMessages(); +} + + +void DerivationBuilder::processSandboxSetupMessages() +{ + std::vector msgs; + while (true) { + std::string msg = [&]() { + try { + return readLine(builderOut.get()); + } catch (Error & e) { + auto status = pid.wait(); + e.addTrace({}, "while waiting for the build environment for '%s' to initialize (%s, previous messages: %s)", + store.printStorePath(drvPath), + statusToString(status), + concatStringsSep("|", msgs)); + throw; + } + }(); + if (msg.substr(0, 1) == "\2") break; + if (msg.substr(0, 1) == "\1") { + FdSource source(builderOut.get()); + auto ex = readError(source); + ex.addTrace({}, "while setting up the build environment"); + throw ex; + } + debug("sandbox setup: " + msg); + msgs.push_back(std::move(msg)); + } +} + + +void DerivationBuilder::initTmpDir() +{ + /* In a sandbox, for determinism, always use the same temporary + directory. */ +#ifdef __linux__ + tmpDirInSandbox = useChroot ? settings.sandboxBuildDir : tmpDir; +#else + tmpDirInSandbox = tmpDir; +#endif + + /* In non-structured mode, set all bindings either directory in the + environment or via a file, as specified by + `DerivationOptions::passAsFile`. */ + if (!parsedDrv->hasStructuredAttrs()) { + for (auto & i : drv->env) { + if (drvOptions->passAsFile.find(i.first) == drvOptions->passAsFile.end()) { + env[i.first] = i.second; + } else { + auto hash = hashString(HashAlgorithm::SHA256, i.first); + std::string fn = ".attr-" + hash.to_string(HashFormat::Nix32, false); + Path p = tmpDir + "/" + fn; + writeFile(p, rewriteStrings(i.second, inputRewrites)); + chownToBuilder(p); + env[i.first + "Path"] = tmpDirInSandbox + "/" + fn; + } + } + + } + + /* For convenience, set an environment pointing to the top build + directory. */ + env["NIX_BUILD_TOP"] = tmpDirInSandbox; + + /* Also set TMPDIR and variants to point to this directory. */ + env["TMPDIR"] = env["TEMPDIR"] = env["TMP"] = env["TEMP"] = tmpDirInSandbox; + + /* Explicitly set PWD to prevent problems with chroot builds. In + particular, dietlibc cannot figure out the cwd because the + inode of the current directory doesn't appear in .. (because + getdents returns the inode of the mount point). */ + env["PWD"] = tmpDirInSandbox; +} + + +void DerivationBuilder::initEnv() +{ + env.clear(); + + /* Most shells initialise PATH to some default (/bin:/usr/bin:...) when + PATH is not set. We don't want this, so we fill it in with some dummy + value. */ + env["PATH"] = "/path-not-set"; + + /* Set HOME to a non-existing path to prevent certain programs from using + /etc/passwd (or NIS, or whatever) to locate the home directory (for + example, wget looks for ~/.wgetrc). I.e., these tools use /etc/passwd + if HOME is not set, but they will just assume that the settings file + they are looking for does not exist if HOME is set but points to some + non-existing path. */ + env["HOME"] = homeDir; + + /* Tell the builder where the Nix store is. Usually they + shouldn't care, but this is useful for purity checking (e.g., + the compiler or linker might only want to accept paths to files + in the store or in the build directory). */ + env["NIX_STORE"] = store.storeDir; + + /* The maximum number of cores to utilize for parallel building. */ + env["NIX_BUILD_CORES"] = fmt("%d", settings.buildCores); + + initTmpDir(); + + /* Compatibility hack with Nix <= 0.7: if this is a fixed-output + derivation, tell the builder, so that for instance `fetchurl' + can skip checking the output. On older Nixes, this environment + variable won't be set, so `fetchurl' will do the check. */ + if (derivationType->isFixed()) env["NIX_OUTPUT_CHECKED"] = "1"; + + /* *Only* if this is a fixed-output derivation, propagate the + values of the environment variables specified in the + `impureEnvVars' attribute to the builder. This allows for + instance environment variables for proxy configuration such as + `http_proxy' to be easily passed to downloaders like + `fetchurl'. Passing such environment variables from the caller + to the builder is generally impure, but the output of + fixed-output derivations is by definition pure (since we + already know the cryptographic hash of the output). */ + if (!derivationType->isSandboxed()) { + auto & impureEnv = settings.impureEnv.get(); + if (!impureEnv.empty()) + experimentalFeatureSettings.require(Xp::ConfigurableImpureEnv); + + for (auto & i : drvOptions->impureEnvVars){ + auto envVar = impureEnv.find(i); + if (envVar != impureEnv.end()) { + env[i] = envVar->second; + } else { + env[i] = getEnv(i).value_or(""); + } + } + } + + /* Currently structured log messages piggyback on stderr, but we + may change that in the future. So tell the builder which file + descriptor to use for that. */ + env["NIX_LOG_FD"] = "2"; + + /* Trigger colored output in various tools. */ + env["TERM"] = "xterm-256color"; +} + + +void DerivationBuilder::writeStructuredAttrs() +{ + if (auto structAttrsJson = parsedDrv->prepareStructuredAttrs(store, inputPaths)) { + auto json = structAttrsJson.value(); + nlohmann::json rewritten; + for (auto & [i, v] : json["outputs"].get()) { + /* The placeholder must have a rewrite, so we use it to cover both the + cases where we know or don't know the output path ahead of time. */ + rewritten[i] = rewriteStrings((std::string) v, inputRewrites); + } + + json["outputs"] = rewritten; + + auto jsonSh = writeStructuredAttrsShell(json); + + writeFile(tmpDir + "/.attrs.sh", rewriteStrings(jsonSh, inputRewrites)); + chownToBuilder(tmpDir + "/.attrs.sh"); + env["NIX_ATTRS_SH_FILE"] = tmpDirInSandbox + "/.attrs.sh"; + writeFile(tmpDir + "/.attrs.json", rewriteStrings(json.dump(), inputRewrites)); + chownToBuilder(tmpDir + "/.attrs.json"); + env["NIX_ATTRS_JSON_FILE"] = tmpDirInSandbox + "/.attrs.json"; + } +} + + +void DerivationBuilder::startDaemon() +{ + experimentalFeatureSettings.require(Xp::RecursiveNix); + + Store::Params params; + params["path-info-cache-size"] = "0"; + params["store"] = store.storeDir; + if (auto & optRoot = getLocalStore().rootDir.get()) + params["root"] = *optRoot; + params["state"] = "/no-such-path"; + params["log"] = "/no-such-path"; + auto store = makeRestrictedStore(params, + ref(std::dynamic_pointer_cast(this->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 || errno == EAGAIN) continue; + if (errno == EINVAL || errno == ECONNABORTED) break; + throw SysError("accepting connection"); + } + + unix::closeOnExec(remote.get()); + + debug("received daemon connection"); + + auto workerThread = std::thread([store, remote{std::move(remote)}]() { + try { + daemon::processConnection( + store, + FdSource(remote.get()), + FdSink(remote.get()), + NotTrusted, daemon::Recursive); + debug("terminated daemon connection"); + } catch (const Interrupted &) { + debug("interrupted daemon connection"); + } catch (SystemError &) { + ignoreExceptionExceptInterrupt(); + } + }); + + daemonWorkerThreads.push_back(std::move(workerThread)); + } + + debug("daemon shutting down"); + }); +} + + +void DerivationBuilder::stopDaemon() +{ + if (daemonSocket && shutdown(daemonSocket.get(), SHUT_RDWR) == -1) { + // According to the POSIX standard, the 'shutdown' function should + // return an ENOTCONN error when attempting to shut down a socket that + // hasn't been connected yet. This situation occurs when the 'accept' + // function is called on a socket without any accepted connections, + // leaving the socket unconnected. While Linux doesn't seem to produce + // an error for sockets that have only been accepted, more + // POSIX-compliant operating systems like OpenBSD, macOS, and others do + // return the ENOTCONN error. Therefore, we handle this error here to + // avoid raising an exception for compliant behaviour. + if (errno == ENOTCONN) { + daemonSocket.close(); + } else { + throw SysError("shutting down daemon socket"); + } + } + + if (daemonThread.joinable()) + daemonThread.join(); + + // FIXME: should prune worker threads more quickly. + // FIXME: shutdown the client socket to speed up worker termination. + for (auto & thread : daemonWorkerThreads) + thread.join(); + daemonWorkerThreads.clear(); + + // release the socket. + daemonSocket.close(); +} + + +void DerivationBuilder::addDependency(const StorePath & 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", store.printStorePath(path)); + + #ifdef __linux__ + + Path source = store.Store::toRealPath(path); + Path target = chrootRootDir + store.printStorePath(path); + + 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", store.printStorePath(path)); + } + + /* 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 (usingUserNamespace && (setns(sandboxUserNamespace.get(), 0) == -1)) + throw SysError("entering sandbox user namespace"); + + if (setns(sandboxMountNamespace.get(), 0) == -1) + throw SysError("entering sandbox mount namespace"); + + doBind(source, target); + + _exit(0); + })); + + int status = child.wait(); + if (status != 0) + throw Error("could not add path '%s' to sandbox", store.printStorePath(path)); + + #else + throw Error("don't know how to make path '%s' (produced by a recursive Nix call) appear in the sandbox", + worker.store.printStorePath(path)); + #endif + + } +} + +void DerivationBuilder::chownToBuilder(const Path & path) +{ + if (!buildUser) return; + if (chown(path.c_str(), buildUser->getUID(), buildUser->getGID()) == -1) + throw SysError("cannot change ownership of '%1%'", path); +} + + +void setupSeccomp() +{ +#ifdef __linux__ + if (!settings.filterSyscalls) return; +#if HAVE_SECCOMP + scmp_filter_ctx ctx; + + if (!(ctx = seccomp_init(SCMP_ACT_ALLOW))) + throw SysError("unable to initialize seccomp mode 2"); + + Finally cleanup([&]() { + seccomp_release(ctx); + }); + + constexpr std::string_view nativeSystem = NIX_LOCAL_SYSTEM; + + if (nativeSystem == "x86_64-linux" && + seccomp_arch_add(ctx, SCMP_ARCH_X86) != 0) + throw SysError("unable to add 32-bit seccomp architecture"); + + if (nativeSystem == "x86_64-linux" && + seccomp_arch_add(ctx, SCMP_ARCH_X32) != 0) + throw SysError("unable to add X32 seccomp architecture"); + + if (nativeSystem == "aarch64-linux" && + seccomp_arch_add(ctx, SCMP_ARCH_ARM) != 0) + printError("unable to add ARM seccomp architecture; this may result in spurious build failures if running 32-bit ARM processes"); + + if (nativeSystem == "mips64-linux" && + seccomp_arch_add(ctx, SCMP_ARCH_MIPS) != 0) + printError("unable to add mips seccomp architecture"); + + if (nativeSystem == "mips64-linux" && + seccomp_arch_add(ctx, SCMP_ARCH_MIPS64N32) != 0) + printError("unable to add mips64-*abin32 seccomp architecture"); + + if (nativeSystem == "mips64el-linux" && + seccomp_arch_add(ctx, SCMP_ARCH_MIPSEL) != 0) + printError("unable to add mipsel seccomp architecture"); + + if (nativeSystem == "mips64el-linux" && + seccomp_arch_add(ctx, SCMP_ARCH_MIPSEL64N32) != 0) + printError("unable to add mips64el-*abin32 seccomp architecture"); + + /* Prevent builders from creating setuid/setgid binaries. */ + for (int perm : { S_ISUID, S_ISGID }) { + if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(chmod), 1, + SCMP_A1(SCMP_CMP_MASKED_EQ, (scmp_datum_t) perm, (scmp_datum_t) perm)) != 0) + throw SysError("unable to add seccomp rule"); + + if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(fchmod), 1, + SCMP_A1(SCMP_CMP_MASKED_EQ, (scmp_datum_t) perm, (scmp_datum_t) perm)) != 0) + throw SysError("unable to add seccomp rule"); + + if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(fchmodat), 1, + SCMP_A2(SCMP_CMP_MASKED_EQ, (scmp_datum_t) perm, (scmp_datum_t) perm)) != 0) + throw SysError("unable to add seccomp rule"); + + if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), NIX_SYSCALL_FCHMODAT2, 1, + SCMP_A2(SCMP_CMP_MASKED_EQ, (scmp_datum_t) perm, (scmp_datum_t) perm)) != 0) + throw SysError("unable to add seccomp rule"); + } + + /* Prevent builders from using EAs or ACLs. Not all filesystems + support these, and they're not allowed in the Nix store because + they're not representable in the NAR serialisation. */ + if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(getxattr), 0) != 0 || + seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(lgetxattr), 0) != 0 || + seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(fgetxattr), 0) != 0 || + seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(setxattr), 0) != 0 || + seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(lsetxattr), 0) != 0 || + seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(fsetxattr), 0) != 0) + throw SysError("unable to add seccomp rule"); + + if (seccomp_attr_set(ctx, SCMP_FLTATR_CTL_NNP, settings.allowNewPrivileges ? 0 : 1) != 0) + throw SysError("unable to set 'no new privileges' seccomp attribute"); + + if (seccomp_load(ctx) != 0) + throw SysError("unable to load seccomp BPF program"); +#else + throw Error( + "seccomp is not supported on this platform; " + "you can bypass this error by setting the option 'filter-syscalls' to false, but note that untrusted builds can then create setuid binaries!"); +#endif +#endif +} + + +void DerivationBuilder::runChild() +{ + /* Warning: in the child we should absolutely not make any SQLite + calls! */ + + bool sendException = true; + + try { /* child */ + + commonChildInit(); + + try { + setupSeccomp(); + } catch (...) { + if (buildUser) throw; + } + + bool setUser = true; + + /* Make the contents of netrc and the CA certificate bundle + available to builtin:fetchurl (which may run under a + different uid and/or in a sandbox). */ + std::string netrcData; + std::string caFileData; + if (drv->isBuiltin() && drv->builder == "builtin:fetchurl") { + try { + netrcData = readFile(settings.netrcFile); + } catch (SystemError &) { } + + try { + caFileData = readFile(settings.caFile); + } catch (SystemError &) { } + } + +#ifdef __linux__ + if (useChroot) { + + userNamespaceSync.writeSide = -1; + + if (drainFD(userNamespaceSync.readSide.get()) != "1") + throw Error("user namespace initialisation failed"); + + userNamespaceSync.readSide = -1; + + if (derivationType->isSandboxed()) { + + /* Initialise the loopback interface. */ + AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP)); + if (!fd) throw SysError("cannot open IP socket"); + + struct ifreq ifr; + strcpy(ifr.ifr_name, "lo"); + ifr.ifr_flags = IFF_UP | IFF_LOOPBACK | IFF_RUNNING; + if (ioctl(fd.get(), SIOCSIFFLAGS, &ifr) == -1) + throw SysError("cannot set loopback interface flags"); + } + + /* Set the hostname etc. to fixed values. */ + char hostname[] = "localhost"; + if (sethostname(hostname, sizeof(hostname)) == -1) + throw SysError("cannot set host name"); + char domainname[] = "(none)"; // kernel default + if (setdomainname(domainname, sizeof(domainname)) == -1) + throw SysError("cannot set domain name"); + + /* Make all filesystems private. This is necessary + because subtrees may have been mounted as "shared" + (MS_SHARED). (Systemd does this, for instance.) Even + though we have a private mount namespace, mounting + filesystems on top of a shared subtree still propagates + outside of the namespace. Making a subtree private is + local to the namespace, though, so setting MS_PRIVATE + does not affect the outside world. */ + if (mount(0, "/", 0, MS_PRIVATE | MS_REC, 0) == -1) + throw SysError("unable to make '/' private"); + + /* Bind-mount chroot directory to itself, to treat it as a + different filesystem from /, as needed for pivot_root. */ + if (mount(chrootRootDir.c_str(), chrootRootDir.c_str(), 0, MS_BIND, 0) == -1) + throw SysError("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 + 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 + bind-mount the host /dev. */ + Strings ss; + if (pathsInChroot.find("/dev") == pathsInChroot.end()) { + createDirs(chrootRootDir + "/dev/shm"); + createDirs(chrootRootDir + "/dev/pts"); + ss.push_back("/dev/full"); + if (store.systemFeatures.get().count("kvm") && pathExists("/dev/kvm")) + ss.push_back("/dev/kvm"); + ss.push_back("/dev/null"); + ss.push_back("/dev/random"); + ss.push_back("/dev/tty"); + ss.push_back("/dev/urandom"); + ss.push_back("/dev/zero"); + createSymlink("/proc/self/fd", chrootRootDir + "/dev/fd"); + createSymlink("/proc/self/fd/0", chrootRootDir + "/dev/stdin"); + createSymlink("/proc/self/fd/1", chrootRootDir + "/dev/stdout"); + createSymlink("/proc/self/fd/2", chrootRootDir + "/dev/stderr"); + } + + /* Fixed-output derivations typically need to access the + network, so give them access to /etc/resolv.conf and so + on. */ + if (!derivationType->isSandboxed()) { + // Only use nss functions to resolve hosts and + // services. Don’t use it for anything else that may + // be configured for this system. This limits the + // potential impurities introduced in fixed-outputs. + writeFile(chrootRootDir + "/etc/nsswitch.conf", "hosts: files dns\nservices: files\n"); + + /* N.B. it is realistic that these paths might not exist. It + happens when testing Nix building fixed-output derivations + within a pure derivation. */ + for (auto & path : { "/etc/resolv.conf", "/etc/services", "/etc/hosts" }) + if (pathExists(path)) + ss.push_back(path); + + if (settings.caFile != "" && pathExists(settings.caFile)) { + Path caFile = settings.caFile; + pathsInChroot.try_emplace("/etc/ssl/certs/ca-certificates.crt", canonPath(caFile, true), true); + } + } + + for (auto & i : ss) { + // For backwards-compatibiliy, resolve all the symlinks in the + // chroot paths + auto canonicalPath = canonPath(i, true); + pathsInChroot.emplace(i, canonicalPath); + } + + /* Bind-mount all the directories from the "host" + filesystem that we want in the chroot + environment. */ + for (auto & i : pathsInChroot) { + if (i.second.source == "/proc") continue; // backwards compatibility + + #if HAVE_EMBEDDED_SANDBOX_SHELL + if (i.second.source == "__embedded_sandbox_shell__") { + static unsigned char sh[] = { + #include "embedded-sandbox-shell.gen.hh" + }; + auto dst = chrootRootDir + i.first; + createDirs(dirOf(dst)); + writeFile(dst, std::string_view((const char *) sh, sizeof(sh))); + chmod_(dst, 0555); + } else + #endif + doBind(i.second.source, chrootRootDir + i.first, i.second.optional); + } + + /* Bind a new instance of procfs on /proc. */ + createDirs(chrootRootDir + "/proc"); + if (mount("none", (chrootRootDir + "/proc").c_str(), "proc", 0, 0) == -1) + throw SysError("mounting /proc"); + + /* Mount sysfs on /sys. */ + if (buildUser && buildUser->getUIDCount() != 1) { + 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, + fmt("size=%s", settings.sandboxShmSize).c_str()) == -1) + throw SysError("mounting /dev/shm"); + + /* Mount a new devpts on /dev/pts. Note that this + requires the kernel to be compiled with + CONFIG_DEVPTS_MULTIPLE_INSTANCES=y (which is the case + if /dev/ptx/ptmx exists). */ + if (pathExists("/dev/pts/ptmx") && + !pathExists(chrootRootDir + "/dev/ptmx") + && !pathsInChroot.count("/dev/pts")) + { + if (mount("none", (chrootRootDir + "/dev/pts").c_str(), "devpts", 0, "newinstance,mode=0620") == 0) + { + createSymlink("/dev/pts/ptmx", chrootRootDir + "/dev/ptmx"); + + /* Make sure /dev/pts/ptmx is world-writable. With some + Linux versions, it is created with permissions 0. */ + chmod_(chrootRootDir + "/dev/pts/ptmx", 0666); + } else { + if (errno != EINVAL) + throw SysError("mounting /dev/pts"); + doBind("/dev/pts", chrootRootDir + "/dev/pts"); + doBind("/dev/ptmx", chrootRootDir + "/dev/ptmx"); + } + } + + /* Make /etc unwritable */ + if (!drvOptions->useUidRange(*drv)) + chmod_(chrootRootDir + "/etc", 0555); + + /* 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"); + + /* 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 (cgroup && 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); + + if (mkdir("real-root", 0500) == -1) + throw SysError("cannot create real-root directory"); + + if (pivot_root(".", "real-root") == -1) + throw SysError("cannot pivot old root directory onto '%1%'", (chrootRootDir + "/real-root")); + + if (chroot(".") == -1) + throw SysError("cannot change root directory to '%1%'", chrootRootDir); + + if (umount2("real-root", MNT_DETACH) == -1) + throw SysError("cannot unmount real root filesystem"); + + if (rmdir("real-root") == -1) + throw SysError("cannot remove real-root directory"); + + /* Switch to the sandbox uid/gid in the user namespace, + which corresponds to the build user or calling user in + the parent namespace. */ + if (setgid(sandboxGid()) == -1) + throw SysError("setgid failed"); + if (setuid(sandboxUid()) == -1) + throw SysError("setuid failed"); + + setUser = false; + } +#endif + + if (chdir(tmpDirInSandbox.c_str()) == -1) + throw SysError("changing into '%1%'", tmpDir); + + /* Close all other file descriptors. */ + unix::closeExtraFDs(); + +#ifdef __linux__ + linux::setPersonality(drv->platform); +#endif + + /* Disable core dumps by default. */ + struct rlimit limit = { 0, RLIM_INFINITY }; + setrlimit(RLIMIT_CORE, &limit); + + // FIXME: set other limits to deterministic values? + + /* Fill in the environment. */ + Strings envStrs; + for (auto & i : env) + envStrs.push_back(rewriteStrings(i.first + "=" + i.second, inputRewrites)); + + /* If we are running in `build-users' mode, then switch to the + user we allocated above. Make sure that we drop all root + privileges. Note that above we have closed all file + descriptors except std*, so that's safe. Also note that + setuid() when run as root sets the real, effective and + saved UIDs. */ + if (setUser && buildUser) { + /* Preserve supplementary groups of the build user, to allow + admins to specify groups such as "kvm". */ + auto gids = buildUser->getSupplementaryGIDs(); + if (setgroups(gids.size(), gids.data()) == -1) + throw SysError("cannot set supplementary groups of build user"); + + if (setgid(buildUser->getGID()) == -1 || + getgid() != buildUser->getGID() || + getegid() != buildUser->getGID()) + throw SysError("setgid failed"); + + if (setuid(buildUser->getUID()) == -1 || + getuid() != buildUser->getUID() || + geteuid() != buildUser->getUID()) + throw SysError("setuid failed"); + } + +#ifdef __APPLE__ + /* This has to appear before import statements. */ + std::string sandboxProfile = "(version 1)\n"; + + if (useChroot) { + + /* Lots and lots and lots of file functions freak out if they can't stat their full ancestry */ + PathSet ancestry; + + /* 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 + particularly efficient... I doubt it'll be a bottleneck in practice */ + for (auto & i : pathsInChroot) { + Path cur = i.first; + while (cur.compare("/") != 0) { + cur = dirOf(cur); + ancestry.insert(cur); + } + } + + /* 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 cur = store.storeDir; + while (cur.compare("/") != 0) { + ancestry.insert(cur); + cur = dirOf(cur); + } + + /* Add all our input paths to the chroot */ + for (auto & i : inputPaths) { + auto p = store.printStorePath(i); + pathsInChroot[p] = p; + } + + /* Violations will go to the syslog if you set this. Unfortunately the destination does not appear to be configurable */ + if (settings.darwinLogSandboxViolations) { + sandboxProfile += "(deny default)\n"; + } else { + sandboxProfile += "(deny default (with no-log))\n"; + } + + sandboxProfile += + #include "sandbox-defaults.sb" + ; + + if (!derivationType->isSandboxed()) + sandboxProfile += + #include "sandbox-network.sb" + ; + + /* Add the output paths we'll use at build-time to the chroot */ + sandboxProfile += "(allow file-read* file-write* process-exec\n"; + for (auto & [_, path] : scratchOutputs) + sandboxProfile += fmt("\t(subpath \"%s\")\n", store.printStorePath(path)); + + sandboxProfile += ")\n"; + + /* Our inputs (transitive dependencies and any impurities computed above) + + without file-write* allowed, access() incorrectly returns EPERM + */ + sandboxProfile += "(allow file-read* file-write* process-exec\n"; + + // We create multiple allow lists, to avoid exceeding a limit in the darwin sandbox interpreter. + // See https://github.com/NixOS/nix/issues/4119 + // We split our allow groups approximately at half the actual limit, 1 << 16 + const size_t breakpoint = sandboxProfile.length() + (1 << 14); + for (auto & i : pathsInChroot) { + + if (sandboxProfile.length() >= breakpoint) { + debug("Sandbox break: %d %d", sandboxProfile.length(), breakpoint); + sandboxProfile += ")\n(allow file-read* file-write* process-exec\n"; + } + + if (i.first != i.second.source) + throw Error( + "can't map '%1%' to '%2%': mismatched impure paths not supported on Darwin", + i.first, i.second.source); + + std::string path = i.first; + auto optSt = maybeLstat(path.c_str()); + if (!optSt) { + if (i.second.optional) + continue; + throw SysError("getting attributes of required path '%s", path); + } + if (S_ISDIR(optSt->st_mode)) + sandboxProfile += fmt("\t(subpath \"%s\")\n", path); + else + sandboxProfile += fmt("\t(literal \"%s\")\n", path); + } + sandboxProfile += ")\n"; + + /* Allow file-read* on full directory hierarchy to self. Allows realpath() */ + sandboxProfile += "(allow file-read*\n"; + for (auto & i : ancestry) { + sandboxProfile += fmt("\t(literal \"%s\")\n", i); + } + sandboxProfile += ")\n"; + + sandboxProfile += drvOptions->additionalSandboxProfile; + } else + sandboxProfile += + #include "sandbox-minimal.sb" + ; + + debug("Generated sandbox profile:"); + debug(sandboxProfile); + + /* The tmpDir in scope points at the temporary build directory for our derivation. Some packages try different mechanisms + to find temporary directories, so we want to open up a broader place for them to put their files, if needed. */ + Path globalTmpDir = canonPath(defaultTempDir(), true); + + /* They don't like trailing slashes on subpath directives */ + while (!globalTmpDir.empty() && globalTmpDir.back() == '/') + globalTmpDir.pop_back(); + + if (getEnv("_NIX_TEST_NO_SANDBOX") != "1") { + Strings sandboxArgs; + sandboxArgs.push_back("_GLOBAL_TMP_DIR"); + sandboxArgs.push_back(globalTmpDir); + if (drvOptions->allowLocalNetworking) { + sandboxArgs.push_back("_ALLOW_LOCAL_NETWORKING"); + sandboxArgs.push_back("1"); + } + char * sandbox_errbuf = nullptr; + if (sandbox_init_with_parameters(sandboxProfile.c_str(), 0, stringsToCharPtrs(sandboxArgs).data(), &sandbox_errbuf)) { + writeFull(STDERR_FILENO, fmt("failed to configure sandbox: %s\n", sandbox_errbuf ? sandbox_errbuf : "(null)")); + _exit(1); + } + } +#endif + + /* Indicate that we managed to set up the build environment. */ + writeFull(STDERR_FILENO, std::string("\2\n")); + + sendException = false; + + /* Execute the program. This should not return. */ + if (drv->isBuiltin()) { + try { + logger = makeJSONLogger(getStandardError()); + + std::map outputs; + for (auto & e : drv->outputs) + outputs.insert_or_assign(e.first, + store.printStorePath(scratchOutputs.at(e.first))); + + if (drv->builder == "builtin:fetchurl") + builtinFetchurl(*drv, outputs, netrcData, caFileData); + else if (drv->builder == "builtin:buildenv") + builtinBuildenv(*drv, outputs); + else if (drv->builder == "builtin:unpack-channel") + builtinUnpackChannel(*drv, outputs); + else + throw Error("unsupported builtin builder '%1%'", drv->builder.substr(8)); + _exit(0); + } catch (std::exception & e) { + writeFull(STDERR_FILENO, e.what() + std::string("\n")); + _exit(1); + } + } + + // Now builder is not builtin + + Strings args; + args.push_back(std::string(baseNameOf(drv->builder))); + + for (auto & i : drv->args) + args.push_back(rewriteStrings(i, inputRewrites)); + +#ifdef __APPLE__ + posix_spawnattr_t attrp; + + if (posix_spawnattr_init(&attrp)) + throw SysError("failed to initialize builder"); + + if (posix_spawnattr_setflags(&attrp, POSIX_SPAWN_SETEXEC)) + throw SysError("failed to initialize builder"); + + if (drv->platform == "aarch64-darwin") { + // Unset kern.curproc_arch_affinity so we can escape Rosetta + int affinity = 0; + sysctlbyname("kern.curproc_arch_affinity", NULL, NULL, &affinity, sizeof(affinity)); + + cpu_type_t cpu = CPU_TYPE_ARM64; + posix_spawnattr_setbinpref_np(&attrp, 1, &cpu, NULL); + } else if (drv->platform == "x86_64-darwin") { + cpu_type_t cpu = CPU_TYPE_X86_64; + posix_spawnattr_setbinpref_np(&attrp, 1, &cpu, NULL); + } + + posix_spawn(NULL, drv->builder.c_str(), NULL, &attrp, stringsToCharPtrs(args).data(), stringsToCharPtrs(envStrs).data()); +#else + execve(drv->builder.c_str(), stringsToCharPtrs(args).data(), stringsToCharPtrs(envStrs).data()); +#endif + + throw SysError("executing '%1%'", drv->builder); + + } catch (...) { + handleChildException(sendException); + _exit(1); + } +} + + +SingleDrvOutputs DerivationBuilder::registerOutputs() +{ + std::map infos; + + /* Set of inodes seen during calls to canonicalisePathMetaData() + for this build's outputs. This needs to be shared between + outputs to allow hard links between outputs. */ + InodesSeen inodesSeen; + + Path checkSuffix = ".check"; + + 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. */ + StorePathSet referenceablePaths; + for (auto & p : inputPaths) referenceablePaths.insert(p); + for (auto & i : scratchOutputs) referenceablePaths.insert(i.second); + for (auto & p : addedPaths) referenceablePaths.insert(p); + + /* FIXME `needsHashRewrite` should probably be removed and we get to the + real reason why we aren't using the chroot dir */ + auto toRealPathChroot = [&](const Path & p) -> Path { + return useChroot && !needsHashRewrite() + ? chrootRootDir + p + : store.toRealPath(p); + }; + + /* Check whether the output paths were created, and make all + output paths read-only. Then get the references of each output (that we + might need to register), so we can topologically sort them. For the ones + that are most definitely already installed, we just store their final + name so we can also use it in rewrites. */ + StringSet outputsToSort; + struct AlreadyRegistered { StorePath path; }; + struct PerhapsNeedToRegister { StorePathSet refs; }; + std::map> outputReferencesIfUnregistered; + std::map outputStats; + for (auto & [outputName, _] : drv->outputs) { + auto scratchOutput = get(scratchOutputs, outputName); + if (!scratchOutput) + throw BuildError( + "builder for '%s' has no scratch output for '%s'", + store.printStorePath(drvPath), outputName); + auto actualPath = toRealPathChroot(store.printStorePath(*scratchOutput)); + + outputsToSort.insert(outputName); + + /* Updated wanted info to remove the outputs we definitely don't need to register */ + auto initialOutput = get(initialOutputs, outputName); + if (!initialOutput) + throw BuildError( + "builder for '%s' has no initial output for '%s'", + store.printStorePath(drvPath), outputName); + auto & initialInfo = *initialOutput; + + /* Don't register if already valid, and not checking */ + initialInfo.wanted = buildMode == bmCheck + || !(initialInfo.known && initialInfo.known->isValid()); + if (!initialInfo.wanted) { + outputReferencesIfUnregistered.insert_or_assign( + outputName, + AlreadyRegistered { .path = initialInfo.known->path }); + continue; + } + + auto optSt = maybeLstat(actualPath.c_str()); + if (!optSt) + throw BuildError( + "builder for '%s' failed to produce output path for output '%s' at '%s'", + store.printStorePath(drvPath), outputName, actualPath); + struct stat & st = *optSt; + +#ifndef __CYGWIN__ + /* Check that the output is not group or world writable, as + that means that someone else can have interfered with the + build. Also, the output should be owned by the build + user. */ + if ((!S_ISLNK(st.st_mode) && (st.st_mode & (S_IWGRP | S_IWOTH))) || + (buildUser && st.st_uid != buildUser->getUID())) + throw BuildError( + "suspicious ownership or permission on '%s' for output '%s'; rejecting this build output", + actualPath, outputName); +#endif + + /* 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 ? std::optional(buildUser->getUIDRange()) : std::nullopt, + inodesSeen); + + bool discardReferences = false; + if (auto udr = get(drvOptions->unsafeDiscardReferences, outputName)) { + discardReferences = *udr; + } + + StorePathSet references; + if (discardReferences) + debug("discarding references of output '%s'", outputName); + else { + debug("scanning for references for output '%s' in temp location '%s'", outputName, actualPath); + + /* Pass blank Sink as we are not ready to hash data at this stage. */ + NullSink blank; + references = scanForReferences(blank, actualPath, referenceablePaths); + } + + outputReferencesIfUnregistered.insert_or_assign( + outputName, + PerhapsNeedToRegister { .refs = references }); + outputStats.insert_or_assign(outputName, std::move(st)); + } + + auto sortedOutputNames = topoSort(outputsToSort, + {[&](const std::string & name) { + auto orifu = get(outputReferencesIfUnregistered, name); + if (!orifu) + throw BuildError( + "no output reference for '%s' in build of '%s'", + name, store.printStorePath(drvPath)); + return std::visit(overloaded { + /* Since we'll use the already installed versions of these, we + can treat them as leaves and ignore any references they + have. */ + [&](const AlreadyRegistered &) { return StringSet {}; }, + [&](const PerhapsNeedToRegister & refs) { + StringSet referencedOutputs; + /* FIXME build inverted map up front so no quadratic waste here */ + for (auto & r : refs.refs) + for (auto & [o, p] : scratchOutputs) + if (r == p) + referencedOutputs.insert(o); + return referencedOutputs; + }, + }, *orifu); + }}, + {[&](const std::string & path, const std::string & parent) { + // TODO with more -vvvv also show the temporary paths for manual inspection. + return BuildError( + "cycle detected in build of '%s' in the references of output '%s' from output '%s'", + store.printStorePath(drvPath), path, parent); + }}); + + std::reverse(sortedOutputNames.begin(), sortedOutputNames.end()); + + OutputPathMap finalOutputs; + + for (auto & outputName : sortedOutputNames) { + auto output = get(drv->outputs, outputName); + auto scratchPath = get(scratchOutputs, outputName); + assert(output && scratchPath); + auto actualPath = toRealPathChroot(store.printStorePath(*scratchPath)); + + auto finish = [&](StorePath finalStorePath) { + /* Store the final path */ + finalOutputs.insert_or_assign(outputName, finalStorePath); + /* The rewrite rule will be used in downstream outputs that refer to + use. This is why the topological sort is essential to do first + before this for loop. */ + if (*scratchPath != finalStorePath) + outputRewrites[std::string { scratchPath->hashPart() }] = std::string { finalStorePath.hashPart() }; + }; + + auto orifu = get(outputReferencesIfUnregistered, outputName); + assert(orifu); + + std::optional referencesOpt = std::visit(overloaded { + [&](const AlreadyRegistered & skippedFinalPath) -> std::optional { + finish(skippedFinalPath.path); + return std::nullopt; + }, + [&](const PerhapsNeedToRegister & r) -> std::optional { + return r.refs; + }, + }, *orifu); + + if (!referencesOpt) + continue; + auto references = *referencesOpt; + + auto rewriteOutput = [&](const StringMap & rewrites) { + /* Apply hash rewriting if necessary. */ + if (!rewrites.empty()) { + debug("rewriting hashes in '%1%'; cross fingers", actualPath); + + /* FIXME: Is this actually streaming? */ + auto source = sinkToSource([&](Sink & nextSink) { + RewritingSink rsink(rewrites, nextSink); + dumpPath(actualPath, rsink); + rsink.flush(); + }); + Path tmpPath = actualPath + ".tmp"; + restorePath(tmpPath, *source); + deletePath(actualPath); + movePath(tmpPath, actualPath); + + /* FIXME: set proper permissions in restorePath() so + we don't have to do another traversal. */ + canonicalisePathMetaData(actualPath, {}, inodesSeen); + } + }; + + auto rewriteRefs = [&]() -> StoreReferences { + /* In the CA case, we need the rewritten refs to calculate the + final path, therefore we look for a *non-rewritten + self-reference, and use a bool rather try to solve the + computationally intractable fixed point. */ + StoreReferences res { + .self = false, + }; + for (auto & r : references) { + auto name = r.name(); + auto origHash = std::string { r.hashPart() }; + if (r == *scratchPath) { + res.self = true; + } else if (auto outputRewrite = get(outputRewrites, origHash)) { + std::string newRef = *outputRewrite; + newRef += '-'; + newRef += name; + res.others.insert(StorePath { newRef }); + } else { + res.others.insert(r); + } + } + return res; + }; + + auto newInfoFromCA = [&](const DerivationOutput::CAFloating outputHash) -> ValidPathInfo { + auto st = get(outputStats, outputName); + if (!st) + throw BuildError( + "output path %1% without valid stats info", + actualPath); + if (outputHash.method.getFileIngestionMethod() == FileIngestionMethod::Flat) + { + /* The output path should be a regular file without execute permission. */ + if (!S_ISREG(st->st_mode) || (st->st_mode & S_IXUSR) != 0) + throw BuildError( + "output path '%1%' should be a non-executable regular file " + "since recursive hashing is not enabled (one of outputHashMode={flat,text} is true)", + actualPath); + } + rewriteOutput(outputRewrites); + /* FIXME optimize and deduplicate with addToStore */ + std::string oldHashPart { scratchPath->hashPart() }; + auto got = [&]{ + auto fim = outputHash.method.getFileIngestionMethod(); + switch (fim) { + case FileIngestionMethod::Flat: + case FileIngestionMethod::NixArchive: + { + HashModuloSink caSink { outputHash.hashAlgo, oldHashPart }; + auto fim = outputHash.method.getFileIngestionMethod(); + dumpPath( + {getFSSourceAccessor(), CanonPath(actualPath)}, + caSink, + (FileSerialisationMethod) fim); + return caSink.finish().first; + } + case FileIngestionMethod::Git: { + return git::dumpHash( + outputHash.hashAlgo, + {getFSSourceAccessor(), CanonPath(actualPath)}).hash; + } + } + assert(false); + }(); + + ValidPathInfo newInfo0 { + store, + outputPathName(drv->name, outputName), + ContentAddressWithReferences::fromParts( + outputHash.method, + std::move(got), + rewriteRefs()), + Hash::dummy, + }; + if (*scratchPath != newInfo0.path) { + // If the path has some self-references, we need to rewrite + // them. + // (note that this doesn't invalidate the ca hash we calculated + // above because it's computed *modulo the self-references*, so + // it already takes this rewrite into account). + rewriteOutput( + StringMap{{oldHashPart, + std::string(newInfo0.path.hashPart())}}); + } + + { + HashResult narHashAndSize = hashPath( + {getFSSourceAccessor(), CanonPath(actualPath)}, + FileSerialisationMethod::NixArchive, HashAlgorithm::SHA256); + newInfo0.narHash = narHashAndSize.first; + newInfo0.narSize = narHashAndSize.second; + } + + assert(newInfo0.ca); + return newInfo0; + }; + + ValidPathInfo newInfo = std::visit(overloaded { + + [&](const DerivationOutput::InputAddressed & output) { + /* input-addressed case */ + auto requiredFinalPath = output.path; + /* Preemptively add rewrite rule for final hash, as that is + what the NAR hash will use rather than normalized-self references */ + if (*scratchPath != requiredFinalPath) + outputRewrites.insert_or_assign( + std::string { scratchPath->hashPart() }, + std::string { requiredFinalPath.hashPart() }); + rewriteOutput(outputRewrites); + HashResult narHashAndSize = hashPath( + {getFSSourceAccessor(), CanonPath(actualPath)}, + FileSerialisationMethod::NixArchive, HashAlgorithm::SHA256); + ValidPathInfo newInfo0 { requiredFinalPath, narHashAndSize.first }; + newInfo0.narSize = narHashAndSize.second; + auto refs = rewriteRefs(); + newInfo0.references = std::move(refs.others); + if (refs.self) + newInfo0.references.insert(newInfo0.path); + return newInfo0; + }, + + [&](const DerivationOutput::CAFixed & dof) { + auto & wanted = dof.ca.hash; + + // Replace the output by a fresh copy of itself to make sure + // that there's no stale file descriptor pointing to it + Path tmpOutput = actualPath + ".tmp"; + copyFile( + std::filesystem::path(actualPath), + std::filesystem::path(tmpOutput), true); + + std::filesystem::rename(tmpOutput, actualPath); + + auto newInfo0 = newInfoFromCA(DerivationOutput::CAFloating { + .method = dof.ca.method, + .hashAlgo = wanted.algo, + }); + + /* Check wanted hash */ + assert(newInfo0.ca); + auto & got = newInfo0.ca->hash; + if (wanted != got) { + /* Throw an error after registering the path as + valid. */ + miscMethods.noteHashMismatch(); + delayedException = std::make_exception_ptr( + BuildError("hash mismatch in fixed-output derivation '%s':\n specified: %s\n got: %s", + store.printStorePath(drvPath), + wanted.to_string(HashFormat::SRI, true), + got.to_string(HashFormat::SRI, true))); + } + if (!newInfo0.references.empty()) { + auto numViolations = newInfo.references.size(); + delayedException = std::make_exception_ptr( + BuildError("fixed-output derivations must not reference store paths: '%s' references %d distinct paths, e.g. '%s'", + store.printStorePath(drvPath), + numViolations, + store.printStorePath(*newInfo.references.begin()))); + } + + return newInfo0; + }, + + [&](const DerivationOutput::CAFloating & dof) { + return newInfoFromCA(dof); + }, + + [&](const DerivationOutput::Deferred &) -> ValidPathInfo { + // No derivation should reach that point without having been + // rewritten first + assert(false); + }, + + [&](const DerivationOutput::Impure & doi) { + return newInfoFromCA(DerivationOutput::CAFloating { + .method = doi.method, + .hashAlgo = doi.hashAlgo, + }); + }, + + }, output->raw); + + /* FIXME: set proper permissions in restorePath() so + we don't have to do another traversal. */ + 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 + their usual "final destination" */ + auto finalDestPath = store.printStorePath(newInfo.path); + + /* Lock final output path, if not already locked. This happens with + floating CA derivations and hash-mismatching fixed-output + derivations. */ + PathLocks dynamicOutputLock; + dynamicOutputLock.setDeletion(true); + auto optFixedPath = output->path(store, drv->name, outputName); + if (!optFixedPath || + store.printStorePath(*optFixedPath) != finalDestPath) + { + assert(newInfo.ca); + dynamicOutputLock.lockPaths({store.toRealPath(finalDestPath)}); + } + + /* Move files, if needed */ + if (store.toRealPath(finalDestPath) != actualPath) { + if (buildMode == bmRepair) { + /* Path already exists, need to replace it */ + replaceValidPath(store.toRealPath(finalDestPath), actualPath); + actualPath = store.toRealPath(finalDestPath); + } else if (buildMode == bmCheck) { + /* Path already exists, and we want to compare, so we leave out + new path in place. */ + } else if (store.isValidPath(newInfo.path)) { + /* Path already exists because CA path produced by something + else. No moving needed. */ + assert(newInfo.ca); + } else { + auto destPath = store.toRealPath(finalDestPath); + deletePath(destPath); + movePath(actualPath, destPath); + actualPath = destPath; + } + } + + auto & localStore = getLocalStore(); + + if (buildMode == bmCheck) { + + if (!store.isValidPath(newInfo.path)) continue; + ValidPathInfo oldInfo(*store.queryPathInfo(newInfo.path)); + if (newInfo.narHash != oldInfo.narHash) { + miscMethods.noteCheckMismatch(); + if (settings.runDiffHook || settings.keepFailed) { + auto dst = store.toRealPath(finalDestPath + checkSuffix); + deletePath(dst); + movePath(actualPath, dst); + + handleDiffHook( + buildUser ? buildUser->getUID() : getuid(), + buildUser ? buildUser->getGID() : getgid(), + finalDestPath, dst, store.printStorePath(drvPath), tmpDir); + + throw NotDeterministic("derivation '%s' may not be deterministic: output '%s' differs from '%s'", + store.printStorePath(drvPath), store.toRealPath(finalDestPath), dst); + } else + throw NotDeterministic("derivation '%s' may not be deterministic: output '%s' differs", + store.printStorePath(drvPath), store.toRealPath(finalDestPath)); + } + + /* Since we verified the build, it's now ultimately trusted. */ + if (!oldInfo.ultimate) { + oldInfo.ultimate = true; + localStore.signPathInfo(oldInfo); + localStore.registerValidPaths({{oldInfo.path, oldInfo}}); + } + + continue; + } + + /* For debugging, print out the referenced and unreferenced paths. */ + for (auto & i : inputPaths) { + if (references.count(i)) + debug("referenced input: '%1%'", store.printStorePath(i)); + else + debug("unreferenced input: '%1%'", store.printStorePath(i)); + } + + localStore.optimisePath(actualPath, NoRepair); // FIXME: combine with scanForReferences() + miscMethods.markContentsGood(newInfo.path); + + newInfo.deriver = drvPath; + newInfo.ultimate = true; + localStore.signPathInfo(newInfo); + + finish(newInfo.path); + + /* If it's a CA path, register it right away. This is necessary if it + isn't statically known so that we can safely unlock the path before + the next iteration */ + if (newInfo.ca) + localStore.registerValidPaths({{newInfo.path, newInfo}}); + + infos.emplace(outputName, std::move(newInfo)); + } + + if (buildMode == bmCheck) { + /* In case of fixed-output derivations, if there are + mismatches on `--check` an error must be thrown as this is + also a source for non-determinism. */ + if (delayedException) + std::rethrow_exception(delayedException); + return miscMethods.assertPathValidity(); + } + + /* Apply output checks. */ + checkOutputs(infos); + + /* Register each output path as valid, and register the sets of + paths referenced by each of them. If there are cycles in the + outputs, this will fail. */ + { + auto & localStore = getLocalStore(); + + ValidPathInfos infos2; + for (auto & [outputName, newInfo] : infos) { + infos2.insert_or_assign(newInfo.path, newInfo); + } + localStore.registerValidPaths(infos2); + } + + /* In case of a fixed-output derivation hash mismatch, throw an + exception now that we have registered the output as valid. */ + if (delayedException) + std::rethrow_exception(delayedException); + + /* If we made it this far, we are sure the output matches the derivation + (since the delayedException would be a fixed output CA mismatch). That + means it's safe to link the derivation to the output hash. We must do + that for floating CA derivations, which otherwise couldn't be cached, + but it's fine to do in all cases. */ + SingleDrvOutputs builtOutputs; + + for (auto & [outputName, newInfo] : infos) { + auto oldinfo = get(initialOutputs, outputName); + assert(oldinfo); + auto thisRealisation = Realisation { + .id = DrvOutput { + oldinfo->outputHash, + outputName + }, + .outPath = newInfo.path + }; + if (experimentalFeatureSettings.isEnabled(Xp::CaDerivations) + && !drv->type().isImpure()) + { + store.signRealisation(thisRealisation); + store.registerDrvOutput(thisRealisation); + } + builtOutputs.emplace(outputName, thisRealisation); + } + + return builtOutputs; +} + + +void DerivationBuilder::checkOutputs(const std::map & outputs) +{ + std::map outputsByPath; + for (auto & output : outputs) + outputsByPath.emplace(store.printStorePath(output.second.path), output.second); + + for (auto & output : outputs) { + auto & outputName = output.first; + auto & info = output.second; + + /* Compute the closure and closure size of some output. This + is slightly tricky because some of its references (namely + other outputs) may not be valid yet. */ + auto getClosure = [&](const StorePath & path) + { + uint64_t closureSize = 0; + StorePathSet pathsDone; + std::queue pathsLeft; + pathsLeft.push(path); + + while (!pathsLeft.empty()) { + auto path = pathsLeft.front(); + pathsLeft.pop(); + if (!pathsDone.insert(path).second) continue; + + auto i = outputsByPath.find(store.printStorePath(path)); + if (i != outputsByPath.end()) { + closureSize += i->second.narSize; + for (auto & ref : i->second.references) + pathsLeft.push(ref); + } else { + auto info = store.queryPathInfo(path); + closureSize += info->narSize; + for (auto & ref : info->references) + pathsLeft.push(ref); + } + } + + return std::make_pair(std::move(pathsDone), closureSize); + }; + + auto applyChecks = [&](const DerivationOptions::OutputChecks & checks) + { + if (checks.maxSize && info.narSize > *checks.maxSize) + throw BuildError("path '%s' is too large at %d bytes; limit is %d bytes", + store.printStorePath(info.path), info.narSize, *checks.maxSize); + + if (checks.maxClosureSize) { + uint64_t closureSize = getClosure(info.path).second; + if (closureSize > *checks.maxClosureSize) + throw BuildError("closure of path '%s' is too large at %d bytes; limit is %d bytes", + store.printStorePath(info.path), closureSize, *checks.maxClosureSize); + } + + auto checkRefs = [&](const StringSet & value, bool allowed, bool recursive) + { + /* Parse a list of reference specifiers. Each element must + either be a store path, or the symbolic name of the output + of the derivation (such as `out'). */ + StorePathSet spec; + for (auto & i : value) { + if (store.isStorePath(i)) + spec.insert(store.parseStorePath(i)); + else if (auto output = get(outputs, i)) + spec.insert(output->path); + else { + std::string outputsListing = concatMapStringsSep(", ", outputs, [](auto & o) { return o.first; }); + throw BuildError("derivation '%s' output check for '%s' contains an illegal reference specifier '%s'," + " expected store path or output name (one of [%s])", + store.printStorePath(drvPath), outputName, i, outputsListing); + } + } + + auto used = recursive + ? getClosure(info.path).first + : info.references; + + if (recursive && checks.ignoreSelfRefs) + used.erase(info.path); + + StorePathSet badPaths; + + for (auto & i : used) + if (allowed) { + if (!spec.count(i)) + badPaths.insert(i); + } else { + if (spec.count(i)) + badPaths.insert(i); + } + + if (!badPaths.empty()) { + std::string badPathsStr; + for (auto & i : badPaths) { + badPathsStr += "\n "; + badPathsStr += store.printStorePath(i); + } + throw BuildError("output '%s' is not allowed to refer to the following paths:%s", + store.printStorePath(info.path), badPathsStr); + } + }; + + /* Mandatory check: absent whitelist, and present but empty + whitelist mean very different things. */ + if (auto & refs = checks.allowedReferences) { + checkRefs(*refs, true, false); + } + if (auto & refs = checks.allowedRequisites) { + checkRefs(*refs, true, true); + } + + /* Optimization: don't need to do anything when + disallowed and empty set. */ + if (!checks.disallowedReferences.empty()) { + checkRefs(checks.disallowedReferences, false, false); + } + if (!checks.disallowedRequisites.empty()) { + checkRefs(checks.disallowedRequisites, false, true); + } + }; + + std::visit(overloaded{ + [&](const DerivationOptions::OutputChecks & checks) { + applyChecks(checks); + }, + [&](const std::map & checksPerOutput) { + if (auto outputChecks = get(checksPerOutput, outputName)) + + applyChecks(*outputChecks); + }, + }, drvOptions->outputChecks); + } +} + + +void DerivationBuilder::deleteTmpDir(bool force) +{ + if (topTmpDir != "") { + /* Don't keep temporary directories for builtins because they + might have privileged stuff (like a copy of netrc). */ + if (settings.keepFailed && !force && !drv->isBuiltin()) { + printError("note: keeping build directory '%s'", tmpDir); + chmod(topTmpDir.c_str(), 0755); + chmod(tmpDir.c_str(), 0755); + } + else + deletePath(topTmpDir); + topTmpDir = ""; + tmpDir = ""; + } +} + + +bool LocalDerivationGoal::isReadDesc(int fd) +{ + return (hook && DerivationGoal::isReadDesc(fd)) || + (!hook && fd == builder.builderOut.get()); +} + + +StorePath DerivationBuilder::makeFallbackPath(OutputNameView outputName) +{ + // This is a bogus path type, constructed this way to ensure that it doesn't collide with any other store path + // See doc/manual/source/protocols/store-path.md for details + // TODO: We may want to separate the responsibilities of constructing the path fingerprint and of actually doing the hashing + auto pathType = "rewrite:" + std::string(drvPath.to_string()) + ":name:" + std::string(outputName); + return store.makeStorePath( + pathType, + // pass an all-zeroes hash + Hash(HashAlgorithm::SHA256), outputPathName(drv->name, outputName)); +} + + +StorePath DerivationBuilder::makeFallbackPath(const StorePath & path) +{ + // This is a bogus path type, constructed this way to ensure that it doesn't collide with any other store path + // See doc/manual/source/protocols/store-path.md for details + auto pathType = "rewrite:" + std::string(drvPath.to_string()) + ":" + std::string(path.to_string()); + return store.makeStorePath( + pathType, + // pass an all-zeroes hash + Hash(HashAlgorithm::SHA256), path.name()); +} + + +} From 681947cba5696089682f0b5fe477b9af5cb8922d Mon Sep 17 00:00:00 2001 From: John Ericson Date: Sun, 16 Mar 2025 23:50:48 -0400 Subject: [PATCH 5/5] Move `DerivationBuilder` to its own file/header The building logic is now free of the scheduling logic! (The interface between them is just what is in the new header. This makes it much easier to audit, and shrink over time.) --- maintainers/flake-module.nix | 2 + src/libstore/build/derivation-goal.cc | 14 - .../store/build/derivation-building-misc.hh | 52 + .../nix/store/build/derivation-goal.hh | 40 +- src/libstore/include/nix/store/meson.build | 1 + .../include/nix/store/restricted-store.hh | 2 +- src/libstore/unix/build/derivation-builder.cc | 433 +-- .../unix/build/local-derivation-goal.cc | 3188 +--------------- .../nix/store/build/derivation-builder.hh | 3254 +---------------- .../unix/include/nix/store/meson.build | 1 + src/libstore/unix/meson.build | 1 + 11 files changed, 145 insertions(+), 6843 deletions(-) create mode 100644 src/libstore/include/nix/store/build/derivation-building-misc.hh diff --git a/maintainers/flake-module.nix b/maintainers/flake-module.nix index a8c52eb46..a9d10386f 100644 --- a/maintainers/flake-module.nix +++ b/maintainers/flake-module.nix @@ -284,6 +284,8 @@ ''^src/libstore/build/goal\.cc$'' ''^src/libstore/include/nix/store/build/goal\.hh$'' ''^src/libstore/unix/build/hook-instance\.cc$'' + ''^src/libstore/unix/build/derivation-builder\.cc$'' + ''^src/libstore/unix/include/nix/store/build/derivation-builder\.hh$'' ''^src/libstore/unix/build/local-derivation-goal\.cc$'' ''^src/libstore/unix/include/nix/store/build/local-derivation-goal\.hh$'' ''^src/libstore/build/substitution-goal\.cc$'' diff --git a/src/libstore/build/derivation-goal.cc b/src/libstore/build/derivation-goal.cc index 0e3163b93..923e409d6 100644 --- a/src/libstore/build/derivation-goal.cc +++ b/src/libstore/build/derivation-goal.cc @@ -5,31 +5,17 @@ #include "nix/util/processes.hh" #include "nix/util/config-global.hh" #include "nix/store/build/worker.hh" -#include "nix/store/builtins.hh" -#include "nix/store/builtins/buildenv.hh" -#include "nix/util/references.hh" -#include "nix/util/finally.hh" #include "nix/util/util.hh" -#include "nix/util/archive.hh" #include "nix/util/compression.hh" #include "nix/store/common-protocol.hh" #include "nix/store/common-protocol-impl.hh" -#include "nix/util/topo-sort.hh" -#include "nix/util/callback.hh" #include "nix/store/local-store.hh" // TODO remove, along with remaining downcasts -#include -#include - #include #include #include #include -#ifndef _WIN32 // TODO abstract over proc exit status -# include -#endif - #include #include "nix/util/strings.hh" diff --git a/src/libstore/include/nix/store/build/derivation-building-misc.hh b/src/libstore/include/nix/store/build/derivation-building-misc.hh new file mode 100644 index 000000000..caf94844d --- /dev/null +++ b/src/libstore/include/nix/store/build/derivation-building-misc.hh @@ -0,0 +1,52 @@ +#pragma once +/** + * @file Misc type defitions for both local building and remote (RPC building) + */ + +#include "nix/util/hash.hh" +#include "nix/store/path.hh" + +namespace nix { + +class Store; + +/** + * Unless we are repairing, we don't both to test validity and just assume it, + * so the choices are `Absent` or `Valid`. + */ +enum struct PathStatus { + Corrupt, + Absent, + Valid, +}; + +struct InitialOutputStatus +{ + StorePath path; + PathStatus status; + /** + * Valid in the store, and additionally non-corrupt if we are repairing + */ + bool isValid() const + { + return status == PathStatus::Valid; + } + /** + * Merely present, allowed to be corrupt + */ + bool isPresent() const + { + return status == PathStatus::Corrupt || status == PathStatus::Valid; + } +}; + +struct InitialOutput +{ + bool wanted; + Hash outputHash; + std::optional known; +}; + +void runPostBuildHook(Store & store, Logger & logger, const StorePath & drvPath, const StorePathSet & outputPaths); + +} diff --git a/src/libstore/include/nix/store/build/derivation-goal.hh b/src/libstore/include/nix/store/build/derivation-goal.hh index 390c60668..3bbaa7027 100644 --- a/src/libstore/include/nix/store/build/derivation-goal.hh +++ b/src/libstore/include/nix/store/build/derivation-goal.hh @@ -3,9 +3,7 @@ #include "nix/store/parsed-derivations.hh" #include "nix/store/derivation-options.hh" -#ifndef _WIN32 -# include "nix/store/user-lock.hh" -#endif +#include "nix/store/build/derivation-building-misc.hh" #include "nix/store/outputs-spec.hh" #include "nix/store/store-api.hh" #include "nix/store/pathlocks.hh" @@ -21,40 +19,6 @@ struct HookInstance; typedef enum {rpAccept, rpDecline, rpPostpone} HookReply; -/** - * Unless we are repairing, we don't both to test validity and just assume it, - * so the choices are `Absent` or `Valid`. - */ -enum struct PathStatus { - Corrupt, - Absent, - Valid, -}; - -struct InitialOutputStatus { - StorePath path; - PathStatus status; - /** - * Valid in the store, and additionally non-corrupt if we are repairing - */ - bool isValid() const { - return status == PathStatus::Valid; - } - /** - * Merely present, allowed to be corrupt - */ - bool isPresent() const { - return status == PathStatus::Corrupt - || status == PathStatus::Valid; - } -}; - -struct InitialOutput { - bool wanted; - Hash outputHash; - std::optional known; -}; - /** Used internally */ void runPostBuildHook( Store & store, @@ -307,6 +271,4 @@ struct DerivationGoal : public Goal }; }; -MakeError(NotDeterministic, BuildError); - } diff --git a/src/libstore/include/nix/store/meson.build b/src/libstore/include/nix/store/meson.build index 551031b32..5298b77a6 100644 --- a/src/libstore/include/nix/store/meson.build +++ b/src/libstore/include/nix/store/meson.build @@ -13,6 +13,7 @@ headers = [config_pub_h] + files( 'binary-cache-store.hh', 'build-result.hh', 'build/derivation-goal.hh', + 'build/derivation-building-misc.hh', 'build/drv-output-substitution-goal.hh', 'build/goal.hh', 'build/substitution-goal.hh', diff --git a/src/libstore/include/nix/store/restricted-store.hh b/src/libstore/include/nix/store/restricted-store.hh index 84b455456..67c26c88b 100644 --- a/src/libstore/include/nix/store/restricted-store.hh +++ b/src/libstore/include/nix/store/restricted-store.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "local-store.hh" +#include "nix/store/local-store.hh" namespace nix { diff --git a/src/libstore/unix/build/derivation-builder.cc b/src/libstore/unix/build/derivation-builder.cc index 93154c7dc..f203e275a 100644 --- a/src/libstore/unix/build/derivation-builder.cc +++ b/src/libstore/unix/build/derivation-builder.cc @@ -1,4 +1,4 @@ -#include "nix/store/build/local-derivation-goal.hh" +#include "nix/store/build/derivation-builder.hh" #include "nix/store/local-store.hh" #include "nix/util/processes.hh" #include "nix/store/indirect-root-store.hh" @@ -22,7 +22,6 @@ #include "nix/store/posix-fs-canonicalise.hh" #include "nix/util/posix-source-accessor.hh" #include "nix/store/restricted-store.hh" -#include "nix/store/config.hh" #include #include @@ -81,108 +80,7 @@ extern "C" int sandbox_init_with_parameters(const char *profile, uint64_t flags, namespace nix { -/** - * Parameters by (mostly) `const` reference for `DerivationBuilder`. - */ -struct DerivationBuilderParams -{ - /** The path of the derivation. */ - const StorePath & drvPath; - - BuildResult & buildResult; - - /** - * The derivation stored at drvPath. - * - * @todo Remove double indirection by delaying when this is - * initialized. - */ - const std::unique_ptr & drv; - - const std::unique_ptr & parsedDrv; - const std::unique_ptr & drvOptions; - - /** - * The remainder is state held during the build. - */ - - /** - * All input paths (that is, the union of FS closures of the - * immediate input paths). - */ - const StorePathSet & inputPaths; - - /** - * @note we do in fact mutate this - */ - std::map & initialOutputs; - - const BuildMode & buildMode; - - DerivationBuilderParams( - const StorePath & drvPath, - const BuildMode & buildMode, - BuildResult & buildResult, - const std::unique_ptr & drv, - const std::unique_ptr & parsedDrv, - const std::unique_ptr & drvOptions, - const StorePathSet & inputPaths, - std::map & initialOutputs) - : drvPath{drvPath} - , buildResult{buildResult} - , drv{drv} - , parsedDrv{parsedDrv} - , drvOptions{drvOptions} - , inputPaths{inputPaths} - , initialOutputs{initialOutputs} - , buildMode{buildMode} - { } - - DerivationBuilderParams(DerivationBuilderParams &&) = default; -}; - -/** - * Callbacks that `DerivationBuilder` needs. - */ -struct DerivationBuilderCallbacks -{ - /** - * Open a log file and a pipe to it. - */ - virtual Path openLogFile() = 0; - - /** - * Close the log file. - */ - virtual void closeLogFile() = 0; - - /** - * Aborts if any output is not valid or corrupt, and otherwise - * returns a 'SingleDrvOutputs' structure containing all outputs. - * - * @todo Probably should just be in `DerivationGoal`. - */ - virtual SingleDrvOutputs assertPathValidity() = 0; - - virtual void appendLogTailErrorMsg(std::string & msg) = 0; - - /** - * Hook up `builderOut` to some mechanism to ingest the log - * - * @todo this should be reworked - */ - virtual void childStarted() = 0; - - /** - * @todo this should be reworked - */ - virtual void childTerminated() = 0; - - virtual void noteHashMismatch(void) = 0; - virtual void noteCheckMismatch(void) = 0; - - virtual void markContentsGood(const StorePath & path) = 0; -}; +MakeError(NotDeterministic, BuildError); /** * This class represents the state for building locally. @@ -195,7 +93,7 @@ struct DerivationBuilderCallbacks * rather than incoming call edges that either should be removed, or * become (higher order) function parameters. */ -class DerivationBuilder : public RestrictionContext, DerivationBuilderParams +class DerivationBuilderImpl : public DerivationBuilder, DerivationBuilderParams { Store & store; @@ -203,7 +101,7 @@ class DerivationBuilder : public RestrictionContext, DerivationBuilderParams public: - DerivationBuilder( + DerivationBuilderImpl( Store & store, DerivationBuilderCallbacks & miscMethods, DerivationBuilderParams params) @@ -214,16 +112,6 @@ public: LocalStore & getLocalStore(); - /** - * User selected for running the builder. - */ - std::unique_ptr buildUser; - - /** - * The process ID of the builder. - */ - Pid pid; - private: /** @@ -247,16 +135,6 @@ private: */ Path tmpDirInSandbox; -public: - - /** - * Master side of the pseudoterminal used for the builder's - * standard output/error. - */ - AutoCloseFD builderOut; - -private: - /** * Pipe for synchronising updates to the builder namespaces. */ @@ -307,17 +185,17 @@ private: : source(source), optional(optional) { } }; - typedef map PathsInChroot; // maps target path to source path + typedef std::map PathsInChroot; // maps target path to source path PathsInChroot pathsInChroot; - typedef map Environment; + typedef std::map Environment; Environment env; /** * Hash rewriting. */ StringMap inputRewrites, outputRewrites; - typedef map RedirectedOutputs; + typedef std::map RedirectedOutputs; RedirectedOutputs redirectedOutputs; /** @@ -389,12 +267,12 @@ public: * @returns true if successful, false if we could not acquire a build * user. In that case, the caller must wait and then try again. */ - bool prepareBuild(); + bool prepareBuild() override; /** * Start building a derivation. */ - void startBuilder(); + void startBuilder() override;; /** * Tear down build environment after the builder exits (either on @@ -405,7 +283,7 @@ public: * more information. The second case indicates success, and * realisations for each output of the derivation are returned. */ - std::variant, SingleDrvOutputs> unprepareBuild(); + std::variant, SingleDrvOutputs> unprepareBuild() override; private: @@ -440,7 +318,7 @@ public: * Stop the in-process nix daemon thread. * @see startDaemon */ - void stopDaemon(); + void stopDaemon() override; private: @@ -474,13 +352,13 @@ public: /** * Delete the temporary directory, if we have one. */ - void deleteTmpDir(bool force); + void deleteTmpDir(bool force) override; /** * Kill any processes running under the build user UID or in the * cgroup of the build. */ - void killSandbox(bool getStats); + void killSandbox(bool getStats) override; private: @@ -503,112 +381,15 @@ private: StorePath makeFallbackPath(OutputNameView outputName); }; -/** - * This hooks up `DerivationBuilder` to the scheduler / goal machinary. - * - * @todo Eventually, this shouldn't exist, because `DerivationGoal` can - * just choose to use `DerivationBuilder` or its remote-building - * equalivalent directly, at the "value level" rather than "class - * inheritance hierarchy" level. - */ -struct LocalDerivationGoal : DerivationGoal, DerivationBuilderCallbacks +std::unique_ptr makeDerivationBuilder( + Store & store, + DerivationBuilderCallbacks & miscMethods, + DerivationBuilderParams params) { - DerivationBuilder builder; - - LocalDerivationGoal(const StorePath & drvPath, - const OutputsSpec & wantedOutputs, Worker & worker, - BuildMode buildMode) - : DerivationGoal{drvPath, wantedOutputs, worker, buildMode} - , builder{ - worker.store, - static_cast(*this), - DerivationBuilderParams { - DerivationGoal::drvPath, - DerivationGoal::buildMode, - DerivationGoal::buildResult, - DerivationGoal::drv, - DerivationGoal::parsedDrv, - DerivationGoal::drvOptions, - DerivationGoal::inputPaths, - DerivationGoal::initialOutputs, - }, - } - {} - - LocalDerivationGoal(const StorePath & drvPath, const BasicDerivation & drv, - const OutputsSpec & wantedOutputs, Worker & worker, - BuildMode buildMode = bmNormal) - : DerivationGoal{drvPath, drv, wantedOutputs, worker, buildMode} - , builder{ - worker.store, - static_cast(*this), - DerivationBuilderParams { - DerivationGoal::drvPath, - DerivationGoal::buildMode, - DerivationGoal::buildResult, - DerivationGoal::drv, - DerivationGoal::parsedDrv, - DerivationGoal::drvOptions, - DerivationGoal::inputPaths, - DerivationGoal::initialOutputs, - }, - } - {} - - virtual ~LocalDerivationGoal() override; - - /** - * The additional states. - */ - Goal::Co tryLocalBuild() override; - - bool isReadDesc(int fd) override; - - /** - * Forcibly kill the child process, if any. - * - * Called by destructor, can't be overridden - */ - void killChild() override final; - - void childStarted() override; - void childTerminated() override; - - void noteHashMismatch(void) override; - void noteCheckMismatch(void) override; - - void markContentsGood(const StorePath &) override; - - // Fake overrides to isntantiate identically-named virtual methods - - Path openLogFile() override { - return DerivationGoal::openLogFile(); - } - void closeLogFile() override { - DerivationGoal::closeLogFile(); - } - SingleDrvOutputs assertPathValidity() override { - return DerivationGoal::assertPathValidity(); - } - void appendLogTailErrorMsg(std::string & msg) override { - DerivationGoal::appendLogTailErrorMsg(msg); - } -}; - -std::shared_ptr makeLocalDerivationGoal( - const StorePath & drvPath, - const OutputsSpec & wantedOutputs, Worker & worker, - BuildMode buildMode) -{ - return std::make_shared(drvPath, wantedOutputs, worker, buildMode); -} - -std::shared_ptr makeLocalDerivationGoal( - const StorePath & drvPath, const BasicDerivation & drv, - const OutputsSpec & wantedOutputs, Worker & worker, - BuildMode buildMode) -{ - return std::make_shared(drvPath, drv, wantedOutputs, worker, buildMode); + return std::make_unique( + store, + miscMethods, + std::move(params)); } void handleDiffHook( @@ -645,20 +426,10 @@ void handleDiffHook( } } -const Path DerivationBuilder::homeDir = "/homeless-shelter"; +const Path DerivationBuilderImpl::homeDir = "/homeless-shelter"; -LocalDerivationGoal::~LocalDerivationGoal() -{ - /* Careful: we should never ever throw an exception from a - destructor. */ - try { builder.deleteTmpDir(false); } catch (...) { ignoreExceptionInDestructor(); } - try { killChild(); } catch (...) { ignoreExceptionInDestructor(); } - try { builder.stopDaemon(); } catch (...) { ignoreExceptionInDestructor(); } -} - - -inline bool DerivationBuilder::needsHashRewrite() +inline bool DerivationBuilderImpl::needsHashRewrite() { #ifdef __linux__ return !useChroot; @@ -669,7 +440,7 @@ inline bool DerivationBuilder::needsHashRewrite() } -LocalStore & DerivationBuilder::getLocalStore() +LocalStore & DerivationBuilderImpl::getLocalStore() { auto p = dynamic_cast(&store); assert(p); @@ -677,28 +448,7 @@ LocalStore & DerivationBuilder::getLocalStore() } -void LocalDerivationGoal::killChild() -{ - if (builder.pid != -1) { - worker.childTerminated(this); - - /* If we're using a build user, then there is a tricky race - condition: if we kill the build user before the child has - done its setuid() to the build user uid, then it won't be - killed, and we'll potentially lock up in pid.wait(). So - also send a conventional kill to the child. */ - ::kill(-builder.pid, SIGKILL); /* ignore the result */ - - builder.killSandbox(true); - - builder.pid.wait(); - } - - DerivationGoal::killChild(); -} - - -void DerivationBuilder::killSandbox(bool getStats) +void DerivationBuilderImpl::killSandbox(bool getStats) { if (cgroup) { #ifdef __linux__ @@ -720,91 +470,7 @@ void DerivationBuilder::killSandbox(bool getStats) } -void LocalDerivationGoal::childStarted() -{ - worker.childStarted(shared_from_this(), {builder.builderOut.get()}, true, true); -} - -void LocalDerivationGoal::childTerminated() -{ - worker.childTerminated(this); -} - -void LocalDerivationGoal::noteHashMismatch() -{ - worker.hashMismatch = true; -} - - -void LocalDerivationGoal::noteCheckMismatch() -{ - worker.checkMismatch = true; -} - - -void LocalDerivationGoal::markContentsGood(const StorePath & path) -{ - worker.markContentsGood(path); -} - - -Goal::Co LocalDerivationGoal::tryLocalBuild() -{ - assert(!hook); - - unsigned int curBuilds = worker.getNrLocalBuilds(); - if (curBuilds >= settings.maxBuildJobs) { - outputLocks.unlock(); - co_await waitForBuildSlot(); - co_return tryToBuild(); - } - - if (!builder.prepareBuild()) { - if (!actLock) - actLock = std::make_unique(*logger, lvlWarn, actBuildWaiting, - fmt("waiting for a free build user ID for '%s'", Magenta(worker.store.printStorePath(drvPath)))); - co_await waitForAWhile(); - co_return tryLocalBuild(); - } - - actLock.reset(); - - try { - - /* Okay, we have to build. */ - builder.startBuilder(); - - } catch (BuildError & e) { - outputLocks.unlock(); - builder.buildUser.reset(); - worker.permanentFailure = true; - co_return done(BuildResult::InputRejected, {}, std::move(e)); - } - - started(); - co_await Suspend{}; - - trace("build done"); - - auto res = builder.unprepareBuild(); - // N.B. cannot use `std::visit` with co-routine return - if (auto * ste = std::get_if<0>(&res)) { - outputLocks.unlock(); - co_return done(std::move(ste->first), {}, std::move(ste->second)); - } else if (auto * builtOutputs = std::get_if<1>(&res)) { - /* It is now safe to delete the lock files, since all future - lockers will see that the output paths are valid; they will - not create new lock files with the same names as the old - (unlinked) lock files. */ - outputLocks.setDeletion(true); - outputLocks.unlock(); - co_return done(BuildResult::Built, std::move(*builtOutputs)); - } else { - unreachable(); - } -} - -bool DerivationBuilder::prepareBuild() +bool DerivationBuilderImpl::prepareBuild() { /* Cache this */ derivationType = drv->type(); @@ -818,7 +484,7 @@ bool DerivationBuilder::prepareBuild() #ifdef __APPLE__ if (drvOptions->additionalSandboxProfile != "") throw Error("derivation '%s' specifies a sandbox profile, " - "but this is only allowed when 'sandbox' is 'relaxed'", worker.store.printStorePath(drvPath)); + "but this is only allowed when 'sandbox' is 'relaxed'", store.printStorePath(drvPath)); #endif useChroot = true; } @@ -861,7 +527,7 @@ bool DerivationBuilder::prepareBuild() } -std::variant, SingleDrvOutputs> DerivationBuilder::unprepareBuild() +std::variant, SingleDrvOutputs> DerivationBuilderImpl::unprepareBuild() { Finally releaseBuildUser([&](){ /* Release the build user at the end of this function. We don't do @@ -999,7 +665,7 @@ static void movePath(const Path & src, const Path & dst) extern void replaceValidPath(const Path & storePath, const Path & tmpPath); -bool DerivationBuilder::cleanupDecideWhetherDiskFull() +bool DerivationBuilderImpl::cleanupDecideWhetherDiskFull() { bool diskFull = false; @@ -1092,7 +758,7 @@ static void rethrowExceptionAsError() /** * Send the current exception to the parent in the format expected by - * `DerivationBuilder::processSandboxSetupMessages()`. + * `DerivationBuilderImpl::processSandboxSetupMessages()`. */ static void handleChildException(bool sendException) { @@ -1109,7 +775,7 @@ static void handleChildException(bool sendException) } } -void DerivationBuilder::startBuilder() +void DerivationBuilderImpl::startBuilder() { if ((buildUser && buildUser->getUIDCount() != 1) #ifdef __linux__ @@ -1741,7 +1407,7 @@ void DerivationBuilder::startBuilder() } -void DerivationBuilder::processSandboxSetupMessages() +void DerivationBuilderImpl::processSandboxSetupMessages() { std::vector msgs; while (true) { @@ -1770,7 +1436,7 @@ void DerivationBuilder::processSandboxSetupMessages() } -void DerivationBuilder::initTmpDir() +void DerivationBuilderImpl::initTmpDir() { /* In a sandbox, for determinism, always use the same temporary directory. */ @@ -1814,7 +1480,7 @@ void DerivationBuilder::initTmpDir() } -void DerivationBuilder::initEnv() +void DerivationBuilderImpl::initEnv() { env.clear(); @@ -1882,7 +1548,7 @@ void DerivationBuilder::initEnv() } -void DerivationBuilder::writeStructuredAttrs() +void DerivationBuilderImpl::writeStructuredAttrs() { if (auto structAttrsJson = parsedDrv->prepareStructuredAttrs(store, inputPaths)) { auto json = structAttrsJson.value(); @@ -1907,7 +1573,7 @@ void DerivationBuilder::writeStructuredAttrs() } -void DerivationBuilder::startDaemon() +void DerivationBuilderImpl::startDaemon() { experimentalFeatureSettings.require(Xp::RecursiveNix); @@ -1975,7 +1641,7 @@ void DerivationBuilder::startDaemon() } -void DerivationBuilder::stopDaemon() +void DerivationBuilderImpl::stopDaemon() { if (daemonSocket && shutdown(daemonSocket.get(), SHUT_RDWR) == -1) { // According to the POSIX standard, the 'shutdown' function should @@ -2008,7 +1674,7 @@ void DerivationBuilder::stopDaemon() } -void DerivationBuilder::addDependency(const StorePath & path) +void DerivationBuilderImpl::addDependency(const StorePath & path) { if (isAllowed(path)) return; @@ -2054,13 +1720,13 @@ void DerivationBuilder::addDependency(const StorePath & path) #else throw Error("don't know how to make path '%s' (produced by a recursive Nix call) appear in the sandbox", - worker.store.printStorePath(path)); + store.printStorePath(path)); #endif } } -void DerivationBuilder::chownToBuilder(const Path & path) +void DerivationBuilderImpl::chownToBuilder(const Path & path) { if (!buildUser) return; if (chown(path.c_str(), buildUser->getUID(), buildUser->getGID()) == -1) @@ -2156,7 +1822,7 @@ void setupSeccomp() } -void DerivationBuilder::runChild() +void DerivationBuilderImpl::runChild() { /* Warning: in the child we should absolutely not make any SQLite calls! */ @@ -2669,7 +2335,7 @@ void DerivationBuilder::runChild() } -SingleDrvOutputs DerivationBuilder::registerOutputs() +SingleDrvOutputs DerivationBuilderImpl::registerOutputs() { std::map infos; @@ -3221,7 +2887,7 @@ SingleDrvOutputs DerivationBuilder::registerOutputs() } -void DerivationBuilder::checkOutputs(const std::map & outputs) +void DerivationBuilderImpl::checkOutputs(const std::map & outputs) { std::map outputsByPath; for (auto & output : outputs) @@ -3356,7 +3022,7 @@ void DerivationBuilder::checkOutputs(const std::map } -void DerivationBuilder::deleteTmpDir(bool force) +void DerivationBuilderImpl::deleteTmpDir(bool force) { if (topTmpDir != "") { /* Don't keep temporary directories for builtins because they @@ -3374,14 +3040,7 @@ void DerivationBuilder::deleteTmpDir(bool force) } -bool LocalDerivationGoal::isReadDesc(int fd) -{ - return (hook && DerivationGoal::isReadDesc(fd)) || - (!hook && fd == builder.builderOut.get()); -} - - -StorePath DerivationBuilder::makeFallbackPath(OutputNameView outputName) +StorePath DerivationBuilderImpl::makeFallbackPath(OutputNameView outputName) { // This is a bogus path type, constructed this way to ensure that it doesn't collide with any other store path // See doc/manual/source/protocols/store-path.md for details @@ -3394,7 +3053,7 @@ StorePath DerivationBuilder::makeFallbackPath(OutputNameView outputName) } -StorePath DerivationBuilder::makeFallbackPath(const StorePath & path) +StorePath DerivationBuilderImpl::makeFallbackPath(const StorePath & path) { // This is a bogus path type, constructed this way to ensure that it doesn't collide with any other store path // See doc/manual/source/protocols/store-path.md for details diff --git a/src/libstore/unix/build/local-derivation-goal.cc b/src/libstore/unix/build/local-derivation-goal.cc index 93154c7dc..ab153a31b 100644 --- a/src/libstore/unix/build/local-derivation-goal.cc +++ b/src/libstore/unix/build/local-derivation-goal.cc @@ -1,31 +1,10 @@ #include "nix/store/build/local-derivation-goal.hh" #include "nix/store/local-store.hh" #include "nix/util/processes.hh" -#include "nix/store/indirect-root-store.hh" -#include "nix/store/build/hook-instance.hh" #include "nix/store/build/worker.hh" -#include "nix/store/builtins.hh" -#include "nix/store/builtins/buildenv.hh" -#include "nix/store/path-references.hh" -#include "nix/util/finally.hh" #include "nix/util/util.hh" -#include "nix/util/archive.hh" -#include "nix/util/git.hh" -#include "nix/util/compression.hh" -#include "nix/store/daemon.hh" -#include "nix/util/topo-sort.hh" -#include "nix/util/callback.hh" -#include "nix/util/json-utils.hh" -#include "nix/util/current-process.hh" -#include "nix/store/build/child.hh" -#include "nix/util/unix-domain-socket.hh" -#include "nix/store/posix-fs-canonicalise.hh" -#include "nix/util/posix-source-accessor.hh" #include "nix/store/restricted-store.hh" -#include "nix/store/config.hh" - -#include -#include +#include "nix/store/build/derivation-builder.hh" #include #include @@ -41,468 +20,11 @@ #include #endif -/* Includes required for chroot support. */ -#ifdef __linux__ -# include "linux/fchmodat2-compat.hh" -# include -# include -# include -# include -# include -# include -# include -# include -# include "nix/util/namespaces.hh" -# if HAVE_SECCOMP -# include -# endif -# define pivot_root(new_root, put_old) (syscall(SYS_pivot_root, new_root, put_old)) -# include "nix/util/cgroup.hh" -# include "nix/store/personality.hh" -#endif - -#ifdef __APPLE__ -#include -#include -#include - -/* This definition is undocumented but depended upon by all major browsers. */ -extern "C" int sandbox_init_with_parameters(const char *profile, uint64_t flags, const char *const parameters[], char **errorbuf); -#endif - #include #include -#include - -#include "nix/util/strings.hh" -#include "nix/util/signals.hh" - -#include "store-config-private.hh" namespace nix { -/** - * Parameters by (mostly) `const` reference for `DerivationBuilder`. - */ -struct DerivationBuilderParams -{ - /** The path of the derivation. */ - const StorePath & drvPath; - - BuildResult & buildResult; - - /** - * The derivation stored at drvPath. - * - * @todo Remove double indirection by delaying when this is - * initialized. - */ - const std::unique_ptr & drv; - - const std::unique_ptr & parsedDrv; - const std::unique_ptr & drvOptions; - - /** - * The remainder is state held during the build. - */ - - /** - * All input paths (that is, the union of FS closures of the - * immediate input paths). - */ - const StorePathSet & inputPaths; - - /** - * @note we do in fact mutate this - */ - std::map & initialOutputs; - - const BuildMode & buildMode; - - DerivationBuilderParams( - const StorePath & drvPath, - const BuildMode & buildMode, - BuildResult & buildResult, - const std::unique_ptr & drv, - const std::unique_ptr & parsedDrv, - const std::unique_ptr & drvOptions, - const StorePathSet & inputPaths, - std::map & initialOutputs) - : drvPath{drvPath} - , buildResult{buildResult} - , drv{drv} - , parsedDrv{parsedDrv} - , drvOptions{drvOptions} - , inputPaths{inputPaths} - , initialOutputs{initialOutputs} - , buildMode{buildMode} - { } - - DerivationBuilderParams(DerivationBuilderParams &&) = default; -}; - -/** - * Callbacks that `DerivationBuilder` needs. - */ -struct DerivationBuilderCallbacks -{ - /** - * Open a log file and a pipe to it. - */ - virtual Path openLogFile() = 0; - - /** - * Close the log file. - */ - virtual void closeLogFile() = 0; - - /** - * Aborts if any output is not valid or corrupt, and otherwise - * returns a 'SingleDrvOutputs' structure containing all outputs. - * - * @todo Probably should just be in `DerivationGoal`. - */ - virtual SingleDrvOutputs assertPathValidity() = 0; - - virtual void appendLogTailErrorMsg(std::string & msg) = 0; - - /** - * Hook up `builderOut` to some mechanism to ingest the log - * - * @todo this should be reworked - */ - virtual void childStarted() = 0; - - /** - * @todo this should be reworked - */ - virtual void childTerminated() = 0; - - virtual void noteHashMismatch(void) = 0; - virtual void noteCheckMismatch(void) = 0; - - virtual void markContentsGood(const StorePath & path) = 0; -}; - -/** - * This class represents the state for building locally. - * - * @todo Ideally, it would not be a class, but a single function. - * However, besides the main entry point, there are a few more methods - * which are externally called, and need to be gotten rid of. There are - * also some virtual methods (either directly here or inherited from - * `DerivationBuilderCallbacks`, a stop-gap) that represent outgoing - * rather than incoming call edges that either should be removed, or - * become (higher order) function parameters. - */ -class DerivationBuilder : public RestrictionContext, DerivationBuilderParams -{ - Store & store; - - DerivationBuilderCallbacks & miscMethods; - -public: - - DerivationBuilder( - Store & store, - DerivationBuilderCallbacks & miscMethods, - DerivationBuilderParams params) - : DerivationBuilderParams{std::move(params)} - , store{store} - , miscMethods{miscMethods} - { } - - LocalStore & getLocalStore(); - - /** - * User selected for running the builder. - */ - std::unique_ptr buildUser; - - /** - * The process ID of the builder. - */ - Pid pid; - -private: - - /** - * The cgroup of the builder, if any. - */ - std::optional cgroup; - - /** - * The temporary directory used for the build. - */ - Path tmpDir; - - /** - * The top-level temporary directory. `tmpDir` is either equal to - * or a child of this directory. - */ - Path topTmpDir; - - /** - * The path of the temporary directory in the sandbox. - */ - Path tmpDirInSandbox; - -public: - - /** - * Master side of the pseudoterminal used for the builder's - * standard output/error. - */ - AutoCloseFD builderOut; - -private: - - /** - * Pipe for synchronising updates to the builder namespaces. - */ - Pipe userNamespaceSync; - - /** - * The mount namespace and user namespace of the builder, used to add additional - * paths to the sandbox as a result of recursive Nix calls. - */ - AutoCloseFD sandboxMountNamespace; - AutoCloseFD sandboxUserNamespace; - - /** - * On Linux, whether we're doing the build in its own user - * namespace. - */ - bool usingUserNamespace = true; - - /** - * Whether we're currently doing a chroot build. - */ - bool useChroot = false; - - /** - * The root of the chroot environment. - */ - Path chrootRootDir; - - /** - * RAII object to delete the chroot directory. - */ - std::shared_ptr autoDelChroot; - - /** - * The sort of derivation we are building. - * - * Just a cached value, can be recomputed from `drv`. - */ - std::optional derivationType; - - /** - * Stuff we need to pass to initChild(). - */ - struct ChrootPath { - Path source; - bool optional; - ChrootPath(Path source = "", bool optional = false) - : source(source), optional(optional) - { } - }; - typedef map PathsInChroot; // maps target path to source path - PathsInChroot pathsInChroot; - - typedef map Environment; - Environment env; - - /** - * Hash rewriting. - */ - StringMap inputRewrites, outputRewrites; - typedef map RedirectedOutputs; - RedirectedOutputs redirectedOutputs; - - /** - * The output paths used during the build. - * - * - Input-addressed derivations or fixed content-addressed outputs are - * sometimes built when some of their outputs already exist, and can not - * be hidden via sandboxing. We use temporary locations instead and - * rewrite after the build. Otherwise the regular predetermined paths are - * put here. - * - * - Floating content-addressing derivations do not know their final build - * output paths until the outputs are hashed, so random locations are - * used, and then renamed. The randomness helps guard against hidden - * self-references. - */ - OutputPathMap scratchOutputs; - - uid_t sandboxUid() { return usingUserNamespace ? (!buildUser || buildUser->getUIDCount() == 1 ? 1000 : 0) : buildUser->getUID(); } - gid_t sandboxGid() { return usingUserNamespace ? (!buildUser || buildUser->getUIDCount() == 1 ? 100 : 0) : buildUser->getGID(); } - - const static Path homeDir; - - /** - * The recursive Nix daemon socket. - */ - AutoCloseFD daemonSocket; - - /** - * The daemon main thread. - */ - std::thread daemonThread; - - /** - * The daemon worker threads. - */ - std::vector daemonWorkerThreads; - - const StorePathSet & originalPaths() override - { - return inputPaths; - } - - bool isAllowed(const StorePath & path) override - { - return inputPaths.count(path) || addedPaths.count(path); - } - bool isAllowed(const DrvOutput & id) override - { - return addedDrvOutputs.count(id); - } - - bool isAllowed(const DerivedPath & req); - - friend struct RestrictedStore; - - /** - * Whether we need to perform hash rewriting if there are valid output paths. - */ - bool needsHashRewrite(); - -public: - - /** - * Set up build environment / sandbox, acquiring resources (e.g. - * locks as needed). After this is run, the builder should be - * started. - * - * @returns true if successful, false if we could not acquire a build - * user. In that case, the caller must wait and then try again. - */ - bool prepareBuild(); - - /** - * Start building a derivation. - */ - void startBuilder(); - - /** - * Tear down build environment after the builder exits (either on - * its own or if it is killed). - * - * @returns The first case indicates failure during output - * processing. A status code and exception are returned, providing - * more information. The second case indicates success, and - * realisations for each output of the derivation are returned. - */ - std::variant, SingleDrvOutputs> unprepareBuild(); - -private: - - /** - * Fill in the environment for the builder. - */ - void initEnv(); - - /** - * Process messages send by the sandbox initialization. - */ - void processSandboxSetupMessages(); - - /** - * Setup tmp dir location. - */ - void initTmpDir(); - - /** - * Write a JSON file containing the derivation attributes. - */ - void writeStructuredAttrs(); - - /** - * Start an in-process nix daemon thread for recursive-nix. - */ - void startDaemon(); - -public: - - /** - * Stop the in-process nix daemon thread. - * @see startDaemon - */ - void stopDaemon(); - -private: - - void addDependency(const StorePath & path) override; - - /** - * Make a file owned by the builder. - */ - void chownToBuilder(const Path & path); - - /** - * Run the builder's process. - */ - void runChild(); - - /** - * Check that the derivation outputs all exist and register them - * as valid. - */ - SingleDrvOutputs registerOutputs(); - - /** - * Check that an output meets the requirements specified by the - * 'outputChecks' attribute (or the legacy - * '{allowed,disallowed}{References,Requisites}' attributes). - */ - void checkOutputs(const std::map & outputs); - -public: - - /** - * Delete the temporary directory, if we have one. - */ - void deleteTmpDir(bool force); - - /** - * Kill any processes running under the build user UID or in the - * cgroup of the build. - */ - void killSandbox(bool getStats); - -private: - - bool cleanupDecideWhetherDiskFull(); - - /** - * Create alternative path calculated from but distinct from the - * input, so we can avoid overwriting outputs (or other store paths) - * that already exist. - */ - StorePath makeFallbackPath(const StorePath & path); - - /** - * Make a path to another based on the output name along with the - * derivation hash. - * - * @todo Add option to randomize, so we can audit whether our - * rewrites caught everything - */ - StorePath makeFallbackPath(OutputNameView outputName); -}; - /** * This hooks up `DerivationBuilder` to the scheduler / goal machinary. * @@ -513,13 +35,13 @@ private: */ struct LocalDerivationGoal : DerivationGoal, DerivationBuilderCallbacks { - DerivationBuilder builder; + std::unique_ptr builder; LocalDerivationGoal(const StorePath & drvPath, const OutputsSpec & wantedOutputs, Worker & worker, BuildMode buildMode) : DerivationGoal{drvPath, wantedOutputs, worker, buildMode} - , builder{ + , builder{makeDerivationBuilder( worker.store, static_cast(*this), DerivationBuilderParams { @@ -531,15 +53,14 @@ struct LocalDerivationGoal : DerivationGoal, DerivationBuilderCallbacks DerivationGoal::drvOptions, DerivationGoal::inputPaths, DerivationGoal::initialOutputs, - }, - } + })} {} LocalDerivationGoal(const StorePath & drvPath, const BasicDerivation & drv, const OutputsSpec & wantedOutputs, Worker & worker, BuildMode buildMode = bmNormal) : DerivationGoal{drvPath, drv, wantedOutputs, worker, buildMode} - , builder{ + , builder{makeDerivationBuilder( worker.store, static_cast(*this), DerivationBuilderParams { @@ -551,8 +72,7 @@ struct LocalDerivationGoal : DerivationGoal, DerivationBuilderCallbacks DerivationGoal::drvOptions, DerivationGoal::inputPaths, DerivationGoal::initialOutputs, - }, - } + })} {} virtual ~LocalDerivationGoal() override; @@ -611,75 +131,20 @@ std::shared_ptr makeLocalDerivationGoal( return std::make_shared(drvPath, drv, wantedOutputs, worker, buildMode); } -void handleDiffHook( - uid_t uid, uid_t gid, - const Path & tryA, const Path & tryB, - const Path & drvPath, const Path & tmpDir) -{ - auto & diffHookOpt = settings.diffHook.get(); - if (diffHookOpt && settings.runDiffHook) { - auto & diffHook = *diffHookOpt; - try { - auto diffRes = runProgram(RunOptions { - .program = diffHook, - .lookupPath = true, - .args = {tryA, tryB, drvPath, tmpDir}, - .uid = uid, - .gid = gid, - .chdir = "/" - }); - if (!statusOk(diffRes.first)) - throw ExecError(diffRes.first, - "diff-hook program '%1%' %2%", - diffHook, - statusToString(diffRes.first)); - - if (diffRes.second != "") - printError(chomp(diffRes.second)); - } catch (Error & error) { - ErrorInfo ei = error.info(); - // FIXME: wrap errors. - ei.msg = HintFmt("diff hook execution failed: %s", ei.msg.str()); - logError(ei); - } - } -} - -const Path DerivationBuilder::homeDir = "/homeless-shelter"; - LocalDerivationGoal::~LocalDerivationGoal() { /* Careful: we should never ever throw an exception from a destructor. */ - try { builder.deleteTmpDir(false); } catch (...) { ignoreExceptionInDestructor(); } + try { builder->deleteTmpDir(false); } catch (...) { ignoreExceptionInDestructor(); } try { killChild(); } catch (...) { ignoreExceptionInDestructor(); } - try { builder.stopDaemon(); } catch (...) { ignoreExceptionInDestructor(); } -} - - -inline bool DerivationBuilder::needsHashRewrite() -{ -#ifdef __linux__ - return !useChroot; -#else - /* Darwin requires hash rewriting even when sandboxing is enabled. */ - return true; -#endif -} - - -LocalStore & DerivationBuilder::getLocalStore() -{ - auto p = dynamic_cast(&store); - assert(p); - return *p; + try { builder->stopDaemon(); } catch (...) { ignoreExceptionInDestructor(); } } void LocalDerivationGoal::killChild() { - if (builder.pid != -1) { + if (builder->pid != -1) { worker.childTerminated(this); /* If we're using a build user, then there is a tricky race @@ -687,42 +152,20 @@ void LocalDerivationGoal::killChild() done its setuid() to the build user uid, then it won't be killed, and we'll potentially lock up in pid.wait(). So also send a conventional kill to the child. */ - ::kill(-builder.pid, SIGKILL); /* ignore the result */ + ::kill(-builder->pid, SIGKILL); /* ignore the result */ - builder.killSandbox(true); + builder->killSandbox(true); - builder.pid.wait(); + builder->pid.wait(); } DerivationGoal::killChild(); } -void DerivationBuilder::killSandbox(bool getStats) -{ - if (cgroup) { - #ifdef __linux__ - auto stats = destroyCgroup(*cgroup); - if (getStats) { - buildResult.cpuUser = stats.cpuUser; - buildResult.cpuSystem = stats.cpuSystem; - } - #else - unreachable(); - #endif - } - - else if (buildUser) { - auto uid = buildUser->getUID(); - assert(uid != 0); - killUser(uid); - } -} - - void LocalDerivationGoal::childStarted() { - worker.childStarted(shared_from_this(), {builder.builderOut.get()}, true, true); + worker.childStarted(shared_from_this(), {builder->builderOut.get()}, true, true); } void LocalDerivationGoal::childTerminated() @@ -759,7 +202,7 @@ Goal::Co LocalDerivationGoal::tryLocalBuild() co_return tryToBuild(); } - if (!builder.prepareBuild()) { + if (!builder->prepareBuild()) { if (!actLock) actLock = std::make_unique(*logger, lvlWarn, actBuildWaiting, fmt("waiting for a free build user ID for '%s'", Magenta(worker.store.printStorePath(drvPath)))); @@ -772,11 +215,11 @@ Goal::Co LocalDerivationGoal::tryLocalBuild() try { /* Okay, we have to build. */ - builder.startBuilder(); + builder->startBuilder(); } catch (BuildError & e) { outputLocks.unlock(); - builder.buildUser.reset(); + builder->buildUser.reset(); worker.permanentFailure = true; co_return done(BuildResult::InputRejected, {}, std::move(e)); } @@ -786,7 +229,7 @@ Goal::Co LocalDerivationGoal::tryLocalBuild() trace("build done"); - auto res = builder.unprepareBuild(); + auto res = builder->unprepareBuild(); // N.B. cannot use `std::visit` with co-routine return if (auto * ste = std::get_if<0>(&res)) { outputLocks.unlock(); @@ -804,2606 +247,11 @@ Goal::Co LocalDerivationGoal::tryLocalBuild() } } -bool DerivationBuilder::prepareBuild() -{ - /* Cache this */ - derivationType = drv->type(); - - /* Are we doing a chroot build? */ - { - if (settings.sandboxMode == smEnabled) { - if (drvOptions->noChroot) - throw Error("derivation '%s' has '__noChroot' set, " - "but that's not allowed when 'sandbox' is 'true'", store.printStorePath(drvPath)); -#ifdef __APPLE__ - if (drvOptions->additionalSandboxProfile != "") - throw Error("derivation '%s' specifies a sandbox profile, " - "but this is only allowed when 'sandbox' is 'relaxed'", worker.store.printStorePath(drvPath)); -#endif - useChroot = true; - } - else if (settings.sandboxMode == smDisabled) - useChroot = false; - else if (settings.sandboxMode == smRelaxed) - useChroot = derivationType->isSandboxed() && !drvOptions->noChroot; - } - - auto & localStore = getLocalStore(); - if (localStore.storeDir != localStore.realStoreDir.get()) { - #ifdef __linux__ - useChroot = true; - #else - throw Error("building using a diverted store is not supported on this platform"); - #endif - } - - #ifdef __linux__ - if (useChroot) { - if (!mountAndPidNamespacesSupported()) { - if (!settings.sandboxFallback) - throw Error("this system does not support the kernel namespaces that are required for sandboxing; use '--no-sandbox' to disable sandboxing"); - debug("auto-disabling sandboxing because the prerequisite namespaces are not available"); - useChroot = false; - } - } - #endif - - if (useBuildUsers()) { - if (!buildUser) - buildUser = acquireUserLock(drvOptions->useUidRange(*drv) ? 65536 : 1, useChroot); - - if (!buildUser) { - return false; - } - } - - return true; -} - - -std::variant, SingleDrvOutputs> DerivationBuilder::unprepareBuild() -{ - Finally releaseBuildUser([&](){ - /* Release the build user at the end of this function. We don't do - it right away because we don't want another build grabbing this - uid and then messing around with our output. */ - buildUser.reset(); - }); - - sandboxMountNamespace = -1; - sandboxUserNamespace = -1; - - /* Since we got an EOF on the logger pipe, the builder is presumed - to have terminated. In fact, the builder could also have - simply have closed its end of the pipe, so just to be sure, - kill it. */ - int status = pid.kill(); - - debug("builder process for '%s' finished", store.printStorePath(drvPath)); - - buildResult.timesBuilt++; - buildResult.stopTime = time(0); - - /* So the child is gone now. */ - miscMethods.childTerminated(); - - /* Close the read side of the logger pipe. */ - builderOut.close(); - - /* Close the log file. */ - miscMethods.closeLogFile(); - - /* When running under a build user, make sure that all processes - running under that uid are gone. This is to prevent a - malicious user from leaving behind a process that keeps files - open and modifies them after they have been chown'ed to - root. */ - killSandbox(true); - - /* Terminate the recursive Nix daemon. */ - stopDaemon(); - - if (buildResult.cpuUser && buildResult.cpuSystem) { - debug("builder for '%s' terminated with status %d, user CPU %.3fs, system CPU %.3fs", - store.printStorePath(drvPath), - status, - ((double) buildResult.cpuUser->count()) / 1000000, - ((double) buildResult.cpuSystem->count()) / 1000000); - } - - bool diskFull = false; - - try { - - /* Check the exit status. */ - if (!statusOk(status)) { - - diskFull |= cleanupDecideWhetherDiskFull(); - - auto msg = fmt("builder for '%s' %s", - Magenta(store.printStorePath(drvPath)), - statusToString(status)); - - miscMethods.appendLogTailErrorMsg(msg); - - if (diskFull) - msg += "\nnote: build failure may have been caused by lack of free disk space"; - - throw BuildError(msg); - } - - /* Compute the FS closure of the outputs and register them as - being valid. */ - auto builtOutputs = registerOutputs(); - - StorePathSet outputPaths; - for (auto & [_, output] : builtOutputs) - outputPaths.insert(output.outPath); - runPostBuildHook( - store, - *logger, - drvPath, - outputPaths - ); - - /* Delete unused redirected outputs (when doing hash rewriting). */ - for (auto & i : redirectedOutputs) - deletePath(store.Store::toRealPath(i.second)); - - /* Delete the chroot (if we were using one). */ - autoDelChroot.reset(); /* this runs the destructor */ - - deleteTmpDir(true); - - return std::move(builtOutputs); - - } catch (BuildError & e) { - assert(derivationType); - BuildResult::Status st = - dynamic_cast(&e) ? BuildResult::NotDeterministic : - statusOk(status) ? BuildResult::OutputRejected : - !derivationType->isSandboxed() || diskFull ? BuildResult::TransientFailure : - BuildResult::PermanentFailure; - - return std::pair{std::move(st), std::move(e)}; - } -} - - -static void chmod_(const Path & path, mode_t mode) -{ - if (chmod(path.c_str(), mode) == -1) - throw SysError("setting permissions on '%s'", path); -} - - -/* Move/rename path 'src' to 'dst'. Temporarily make 'src' writable if - it's a directory and we're not root (to be able to update the - directory's parent link ".."). */ -static void movePath(const Path & src, const Path & dst) -{ - auto st = lstat(src); - - bool changePerm = (geteuid() && S_ISDIR(st.st_mode) && !(st.st_mode & S_IWUSR)); - - if (changePerm) - chmod_(src, st.st_mode | S_IWUSR); - - std::filesystem::rename(src, dst); - - if (changePerm) - chmod_(dst, st.st_mode); -} - - -extern void replaceValidPath(const Path & storePath, const Path & tmpPath); - - -bool DerivationBuilder::cleanupDecideWhetherDiskFull() -{ - bool diskFull = false; - - /* Heuristically check whether the build failure may have - been caused by a disk full condition. We have no way - of knowing whether the build actually got an ENOSPC. - So instead, check if the disk is (nearly) full now. If - so, we don't mark this build as a permanent failure. */ -#if HAVE_STATVFS - { - auto & localStore = getLocalStore(); - uint64_t required = 8ULL * 1024 * 1024; // FIXME: make configurable - struct statvfs st; - if (statvfs(localStore.realStoreDir.get().c_str(), &st) == 0 && - (uint64_t) st.f_bavail * st.f_bsize < required) - diskFull = true; - if (statvfs(tmpDir.c_str(), &st) == 0 && - (uint64_t) st.f_bavail * st.f_bsize < required) - diskFull = true; - } -#endif - - deleteTmpDir(false); - - /* Move paths out of the chroot for easier debugging of - build failures. */ - if (useChroot && buildMode == bmNormal) - for (auto & [_, status] : initialOutputs) { - if (!status.known) continue; - if (buildMode != bmCheck && status.known->isValid()) continue; - auto p = store.toRealPath(status.known->path); - if (pathExists(chrootRootDir + p)) - std::filesystem::rename((chrootRootDir + p), p); - } - - return diskFull; -} - - -#ifdef __linux__ -static void doBind(const Path & source, const Path & target, bool optional = false) { - debug("bind mounting '%1%' to '%2%'", source, target); - - auto bindMount = [&]() { - 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); - }; - - auto maybeSt = maybeLstat(source); - if (!maybeSt) { - if (optional) - return; - else - throw SysError("getting attributes of path '%1%'", source); - } - auto st = *maybeSt; - - if (S_ISDIR(st.st_mode)) { - createDirs(target); - bindMount(); - } else if (S_ISLNK(st.st_mode)) { - // Symlinks can (apparently) not be bind-mounted, so just copy it - createDirs(dirOf(target)); - copyFile( - std::filesystem::path(source), - std::filesystem::path(target), false); - } else { - createDirs(dirOf(target)); - writeFile(target, ""); - bindMount(); - } -}; -#endif - -/** - * Rethrow the current exception as a subclass of `Error`. - */ -static void rethrowExceptionAsError() -{ - try { - throw; - } catch (Error &) { - throw; - } catch (std::exception & e) { - throw Error(e.what()); - } catch (...) { - throw Error("unknown exception"); - } -} - -/** - * Send the current exception to the parent in the format expected by - * `DerivationBuilder::processSandboxSetupMessages()`. - */ -static void handleChildException(bool sendException) -{ - try { - rethrowExceptionAsError(); - } catch (Error & e) { - if (sendException) { - writeFull(STDERR_FILENO, "\1\n"); - FdSink sink(STDERR_FILENO); - sink << e; - sink.flush(); - } else - std::cerr << e.msg(); - } -} - -void DerivationBuilder::startBuilder() -{ - if ((buildUser && buildUser->getUIDCount() != 1) - #ifdef __linux__ - || settings.useCgroups - #endif - ) - { - #ifdef __linux__ - experimentalFeatureSettings.require(Xp::Cgroups); - - /* If we're running from the daemon, then this will return the - root cgroup of the service. Otherwise, it will return the - current cgroup. */ - auto rootCgroup = getRootCgroup(); - auto cgroupFS = getCgroupFS(); - if (!cgroupFS) - throw Error("cannot determine the cgroups file system"); - auto rootCgroupPath = canonPath(*cgroupFS + "/" + rootCgroup); - if (!pathExists(rootCgroupPath)) - throw Error("expected cgroup directory '%s'", rootCgroupPath); - - static std::atomic counter{0}; - - cgroup = buildUser - ? fmt("%s/nix-build-uid-%d", rootCgroupPath, buildUser->getUID()) - : fmt("%s/nix-build-pid-%d-%d", rootCgroupPath, getpid(), counter++); - - debug("using cgroup '%s'", *cgroup); - - /* When using a build user, record the cgroup we used for that - user so that if we got interrupted previously, we can kill - any left-over cgroup first. */ - if (buildUser) { - auto cgroupsDir = settings.nixStateDir + "/cgroups"; - createDirs(cgroupsDir); - - auto cgroupFile = fmt("%s/%d", cgroupsDir, buildUser->getUID()); - - if (pathExists(cgroupFile)) { - auto prevCgroup = readFile(cgroupFile); - destroyCgroup(prevCgroup); - } - - writeFile(cgroupFile, *cgroup); - } - - #else - throw Error("cgroups are not supported on this platform"); - #endif - } - - /* Make sure that no other processes are executing under the - sandbox uids. This must be done before any chownToBuilder() - calls. */ - killSandbox(false); - - /* Right platform? */ - if (!drvOptions->canBuildLocally(store, *drv)) { - // since aarch64-darwin has Rosetta 2, this user can actually run x86_64-darwin on their hardware - we should tell them to run the command to install Darwin 2 - if (drv->platform == "x86_64-darwin" && settings.thisSystem == "aarch64-darwin") { - throw Error("run `/usr/sbin/softwareupdate --install-rosetta` to enable your %s to run programs for %s", settings.thisSystem, drv->platform); - } else { - throw Error("a '%s' with features {%s} is required to build '%s', but I am a '%s' with features {%s}", - drv->platform, - concatStringsSep(", ", drvOptions->getRequiredSystemFeatures(*drv)), - store.printStorePath(drvPath), - settings.thisSystem, - concatStringsSep(", ", store.systemFeatures)); - } - } - - /* Create a temporary directory where the build will take - place. */ - topTmpDir = createTempDir(settings.buildDir.get().value_or(""), "nix-build-" + std::string(drvPath.name()), false, false, 0700); -#ifdef __APPLE__ - if (false) { -#else - if (useChroot) { -#endif - /* If sandboxing is enabled, put the actual TMPDIR underneath - an inaccessible root-owned directory, to prevent outside - access. - - On macOS, we don't use an actual chroot, so this isn't - possible. Any mitigation along these lines would have to be - done directly in the sandbox profile. */ - tmpDir = topTmpDir + "/build"; - createDir(tmpDir, 0700); - } else { - tmpDir = topTmpDir; - } - chownToBuilder(tmpDir); - - for (auto & [outputName, status] : initialOutputs) { - /* Set scratch path we'll actually use during the build. - - If we're not doing a chroot build, but we have some valid - output paths. Since we can't just overwrite or delete - them, we have to do hash rewriting: i.e. in the - environment/arguments passed to the build, we replace the - hashes of the valid outputs with unique dummy strings; - after the build, we discard the redirected outputs - corresponding to the valid outputs, and rewrite the - contents of the new outputs to replace the dummy strings - with the actual hashes. */ - auto scratchPath = - !status.known - ? makeFallbackPath(outputName) - : !needsHashRewrite() - /* Can always use original path in sandbox */ - ? status.known->path - : !status.known->isPresent() - /* If path doesn't yet exist can just use it */ - ? status.known->path - : buildMode != bmRepair && !status.known->isValid() - /* If we aren't repairing we'll delete a corrupted path, so we - can use original path */ - ? status.known->path - : /* If we are repairing or the path is totally valid, we'll need - to use a temporary path */ - makeFallbackPath(status.known->path); - scratchOutputs.insert_or_assign(outputName, scratchPath); - - /* Substitute output placeholders with the scratch output paths. - We'll use during the build. */ - inputRewrites[hashPlaceholder(outputName)] = store.printStorePath(scratchPath); - - /* Additional tasks if we know the final path a priori. */ - if (!status.known) continue; - auto fixedFinalPath = status.known->path; - - /* Additional tasks if the final and scratch are both known and - differ. */ - if (fixedFinalPath == scratchPath) continue; - - /* Ensure scratch path is ours to use. */ - deletePath(store.printStorePath(scratchPath)); - - /* Rewrite and unrewrite paths */ - { - std::string h1 { fixedFinalPath.hashPart() }; - std::string h2 { scratchPath.hashPart() }; - inputRewrites[h1] = h2; - } - - redirectedOutputs.insert_or_assign(std::move(fixedFinalPath), std::move(scratchPath)); - } - - /* Construct the environment passed to the builder. */ - initEnv(); - - writeStructuredAttrs(); - - /* Handle exportReferencesGraph(), if set. */ - if (!parsedDrv->hasStructuredAttrs()) { - /* The `exportReferencesGraph' feature allows the references graph - to be passed to a builder. This attribute should be a list of - pairs [name1 path1 name2 path2 ...]. The references graph of - each `pathN' will be stored in a text file `nameN' in the - temporary build directory. The text files have the format used - by `nix-store --register-validity'. However, the deriver - fields are left empty. */ - auto s = getOr(drv->env, "exportReferencesGraph", ""); - Strings ss = tokenizeString(s); - if (ss.size() % 2 != 0) - throw BuildError("odd number of tokens in 'exportReferencesGraph': '%1%'", s); - for (Strings::iterator i = ss.begin(); i != ss.end(); ) { - auto fileName = *i++; - static std::regex regex("[A-Za-z_][A-Za-z0-9_.-]*"); - if (!std::regex_match(fileName, regex)) - throw Error("invalid file name '%s' in 'exportReferencesGraph'", fileName); - - auto storePathS = *i++; - if (!store.isInStore(storePathS)) - throw BuildError("'exportReferencesGraph' contains a non-store path '%1%'", storePathS); - auto storePath = store.toStorePath(storePathS).first; - - /* Write closure info to . */ - writeFile(tmpDir + "/" + fileName, - store.makeValidityRegistration( - store.exportReferences({storePath}, inputPaths), false, false)); - } - } - - if (useChroot) { - - /* Allow a user-configurable set of directories from the - host file system. */ - pathsInChroot.clear(); - - for (auto i : settings.sandboxPaths.get()) { - if (i.empty()) continue; - bool optional = false; - if (i[i.size() - 1] == '?') { - optional = true; - i.pop_back(); - } - size_t p = i.find('='); - if (p == std::string::npos) - pathsInChroot[i] = {i, optional}; - else - pathsInChroot[i.substr(0, p)] = {i.substr(p + 1), optional}; - } - if (hasPrefix(store.storeDir, tmpDirInSandbox)) - { - throw Error("`sandbox-build-dir` must not contain the storeDir"); - } - pathsInChroot[tmpDirInSandbox] = tmpDir; - - /* Add the closure of store paths to the chroot. */ - StorePathSet closure; - for (auto & i : pathsInChroot) - try { - if (store.isInStore(i.second.source)) - store.computeFSClosure(store.toStorePath(i.second.source).first, closure); - } catch (InvalidPath & e) { - } catch (Error & e) { - e.addTrace({}, "while processing 'sandbox-paths'"); - throw; - } - for (auto & i : closure) { - auto p = store.printStorePath(i); - pathsInChroot.insert_or_assign(p, p); - } - - PathSet allowedPaths = settings.allowedImpureHostPrefixes; - - /* This works like the above, except on a per-derivation level */ - auto impurePaths = drvOptions->impureHostDeps; - - for (auto & i : impurePaths) { - bool found = false; - /* Note: we're not resolving symlinks here to prevent - giving a non-root user info about inaccessible - files. */ - Path canonI = canonPath(i); - /* If only we had a trie to do this more efficiently :) luckily, these are generally going to be pretty small */ - for (auto & a : allowedPaths) { - Path canonA = canonPath(a); - if (isDirOrInDir(canonI, canonA)) { - found = true; - break; - } - } - if (!found) - throw Error("derivation '%s' requested impure path '%s', but it was not in allowed-impure-host-deps", - store.printStorePath(drvPath), i); - - /* Allow files in drvOptions->impureHostDeps to be missing; e.g. - macOS 11+ has no /usr/lib/libSystem*.dylib */ - pathsInChroot[i] = {i, true}; - } - -#ifdef __linux__ - /* Create a temporary directory in which we set up the chroot - environment using bind-mounts. We put it in the Nix store - so that the build outputs can be moved efficiently from the - chroot to their final location. */ - auto chrootParentDir = store.Store::toRealPath(drvPath) + ".chroot"; - deletePath(chrootParentDir); - - /* Clean up the chroot directory automatically. */ - autoDelChroot = std::make_shared(chrootParentDir); - - printMsg(lvlChatty, "setting up chroot environment in '%1%'", chrootParentDir); - - if (mkdir(chrootParentDir.c_str(), 0700) == -1) - throw SysError("cannot create '%s'", chrootRootDir); - - chrootRootDir = chrootParentDir + "/root"; - - if (mkdir(chrootRootDir.c_str(), buildUser && buildUser->getUIDCount() != 1 ? 0755 : 0750) == -1) - throw SysError("cannot create '%1%'", chrootRootDir); - - if (buildUser && chown(chrootRootDir.c_str(), buildUser->getUIDCount() != 1 ? buildUser->getUID() : 0, buildUser->getGID()) == -1) - throw SysError("cannot change ownership of '%1%'", chrootRootDir); - - /* Create a writable /tmp in the chroot. Many builders need - this. (Of course they should really respect $TMPDIR - instead.) */ - Path chrootTmpDir = chrootRootDir + "/tmp"; - createDirs(chrootTmpDir); - chmod_(chrootTmpDir, 01777); - - /* Create a /etc/passwd with entries for the build user and the - nobody account. The latter is kind of a hack to support - Samba-in-QEMU. */ - createDirs(chrootRootDir + "/etc"); - if (drvOptions->useUidRange(*drv)) - chownToBuilder(chrootRootDir + "/etc"); - - if (drvOptions->useUidRange(*drv) && (!buildUser || buildUser->getUIDCount() < 65536)) - throw Error("feature 'uid-range' requires the setting '%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"). */ - writeFile(chrootRootDir + "/etc/group", - fmt("root:x:0:\n" - "nixbld:!:%1%:\n" - "nogroup:x:65534:\n", sandboxGid())); - - /* Create /etc/hosts with localhost entry. */ - if (derivationType->isSandboxed()) - writeFile(chrootRootDir + "/etc/hosts", "127.0.0.1 localhost\n::1 localhost\n"); - - /* Make the closure of the inputs available in the chroot, - rather than the whole Nix store. This prevents any access - to undeclared dependencies. Directories are bind-mounted, - while other inputs are hard-linked (since only directories - can be bind-mounted). !!! As an extra security - precaution, make the fake Nix store only writable by the - build user. */ - Path chrootStoreDir = chrootRootDir + store.storeDir; - createDirs(chrootStoreDir); - chmod_(chrootStoreDir, 01775); - - if (buildUser && chown(chrootStoreDir.c_str(), 0, buildUser->getGID()) == -1) - throw SysError("cannot change ownership of '%1%'", chrootStoreDir); - - for (auto & i : inputPaths) { - auto p = store.printStorePath(i); - Path r = store.toRealPath(p); - pathsInChroot.insert_or_assign(p, r); - } - - /* If we're repairing, checking or rebuilding part of a - multiple-outputs derivation, it's possible that we're - rebuilding a path that is in settings.sandbox-paths - (typically the dependencies of /bin/sh). Throw them - out. */ - for (auto & i : drv->outputsAndOptPaths(store)) { - /* If the name isn't known a priori (i.e. floating - content-addressing derivation), the temporary location we use - should be fresh. Freshness means it is impossible that the path - is already in the sandbox, so we don't need to worry about - removing it. */ - if (i.second.second) - pathsInChroot.erase(store.printStorePath(*i.second.second)); - } - - if (cgroup) { - if (mkdir(cgroup->c_str(), 0755) != 0) - throw SysError("creating cgroup '%s'", *cgroup); - chownToBuilder(*cgroup); - chownToBuilder(*cgroup + "/cgroup.procs"); - chownToBuilder(*cgroup + "/cgroup.threads"); - //chownToBuilder(*cgroup + "/cgroup.subtree_control"); - } - -#else - if (drvOptions->useUidRange(*drv)) - throw Error("feature 'uid-range' is not supported on this platform"); - #ifdef __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 (drvOptions->useUidRange(*drv)) - throw Error("feature 'uid-range' is only supported in sandboxed builds"); - } - - if (needsHashRewrite() && pathExists(homeDir)) - throw Error("home directory '%1%' exists; please remove it to assure purity of builds without sandboxing", homeDir); - - if (useChroot && settings.preBuildHook != "" && dynamic_cast(drv.get())) { - printMsg(lvlChatty, "executing pre-build hook '%1%'", settings.preBuildHook); - auto args = useChroot ? Strings({store.printStorePath(drvPath), chrootRootDir}) : - Strings({ store.printStorePath(drvPath) }); - enum BuildHookState { - stBegin, - stExtraChrootDirs - }; - auto state = stBegin; - auto lines = runProgram(settings.preBuildHook, false, args); - auto lastPos = std::string::size_type{0}; - for (auto nlPos = lines.find('\n'); nlPos != std::string::npos; - nlPos = lines.find('\n', lastPos)) - { - auto line = lines.substr(lastPos, nlPos - lastPos); - lastPos = nlPos + 1; - if (state == stBegin) { - if (line == "extra-sandbox-paths" || line == "extra-chroot-dirs") { - state = stExtraChrootDirs; - } else { - throw Error("unknown pre-build hook command '%1%'", line); - } - } else if (state == stExtraChrootDirs) { - if (line == "") { - state = stBegin; - } else { - auto p = line.find('='); - if (p == std::string::npos) - pathsInChroot[line] = line; - else - pathsInChroot[line.substr(0, p)] = line.substr(p + 1); - } - } - } - } - - /* Fire up a Nix daemon to process recursive Nix calls from the - builder. */ - if (drvOptions->getRequiredSystemFeatures(*drv).count("recursive-nix")) - startDaemon(); - - /* Run the builder. */ - printMsg(lvlChatty, "executing builder '%1%'", drv->builder); - printMsg(lvlChatty, "using builder args '%1%'", concatStringsSep(" ", drv->args)); - for (auto & i : drv->env) - printMsg(lvlVomit, "setting builder env variable '%1%'='%2%'", i.first, i.second); - - /* Create the log file. */ - [[maybe_unused]] Path logFile = miscMethods.openLogFile(); - - /* Create a pseudoterminal to get the output of the builder. */ - builderOut = posix_openpt(O_RDWR | O_NOCTTY); - if (!builderOut) - throw SysError("opening pseudoterminal master"); - - // FIXME: not thread-safe, use ptsname_r - std::string slaveName = ptsname(builderOut.get()); - - if (buildUser) { - if (chmod(slaveName.c_str(), 0600)) - throw SysError("changing mode of pseudoterminal slave"); - - if (chown(slaveName.c_str(), buildUser->getUID(), 0)) - throw SysError("changing owner of pseudoterminal slave"); - } -#ifdef __APPLE__ - else { - if (grantpt(builderOut.get())) - throw SysError("granting access to pseudoterminal slave"); - } -#endif - - if (unlockpt(builderOut.get())) - throw SysError("unlocking pseudoterminal"); - - /* Open the slave side of the pseudoterminal and use it as stderr. */ - auto openSlave = [&]() - { - AutoCloseFD builderOut = open(slaveName.c_str(), O_RDWR | O_NOCTTY); - if (!builderOut) - throw SysError("opening pseudoterminal slave"); - - // Put the pt into raw mode to prevent \n -> \r\n translation. - struct termios term; - if (tcgetattr(builderOut.get(), &term)) - throw SysError("getting pseudoterminal attributes"); - - cfmakeraw(&term); - - if (tcsetattr(builderOut.get(), TCSANOW, &term)) - throw SysError("putting pseudoterminal into raw mode"); - - if (dup2(builderOut.get(), STDERR_FILENO) == -1) - throw SysError("cannot pipe standard error into log file"); - }; - - buildResult.startTime = time(0); - - /* Fork a child to build the package. */ - -#ifdef __linux__ - if (useChroot) { - /* Set up private namespaces for the build: - - - The PID namespace causes the build to start as PID 1. - Processes outside of the chroot are not visible to those - on the inside, but processes inside the chroot are - visible from the outside (though with different PIDs). - - - The private mount namespace ensures that all the bind - mounts we do will only show up in this process and its - children, and will disappear automatically when we're - done. - - - The private network namespace ensures that the builder - cannot talk to the outside world (or vice versa). It - only has a private loopback interface. (Fixed-output - derivations are not run in a private network namespace - to allow functions like fetchurl to work.) - - - The IPC namespace prevents the builder from communicating - with outside processes using SysV IPC mechanisms (shared - memory, message queues, semaphores). It also ensures - that all IPC objects are destroyed when the builder - exits. - - - The UTS namespace ensures that builders see a hostname of - localhost rather than the actual hostname. - - We use a helper process to do the clone() to work around - clone() being broken in multi-threaded programs due to - at-fork handlers not being run. Note that we use - CLONE_PARENT to ensure that the real builder is parented to - us. - */ - - userNamespaceSync.create(); - - usingUserNamespace = userNamespacesSupported(); - - Pipe sendPid; - sendPid.create(); - - Pid helper = startProcess([&]() { - sendPid.readSide.close(); - - /* We need to open the slave early, before - CLONE_NEWUSER. Otherwise we get EPERM when running as - root. */ - openSlave(); - - try { - /* Drop additional groups here because we can't do it - after we've created the new user namespace. */ - if (setgroups(0, 0) == -1) { - if (errno != EPERM) - throw SysError("setgroups failed"); - if (settings.requireDropSupplementaryGroups) - throw Error("setgroups failed. Set the require-drop-supplementary-groups option to false to skip this step."); - } - - ProcessOptions options; - options.cloneFlags = CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWIPC | CLONE_NEWUTS | CLONE_PARENT | SIGCHLD; - if (derivationType->isSandboxed()) - options.cloneFlags |= CLONE_NEWNET; - if (usingUserNamespace) - options.cloneFlags |= CLONE_NEWUSER; - - pid_t child = startProcess([&]() { runChild(); }, options); - - writeFull(sendPid.writeSide.get(), fmt("%d\n", child)); - _exit(0); - } catch (...) { - handleChildException(true); - _exit(1); - } - }); - - sendPid.writeSide.close(); - - if (helper.wait() != 0) { - processSandboxSetupMessages(); - // Only reached if the child process didn't send an exception. - throw Error("unable to start build process"); - } - - userNamespaceSync.readSide = -1; - - /* Close the write side to prevent runChild() from hanging - reading from this. */ - Finally cleanup([&]() { - userNamespaceSync.writeSide = -1; - }); - - auto ss = tokenizeString>(readLine(sendPid.readSide.get())); - assert(ss.size() == 1); - pid = string2Int(ss[0]).value(); - - if (usingUserNamespace) { - /* Set the UID/GID mapping of the builder's user namespace - such that the sandbox user maps to the build user, or to - the calling user (if build users are disabled). */ - uid_t hostUid = buildUser ? buildUser->getUID() : getuid(); - uid_t hostGid = buildUser ? buildUser->getGID() : getgid(); - uid_t nrIds = buildUser ? buildUser->getUIDCount() : 1; - - writeFile("/proc/" + std::to_string(pid) + "/uid_map", - fmt("%d %d %d", sandboxUid(), hostUid, nrIds)); - - if (!buildUser || buildUser->getUIDCount() == 1) - writeFile("/proc/" + std::to_string(pid) + "/setgroups", "deny"); - - writeFile("/proc/" + std::to_string(pid) + "/gid_map", - fmt("%d %d %d", sandboxGid(), hostGid, nrIds)); - } else { - debug("note: not using a user namespace"); - if (!buildUser) - throw Error("cannot perform a sandboxed build because user namespaces are not enabled; check /proc/sys/user/max_user_namespaces"); - } - - /* Now that we now the sandbox uid, we can write - /etc/passwd. */ - writeFile(chrootRootDir + "/etc/passwd", fmt( - "root:x:0:0:Nix build user:%3%:/noshell\n" - "nixbld:x:%1%:%2%:Nix build user:%3%:/noshell\n" - "nobody:x:65534:65534:Nobody:/:/noshell\n", - sandboxUid(), sandboxGid(), settings.sandboxBuildDir)); - - /* Save the mount- and user 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"); - - if (usingUserNamespace) { - sandboxUserNamespace = open(fmt("/proc/%d/ns/user", (pid_t) pid).c_str(), O_RDONLY); - if (sandboxUserNamespace.get() == -1) - throw SysError("getting sandbox user namespace"); - } - - /* Move the child into its own cgroup. */ - if (cgroup) - writeFile(*cgroup + "/cgroup.procs", fmt("%d", (pid_t) pid)); - - /* Signal the builder that we've updated its user namespace. */ - writeFull(userNamespaceSync.writeSide.get(), "1"); - - } else -#endif - { - pid = startProcess([&]() { - openSlave(); - runChild(); - }); - } - - /* parent */ - pid.setSeparatePG(true); - miscMethods.childStarted(); - - processSandboxSetupMessages(); -} - - -void DerivationBuilder::processSandboxSetupMessages() -{ - std::vector msgs; - while (true) { - std::string msg = [&]() { - try { - return readLine(builderOut.get()); - } catch (Error & e) { - auto status = pid.wait(); - e.addTrace({}, "while waiting for the build environment for '%s' to initialize (%s, previous messages: %s)", - store.printStorePath(drvPath), - statusToString(status), - concatStringsSep("|", msgs)); - throw; - } - }(); - if (msg.substr(0, 1) == "\2") break; - if (msg.substr(0, 1) == "\1") { - FdSource source(builderOut.get()); - auto ex = readError(source); - ex.addTrace({}, "while setting up the build environment"); - throw ex; - } - debug("sandbox setup: " + msg); - msgs.push_back(std::move(msg)); - } -} - - -void DerivationBuilder::initTmpDir() -{ - /* In a sandbox, for determinism, always use the same temporary - directory. */ -#ifdef __linux__ - tmpDirInSandbox = useChroot ? settings.sandboxBuildDir : tmpDir; -#else - tmpDirInSandbox = tmpDir; -#endif - - /* In non-structured mode, set all bindings either directory in the - environment or via a file, as specified by - `DerivationOptions::passAsFile`. */ - if (!parsedDrv->hasStructuredAttrs()) { - for (auto & i : drv->env) { - if (drvOptions->passAsFile.find(i.first) == drvOptions->passAsFile.end()) { - env[i.first] = i.second; - } else { - auto hash = hashString(HashAlgorithm::SHA256, i.first); - std::string fn = ".attr-" + hash.to_string(HashFormat::Nix32, false); - Path p = tmpDir + "/" + fn; - writeFile(p, rewriteStrings(i.second, inputRewrites)); - chownToBuilder(p); - env[i.first + "Path"] = tmpDirInSandbox + "/" + fn; - } - } - - } - - /* For convenience, set an environment pointing to the top build - directory. */ - env["NIX_BUILD_TOP"] = tmpDirInSandbox; - - /* Also set TMPDIR and variants to point to this directory. */ - env["TMPDIR"] = env["TEMPDIR"] = env["TMP"] = env["TEMP"] = tmpDirInSandbox; - - /* Explicitly set PWD to prevent problems with chroot builds. In - particular, dietlibc cannot figure out the cwd because the - inode of the current directory doesn't appear in .. (because - getdents returns the inode of the mount point). */ - env["PWD"] = tmpDirInSandbox; -} - - -void DerivationBuilder::initEnv() -{ - env.clear(); - - /* Most shells initialise PATH to some default (/bin:/usr/bin:...) when - PATH is not set. We don't want this, so we fill it in with some dummy - value. */ - env["PATH"] = "/path-not-set"; - - /* Set HOME to a non-existing path to prevent certain programs from using - /etc/passwd (or NIS, or whatever) to locate the home directory (for - example, wget looks for ~/.wgetrc). I.e., these tools use /etc/passwd - if HOME is not set, but they will just assume that the settings file - they are looking for does not exist if HOME is set but points to some - non-existing path. */ - env["HOME"] = homeDir; - - /* Tell the builder where the Nix store is. Usually they - shouldn't care, but this is useful for purity checking (e.g., - the compiler or linker might only want to accept paths to files - in the store or in the build directory). */ - env["NIX_STORE"] = store.storeDir; - - /* The maximum number of cores to utilize for parallel building. */ - env["NIX_BUILD_CORES"] = fmt("%d", settings.buildCores); - - initTmpDir(); - - /* Compatibility hack with Nix <= 0.7: if this is a fixed-output - derivation, tell the builder, so that for instance `fetchurl' - can skip checking the output. On older Nixes, this environment - variable won't be set, so `fetchurl' will do the check. */ - if (derivationType->isFixed()) env["NIX_OUTPUT_CHECKED"] = "1"; - - /* *Only* if this is a fixed-output derivation, propagate the - values of the environment variables specified in the - `impureEnvVars' attribute to the builder. This allows for - instance environment variables for proxy configuration such as - `http_proxy' to be easily passed to downloaders like - `fetchurl'. Passing such environment variables from the caller - to the builder is generally impure, but the output of - fixed-output derivations is by definition pure (since we - already know the cryptographic hash of the output). */ - if (!derivationType->isSandboxed()) { - auto & impureEnv = settings.impureEnv.get(); - if (!impureEnv.empty()) - experimentalFeatureSettings.require(Xp::ConfigurableImpureEnv); - - for (auto & i : drvOptions->impureEnvVars){ - auto envVar = impureEnv.find(i); - if (envVar != impureEnv.end()) { - env[i] = envVar->second; - } else { - env[i] = getEnv(i).value_or(""); - } - } - } - - /* Currently structured log messages piggyback on stderr, but we - may change that in the future. So tell the builder which file - descriptor to use for that. */ - env["NIX_LOG_FD"] = "2"; - - /* Trigger colored output in various tools. */ - env["TERM"] = "xterm-256color"; -} - - -void DerivationBuilder::writeStructuredAttrs() -{ - if (auto structAttrsJson = parsedDrv->prepareStructuredAttrs(store, inputPaths)) { - auto json = structAttrsJson.value(); - nlohmann::json rewritten; - for (auto & [i, v] : json["outputs"].get()) { - /* The placeholder must have a rewrite, so we use it to cover both the - cases where we know or don't know the output path ahead of time. */ - rewritten[i] = rewriteStrings((std::string) v, inputRewrites); - } - - json["outputs"] = rewritten; - - auto jsonSh = writeStructuredAttrsShell(json); - - writeFile(tmpDir + "/.attrs.sh", rewriteStrings(jsonSh, inputRewrites)); - chownToBuilder(tmpDir + "/.attrs.sh"); - env["NIX_ATTRS_SH_FILE"] = tmpDirInSandbox + "/.attrs.sh"; - writeFile(tmpDir + "/.attrs.json", rewriteStrings(json.dump(), inputRewrites)); - chownToBuilder(tmpDir + "/.attrs.json"); - env["NIX_ATTRS_JSON_FILE"] = tmpDirInSandbox + "/.attrs.json"; - } -} - - -void DerivationBuilder::startDaemon() -{ - experimentalFeatureSettings.require(Xp::RecursiveNix); - - Store::Params params; - params["path-info-cache-size"] = "0"; - params["store"] = store.storeDir; - if (auto & optRoot = getLocalStore().rootDir.get()) - params["root"] = *optRoot; - params["state"] = "/no-such-path"; - params["log"] = "/no-such-path"; - auto store = makeRestrictedStore(params, - ref(std::dynamic_pointer_cast(this->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 || errno == EAGAIN) continue; - if (errno == EINVAL || errno == ECONNABORTED) break; - throw SysError("accepting connection"); - } - - unix::closeOnExec(remote.get()); - - debug("received daemon connection"); - - auto workerThread = std::thread([store, remote{std::move(remote)}]() { - try { - daemon::processConnection( - store, - FdSource(remote.get()), - FdSink(remote.get()), - NotTrusted, daemon::Recursive); - debug("terminated daemon connection"); - } catch (const Interrupted &) { - debug("interrupted daemon connection"); - } catch (SystemError &) { - ignoreExceptionExceptInterrupt(); - } - }); - - daemonWorkerThreads.push_back(std::move(workerThread)); - } - - debug("daemon shutting down"); - }); -} - - -void DerivationBuilder::stopDaemon() -{ - if (daemonSocket && shutdown(daemonSocket.get(), SHUT_RDWR) == -1) { - // According to the POSIX standard, the 'shutdown' function should - // return an ENOTCONN error when attempting to shut down a socket that - // hasn't been connected yet. This situation occurs when the 'accept' - // function is called on a socket without any accepted connections, - // leaving the socket unconnected. While Linux doesn't seem to produce - // an error for sockets that have only been accepted, more - // POSIX-compliant operating systems like OpenBSD, macOS, and others do - // return the ENOTCONN error. Therefore, we handle this error here to - // avoid raising an exception for compliant behaviour. - if (errno == ENOTCONN) { - daemonSocket.close(); - } else { - throw SysError("shutting down daemon socket"); - } - } - - if (daemonThread.joinable()) - daemonThread.join(); - - // FIXME: should prune worker threads more quickly. - // FIXME: shutdown the client socket to speed up worker termination. - for (auto & thread : daemonWorkerThreads) - thread.join(); - daemonWorkerThreads.clear(); - - // release the socket. - daemonSocket.close(); -} - - -void DerivationBuilder::addDependency(const StorePath & 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", store.printStorePath(path)); - - #ifdef __linux__ - - Path source = store.Store::toRealPath(path); - Path target = chrootRootDir + store.printStorePath(path); - - 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", store.printStorePath(path)); - } - - /* 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 (usingUserNamespace && (setns(sandboxUserNamespace.get(), 0) == -1)) - throw SysError("entering sandbox user namespace"); - - if (setns(sandboxMountNamespace.get(), 0) == -1) - throw SysError("entering sandbox mount namespace"); - - doBind(source, target); - - _exit(0); - })); - - int status = child.wait(); - if (status != 0) - throw Error("could not add path '%s' to sandbox", store.printStorePath(path)); - - #else - throw Error("don't know how to make path '%s' (produced by a recursive Nix call) appear in the sandbox", - worker.store.printStorePath(path)); - #endif - - } -} - -void DerivationBuilder::chownToBuilder(const Path & path) -{ - if (!buildUser) return; - if (chown(path.c_str(), buildUser->getUID(), buildUser->getGID()) == -1) - throw SysError("cannot change ownership of '%1%'", path); -} - - -void setupSeccomp() -{ -#ifdef __linux__ - if (!settings.filterSyscalls) return; -#if HAVE_SECCOMP - scmp_filter_ctx ctx; - - if (!(ctx = seccomp_init(SCMP_ACT_ALLOW))) - throw SysError("unable to initialize seccomp mode 2"); - - Finally cleanup([&]() { - seccomp_release(ctx); - }); - - constexpr std::string_view nativeSystem = NIX_LOCAL_SYSTEM; - - if (nativeSystem == "x86_64-linux" && - seccomp_arch_add(ctx, SCMP_ARCH_X86) != 0) - throw SysError("unable to add 32-bit seccomp architecture"); - - if (nativeSystem == "x86_64-linux" && - seccomp_arch_add(ctx, SCMP_ARCH_X32) != 0) - throw SysError("unable to add X32 seccomp architecture"); - - if (nativeSystem == "aarch64-linux" && - seccomp_arch_add(ctx, SCMP_ARCH_ARM) != 0) - printError("unable to add ARM seccomp architecture; this may result in spurious build failures if running 32-bit ARM processes"); - - if (nativeSystem == "mips64-linux" && - seccomp_arch_add(ctx, SCMP_ARCH_MIPS) != 0) - printError("unable to add mips seccomp architecture"); - - if (nativeSystem == "mips64-linux" && - seccomp_arch_add(ctx, SCMP_ARCH_MIPS64N32) != 0) - printError("unable to add mips64-*abin32 seccomp architecture"); - - if (nativeSystem == "mips64el-linux" && - seccomp_arch_add(ctx, SCMP_ARCH_MIPSEL) != 0) - printError("unable to add mipsel seccomp architecture"); - - if (nativeSystem == "mips64el-linux" && - seccomp_arch_add(ctx, SCMP_ARCH_MIPSEL64N32) != 0) - printError("unable to add mips64el-*abin32 seccomp architecture"); - - /* Prevent builders from creating setuid/setgid binaries. */ - for (int perm : { S_ISUID, S_ISGID }) { - if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(chmod), 1, - SCMP_A1(SCMP_CMP_MASKED_EQ, (scmp_datum_t) perm, (scmp_datum_t) perm)) != 0) - throw SysError("unable to add seccomp rule"); - - if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(fchmod), 1, - SCMP_A1(SCMP_CMP_MASKED_EQ, (scmp_datum_t) perm, (scmp_datum_t) perm)) != 0) - throw SysError("unable to add seccomp rule"); - - if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(fchmodat), 1, - SCMP_A2(SCMP_CMP_MASKED_EQ, (scmp_datum_t) perm, (scmp_datum_t) perm)) != 0) - throw SysError("unable to add seccomp rule"); - - if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), NIX_SYSCALL_FCHMODAT2, 1, - SCMP_A2(SCMP_CMP_MASKED_EQ, (scmp_datum_t) perm, (scmp_datum_t) perm)) != 0) - throw SysError("unable to add seccomp rule"); - } - - /* Prevent builders from using EAs or ACLs. Not all filesystems - support these, and they're not allowed in the Nix store because - they're not representable in the NAR serialisation. */ - if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(getxattr), 0) != 0 || - seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(lgetxattr), 0) != 0 || - seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(fgetxattr), 0) != 0 || - seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(setxattr), 0) != 0 || - seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(lsetxattr), 0) != 0 || - seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(fsetxattr), 0) != 0) - throw SysError("unable to add seccomp rule"); - - if (seccomp_attr_set(ctx, SCMP_FLTATR_CTL_NNP, settings.allowNewPrivileges ? 0 : 1) != 0) - throw SysError("unable to set 'no new privileges' seccomp attribute"); - - if (seccomp_load(ctx) != 0) - throw SysError("unable to load seccomp BPF program"); -#else - throw Error( - "seccomp is not supported on this platform; " - "you can bypass this error by setting the option 'filter-syscalls' to false, but note that untrusted builds can then create setuid binaries!"); -#endif -#endif -} - - -void DerivationBuilder::runChild() -{ - /* Warning: in the child we should absolutely not make any SQLite - calls! */ - - bool sendException = true; - - try { /* child */ - - commonChildInit(); - - try { - setupSeccomp(); - } catch (...) { - if (buildUser) throw; - } - - bool setUser = true; - - /* Make the contents of netrc and the CA certificate bundle - available to builtin:fetchurl (which may run under a - different uid and/or in a sandbox). */ - std::string netrcData; - std::string caFileData; - if (drv->isBuiltin() && drv->builder == "builtin:fetchurl") { - try { - netrcData = readFile(settings.netrcFile); - } catch (SystemError &) { } - - try { - caFileData = readFile(settings.caFile); - } catch (SystemError &) { } - } - -#ifdef __linux__ - if (useChroot) { - - userNamespaceSync.writeSide = -1; - - if (drainFD(userNamespaceSync.readSide.get()) != "1") - throw Error("user namespace initialisation failed"); - - userNamespaceSync.readSide = -1; - - if (derivationType->isSandboxed()) { - - /* Initialise the loopback interface. */ - AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP)); - if (!fd) throw SysError("cannot open IP socket"); - - struct ifreq ifr; - strcpy(ifr.ifr_name, "lo"); - ifr.ifr_flags = IFF_UP | IFF_LOOPBACK | IFF_RUNNING; - if (ioctl(fd.get(), SIOCSIFFLAGS, &ifr) == -1) - throw SysError("cannot set loopback interface flags"); - } - - /* Set the hostname etc. to fixed values. */ - char hostname[] = "localhost"; - if (sethostname(hostname, sizeof(hostname)) == -1) - throw SysError("cannot set host name"); - char domainname[] = "(none)"; // kernel default - if (setdomainname(domainname, sizeof(domainname)) == -1) - throw SysError("cannot set domain name"); - - /* Make all filesystems private. This is necessary - because subtrees may have been mounted as "shared" - (MS_SHARED). (Systemd does this, for instance.) Even - though we have a private mount namespace, mounting - filesystems on top of a shared subtree still propagates - outside of the namespace. Making a subtree private is - local to the namespace, though, so setting MS_PRIVATE - does not affect the outside world. */ - if (mount(0, "/", 0, MS_PRIVATE | MS_REC, 0) == -1) - throw SysError("unable to make '/' private"); - - /* Bind-mount chroot directory to itself, to treat it as a - different filesystem from /, as needed for pivot_root. */ - if (mount(chrootRootDir.c_str(), chrootRootDir.c_str(), 0, MS_BIND, 0) == -1) - throw SysError("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 + 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 - bind-mount the host /dev. */ - Strings ss; - if (pathsInChroot.find("/dev") == pathsInChroot.end()) { - createDirs(chrootRootDir + "/dev/shm"); - createDirs(chrootRootDir + "/dev/pts"); - ss.push_back("/dev/full"); - if (store.systemFeatures.get().count("kvm") && pathExists("/dev/kvm")) - ss.push_back("/dev/kvm"); - ss.push_back("/dev/null"); - ss.push_back("/dev/random"); - ss.push_back("/dev/tty"); - ss.push_back("/dev/urandom"); - ss.push_back("/dev/zero"); - createSymlink("/proc/self/fd", chrootRootDir + "/dev/fd"); - createSymlink("/proc/self/fd/0", chrootRootDir + "/dev/stdin"); - createSymlink("/proc/self/fd/1", chrootRootDir + "/dev/stdout"); - createSymlink("/proc/self/fd/2", chrootRootDir + "/dev/stderr"); - } - - /* Fixed-output derivations typically need to access the - network, so give them access to /etc/resolv.conf and so - on. */ - if (!derivationType->isSandboxed()) { - // Only use nss functions to resolve hosts and - // services. Don’t use it for anything else that may - // be configured for this system. This limits the - // potential impurities introduced in fixed-outputs. - writeFile(chrootRootDir + "/etc/nsswitch.conf", "hosts: files dns\nservices: files\n"); - - /* N.B. it is realistic that these paths might not exist. It - happens when testing Nix building fixed-output derivations - within a pure derivation. */ - for (auto & path : { "/etc/resolv.conf", "/etc/services", "/etc/hosts" }) - if (pathExists(path)) - ss.push_back(path); - - if (settings.caFile != "" && pathExists(settings.caFile)) { - Path caFile = settings.caFile; - pathsInChroot.try_emplace("/etc/ssl/certs/ca-certificates.crt", canonPath(caFile, true), true); - } - } - - for (auto & i : ss) { - // For backwards-compatibiliy, resolve all the symlinks in the - // chroot paths - auto canonicalPath = canonPath(i, true); - pathsInChroot.emplace(i, canonicalPath); - } - - /* Bind-mount all the directories from the "host" - filesystem that we want in the chroot - environment. */ - for (auto & i : pathsInChroot) { - if (i.second.source == "/proc") continue; // backwards compatibility - - #if HAVE_EMBEDDED_SANDBOX_SHELL - if (i.second.source == "__embedded_sandbox_shell__") { - static unsigned char sh[] = { - #include "embedded-sandbox-shell.gen.hh" - }; - auto dst = chrootRootDir + i.first; - createDirs(dirOf(dst)); - writeFile(dst, std::string_view((const char *) sh, sizeof(sh))); - chmod_(dst, 0555); - } else - #endif - doBind(i.second.source, chrootRootDir + i.first, i.second.optional); - } - - /* Bind a new instance of procfs on /proc. */ - createDirs(chrootRootDir + "/proc"); - if (mount("none", (chrootRootDir + "/proc").c_str(), "proc", 0, 0) == -1) - throw SysError("mounting /proc"); - - /* Mount sysfs on /sys. */ - if (buildUser && buildUser->getUIDCount() != 1) { - 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, - fmt("size=%s", settings.sandboxShmSize).c_str()) == -1) - throw SysError("mounting /dev/shm"); - - /* Mount a new devpts on /dev/pts. Note that this - requires the kernel to be compiled with - CONFIG_DEVPTS_MULTIPLE_INSTANCES=y (which is the case - if /dev/ptx/ptmx exists). */ - if (pathExists("/dev/pts/ptmx") && - !pathExists(chrootRootDir + "/dev/ptmx") - && !pathsInChroot.count("/dev/pts")) - { - if (mount("none", (chrootRootDir + "/dev/pts").c_str(), "devpts", 0, "newinstance,mode=0620") == 0) - { - createSymlink("/dev/pts/ptmx", chrootRootDir + "/dev/ptmx"); - - /* Make sure /dev/pts/ptmx is world-writable. With some - Linux versions, it is created with permissions 0. */ - chmod_(chrootRootDir + "/dev/pts/ptmx", 0666); - } else { - if (errno != EINVAL) - throw SysError("mounting /dev/pts"); - doBind("/dev/pts", chrootRootDir + "/dev/pts"); - doBind("/dev/ptmx", chrootRootDir + "/dev/ptmx"); - } - } - - /* Make /etc unwritable */ - if (!drvOptions->useUidRange(*drv)) - chmod_(chrootRootDir + "/etc", 0555); - - /* 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"); - - /* 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 (cgroup && 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); - - if (mkdir("real-root", 0500) == -1) - throw SysError("cannot create real-root directory"); - - if (pivot_root(".", "real-root") == -1) - throw SysError("cannot pivot old root directory onto '%1%'", (chrootRootDir + "/real-root")); - - if (chroot(".") == -1) - throw SysError("cannot change root directory to '%1%'", chrootRootDir); - - if (umount2("real-root", MNT_DETACH) == -1) - throw SysError("cannot unmount real root filesystem"); - - if (rmdir("real-root") == -1) - throw SysError("cannot remove real-root directory"); - - /* Switch to the sandbox uid/gid in the user namespace, - which corresponds to the build user or calling user in - the parent namespace. */ - if (setgid(sandboxGid()) == -1) - throw SysError("setgid failed"); - if (setuid(sandboxUid()) == -1) - throw SysError("setuid failed"); - - setUser = false; - } -#endif - - if (chdir(tmpDirInSandbox.c_str()) == -1) - throw SysError("changing into '%1%'", tmpDir); - - /* Close all other file descriptors. */ - unix::closeExtraFDs(); - -#ifdef __linux__ - linux::setPersonality(drv->platform); -#endif - - /* Disable core dumps by default. */ - struct rlimit limit = { 0, RLIM_INFINITY }; - setrlimit(RLIMIT_CORE, &limit); - - // FIXME: set other limits to deterministic values? - - /* Fill in the environment. */ - Strings envStrs; - for (auto & i : env) - envStrs.push_back(rewriteStrings(i.first + "=" + i.second, inputRewrites)); - - /* If we are running in `build-users' mode, then switch to the - user we allocated above. Make sure that we drop all root - privileges. Note that above we have closed all file - descriptors except std*, so that's safe. Also note that - setuid() when run as root sets the real, effective and - saved UIDs. */ - if (setUser && buildUser) { - /* Preserve supplementary groups of the build user, to allow - admins to specify groups such as "kvm". */ - auto gids = buildUser->getSupplementaryGIDs(); - if (setgroups(gids.size(), gids.data()) == -1) - throw SysError("cannot set supplementary groups of build user"); - - if (setgid(buildUser->getGID()) == -1 || - getgid() != buildUser->getGID() || - getegid() != buildUser->getGID()) - throw SysError("setgid failed"); - - if (setuid(buildUser->getUID()) == -1 || - getuid() != buildUser->getUID() || - geteuid() != buildUser->getUID()) - throw SysError("setuid failed"); - } - -#ifdef __APPLE__ - /* This has to appear before import statements. */ - std::string sandboxProfile = "(version 1)\n"; - - if (useChroot) { - - /* Lots and lots and lots of file functions freak out if they can't stat their full ancestry */ - PathSet ancestry; - - /* 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 - particularly efficient... I doubt it'll be a bottleneck in practice */ - for (auto & i : pathsInChroot) { - Path cur = i.first; - while (cur.compare("/") != 0) { - cur = dirOf(cur); - ancestry.insert(cur); - } - } - - /* 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 cur = store.storeDir; - while (cur.compare("/") != 0) { - ancestry.insert(cur); - cur = dirOf(cur); - } - - /* Add all our input paths to the chroot */ - for (auto & i : inputPaths) { - auto p = store.printStorePath(i); - pathsInChroot[p] = p; - } - - /* Violations will go to the syslog if you set this. Unfortunately the destination does not appear to be configurable */ - if (settings.darwinLogSandboxViolations) { - sandboxProfile += "(deny default)\n"; - } else { - sandboxProfile += "(deny default (with no-log))\n"; - } - - sandboxProfile += - #include "sandbox-defaults.sb" - ; - - if (!derivationType->isSandboxed()) - sandboxProfile += - #include "sandbox-network.sb" - ; - - /* Add the output paths we'll use at build-time to the chroot */ - sandboxProfile += "(allow file-read* file-write* process-exec\n"; - for (auto & [_, path] : scratchOutputs) - sandboxProfile += fmt("\t(subpath \"%s\")\n", store.printStorePath(path)); - - sandboxProfile += ")\n"; - - /* Our inputs (transitive dependencies and any impurities computed above) - - without file-write* allowed, access() incorrectly returns EPERM - */ - sandboxProfile += "(allow file-read* file-write* process-exec\n"; - - // We create multiple allow lists, to avoid exceeding a limit in the darwin sandbox interpreter. - // See https://github.com/NixOS/nix/issues/4119 - // We split our allow groups approximately at half the actual limit, 1 << 16 - const size_t breakpoint = sandboxProfile.length() + (1 << 14); - for (auto & i : pathsInChroot) { - - if (sandboxProfile.length() >= breakpoint) { - debug("Sandbox break: %d %d", sandboxProfile.length(), breakpoint); - sandboxProfile += ")\n(allow file-read* file-write* process-exec\n"; - } - - if (i.first != i.second.source) - throw Error( - "can't map '%1%' to '%2%': mismatched impure paths not supported on Darwin", - i.first, i.second.source); - - std::string path = i.first; - auto optSt = maybeLstat(path.c_str()); - if (!optSt) { - if (i.second.optional) - continue; - throw SysError("getting attributes of required path '%s", path); - } - if (S_ISDIR(optSt->st_mode)) - sandboxProfile += fmt("\t(subpath \"%s\")\n", path); - else - sandboxProfile += fmt("\t(literal \"%s\")\n", path); - } - sandboxProfile += ")\n"; - - /* Allow file-read* on full directory hierarchy to self. Allows realpath() */ - sandboxProfile += "(allow file-read*\n"; - for (auto & i : ancestry) { - sandboxProfile += fmt("\t(literal \"%s\")\n", i); - } - sandboxProfile += ")\n"; - - sandboxProfile += drvOptions->additionalSandboxProfile; - } else - sandboxProfile += - #include "sandbox-minimal.sb" - ; - - debug("Generated sandbox profile:"); - debug(sandboxProfile); - - /* The tmpDir in scope points at the temporary build directory for our derivation. Some packages try different mechanisms - to find temporary directories, so we want to open up a broader place for them to put their files, if needed. */ - Path globalTmpDir = canonPath(defaultTempDir(), true); - - /* They don't like trailing slashes on subpath directives */ - while (!globalTmpDir.empty() && globalTmpDir.back() == '/') - globalTmpDir.pop_back(); - - if (getEnv("_NIX_TEST_NO_SANDBOX") != "1") { - Strings sandboxArgs; - sandboxArgs.push_back("_GLOBAL_TMP_DIR"); - sandboxArgs.push_back(globalTmpDir); - if (drvOptions->allowLocalNetworking) { - sandboxArgs.push_back("_ALLOW_LOCAL_NETWORKING"); - sandboxArgs.push_back("1"); - } - char * sandbox_errbuf = nullptr; - if (sandbox_init_with_parameters(sandboxProfile.c_str(), 0, stringsToCharPtrs(sandboxArgs).data(), &sandbox_errbuf)) { - writeFull(STDERR_FILENO, fmt("failed to configure sandbox: %s\n", sandbox_errbuf ? sandbox_errbuf : "(null)")); - _exit(1); - } - } -#endif - - /* Indicate that we managed to set up the build environment. */ - writeFull(STDERR_FILENO, std::string("\2\n")); - - sendException = false; - - /* Execute the program. This should not return. */ - if (drv->isBuiltin()) { - try { - logger = makeJSONLogger(getStandardError()); - - std::map outputs; - for (auto & e : drv->outputs) - outputs.insert_or_assign(e.first, - store.printStorePath(scratchOutputs.at(e.first))); - - if (drv->builder == "builtin:fetchurl") - builtinFetchurl(*drv, outputs, netrcData, caFileData); - else if (drv->builder == "builtin:buildenv") - builtinBuildenv(*drv, outputs); - else if (drv->builder == "builtin:unpack-channel") - builtinUnpackChannel(*drv, outputs); - else - throw Error("unsupported builtin builder '%1%'", drv->builder.substr(8)); - _exit(0); - } catch (std::exception & e) { - writeFull(STDERR_FILENO, e.what() + std::string("\n")); - _exit(1); - } - } - - // Now builder is not builtin - - Strings args; - args.push_back(std::string(baseNameOf(drv->builder))); - - for (auto & i : drv->args) - args.push_back(rewriteStrings(i, inputRewrites)); - -#ifdef __APPLE__ - posix_spawnattr_t attrp; - - if (posix_spawnattr_init(&attrp)) - throw SysError("failed to initialize builder"); - - if (posix_spawnattr_setflags(&attrp, POSIX_SPAWN_SETEXEC)) - throw SysError("failed to initialize builder"); - - if (drv->platform == "aarch64-darwin") { - // Unset kern.curproc_arch_affinity so we can escape Rosetta - int affinity = 0; - sysctlbyname("kern.curproc_arch_affinity", NULL, NULL, &affinity, sizeof(affinity)); - - cpu_type_t cpu = CPU_TYPE_ARM64; - posix_spawnattr_setbinpref_np(&attrp, 1, &cpu, NULL); - } else if (drv->platform == "x86_64-darwin") { - cpu_type_t cpu = CPU_TYPE_X86_64; - posix_spawnattr_setbinpref_np(&attrp, 1, &cpu, NULL); - } - - posix_spawn(NULL, drv->builder.c_str(), NULL, &attrp, stringsToCharPtrs(args).data(), stringsToCharPtrs(envStrs).data()); -#else - execve(drv->builder.c_str(), stringsToCharPtrs(args).data(), stringsToCharPtrs(envStrs).data()); -#endif - - throw SysError("executing '%1%'", drv->builder); - - } catch (...) { - handleChildException(sendException); - _exit(1); - } -} - - -SingleDrvOutputs DerivationBuilder::registerOutputs() -{ - std::map infos; - - /* Set of inodes seen during calls to canonicalisePathMetaData() - for this build's outputs. This needs to be shared between - outputs to allow hard links between outputs. */ - InodesSeen inodesSeen; - - Path checkSuffix = ".check"; - - 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. */ - StorePathSet referenceablePaths; - for (auto & p : inputPaths) referenceablePaths.insert(p); - for (auto & i : scratchOutputs) referenceablePaths.insert(i.second); - for (auto & p : addedPaths) referenceablePaths.insert(p); - - /* FIXME `needsHashRewrite` should probably be removed and we get to the - real reason why we aren't using the chroot dir */ - auto toRealPathChroot = [&](const Path & p) -> Path { - return useChroot && !needsHashRewrite() - ? chrootRootDir + p - : store.toRealPath(p); - }; - - /* Check whether the output paths were created, and make all - output paths read-only. Then get the references of each output (that we - might need to register), so we can topologically sort them. For the ones - that are most definitely already installed, we just store their final - name so we can also use it in rewrites. */ - StringSet outputsToSort; - struct AlreadyRegistered { StorePath path; }; - struct PerhapsNeedToRegister { StorePathSet refs; }; - std::map> outputReferencesIfUnregistered; - std::map outputStats; - for (auto & [outputName, _] : drv->outputs) { - auto scratchOutput = get(scratchOutputs, outputName); - if (!scratchOutput) - throw BuildError( - "builder for '%s' has no scratch output for '%s'", - store.printStorePath(drvPath), outputName); - auto actualPath = toRealPathChroot(store.printStorePath(*scratchOutput)); - - outputsToSort.insert(outputName); - - /* Updated wanted info to remove the outputs we definitely don't need to register */ - auto initialOutput = get(initialOutputs, outputName); - if (!initialOutput) - throw BuildError( - "builder for '%s' has no initial output for '%s'", - store.printStorePath(drvPath), outputName); - auto & initialInfo = *initialOutput; - - /* Don't register if already valid, and not checking */ - initialInfo.wanted = buildMode == bmCheck - || !(initialInfo.known && initialInfo.known->isValid()); - if (!initialInfo.wanted) { - outputReferencesIfUnregistered.insert_or_assign( - outputName, - AlreadyRegistered { .path = initialInfo.known->path }); - continue; - } - - auto optSt = maybeLstat(actualPath.c_str()); - if (!optSt) - throw BuildError( - "builder for '%s' failed to produce output path for output '%s' at '%s'", - store.printStorePath(drvPath), outputName, actualPath); - struct stat & st = *optSt; - -#ifndef __CYGWIN__ - /* Check that the output is not group or world writable, as - that means that someone else can have interfered with the - build. Also, the output should be owned by the build - user. */ - if ((!S_ISLNK(st.st_mode) && (st.st_mode & (S_IWGRP | S_IWOTH))) || - (buildUser && st.st_uid != buildUser->getUID())) - throw BuildError( - "suspicious ownership or permission on '%s' for output '%s'; rejecting this build output", - actualPath, outputName); -#endif - - /* 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 ? std::optional(buildUser->getUIDRange()) : std::nullopt, - inodesSeen); - - bool discardReferences = false; - if (auto udr = get(drvOptions->unsafeDiscardReferences, outputName)) { - discardReferences = *udr; - } - - StorePathSet references; - if (discardReferences) - debug("discarding references of output '%s'", outputName); - else { - debug("scanning for references for output '%s' in temp location '%s'", outputName, actualPath); - - /* Pass blank Sink as we are not ready to hash data at this stage. */ - NullSink blank; - references = scanForReferences(blank, actualPath, referenceablePaths); - } - - outputReferencesIfUnregistered.insert_or_assign( - outputName, - PerhapsNeedToRegister { .refs = references }); - outputStats.insert_or_assign(outputName, std::move(st)); - } - - auto sortedOutputNames = topoSort(outputsToSort, - {[&](const std::string & name) { - auto orifu = get(outputReferencesIfUnregistered, name); - if (!orifu) - throw BuildError( - "no output reference for '%s' in build of '%s'", - name, store.printStorePath(drvPath)); - return std::visit(overloaded { - /* Since we'll use the already installed versions of these, we - can treat them as leaves and ignore any references they - have. */ - [&](const AlreadyRegistered &) { return StringSet {}; }, - [&](const PerhapsNeedToRegister & refs) { - StringSet referencedOutputs; - /* FIXME build inverted map up front so no quadratic waste here */ - for (auto & r : refs.refs) - for (auto & [o, p] : scratchOutputs) - if (r == p) - referencedOutputs.insert(o); - return referencedOutputs; - }, - }, *orifu); - }}, - {[&](const std::string & path, const std::string & parent) { - // TODO with more -vvvv also show the temporary paths for manual inspection. - return BuildError( - "cycle detected in build of '%s' in the references of output '%s' from output '%s'", - store.printStorePath(drvPath), path, parent); - }}); - - std::reverse(sortedOutputNames.begin(), sortedOutputNames.end()); - - OutputPathMap finalOutputs; - - for (auto & outputName : sortedOutputNames) { - auto output = get(drv->outputs, outputName); - auto scratchPath = get(scratchOutputs, outputName); - assert(output && scratchPath); - auto actualPath = toRealPathChroot(store.printStorePath(*scratchPath)); - - auto finish = [&](StorePath finalStorePath) { - /* Store the final path */ - finalOutputs.insert_or_assign(outputName, finalStorePath); - /* The rewrite rule will be used in downstream outputs that refer to - use. This is why the topological sort is essential to do first - before this for loop. */ - if (*scratchPath != finalStorePath) - outputRewrites[std::string { scratchPath->hashPart() }] = std::string { finalStorePath.hashPart() }; - }; - - auto orifu = get(outputReferencesIfUnregistered, outputName); - assert(orifu); - - std::optional referencesOpt = std::visit(overloaded { - [&](const AlreadyRegistered & skippedFinalPath) -> std::optional { - finish(skippedFinalPath.path); - return std::nullopt; - }, - [&](const PerhapsNeedToRegister & r) -> std::optional { - return r.refs; - }, - }, *orifu); - - if (!referencesOpt) - continue; - auto references = *referencesOpt; - - auto rewriteOutput = [&](const StringMap & rewrites) { - /* Apply hash rewriting if necessary. */ - if (!rewrites.empty()) { - debug("rewriting hashes in '%1%'; cross fingers", actualPath); - - /* FIXME: Is this actually streaming? */ - auto source = sinkToSource([&](Sink & nextSink) { - RewritingSink rsink(rewrites, nextSink); - dumpPath(actualPath, rsink); - rsink.flush(); - }); - Path tmpPath = actualPath + ".tmp"; - restorePath(tmpPath, *source); - deletePath(actualPath); - movePath(tmpPath, actualPath); - - /* FIXME: set proper permissions in restorePath() so - we don't have to do another traversal. */ - canonicalisePathMetaData(actualPath, {}, inodesSeen); - } - }; - - auto rewriteRefs = [&]() -> StoreReferences { - /* In the CA case, we need the rewritten refs to calculate the - final path, therefore we look for a *non-rewritten - self-reference, and use a bool rather try to solve the - computationally intractable fixed point. */ - StoreReferences res { - .self = false, - }; - for (auto & r : references) { - auto name = r.name(); - auto origHash = std::string { r.hashPart() }; - if (r == *scratchPath) { - res.self = true; - } else if (auto outputRewrite = get(outputRewrites, origHash)) { - std::string newRef = *outputRewrite; - newRef += '-'; - newRef += name; - res.others.insert(StorePath { newRef }); - } else { - res.others.insert(r); - } - } - return res; - }; - - auto newInfoFromCA = [&](const DerivationOutput::CAFloating outputHash) -> ValidPathInfo { - auto st = get(outputStats, outputName); - if (!st) - throw BuildError( - "output path %1% without valid stats info", - actualPath); - if (outputHash.method.getFileIngestionMethod() == FileIngestionMethod::Flat) - { - /* The output path should be a regular file without execute permission. */ - if (!S_ISREG(st->st_mode) || (st->st_mode & S_IXUSR) != 0) - throw BuildError( - "output path '%1%' should be a non-executable regular file " - "since recursive hashing is not enabled (one of outputHashMode={flat,text} is true)", - actualPath); - } - rewriteOutput(outputRewrites); - /* FIXME optimize and deduplicate with addToStore */ - std::string oldHashPart { scratchPath->hashPart() }; - auto got = [&]{ - auto fim = outputHash.method.getFileIngestionMethod(); - switch (fim) { - case FileIngestionMethod::Flat: - case FileIngestionMethod::NixArchive: - { - HashModuloSink caSink { outputHash.hashAlgo, oldHashPart }; - auto fim = outputHash.method.getFileIngestionMethod(); - dumpPath( - {getFSSourceAccessor(), CanonPath(actualPath)}, - caSink, - (FileSerialisationMethod) fim); - return caSink.finish().first; - } - case FileIngestionMethod::Git: { - return git::dumpHash( - outputHash.hashAlgo, - {getFSSourceAccessor(), CanonPath(actualPath)}).hash; - } - } - assert(false); - }(); - - ValidPathInfo newInfo0 { - store, - outputPathName(drv->name, outputName), - ContentAddressWithReferences::fromParts( - outputHash.method, - std::move(got), - rewriteRefs()), - Hash::dummy, - }; - if (*scratchPath != newInfo0.path) { - // If the path has some self-references, we need to rewrite - // them. - // (note that this doesn't invalidate the ca hash we calculated - // above because it's computed *modulo the self-references*, so - // it already takes this rewrite into account). - rewriteOutput( - StringMap{{oldHashPart, - std::string(newInfo0.path.hashPart())}}); - } - - { - HashResult narHashAndSize = hashPath( - {getFSSourceAccessor(), CanonPath(actualPath)}, - FileSerialisationMethod::NixArchive, HashAlgorithm::SHA256); - newInfo0.narHash = narHashAndSize.first; - newInfo0.narSize = narHashAndSize.second; - } - - assert(newInfo0.ca); - return newInfo0; - }; - - ValidPathInfo newInfo = std::visit(overloaded { - - [&](const DerivationOutput::InputAddressed & output) { - /* input-addressed case */ - auto requiredFinalPath = output.path; - /* Preemptively add rewrite rule for final hash, as that is - what the NAR hash will use rather than normalized-self references */ - if (*scratchPath != requiredFinalPath) - outputRewrites.insert_or_assign( - std::string { scratchPath->hashPart() }, - std::string { requiredFinalPath.hashPart() }); - rewriteOutput(outputRewrites); - HashResult narHashAndSize = hashPath( - {getFSSourceAccessor(), CanonPath(actualPath)}, - FileSerialisationMethod::NixArchive, HashAlgorithm::SHA256); - ValidPathInfo newInfo0 { requiredFinalPath, narHashAndSize.first }; - newInfo0.narSize = narHashAndSize.second; - auto refs = rewriteRefs(); - newInfo0.references = std::move(refs.others); - if (refs.self) - newInfo0.references.insert(newInfo0.path); - return newInfo0; - }, - - [&](const DerivationOutput::CAFixed & dof) { - auto & wanted = dof.ca.hash; - - // Replace the output by a fresh copy of itself to make sure - // that there's no stale file descriptor pointing to it - Path tmpOutput = actualPath + ".tmp"; - copyFile( - std::filesystem::path(actualPath), - std::filesystem::path(tmpOutput), true); - - std::filesystem::rename(tmpOutput, actualPath); - - auto newInfo0 = newInfoFromCA(DerivationOutput::CAFloating { - .method = dof.ca.method, - .hashAlgo = wanted.algo, - }); - - /* Check wanted hash */ - assert(newInfo0.ca); - auto & got = newInfo0.ca->hash; - if (wanted != got) { - /* Throw an error after registering the path as - valid. */ - miscMethods.noteHashMismatch(); - delayedException = std::make_exception_ptr( - BuildError("hash mismatch in fixed-output derivation '%s':\n specified: %s\n got: %s", - store.printStorePath(drvPath), - wanted.to_string(HashFormat::SRI, true), - got.to_string(HashFormat::SRI, true))); - } - if (!newInfo0.references.empty()) { - auto numViolations = newInfo.references.size(); - delayedException = std::make_exception_ptr( - BuildError("fixed-output derivations must not reference store paths: '%s' references %d distinct paths, e.g. '%s'", - store.printStorePath(drvPath), - numViolations, - store.printStorePath(*newInfo.references.begin()))); - } - - return newInfo0; - }, - - [&](const DerivationOutput::CAFloating & dof) { - return newInfoFromCA(dof); - }, - - [&](const DerivationOutput::Deferred &) -> ValidPathInfo { - // No derivation should reach that point without having been - // rewritten first - assert(false); - }, - - [&](const DerivationOutput::Impure & doi) { - return newInfoFromCA(DerivationOutput::CAFloating { - .method = doi.method, - .hashAlgo = doi.hashAlgo, - }); - }, - - }, output->raw); - - /* FIXME: set proper permissions in restorePath() so - we don't have to do another traversal. */ - 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 - their usual "final destination" */ - auto finalDestPath = store.printStorePath(newInfo.path); - - /* Lock final output path, if not already locked. This happens with - floating CA derivations and hash-mismatching fixed-output - derivations. */ - PathLocks dynamicOutputLock; - dynamicOutputLock.setDeletion(true); - auto optFixedPath = output->path(store, drv->name, outputName); - if (!optFixedPath || - store.printStorePath(*optFixedPath) != finalDestPath) - { - assert(newInfo.ca); - dynamicOutputLock.lockPaths({store.toRealPath(finalDestPath)}); - } - - /* Move files, if needed */ - if (store.toRealPath(finalDestPath) != actualPath) { - if (buildMode == bmRepair) { - /* Path already exists, need to replace it */ - replaceValidPath(store.toRealPath(finalDestPath), actualPath); - actualPath = store.toRealPath(finalDestPath); - } else if (buildMode == bmCheck) { - /* Path already exists, and we want to compare, so we leave out - new path in place. */ - } else if (store.isValidPath(newInfo.path)) { - /* Path already exists because CA path produced by something - else. No moving needed. */ - assert(newInfo.ca); - } else { - auto destPath = store.toRealPath(finalDestPath); - deletePath(destPath); - movePath(actualPath, destPath); - actualPath = destPath; - } - } - - auto & localStore = getLocalStore(); - - if (buildMode == bmCheck) { - - if (!store.isValidPath(newInfo.path)) continue; - ValidPathInfo oldInfo(*store.queryPathInfo(newInfo.path)); - if (newInfo.narHash != oldInfo.narHash) { - miscMethods.noteCheckMismatch(); - if (settings.runDiffHook || settings.keepFailed) { - auto dst = store.toRealPath(finalDestPath + checkSuffix); - deletePath(dst); - movePath(actualPath, dst); - - handleDiffHook( - buildUser ? buildUser->getUID() : getuid(), - buildUser ? buildUser->getGID() : getgid(), - finalDestPath, dst, store.printStorePath(drvPath), tmpDir); - - throw NotDeterministic("derivation '%s' may not be deterministic: output '%s' differs from '%s'", - store.printStorePath(drvPath), store.toRealPath(finalDestPath), dst); - } else - throw NotDeterministic("derivation '%s' may not be deterministic: output '%s' differs", - store.printStorePath(drvPath), store.toRealPath(finalDestPath)); - } - - /* Since we verified the build, it's now ultimately trusted. */ - if (!oldInfo.ultimate) { - oldInfo.ultimate = true; - localStore.signPathInfo(oldInfo); - localStore.registerValidPaths({{oldInfo.path, oldInfo}}); - } - - continue; - } - - /* For debugging, print out the referenced and unreferenced paths. */ - for (auto & i : inputPaths) { - if (references.count(i)) - debug("referenced input: '%1%'", store.printStorePath(i)); - else - debug("unreferenced input: '%1%'", store.printStorePath(i)); - } - - localStore.optimisePath(actualPath, NoRepair); // FIXME: combine with scanForReferences() - miscMethods.markContentsGood(newInfo.path); - - newInfo.deriver = drvPath; - newInfo.ultimate = true; - localStore.signPathInfo(newInfo); - - finish(newInfo.path); - - /* If it's a CA path, register it right away. This is necessary if it - isn't statically known so that we can safely unlock the path before - the next iteration */ - if (newInfo.ca) - localStore.registerValidPaths({{newInfo.path, newInfo}}); - - infos.emplace(outputName, std::move(newInfo)); - } - - if (buildMode == bmCheck) { - /* In case of fixed-output derivations, if there are - mismatches on `--check` an error must be thrown as this is - also a source for non-determinism. */ - if (delayedException) - std::rethrow_exception(delayedException); - return miscMethods.assertPathValidity(); - } - - /* Apply output checks. */ - checkOutputs(infos); - - /* Register each output path as valid, and register the sets of - paths referenced by each of them. If there are cycles in the - outputs, this will fail. */ - { - auto & localStore = getLocalStore(); - - ValidPathInfos infos2; - for (auto & [outputName, newInfo] : infos) { - infos2.insert_or_assign(newInfo.path, newInfo); - } - localStore.registerValidPaths(infos2); - } - - /* In case of a fixed-output derivation hash mismatch, throw an - exception now that we have registered the output as valid. */ - if (delayedException) - std::rethrow_exception(delayedException); - - /* If we made it this far, we are sure the output matches the derivation - (since the delayedException would be a fixed output CA mismatch). That - means it's safe to link the derivation to the output hash. We must do - that for floating CA derivations, which otherwise couldn't be cached, - but it's fine to do in all cases. */ - SingleDrvOutputs builtOutputs; - - for (auto & [outputName, newInfo] : infos) { - auto oldinfo = get(initialOutputs, outputName); - assert(oldinfo); - auto thisRealisation = Realisation { - .id = DrvOutput { - oldinfo->outputHash, - outputName - }, - .outPath = newInfo.path - }; - if (experimentalFeatureSettings.isEnabled(Xp::CaDerivations) - && !drv->type().isImpure()) - { - store.signRealisation(thisRealisation); - store.registerDrvOutput(thisRealisation); - } - builtOutputs.emplace(outputName, thisRealisation); - } - - return builtOutputs; -} - - -void DerivationBuilder::checkOutputs(const std::map & outputs) -{ - std::map outputsByPath; - for (auto & output : outputs) - outputsByPath.emplace(store.printStorePath(output.second.path), output.second); - - for (auto & output : outputs) { - auto & outputName = output.first; - auto & info = output.second; - - /* Compute the closure and closure size of some output. This - is slightly tricky because some of its references (namely - other outputs) may not be valid yet. */ - auto getClosure = [&](const StorePath & path) - { - uint64_t closureSize = 0; - StorePathSet pathsDone; - std::queue pathsLeft; - pathsLeft.push(path); - - while (!pathsLeft.empty()) { - auto path = pathsLeft.front(); - pathsLeft.pop(); - if (!pathsDone.insert(path).second) continue; - - auto i = outputsByPath.find(store.printStorePath(path)); - if (i != outputsByPath.end()) { - closureSize += i->second.narSize; - for (auto & ref : i->second.references) - pathsLeft.push(ref); - } else { - auto info = store.queryPathInfo(path); - closureSize += info->narSize; - for (auto & ref : info->references) - pathsLeft.push(ref); - } - } - - return std::make_pair(std::move(pathsDone), closureSize); - }; - - auto applyChecks = [&](const DerivationOptions::OutputChecks & checks) - { - if (checks.maxSize && info.narSize > *checks.maxSize) - throw BuildError("path '%s' is too large at %d bytes; limit is %d bytes", - store.printStorePath(info.path), info.narSize, *checks.maxSize); - - if (checks.maxClosureSize) { - uint64_t closureSize = getClosure(info.path).second; - if (closureSize > *checks.maxClosureSize) - throw BuildError("closure of path '%s' is too large at %d bytes; limit is %d bytes", - store.printStorePath(info.path), closureSize, *checks.maxClosureSize); - } - - auto checkRefs = [&](const StringSet & value, bool allowed, bool recursive) - { - /* Parse a list of reference specifiers. Each element must - either be a store path, or the symbolic name of the output - of the derivation (such as `out'). */ - StorePathSet spec; - for (auto & i : value) { - if (store.isStorePath(i)) - spec.insert(store.parseStorePath(i)); - else if (auto output = get(outputs, i)) - spec.insert(output->path); - else { - std::string outputsListing = concatMapStringsSep(", ", outputs, [](auto & o) { return o.first; }); - throw BuildError("derivation '%s' output check for '%s' contains an illegal reference specifier '%s'," - " expected store path or output name (one of [%s])", - store.printStorePath(drvPath), outputName, i, outputsListing); - } - } - - auto used = recursive - ? getClosure(info.path).first - : info.references; - - if (recursive && checks.ignoreSelfRefs) - used.erase(info.path); - - StorePathSet badPaths; - - for (auto & i : used) - if (allowed) { - if (!spec.count(i)) - badPaths.insert(i); - } else { - if (spec.count(i)) - badPaths.insert(i); - } - - if (!badPaths.empty()) { - std::string badPathsStr; - for (auto & i : badPaths) { - badPathsStr += "\n "; - badPathsStr += store.printStorePath(i); - } - throw BuildError("output '%s' is not allowed to refer to the following paths:%s", - store.printStorePath(info.path), badPathsStr); - } - }; - - /* Mandatory check: absent whitelist, and present but empty - whitelist mean very different things. */ - if (auto & refs = checks.allowedReferences) { - checkRefs(*refs, true, false); - } - if (auto & refs = checks.allowedRequisites) { - checkRefs(*refs, true, true); - } - - /* Optimization: don't need to do anything when - disallowed and empty set. */ - if (!checks.disallowedReferences.empty()) { - checkRefs(checks.disallowedReferences, false, false); - } - if (!checks.disallowedRequisites.empty()) { - checkRefs(checks.disallowedRequisites, false, true); - } - }; - - std::visit(overloaded{ - [&](const DerivationOptions::OutputChecks & checks) { - applyChecks(checks); - }, - [&](const std::map & checksPerOutput) { - if (auto outputChecks = get(checksPerOutput, outputName)) - - applyChecks(*outputChecks); - }, - }, drvOptions->outputChecks); - } -} - - -void DerivationBuilder::deleteTmpDir(bool force) -{ - if (topTmpDir != "") { - /* Don't keep temporary directories for builtins because they - might have privileged stuff (like a copy of netrc). */ - if (settings.keepFailed && !force && !drv->isBuiltin()) { - printError("note: keeping build directory '%s'", tmpDir); - chmod(topTmpDir.c_str(), 0755); - chmod(tmpDir.c_str(), 0755); - } - else - deletePath(topTmpDir); - topTmpDir = ""; - tmpDir = ""; - } -} - bool LocalDerivationGoal::isReadDesc(int fd) { return (hook && DerivationGoal::isReadDesc(fd)) || - (!hook && fd == builder.builderOut.get()); + (!hook && fd == builder->builderOut.get()); } - -StorePath DerivationBuilder::makeFallbackPath(OutputNameView outputName) -{ - // This is a bogus path type, constructed this way to ensure that it doesn't collide with any other store path - // See doc/manual/source/protocols/store-path.md for details - // TODO: We may want to separate the responsibilities of constructing the path fingerprint and of actually doing the hashing - auto pathType = "rewrite:" + std::string(drvPath.to_string()) + ":name:" + std::string(outputName); - return store.makeStorePath( - pathType, - // pass an all-zeroes hash - Hash(HashAlgorithm::SHA256), outputPathName(drv->name, outputName)); -} - - -StorePath DerivationBuilder::makeFallbackPath(const StorePath & path) -{ - // This is a bogus path type, constructed this way to ensure that it doesn't collide with any other store path - // See doc/manual/source/protocols/store-path.md for details - auto pathType = "rewrite:" + std::string(drvPath.to_string()) + ":" + std::string(path.to_string()); - return store.makeStorePath( - pathType, - // pass an all-zeroes hash - Hash(HashAlgorithm::SHA256), path.name()); -} - - } diff --git a/src/libstore/unix/include/nix/store/build/derivation-builder.hh b/src/libstore/unix/include/nix/store/build/derivation-builder.hh index 93154c7dc..e2c7791a3 100644 --- a/src/libstore/unix/include/nix/store/build/derivation-builder.hh +++ b/src/libstore/unix/include/nix/store/build/derivation-builder.hh @@ -1,83 +1,14 @@ -#include "nix/store/build/local-derivation-goal.hh" -#include "nix/store/local-store.hh" +#pragma once +///@file + +#include "nix/store/build-result.hh" +#include "nix/store/derivation-options.hh" +#include "nix/store/build/derivation-building-misc.hh" +#include "nix/store/derivations.hh" +#include "nix/store/parsed-derivations.hh" #include "nix/util/processes.hh" -#include "nix/store/indirect-root-store.hh" -#include "nix/store/build/hook-instance.hh" -#include "nix/store/build/worker.hh" -#include "nix/store/builtins.hh" -#include "nix/store/builtins/buildenv.hh" -#include "nix/store/path-references.hh" -#include "nix/util/finally.hh" -#include "nix/util/util.hh" -#include "nix/util/archive.hh" -#include "nix/util/git.hh" -#include "nix/util/compression.hh" -#include "nix/store/daemon.hh" -#include "nix/util/topo-sort.hh" -#include "nix/util/callback.hh" -#include "nix/util/json-utils.hh" -#include "nix/util/current-process.hh" -#include "nix/store/build/child.hh" -#include "nix/util/unix-domain-socket.hh" -#include "nix/store/posix-fs-canonicalise.hh" -#include "nix/util/posix-source-accessor.hh" #include "nix/store/restricted-store.hh" -#include "nix/store/config.hh" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "store-config-private.hh" - -#if HAVE_STATVFS -#include -#endif - -/* Includes required for chroot support. */ -#ifdef __linux__ -# include "linux/fchmodat2-compat.hh" -# include -# include -# include -# include -# include -# include -# include -# include -# include "nix/util/namespaces.hh" -# if HAVE_SECCOMP -# include -# endif -# define pivot_root(new_root, put_old) (syscall(SYS_pivot_root, new_root, put_old)) -# include "nix/util/cgroup.hh" -# include "nix/store/personality.hh" -#endif - -#ifdef __APPLE__ -#include -#include -#include - -/* This definition is undocumented but depended upon by all major browsers. */ -extern "C" int sandbox_init_with_parameters(const char *profile, uint64_t flags, const char *const parameters[], char **errorbuf); -#endif - -#include -#include -#include - -#include "nix/util/strings.hh" -#include "nix/util/signals.hh" - -#include "store-config-private.hh" +#include "nix/store/user-lock.hh" namespace nix { @@ -195,25 +126,8 @@ struct DerivationBuilderCallbacks * rather than incoming call edges that either should be removed, or * become (higher order) function parameters. */ -class DerivationBuilder : public RestrictionContext, DerivationBuilderParams +struct DerivationBuilder : RestrictionContext { - Store & store; - - DerivationBuilderCallbacks & miscMethods; - -public: - - DerivationBuilder( - Store & store, - DerivationBuilderCallbacks & miscMethods, - DerivationBuilderParams params) - : DerivationBuilderParams{std::move(params)} - , store{store} - , miscMethods{miscMethods} - { } - - LocalStore & getLocalStore(); - /** * User selected for running the builder. */ @@ -224,30 +138,8 @@ public: */ Pid pid; -private: - - /** - * The cgroup of the builder, if any. - */ - std::optional cgroup; - - /** - * The temporary directory used for the build. - */ - Path tmpDir; - - /** - * The top-level temporary directory. `tmpDir` is either equal to - * or a child of this directory. - */ - Path topTmpDir; - - /** - * The path of the temporary directory in the sandbox. - */ - Path tmpDirInSandbox; - -public: + DerivationBuilder() = default; + virtual ~DerivationBuilder() = default; /** * Master side of the pseudoterminal used for the builder's @@ -255,132 +147,6 @@ public: */ AutoCloseFD builderOut; -private: - - /** - * Pipe for synchronising updates to the builder namespaces. - */ - Pipe userNamespaceSync; - - /** - * The mount namespace and user namespace of the builder, used to add additional - * paths to the sandbox as a result of recursive Nix calls. - */ - AutoCloseFD sandboxMountNamespace; - AutoCloseFD sandboxUserNamespace; - - /** - * On Linux, whether we're doing the build in its own user - * namespace. - */ - bool usingUserNamespace = true; - - /** - * Whether we're currently doing a chroot build. - */ - bool useChroot = false; - - /** - * The root of the chroot environment. - */ - Path chrootRootDir; - - /** - * RAII object to delete the chroot directory. - */ - std::shared_ptr autoDelChroot; - - /** - * The sort of derivation we are building. - * - * Just a cached value, can be recomputed from `drv`. - */ - std::optional derivationType; - - /** - * Stuff we need to pass to initChild(). - */ - struct ChrootPath { - Path source; - bool optional; - ChrootPath(Path source = "", bool optional = false) - : source(source), optional(optional) - { } - }; - typedef map PathsInChroot; // maps target path to source path - PathsInChroot pathsInChroot; - - typedef map Environment; - Environment env; - - /** - * Hash rewriting. - */ - StringMap inputRewrites, outputRewrites; - typedef map RedirectedOutputs; - RedirectedOutputs redirectedOutputs; - - /** - * The output paths used during the build. - * - * - Input-addressed derivations or fixed content-addressed outputs are - * sometimes built when some of their outputs already exist, and can not - * be hidden via sandboxing. We use temporary locations instead and - * rewrite after the build. Otherwise the regular predetermined paths are - * put here. - * - * - Floating content-addressing derivations do not know their final build - * output paths until the outputs are hashed, so random locations are - * used, and then renamed. The randomness helps guard against hidden - * self-references. - */ - OutputPathMap scratchOutputs; - - uid_t sandboxUid() { return usingUserNamespace ? (!buildUser || buildUser->getUIDCount() == 1 ? 1000 : 0) : buildUser->getUID(); } - gid_t sandboxGid() { return usingUserNamespace ? (!buildUser || buildUser->getUIDCount() == 1 ? 100 : 0) : buildUser->getGID(); } - - const static Path homeDir; - - /** - * The recursive Nix daemon socket. - */ - AutoCloseFD daemonSocket; - - /** - * The daemon main thread. - */ - std::thread daemonThread; - - /** - * The daemon worker threads. - */ - std::vector daemonWorkerThreads; - - const StorePathSet & originalPaths() override - { - return inputPaths; - } - - bool isAllowed(const StorePath & path) override - { - return inputPaths.count(path) || addedPaths.count(path); - } - bool isAllowed(const DrvOutput & id) override - { - return addedDrvOutputs.count(id); - } - - bool isAllowed(const DerivedPath & req); - - friend struct RestrictedStore; - - /** - * Whether we need to perform hash rewriting if there are valid output paths. - */ - bool needsHashRewrite(); - -public: - /** * Set up build environment / sandbox, acquiring resources (e.g. * locks as needed). After this is run, the builder should be @@ -389,12 +155,12 @@ public: * @returns true if successful, false if we could not acquire a build * user. In that case, the caller must wait and then try again. */ - bool prepareBuild(); + virtual bool prepareBuild() = 0; /** * Start building a derivation. */ - void startBuilder(); + virtual void startBuilder() = 0; /** * Tear down build environment after the builder exits (either on @@ -405,3005 +171,29 @@ public: * more information. The second case indicates success, and * realisations for each output of the derivation are returned. */ - std::variant, SingleDrvOutputs> unprepareBuild(); - -private: - - /** - * Fill in the environment for the builder. - */ - void initEnv(); - - /** - * Process messages send by the sandbox initialization. - */ - void processSandboxSetupMessages(); - - /** - * Setup tmp dir location. - */ - void initTmpDir(); - - /** - * Write a JSON file containing the derivation attributes. - */ - void writeStructuredAttrs(); - - /** - * Start an in-process nix daemon thread for recursive-nix. - */ - void startDaemon(); - -public: + virtual std::variant, SingleDrvOutputs> unprepareBuild() = 0; /** * Stop the in-process nix daemon thread. * @see startDaemon */ - void stopDaemon(); - -private: - - void addDependency(const StorePath & path) override; - - /** - * Make a file owned by the builder. - */ - void chownToBuilder(const Path & path); - - /** - * Run the builder's process. - */ - void runChild(); - - /** - * Check that the derivation outputs all exist and register them - * as valid. - */ - SingleDrvOutputs registerOutputs(); - - /** - * Check that an output meets the requirements specified by the - * 'outputChecks' attribute (or the legacy - * '{allowed,disallowed}{References,Requisites}' attributes). - */ - void checkOutputs(const std::map & outputs); - -public: + virtual void stopDaemon() = 0; /** * Delete the temporary directory, if we have one. */ - void deleteTmpDir(bool force); + virtual void deleteTmpDir(bool force) = 0; /** * Kill any processes running under the build user UID or in the * cgroup of the build. */ - void killSandbox(bool getStats); - -private: - - bool cleanupDecideWhetherDiskFull(); - - /** - * Create alternative path calculated from but distinct from the - * input, so we can avoid overwriting outputs (or other store paths) - * that already exist. - */ - StorePath makeFallbackPath(const StorePath & path); - - /** - * Make a path to another based on the output name along with the - * derivation hash. - * - * @todo Add option to randomize, so we can audit whether our - * rewrites caught everything - */ - StorePath makeFallbackPath(OutputNameView outputName); + virtual void killSandbox(bool getStats) = 0; }; -/** - * This hooks up `DerivationBuilder` to the scheduler / goal machinary. - * - * @todo Eventually, this shouldn't exist, because `DerivationGoal` can - * just choose to use `DerivationBuilder` or its remote-building - * equalivalent directly, at the "value level" rather than "class - * inheritance hierarchy" level. - */ -struct LocalDerivationGoal : DerivationGoal, DerivationBuilderCallbacks -{ - DerivationBuilder builder; - - LocalDerivationGoal(const StorePath & drvPath, - const OutputsSpec & wantedOutputs, Worker & worker, - BuildMode buildMode) - : DerivationGoal{drvPath, wantedOutputs, worker, buildMode} - , builder{ - worker.store, - static_cast(*this), - DerivationBuilderParams { - DerivationGoal::drvPath, - DerivationGoal::buildMode, - DerivationGoal::buildResult, - DerivationGoal::drv, - DerivationGoal::parsedDrv, - DerivationGoal::drvOptions, - DerivationGoal::inputPaths, - DerivationGoal::initialOutputs, - }, - } - {} - - LocalDerivationGoal(const StorePath & drvPath, const BasicDerivation & drv, - const OutputsSpec & wantedOutputs, Worker & worker, - BuildMode buildMode = bmNormal) - : DerivationGoal{drvPath, drv, wantedOutputs, worker, buildMode} - , builder{ - worker.store, - static_cast(*this), - DerivationBuilderParams { - DerivationGoal::drvPath, - DerivationGoal::buildMode, - DerivationGoal::buildResult, - DerivationGoal::drv, - DerivationGoal::parsedDrv, - DerivationGoal::drvOptions, - DerivationGoal::inputPaths, - DerivationGoal::initialOutputs, - }, - } - {} - - virtual ~LocalDerivationGoal() override; - - /** - * The additional states. - */ - Goal::Co tryLocalBuild() override; - - bool isReadDesc(int fd) override; - - /** - * Forcibly kill the child process, if any. - * - * Called by destructor, can't be overridden - */ - void killChild() override final; - - void childStarted() override; - void childTerminated() override; - - void noteHashMismatch(void) override; - void noteCheckMismatch(void) override; - - void markContentsGood(const StorePath &) override; - - // Fake overrides to isntantiate identically-named virtual methods - - Path openLogFile() override { - return DerivationGoal::openLogFile(); - } - void closeLogFile() override { - DerivationGoal::closeLogFile(); - } - SingleDrvOutputs assertPathValidity() override { - return DerivationGoal::assertPathValidity(); - } - void appendLogTailErrorMsg(std::string & msg) override { - DerivationGoal::appendLogTailErrorMsg(msg); - } -}; - -std::shared_ptr makeLocalDerivationGoal( - const StorePath & drvPath, - const OutputsSpec & wantedOutputs, Worker & worker, - BuildMode buildMode) -{ - return std::make_shared(drvPath, wantedOutputs, worker, buildMode); -} - -std::shared_ptr makeLocalDerivationGoal( - const StorePath & drvPath, const BasicDerivation & drv, - const OutputsSpec & wantedOutputs, Worker & worker, - BuildMode buildMode) -{ - return std::make_shared(drvPath, drv, wantedOutputs, worker, buildMode); -} - -void handleDiffHook( - uid_t uid, uid_t gid, - const Path & tryA, const Path & tryB, - const Path & drvPath, const Path & tmpDir) -{ - auto & diffHookOpt = settings.diffHook.get(); - if (diffHookOpt && settings.runDiffHook) { - auto & diffHook = *diffHookOpt; - try { - auto diffRes = runProgram(RunOptions { - .program = diffHook, - .lookupPath = true, - .args = {tryA, tryB, drvPath, tmpDir}, - .uid = uid, - .gid = gid, - .chdir = "/" - }); - if (!statusOk(diffRes.first)) - throw ExecError(diffRes.first, - "diff-hook program '%1%' %2%", - diffHook, - statusToString(diffRes.first)); - - if (diffRes.second != "") - printError(chomp(diffRes.second)); - } catch (Error & error) { - ErrorInfo ei = error.info(); - // FIXME: wrap errors. - ei.msg = HintFmt("diff hook execution failed: %s", ei.msg.str()); - logError(ei); - } - } -} - -const Path DerivationBuilder::homeDir = "/homeless-shelter"; - - -LocalDerivationGoal::~LocalDerivationGoal() -{ - /* Careful: we should never ever throw an exception from a - destructor. */ - try { builder.deleteTmpDir(false); } catch (...) { ignoreExceptionInDestructor(); } - try { killChild(); } catch (...) { ignoreExceptionInDestructor(); } - try { builder.stopDaemon(); } catch (...) { ignoreExceptionInDestructor(); } -} - - -inline bool DerivationBuilder::needsHashRewrite() -{ -#ifdef __linux__ - return !useChroot; -#else - /* Darwin requires hash rewriting even when sandboxing is enabled. */ - return true; -#endif -} - - -LocalStore & DerivationBuilder::getLocalStore() -{ - auto p = dynamic_cast(&store); - assert(p); - return *p; -} - - -void LocalDerivationGoal::killChild() -{ - if (builder.pid != -1) { - worker.childTerminated(this); - - /* If we're using a build user, then there is a tricky race - condition: if we kill the build user before the child has - done its setuid() to the build user uid, then it won't be - killed, and we'll potentially lock up in pid.wait(). So - also send a conventional kill to the child. */ - ::kill(-builder.pid, SIGKILL); /* ignore the result */ - - builder.killSandbox(true); - - builder.pid.wait(); - } - - DerivationGoal::killChild(); -} - - -void DerivationBuilder::killSandbox(bool getStats) -{ - if (cgroup) { - #ifdef __linux__ - auto stats = destroyCgroup(*cgroup); - if (getStats) { - buildResult.cpuUser = stats.cpuUser; - buildResult.cpuSystem = stats.cpuSystem; - } - #else - unreachable(); - #endif - } - - else if (buildUser) { - auto uid = buildUser->getUID(); - assert(uid != 0); - killUser(uid); - } -} - - -void LocalDerivationGoal::childStarted() -{ - worker.childStarted(shared_from_this(), {builder.builderOut.get()}, true, true); -} - -void LocalDerivationGoal::childTerminated() -{ - worker.childTerminated(this); -} - -void LocalDerivationGoal::noteHashMismatch() -{ - worker.hashMismatch = true; -} - - -void LocalDerivationGoal::noteCheckMismatch() -{ - worker.checkMismatch = true; -} - - -void LocalDerivationGoal::markContentsGood(const StorePath & path) -{ - worker.markContentsGood(path); -} - - -Goal::Co LocalDerivationGoal::tryLocalBuild() -{ - assert(!hook); - - unsigned int curBuilds = worker.getNrLocalBuilds(); - if (curBuilds >= settings.maxBuildJobs) { - outputLocks.unlock(); - co_await waitForBuildSlot(); - co_return tryToBuild(); - } - - if (!builder.prepareBuild()) { - if (!actLock) - actLock = std::make_unique(*logger, lvlWarn, actBuildWaiting, - fmt("waiting for a free build user ID for '%s'", Magenta(worker.store.printStorePath(drvPath)))); - co_await waitForAWhile(); - co_return tryLocalBuild(); - } - - actLock.reset(); - - try { - - /* Okay, we have to build. */ - builder.startBuilder(); - - } catch (BuildError & e) { - outputLocks.unlock(); - builder.buildUser.reset(); - worker.permanentFailure = true; - co_return done(BuildResult::InputRejected, {}, std::move(e)); - } - - started(); - co_await Suspend{}; - - trace("build done"); - - auto res = builder.unprepareBuild(); - // N.B. cannot use `std::visit` with co-routine return - if (auto * ste = std::get_if<0>(&res)) { - outputLocks.unlock(); - co_return done(std::move(ste->first), {}, std::move(ste->second)); - } else if (auto * builtOutputs = std::get_if<1>(&res)) { - /* It is now safe to delete the lock files, since all future - lockers will see that the output paths are valid; they will - not create new lock files with the same names as the old - (unlinked) lock files. */ - outputLocks.setDeletion(true); - outputLocks.unlock(); - co_return done(BuildResult::Built, std::move(*builtOutputs)); - } else { - unreachable(); - } -} - -bool DerivationBuilder::prepareBuild() -{ - /* Cache this */ - derivationType = drv->type(); - - /* Are we doing a chroot build? */ - { - if (settings.sandboxMode == smEnabled) { - if (drvOptions->noChroot) - throw Error("derivation '%s' has '__noChroot' set, " - "but that's not allowed when 'sandbox' is 'true'", store.printStorePath(drvPath)); -#ifdef __APPLE__ - if (drvOptions->additionalSandboxProfile != "") - throw Error("derivation '%s' specifies a sandbox profile, " - "but this is only allowed when 'sandbox' is 'relaxed'", worker.store.printStorePath(drvPath)); -#endif - useChroot = true; - } - else if (settings.sandboxMode == smDisabled) - useChroot = false; - else if (settings.sandboxMode == smRelaxed) - useChroot = derivationType->isSandboxed() && !drvOptions->noChroot; - } - - auto & localStore = getLocalStore(); - if (localStore.storeDir != localStore.realStoreDir.get()) { - #ifdef __linux__ - useChroot = true; - #else - throw Error("building using a diverted store is not supported on this platform"); - #endif - } - - #ifdef __linux__ - if (useChroot) { - if (!mountAndPidNamespacesSupported()) { - if (!settings.sandboxFallback) - throw Error("this system does not support the kernel namespaces that are required for sandboxing; use '--no-sandbox' to disable sandboxing"); - debug("auto-disabling sandboxing because the prerequisite namespaces are not available"); - useChroot = false; - } - } - #endif - - if (useBuildUsers()) { - if (!buildUser) - buildUser = acquireUserLock(drvOptions->useUidRange(*drv) ? 65536 : 1, useChroot); - - if (!buildUser) { - return false; - } - } - - return true; -} - - -std::variant, SingleDrvOutputs> DerivationBuilder::unprepareBuild() -{ - Finally releaseBuildUser([&](){ - /* Release the build user at the end of this function. We don't do - it right away because we don't want another build grabbing this - uid and then messing around with our output. */ - buildUser.reset(); - }); - - sandboxMountNamespace = -1; - sandboxUserNamespace = -1; - - /* Since we got an EOF on the logger pipe, the builder is presumed - to have terminated. In fact, the builder could also have - simply have closed its end of the pipe, so just to be sure, - kill it. */ - int status = pid.kill(); - - debug("builder process for '%s' finished", store.printStorePath(drvPath)); - - buildResult.timesBuilt++; - buildResult.stopTime = time(0); - - /* So the child is gone now. */ - miscMethods.childTerminated(); - - /* Close the read side of the logger pipe. */ - builderOut.close(); - - /* Close the log file. */ - miscMethods.closeLogFile(); - - /* When running under a build user, make sure that all processes - running under that uid are gone. This is to prevent a - malicious user from leaving behind a process that keeps files - open and modifies them after they have been chown'ed to - root. */ - killSandbox(true); - - /* Terminate the recursive Nix daemon. */ - stopDaemon(); - - if (buildResult.cpuUser && buildResult.cpuSystem) { - debug("builder for '%s' terminated with status %d, user CPU %.3fs, system CPU %.3fs", - store.printStorePath(drvPath), - status, - ((double) buildResult.cpuUser->count()) / 1000000, - ((double) buildResult.cpuSystem->count()) / 1000000); - } - - bool diskFull = false; - - try { - - /* Check the exit status. */ - if (!statusOk(status)) { - - diskFull |= cleanupDecideWhetherDiskFull(); - - auto msg = fmt("builder for '%s' %s", - Magenta(store.printStorePath(drvPath)), - statusToString(status)); - - miscMethods.appendLogTailErrorMsg(msg); - - if (diskFull) - msg += "\nnote: build failure may have been caused by lack of free disk space"; - - throw BuildError(msg); - } - - /* Compute the FS closure of the outputs and register them as - being valid. */ - auto builtOutputs = registerOutputs(); - - StorePathSet outputPaths; - for (auto & [_, output] : builtOutputs) - outputPaths.insert(output.outPath); - runPostBuildHook( - store, - *logger, - drvPath, - outputPaths - ); - - /* Delete unused redirected outputs (when doing hash rewriting). */ - for (auto & i : redirectedOutputs) - deletePath(store.Store::toRealPath(i.second)); - - /* Delete the chroot (if we were using one). */ - autoDelChroot.reset(); /* this runs the destructor */ - - deleteTmpDir(true); - - return std::move(builtOutputs); - - } catch (BuildError & e) { - assert(derivationType); - BuildResult::Status st = - dynamic_cast(&e) ? BuildResult::NotDeterministic : - statusOk(status) ? BuildResult::OutputRejected : - !derivationType->isSandboxed() || diskFull ? BuildResult::TransientFailure : - BuildResult::PermanentFailure; - - return std::pair{std::move(st), std::move(e)}; - } -} - - -static void chmod_(const Path & path, mode_t mode) -{ - if (chmod(path.c_str(), mode) == -1) - throw SysError("setting permissions on '%s'", path); -} - - -/* Move/rename path 'src' to 'dst'. Temporarily make 'src' writable if - it's a directory and we're not root (to be able to update the - directory's parent link ".."). */ -static void movePath(const Path & src, const Path & dst) -{ - auto st = lstat(src); - - bool changePerm = (geteuid() && S_ISDIR(st.st_mode) && !(st.st_mode & S_IWUSR)); - - if (changePerm) - chmod_(src, st.st_mode | S_IWUSR); - - std::filesystem::rename(src, dst); - - if (changePerm) - chmod_(dst, st.st_mode); -} - - -extern void replaceValidPath(const Path & storePath, const Path & tmpPath); - - -bool DerivationBuilder::cleanupDecideWhetherDiskFull() -{ - bool diskFull = false; - - /* Heuristically check whether the build failure may have - been caused by a disk full condition. We have no way - of knowing whether the build actually got an ENOSPC. - So instead, check if the disk is (nearly) full now. If - so, we don't mark this build as a permanent failure. */ -#if HAVE_STATVFS - { - auto & localStore = getLocalStore(); - uint64_t required = 8ULL * 1024 * 1024; // FIXME: make configurable - struct statvfs st; - if (statvfs(localStore.realStoreDir.get().c_str(), &st) == 0 && - (uint64_t) st.f_bavail * st.f_bsize < required) - diskFull = true; - if (statvfs(tmpDir.c_str(), &st) == 0 && - (uint64_t) st.f_bavail * st.f_bsize < required) - diskFull = true; - } -#endif - - deleteTmpDir(false); - - /* Move paths out of the chroot for easier debugging of - build failures. */ - if (useChroot && buildMode == bmNormal) - for (auto & [_, status] : initialOutputs) { - if (!status.known) continue; - if (buildMode != bmCheck && status.known->isValid()) continue; - auto p = store.toRealPath(status.known->path); - if (pathExists(chrootRootDir + p)) - std::filesystem::rename((chrootRootDir + p), p); - } - - return diskFull; -} - - -#ifdef __linux__ -static void doBind(const Path & source, const Path & target, bool optional = false) { - debug("bind mounting '%1%' to '%2%'", source, target); - - auto bindMount = [&]() { - 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); - }; - - auto maybeSt = maybeLstat(source); - if (!maybeSt) { - if (optional) - return; - else - throw SysError("getting attributes of path '%1%'", source); - } - auto st = *maybeSt; - - if (S_ISDIR(st.st_mode)) { - createDirs(target); - bindMount(); - } else if (S_ISLNK(st.st_mode)) { - // Symlinks can (apparently) not be bind-mounted, so just copy it - createDirs(dirOf(target)); - copyFile( - std::filesystem::path(source), - std::filesystem::path(target), false); - } else { - createDirs(dirOf(target)); - writeFile(target, ""); - bindMount(); - } -}; -#endif - -/** - * Rethrow the current exception as a subclass of `Error`. - */ -static void rethrowExceptionAsError() -{ - try { - throw; - } catch (Error &) { - throw; - } catch (std::exception & e) { - throw Error(e.what()); - } catch (...) { - throw Error("unknown exception"); - } -} - -/** - * Send the current exception to the parent in the format expected by - * `DerivationBuilder::processSandboxSetupMessages()`. - */ -static void handleChildException(bool sendException) -{ - try { - rethrowExceptionAsError(); - } catch (Error & e) { - if (sendException) { - writeFull(STDERR_FILENO, "\1\n"); - FdSink sink(STDERR_FILENO); - sink << e; - sink.flush(); - } else - std::cerr << e.msg(); - } -} - -void DerivationBuilder::startBuilder() -{ - if ((buildUser && buildUser->getUIDCount() != 1) - #ifdef __linux__ - || settings.useCgroups - #endif - ) - { - #ifdef __linux__ - experimentalFeatureSettings.require(Xp::Cgroups); - - /* If we're running from the daemon, then this will return the - root cgroup of the service. Otherwise, it will return the - current cgroup. */ - auto rootCgroup = getRootCgroup(); - auto cgroupFS = getCgroupFS(); - if (!cgroupFS) - throw Error("cannot determine the cgroups file system"); - auto rootCgroupPath = canonPath(*cgroupFS + "/" + rootCgroup); - if (!pathExists(rootCgroupPath)) - throw Error("expected cgroup directory '%s'", rootCgroupPath); - - static std::atomic counter{0}; - - cgroup = buildUser - ? fmt("%s/nix-build-uid-%d", rootCgroupPath, buildUser->getUID()) - : fmt("%s/nix-build-pid-%d-%d", rootCgroupPath, getpid(), counter++); - - debug("using cgroup '%s'", *cgroup); - - /* When using a build user, record the cgroup we used for that - user so that if we got interrupted previously, we can kill - any left-over cgroup first. */ - if (buildUser) { - auto cgroupsDir = settings.nixStateDir + "/cgroups"; - createDirs(cgroupsDir); - - auto cgroupFile = fmt("%s/%d", cgroupsDir, buildUser->getUID()); - - if (pathExists(cgroupFile)) { - auto prevCgroup = readFile(cgroupFile); - destroyCgroup(prevCgroup); - } - - writeFile(cgroupFile, *cgroup); - } - - #else - throw Error("cgroups are not supported on this platform"); - #endif - } - - /* Make sure that no other processes are executing under the - sandbox uids. This must be done before any chownToBuilder() - calls. */ - killSandbox(false); - - /* Right platform? */ - if (!drvOptions->canBuildLocally(store, *drv)) { - // since aarch64-darwin has Rosetta 2, this user can actually run x86_64-darwin on their hardware - we should tell them to run the command to install Darwin 2 - if (drv->platform == "x86_64-darwin" && settings.thisSystem == "aarch64-darwin") { - throw Error("run `/usr/sbin/softwareupdate --install-rosetta` to enable your %s to run programs for %s", settings.thisSystem, drv->platform); - } else { - throw Error("a '%s' with features {%s} is required to build '%s', but I am a '%s' with features {%s}", - drv->platform, - concatStringsSep(", ", drvOptions->getRequiredSystemFeatures(*drv)), - store.printStorePath(drvPath), - settings.thisSystem, - concatStringsSep(", ", store.systemFeatures)); - } - } - - /* Create a temporary directory where the build will take - place. */ - topTmpDir = createTempDir(settings.buildDir.get().value_or(""), "nix-build-" + std::string(drvPath.name()), false, false, 0700); -#ifdef __APPLE__ - if (false) { -#else - if (useChroot) { -#endif - /* If sandboxing is enabled, put the actual TMPDIR underneath - an inaccessible root-owned directory, to prevent outside - access. - - On macOS, we don't use an actual chroot, so this isn't - possible. Any mitigation along these lines would have to be - done directly in the sandbox profile. */ - tmpDir = topTmpDir + "/build"; - createDir(tmpDir, 0700); - } else { - tmpDir = topTmpDir; - } - chownToBuilder(tmpDir); - - for (auto & [outputName, status] : initialOutputs) { - /* Set scratch path we'll actually use during the build. - - If we're not doing a chroot build, but we have some valid - output paths. Since we can't just overwrite or delete - them, we have to do hash rewriting: i.e. in the - environment/arguments passed to the build, we replace the - hashes of the valid outputs with unique dummy strings; - after the build, we discard the redirected outputs - corresponding to the valid outputs, and rewrite the - contents of the new outputs to replace the dummy strings - with the actual hashes. */ - auto scratchPath = - !status.known - ? makeFallbackPath(outputName) - : !needsHashRewrite() - /* Can always use original path in sandbox */ - ? status.known->path - : !status.known->isPresent() - /* If path doesn't yet exist can just use it */ - ? status.known->path - : buildMode != bmRepair && !status.known->isValid() - /* If we aren't repairing we'll delete a corrupted path, so we - can use original path */ - ? status.known->path - : /* If we are repairing or the path is totally valid, we'll need - to use a temporary path */ - makeFallbackPath(status.known->path); - scratchOutputs.insert_or_assign(outputName, scratchPath); - - /* Substitute output placeholders with the scratch output paths. - We'll use during the build. */ - inputRewrites[hashPlaceholder(outputName)] = store.printStorePath(scratchPath); - - /* Additional tasks if we know the final path a priori. */ - if (!status.known) continue; - auto fixedFinalPath = status.known->path; - - /* Additional tasks if the final and scratch are both known and - differ. */ - if (fixedFinalPath == scratchPath) continue; - - /* Ensure scratch path is ours to use. */ - deletePath(store.printStorePath(scratchPath)); - - /* Rewrite and unrewrite paths */ - { - std::string h1 { fixedFinalPath.hashPart() }; - std::string h2 { scratchPath.hashPart() }; - inputRewrites[h1] = h2; - } - - redirectedOutputs.insert_or_assign(std::move(fixedFinalPath), std::move(scratchPath)); - } - - /* Construct the environment passed to the builder. */ - initEnv(); - - writeStructuredAttrs(); - - /* Handle exportReferencesGraph(), if set. */ - if (!parsedDrv->hasStructuredAttrs()) { - /* The `exportReferencesGraph' feature allows the references graph - to be passed to a builder. This attribute should be a list of - pairs [name1 path1 name2 path2 ...]. The references graph of - each `pathN' will be stored in a text file `nameN' in the - temporary build directory. The text files have the format used - by `nix-store --register-validity'. However, the deriver - fields are left empty. */ - auto s = getOr(drv->env, "exportReferencesGraph", ""); - Strings ss = tokenizeString(s); - if (ss.size() % 2 != 0) - throw BuildError("odd number of tokens in 'exportReferencesGraph': '%1%'", s); - for (Strings::iterator i = ss.begin(); i != ss.end(); ) { - auto fileName = *i++; - static std::regex regex("[A-Za-z_][A-Za-z0-9_.-]*"); - if (!std::regex_match(fileName, regex)) - throw Error("invalid file name '%s' in 'exportReferencesGraph'", fileName); - - auto storePathS = *i++; - if (!store.isInStore(storePathS)) - throw BuildError("'exportReferencesGraph' contains a non-store path '%1%'", storePathS); - auto storePath = store.toStorePath(storePathS).first; - - /* Write closure info to . */ - writeFile(tmpDir + "/" + fileName, - store.makeValidityRegistration( - store.exportReferences({storePath}, inputPaths), false, false)); - } - } - - if (useChroot) { - - /* Allow a user-configurable set of directories from the - host file system. */ - pathsInChroot.clear(); - - for (auto i : settings.sandboxPaths.get()) { - if (i.empty()) continue; - bool optional = false; - if (i[i.size() - 1] == '?') { - optional = true; - i.pop_back(); - } - size_t p = i.find('='); - if (p == std::string::npos) - pathsInChroot[i] = {i, optional}; - else - pathsInChroot[i.substr(0, p)] = {i.substr(p + 1), optional}; - } - if (hasPrefix(store.storeDir, tmpDirInSandbox)) - { - throw Error("`sandbox-build-dir` must not contain the storeDir"); - } - pathsInChroot[tmpDirInSandbox] = tmpDir; - - /* Add the closure of store paths to the chroot. */ - StorePathSet closure; - for (auto & i : pathsInChroot) - try { - if (store.isInStore(i.second.source)) - store.computeFSClosure(store.toStorePath(i.second.source).first, closure); - } catch (InvalidPath & e) { - } catch (Error & e) { - e.addTrace({}, "while processing 'sandbox-paths'"); - throw; - } - for (auto & i : closure) { - auto p = store.printStorePath(i); - pathsInChroot.insert_or_assign(p, p); - } - - PathSet allowedPaths = settings.allowedImpureHostPrefixes; - - /* This works like the above, except on a per-derivation level */ - auto impurePaths = drvOptions->impureHostDeps; - - for (auto & i : impurePaths) { - bool found = false; - /* Note: we're not resolving symlinks here to prevent - giving a non-root user info about inaccessible - files. */ - Path canonI = canonPath(i); - /* If only we had a trie to do this more efficiently :) luckily, these are generally going to be pretty small */ - for (auto & a : allowedPaths) { - Path canonA = canonPath(a); - if (isDirOrInDir(canonI, canonA)) { - found = true; - break; - } - } - if (!found) - throw Error("derivation '%s' requested impure path '%s', but it was not in allowed-impure-host-deps", - store.printStorePath(drvPath), i); - - /* Allow files in drvOptions->impureHostDeps to be missing; e.g. - macOS 11+ has no /usr/lib/libSystem*.dylib */ - pathsInChroot[i] = {i, true}; - } - -#ifdef __linux__ - /* Create a temporary directory in which we set up the chroot - environment using bind-mounts. We put it in the Nix store - so that the build outputs can be moved efficiently from the - chroot to their final location. */ - auto chrootParentDir = store.Store::toRealPath(drvPath) + ".chroot"; - deletePath(chrootParentDir); - - /* Clean up the chroot directory automatically. */ - autoDelChroot = std::make_shared(chrootParentDir); - - printMsg(lvlChatty, "setting up chroot environment in '%1%'", chrootParentDir); - - if (mkdir(chrootParentDir.c_str(), 0700) == -1) - throw SysError("cannot create '%s'", chrootRootDir); - - chrootRootDir = chrootParentDir + "/root"; - - if (mkdir(chrootRootDir.c_str(), buildUser && buildUser->getUIDCount() != 1 ? 0755 : 0750) == -1) - throw SysError("cannot create '%1%'", chrootRootDir); - - if (buildUser && chown(chrootRootDir.c_str(), buildUser->getUIDCount() != 1 ? buildUser->getUID() : 0, buildUser->getGID()) == -1) - throw SysError("cannot change ownership of '%1%'", chrootRootDir); - - /* Create a writable /tmp in the chroot. Many builders need - this. (Of course they should really respect $TMPDIR - instead.) */ - Path chrootTmpDir = chrootRootDir + "/tmp"; - createDirs(chrootTmpDir); - chmod_(chrootTmpDir, 01777); - - /* Create a /etc/passwd with entries for the build user and the - nobody account. The latter is kind of a hack to support - Samba-in-QEMU. */ - createDirs(chrootRootDir + "/etc"); - if (drvOptions->useUidRange(*drv)) - chownToBuilder(chrootRootDir + "/etc"); - - if (drvOptions->useUidRange(*drv) && (!buildUser || buildUser->getUIDCount() < 65536)) - throw Error("feature 'uid-range' requires the setting '%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"). */ - writeFile(chrootRootDir + "/etc/group", - fmt("root:x:0:\n" - "nixbld:!:%1%:\n" - "nogroup:x:65534:\n", sandboxGid())); - - /* Create /etc/hosts with localhost entry. */ - if (derivationType->isSandboxed()) - writeFile(chrootRootDir + "/etc/hosts", "127.0.0.1 localhost\n::1 localhost\n"); - - /* Make the closure of the inputs available in the chroot, - rather than the whole Nix store. This prevents any access - to undeclared dependencies. Directories are bind-mounted, - while other inputs are hard-linked (since only directories - can be bind-mounted). !!! As an extra security - precaution, make the fake Nix store only writable by the - build user. */ - Path chrootStoreDir = chrootRootDir + store.storeDir; - createDirs(chrootStoreDir); - chmod_(chrootStoreDir, 01775); - - if (buildUser && chown(chrootStoreDir.c_str(), 0, buildUser->getGID()) == -1) - throw SysError("cannot change ownership of '%1%'", chrootStoreDir); - - for (auto & i : inputPaths) { - auto p = store.printStorePath(i); - Path r = store.toRealPath(p); - pathsInChroot.insert_or_assign(p, r); - } - - /* If we're repairing, checking or rebuilding part of a - multiple-outputs derivation, it's possible that we're - rebuilding a path that is in settings.sandbox-paths - (typically the dependencies of /bin/sh). Throw them - out. */ - for (auto & i : drv->outputsAndOptPaths(store)) { - /* If the name isn't known a priori (i.e. floating - content-addressing derivation), the temporary location we use - should be fresh. Freshness means it is impossible that the path - is already in the sandbox, so we don't need to worry about - removing it. */ - if (i.second.second) - pathsInChroot.erase(store.printStorePath(*i.second.second)); - } - - if (cgroup) { - if (mkdir(cgroup->c_str(), 0755) != 0) - throw SysError("creating cgroup '%s'", *cgroup); - chownToBuilder(*cgroup); - chownToBuilder(*cgroup + "/cgroup.procs"); - chownToBuilder(*cgroup + "/cgroup.threads"); - //chownToBuilder(*cgroup + "/cgroup.subtree_control"); - } - -#else - if (drvOptions->useUidRange(*drv)) - throw Error("feature 'uid-range' is not supported on this platform"); - #ifdef __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 (drvOptions->useUidRange(*drv)) - throw Error("feature 'uid-range' is only supported in sandboxed builds"); - } - - if (needsHashRewrite() && pathExists(homeDir)) - throw Error("home directory '%1%' exists; please remove it to assure purity of builds without sandboxing", homeDir); - - if (useChroot && settings.preBuildHook != "" && dynamic_cast(drv.get())) { - printMsg(lvlChatty, "executing pre-build hook '%1%'", settings.preBuildHook); - auto args = useChroot ? Strings({store.printStorePath(drvPath), chrootRootDir}) : - Strings({ store.printStorePath(drvPath) }); - enum BuildHookState { - stBegin, - stExtraChrootDirs - }; - auto state = stBegin; - auto lines = runProgram(settings.preBuildHook, false, args); - auto lastPos = std::string::size_type{0}; - for (auto nlPos = lines.find('\n'); nlPos != std::string::npos; - nlPos = lines.find('\n', lastPos)) - { - auto line = lines.substr(lastPos, nlPos - lastPos); - lastPos = nlPos + 1; - if (state == stBegin) { - if (line == "extra-sandbox-paths" || line == "extra-chroot-dirs") { - state = stExtraChrootDirs; - } else { - throw Error("unknown pre-build hook command '%1%'", line); - } - } else if (state == stExtraChrootDirs) { - if (line == "") { - state = stBegin; - } else { - auto p = line.find('='); - if (p == std::string::npos) - pathsInChroot[line] = line; - else - pathsInChroot[line.substr(0, p)] = line.substr(p + 1); - } - } - } - } - - /* Fire up a Nix daemon to process recursive Nix calls from the - builder. */ - if (drvOptions->getRequiredSystemFeatures(*drv).count("recursive-nix")) - startDaemon(); - - /* Run the builder. */ - printMsg(lvlChatty, "executing builder '%1%'", drv->builder); - printMsg(lvlChatty, "using builder args '%1%'", concatStringsSep(" ", drv->args)); - for (auto & i : drv->env) - printMsg(lvlVomit, "setting builder env variable '%1%'='%2%'", i.first, i.second); - - /* Create the log file. */ - [[maybe_unused]] Path logFile = miscMethods.openLogFile(); - - /* Create a pseudoterminal to get the output of the builder. */ - builderOut = posix_openpt(O_RDWR | O_NOCTTY); - if (!builderOut) - throw SysError("opening pseudoterminal master"); - - // FIXME: not thread-safe, use ptsname_r - std::string slaveName = ptsname(builderOut.get()); - - if (buildUser) { - if (chmod(slaveName.c_str(), 0600)) - throw SysError("changing mode of pseudoterminal slave"); - - if (chown(slaveName.c_str(), buildUser->getUID(), 0)) - throw SysError("changing owner of pseudoterminal slave"); - } -#ifdef __APPLE__ - else { - if (grantpt(builderOut.get())) - throw SysError("granting access to pseudoterminal slave"); - } -#endif - - if (unlockpt(builderOut.get())) - throw SysError("unlocking pseudoterminal"); - - /* Open the slave side of the pseudoterminal and use it as stderr. */ - auto openSlave = [&]() - { - AutoCloseFD builderOut = open(slaveName.c_str(), O_RDWR | O_NOCTTY); - if (!builderOut) - throw SysError("opening pseudoterminal slave"); - - // Put the pt into raw mode to prevent \n -> \r\n translation. - struct termios term; - if (tcgetattr(builderOut.get(), &term)) - throw SysError("getting pseudoterminal attributes"); - - cfmakeraw(&term); - - if (tcsetattr(builderOut.get(), TCSANOW, &term)) - throw SysError("putting pseudoterminal into raw mode"); - - if (dup2(builderOut.get(), STDERR_FILENO) == -1) - throw SysError("cannot pipe standard error into log file"); - }; - - buildResult.startTime = time(0); - - /* Fork a child to build the package. */ - -#ifdef __linux__ - if (useChroot) { - /* Set up private namespaces for the build: - - - The PID namespace causes the build to start as PID 1. - Processes outside of the chroot are not visible to those - on the inside, but processes inside the chroot are - visible from the outside (though with different PIDs). - - - The private mount namespace ensures that all the bind - mounts we do will only show up in this process and its - children, and will disappear automatically when we're - done. - - - The private network namespace ensures that the builder - cannot talk to the outside world (or vice versa). It - only has a private loopback interface. (Fixed-output - derivations are not run in a private network namespace - to allow functions like fetchurl to work.) - - - The IPC namespace prevents the builder from communicating - with outside processes using SysV IPC mechanisms (shared - memory, message queues, semaphores). It also ensures - that all IPC objects are destroyed when the builder - exits. - - - The UTS namespace ensures that builders see a hostname of - localhost rather than the actual hostname. - - We use a helper process to do the clone() to work around - clone() being broken in multi-threaded programs due to - at-fork handlers not being run. Note that we use - CLONE_PARENT to ensure that the real builder is parented to - us. - */ - - userNamespaceSync.create(); - - usingUserNamespace = userNamespacesSupported(); - - Pipe sendPid; - sendPid.create(); - - Pid helper = startProcess([&]() { - sendPid.readSide.close(); - - /* We need to open the slave early, before - CLONE_NEWUSER. Otherwise we get EPERM when running as - root. */ - openSlave(); - - try { - /* Drop additional groups here because we can't do it - after we've created the new user namespace. */ - if (setgroups(0, 0) == -1) { - if (errno != EPERM) - throw SysError("setgroups failed"); - if (settings.requireDropSupplementaryGroups) - throw Error("setgroups failed. Set the require-drop-supplementary-groups option to false to skip this step."); - } - - ProcessOptions options; - options.cloneFlags = CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWIPC | CLONE_NEWUTS | CLONE_PARENT | SIGCHLD; - if (derivationType->isSandboxed()) - options.cloneFlags |= CLONE_NEWNET; - if (usingUserNamespace) - options.cloneFlags |= CLONE_NEWUSER; - - pid_t child = startProcess([&]() { runChild(); }, options); - - writeFull(sendPid.writeSide.get(), fmt("%d\n", child)); - _exit(0); - } catch (...) { - handleChildException(true); - _exit(1); - } - }); - - sendPid.writeSide.close(); - - if (helper.wait() != 0) { - processSandboxSetupMessages(); - // Only reached if the child process didn't send an exception. - throw Error("unable to start build process"); - } - - userNamespaceSync.readSide = -1; - - /* Close the write side to prevent runChild() from hanging - reading from this. */ - Finally cleanup([&]() { - userNamespaceSync.writeSide = -1; - }); - - auto ss = tokenizeString>(readLine(sendPid.readSide.get())); - assert(ss.size() == 1); - pid = string2Int(ss[0]).value(); - - if (usingUserNamespace) { - /* Set the UID/GID mapping of the builder's user namespace - such that the sandbox user maps to the build user, or to - the calling user (if build users are disabled). */ - uid_t hostUid = buildUser ? buildUser->getUID() : getuid(); - uid_t hostGid = buildUser ? buildUser->getGID() : getgid(); - uid_t nrIds = buildUser ? buildUser->getUIDCount() : 1; - - writeFile("/proc/" + std::to_string(pid) + "/uid_map", - fmt("%d %d %d", sandboxUid(), hostUid, nrIds)); - - if (!buildUser || buildUser->getUIDCount() == 1) - writeFile("/proc/" + std::to_string(pid) + "/setgroups", "deny"); - - writeFile("/proc/" + std::to_string(pid) + "/gid_map", - fmt("%d %d %d", sandboxGid(), hostGid, nrIds)); - } else { - debug("note: not using a user namespace"); - if (!buildUser) - throw Error("cannot perform a sandboxed build because user namespaces are not enabled; check /proc/sys/user/max_user_namespaces"); - } - - /* Now that we now the sandbox uid, we can write - /etc/passwd. */ - writeFile(chrootRootDir + "/etc/passwd", fmt( - "root:x:0:0:Nix build user:%3%:/noshell\n" - "nixbld:x:%1%:%2%:Nix build user:%3%:/noshell\n" - "nobody:x:65534:65534:Nobody:/:/noshell\n", - sandboxUid(), sandboxGid(), settings.sandboxBuildDir)); - - /* Save the mount- and user 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"); - - if (usingUserNamespace) { - sandboxUserNamespace = open(fmt("/proc/%d/ns/user", (pid_t) pid).c_str(), O_RDONLY); - if (sandboxUserNamespace.get() == -1) - throw SysError("getting sandbox user namespace"); - } - - /* Move the child into its own cgroup. */ - if (cgroup) - writeFile(*cgroup + "/cgroup.procs", fmt("%d", (pid_t) pid)); - - /* Signal the builder that we've updated its user namespace. */ - writeFull(userNamespaceSync.writeSide.get(), "1"); - - } else -#endif - { - pid = startProcess([&]() { - openSlave(); - runChild(); - }); - } - - /* parent */ - pid.setSeparatePG(true); - miscMethods.childStarted(); - - processSandboxSetupMessages(); -} - - -void DerivationBuilder::processSandboxSetupMessages() -{ - std::vector msgs; - while (true) { - std::string msg = [&]() { - try { - return readLine(builderOut.get()); - } catch (Error & e) { - auto status = pid.wait(); - e.addTrace({}, "while waiting for the build environment for '%s' to initialize (%s, previous messages: %s)", - store.printStorePath(drvPath), - statusToString(status), - concatStringsSep("|", msgs)); - throw; - } - }(); - if (msg.substr(0, 1) == "\2") break; - if (msg.substr(0, 1) == "\1") { - FdSource source(builderOut.get()); - auto ex = readError(source); - ex.addTrace({}, "while setting up the build environment"); - throw ex; - } - debug("sandbox setup: " + msg); - msgs.push_back(std::move(msg)); - } -} - - -void DerivationBuilder::initTmpDir() -{ - /* In a sandbox, for determinism, always use the same temporary - directory. */ -#ifdef __linux__ - tmpDirInSandbox = useChroot ? settings.sandboxBuildDir : tmpDir; -#else - tmpDirInSandbox = tmpDir; -#endif - - /* In non-structured mode, set all bindings either directory in the - environment or via a file, as specified by - `DerivationOptions::passAsFile`. */ - if (!parsedDrv->hasStructuredAttrs()) { - for (auto & i : drv->env) { - if (drvOptions->passAsFile.find(i.first) == drvOptions->passAsFile.end()) { - env[i.first] = i.second; - } else { - auto hash = hashString(HashAlgorithm::SHA256, i.first); - std::string fn = ".attr-" + hash.to_string(HashFormat::Nix32, false); - Path p = tmpDir + "/" + fn; - writeFile(p, rewriteStrings(i.second, inputRewrites)); - chownToBuilder(p); - env[i.first + "Path"] = tmpDirInSandbox + "/" + fn; - } - } - - } - - /* For convenience, set an environment pointing to the top build - directory. */ - env["NIX_BUILD_TOP"] = tmpDirInSandbox; - - /* Also set TMPDIR and variants to point to this directory. */ - env["TMPDIR"] = env["TEMPDIR"] = env["TMP"] = env["TEMP"] = tmpDirInSandbox; - - /* Explicitly set PWD to prevent problems with chroot builds. In - particular, dietlibc cannot figure out the cwd because the - inode of the current directory doesn't appear in .. (because - getdents returns the inode of the mount point). */ - env["PWD"] = tmpDirInSandbox; -} - - -void DerivationBuilder::initEnv() -{ - env.clear(); - - /* Most shells initialise PATH to some default (/bin:/usr/bin:...) when - PATH is not set. We don't want this, so we fill it in with some dummy - value. */ - env["PATH"] = "/path-not-set"; - - /* Set HOME to a non-existing path to prevent certain programs from using - /etc/passwd (or NIS, or whatever) to locate the home directory (for - example, wget looks for ~/.wgetrc). I.e., these tools use /etc/passwd - if HOME is not set, but they will just assume that the settings file - they are looking for does not exist if HOME is set but points to some - non-existing path. */ - env["HOME"] = homeDir; - - /* Tell the builder where the Nix store is. Usually they - shouldn't care, but this is useful for purity checking (e.g., - the compiler or linker might only want to accept paths to files - in the store or in the build directory). */ - env["NIX_STORE"] = store.storeDir; - - /* The maximum number of cores to utilize for parallel building. */ - env["NIX_BUILD_CORES"] = fmt("%d", settings.buildCores); - - initTmpDir(); - - /* Compatibility hack with Nix <= 0.7: if this is a fixed-output - derivation, tell the builder, so that for instance `fetchurl' - can skip checking the output. On older Nixes, this environment - variable won't be set, so `fetchurl' will do the check. */ - if (derivationType->isFixed()) env["NIX_OUTPUT_CHECKED"] = "1"; - - /* *Only* if this is a fixed-output derivation, propagate the - values of the environment variables specified in the - `impureEnvVars' attribute to the builder. This allows for - instance environment variables for proxy configuration such as - `http_proxy' to be easily passed to downloaders like - `fetchurl'. Passing such environment variables from the caller - to the builder is generally impure, but the output of - fixed-output derivations is by definition pure (since we - already know the cryptographic hash of the output). */ - if (!derivationType->isSandboxed()) { - auto & impureEnv = settings.impureEnv.get(); - if (!impureEnv.empty()) - experimentalFeatureSettings.require(Xp::ConfigurableImpureEnv); - - for (auto & i : drvOptions->impureEnvVars){ - auto envVar = impureEnv.find(i); - if (envVar != impureEnv.end()) { - env[i] = envVar->second; - } else { - env[i] = getEnv(i).value_or(""); - } - } - } - - /* Currently structured log messages piggyback on stderr, but we - may change that in the future. So tell the builder which file - descriptor to use for that. */ - env["NIX_LOG_FD"] = "2"; - - /* Trigger colored output in various tools. */ - env["TERM"] = "xterm-256color"; -} - - -void DerivationBuilder::writeStructuredAttrs() -{ - if (auto structAttrsJson = parsedDrv->prepareStructuredAttrs(store, inputPaths)) { - auto json = structAttrsJson.value(); - nlohmann::json rewritten; - for (auto & [i, v] : json["outputs"].get()) { - /* The placeholder must have a rewrite, so we use it to cover both the - cases where we know or don't know the output path ahead of time. */ - rewritten[i] = rewriteStrings((std::string) v, inputRewrites); - } - - json["outputs"] = rewritten; - - auto jsonSh = writeStructuredAttrsShell(json); - - writeFile(tmpDir + "/.attrs.sh", rewriteStrings(jsonSh, inputRewrites)); - chownToBuilder(tmpDir + "/.attrs.sh"); - env["NIX_ATTRS_SH_FILE"] = tmpDirInSandbox + "/.attrs.sh"; - writeFile(tmpDir + "/.attrs.json", rewriteStrings(json.dump(), inputRewrites)); - chownToBuilder(tmpDir + "/.attrs.json"); - env["NIX_ATTRS_JSON_FILE"] = tmpDirInSandbox + "/.attrs.json"; - } -} - - -void DerivationBuilder::startDaemon() -{ - experimentalFeatureSettings.require(Xp::RecursiveNix); - - Store::Params params; - params["path-info-cache-size"] = "0"; - params["store"] = store.storeDir; - if (auto & optRoot = getLocalStore().rootDir.get()) - params["root"] = *optRoot; - params["state"] = "/no-such-path"; - params["log"] = "/no-such-path"; - auto store = makeRestrictedStore(params, - ref(std::dynamic_pointer_cast(this->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 || errno == EAGAIN) continue; - if (errno == EINVAL || errno == ECONNABORTED) break; - throw SysError("accepting connection"); - } - - unix::closeOnExec(remote.get()); - - debug("received daemon connection"); - - auto workerThread = std::thread([store, remote{std::move(remote)}]() { - try { - daemon::processConnection( - store, - FdSource(remote.get()), - FdSink(remote.get()), - NotTrusted, daemon::Recursive); - debug("terminated daemon connection"); - } catch (const Interrupted &) { - debug("interrupted daemon connection"); - } catch (SystemError &) { - ignoreExceptionExceptInterrupt(); - } - }); - - daemonWorkerThreads.push_back(std::move(workerThread)); - } - - debug("daemon shutting down"); - }); -} - - -void DerivationBuilder::stopDaemon() -{ - if (daemonSocket && shutdown(daemonSocket.get(), SHUT_RDWR) == -1) { - // According to the POSIX standard, the 'shutdown' function should - // return an ENOTCONN error when attempting to shut down a socket that - // hasn't been connected yet. This situation occurs when the 'accept' - // function is called on a socket without any accepted connections, - // leaving the socket unconnected. While Linux doesn't seem to produce - // an error for sockets that have only been accepted, more - // POSIX-compliant operating systems like OpenBSD, macOS, and others do - // return the ENOTCONN error. Therefore, we handle this error here to - // avoid raising an exception for compliant behaviour. - if (errno == ENOTCONN) { - daemonSocket.close(); - } else { - throw SysError("shutting down daemon socket"); - } - } - - if (daemonThread.joinable()) - daemonThread.join(); - - // FIXME: should prune worker threads more quickly. - // FIXME: shutdown the client socket to speed up worker termination. - for (auto & thread : daemonWorkerThreads) - thread.join(); - daemonWorkerThreads.clear(); - - // release the socket. - daemonSocket.close(); -} - - -void DerivationBuilder::addDependency(const StorePath & 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", store.printStorePath(path)); - - #ifdef __linux__ - - Path source = store.Store::toRealPath(path); - Path target = chrootRootDir + store.printStorePath(path); - - 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", store.printStorePath(path)); - } - - /* 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 (usingUserNamespace && (setns(sandboxUserNamespace.get(), 0) == -1)) - throw SysError("entering sandbox user namespace"); - - if (setns(sandboxMountNamespace.get(), 0) == -1) - throw SysError("entering sandbox mount namespace"); - - doBind(source, target); - - _exit(0); - })); - - int status = child.wait(); - if (status != 0) - throw Error("could not add path '%s' to sandbox", store.printStorePath(path)); - - #else - throw Error("don't know how to make path '%s' (produced by a recursive Nix call) appear in the sandbox", - worker.store.printStorePath(path)); - #endif - - } -} - -void DerivationBuilder::chownToBuilder(const Path & path) -{ - if (!buildUser) return; - if (chown(path.c_str(), buildUser->getUID(), buildUser->getGID()) == -1) - throw SysError("cannot change ownership of '%1%'", path); -} - - -void setupSeccomp() -{ -#ifdef __linux__ - if (!settings.filterSyscalls) return; -#if HAVE_SECCOMP - scmp_filter_ctx ctx; - - if (!(ctx = seccomp_init(SCMP_ACT_ALLOW))) - throw SysError("unable to initialize seccomp mode 2"); - - Finally cleanup([&]() { - seccomp_release(ctx); - }); - - constexpr std::string_view nativeSystem = NIX_LOCAL_SYSTEM; - - if (nativeSystem == "x86_64-linux" && - seccomp_arch_add(ctx, SCMP_ARCH_X86) != 0) - throw SysError("unable to add 32-bit seccomp architecture"); - - if (nativeSystem == "x86_64-linux" && - seccomp_arch_add(ctx, SCMP_ARCH_X32) != 0) - throw SysError("unable to add X32 seccomp architecture"); - - if (nativeSystem == "aarch64-linux" && - seccomp_arch_add(ctx, SCMP_ARCH_ARM) != 0) - printError("unable to add ARM seccomp architecture; this may result in spurious build failures if running 32-bit ARM processes"); - - if (nativeSystem == "mips64-linux" && - seccomp_arch_add(ctx, SCMP_ARCH_MIPS) != 0) - printError("unable to add mips seccomp architecture"); - - if (nativeSystem == "mips64-linux" && - seccomp_arch_add(ctx, SCMP_ARCH_MIPS64N32) != 0) - printError("unable to add mips64-*abin32 seccomp architecture"); - - if (nativeSystem == "mips64el-linux" && - seccomp_arch_add(ctx, SCMP_ARCH_MIPSEL) != 0) - printError("unable to add mipsel seccomp architecture"); - - if (nativeSystem == "mips64el-linux" && - seccomp_arch_add(ctx, SCMP_ARCH_MIPSEL64N32) != 0) - printError("unable to add mips64el-*abin32 seccomp architecture"); - - /* Prevent builders from creating setuid/setgid binaries. */ - for (int perm : { S_ISUID, S_ISGID }) { - if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(chmod), 1, - SCMP_A1(SCMP_CMP_MASKED_EQ, (scmp_datum_t) perm, (scmp_datum_t) perm)) != 0) - throw SysError("unable to add seccomp rule"); - - if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(fchmod), 1, - SCMP_A1(SCMP_CMP_MASKED_EQ, (scmp_datum_t) perm, (scmp_datum_t) perm)) != 0) - throw SysError("unable to add seccomp rule"); - - if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(fchmodat), 1, - SCMP_A2(SCMP_CMP_MASKED_EQ, (scmp_datum_t) perm, (scmp_datum_t) perm)) != 0) - throw SysError("unable to add seccomp rule"); - - if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), NIX_SYSCALL_FCHMODAT2, 1, - SCMP_A2(SCMP_CMP_MASKED_EQ, (scmp_datum_t) perm, (scmp_datum_t) perm)) != 0) - throw SysError("unable to add seccomp rule"); - } - - /* Prevent builders from using EAs or ACLs. Not all filesystems - support these, and they're not allowed in the Nix store because - they're not representable in the NAR serialisation. */ - if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(getxattr), 0) != 0 || - seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(lgetxattr), 0) != 0 || - seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(fgetxattr), 0) != 0 || - seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(setxattr), 0) != 0 || - seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(lsetxattr), 0) != 0 || - seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(fsetxattr), 0) != 0) - throw SysError("unable to add seccomp rule"); - - if (seccomp_attr_set(ctx, SCMP_FLTATR_CTL_NNP, settings.allowNewPrivileges ? 0 : 1) != 0) - throw SysError("unable to set 'no new privileges' seccomp attribute"); - - if (seccomp_load(ctx) != 0) - throw SysError("unable to load seccomp BPF program"); -#else - throw Error( - "seccomp is not supported on this platform; " - "you can bypass this error by setting the option 'filter-syscalls' to false, but note that untrusted builds can then create setuid binaries!"); -#endif -#endif -} - - -void DerivationBuilder::runChild() -{ - /* Warning: in the child we should absolutely not make any SQLite - calls! */ - - bool sendException = true; - - try { /* child */ - - commonChildInit(); - - try { - setupSeccomp(); - } catch (...) { - if (buildUser) throw; - } - - bool setUser = true; - - /* Make the contents of netrc and the CA certificate bundle - available to builtin:fetchurl (which may run under a - different uid and/or in a sandbox). */ - std::string netrcData; - std::string caFileData; - if (drv->isBuiltin() && drv->builder == "builtin:fetchurl") { - try { - netrcData = readFile(settings.netrcFile); - } catch (SystemError &) { } - - try { - caFileData = readFile(settings.caFile); - } catch (SystemError &) { } - } - -#ifdef __linux__ - if (useChroot) { - - userNamespaceSync.writeSide = -1; - - if (drainFD(userNamespaceSync.readSide.get()) != "1") - throw Error("user namespace initialisation failed"); - - userNamespaceSync.readSide = -1; - - if (derivationType->isSandboxed()) { - - /* Initialise the loopback interface. */ - AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP)); - if (!fd) throw SysError("cannot open IP socket"); - - struct ifreq ifr; - strcpy(ifr.ifr_name, "lo"); - ifr.ifr_flags = IFF_UP | IFF_LOOPBACK | IFF_RUNNING; - if (ioctl(fd.get(), SIOCSIFFLAGS, &ifr) == -1) - throw SysError("cannot set loopback interface flags"); - } - - /* Set the hostname etc. to fixed values. */ - char hostname[] = "localhost"; - if (sethostname(hostname, sizeof(hostname)) == -1) - throw SysError("cannot set host name"); - char domainname[] = "(none)"; // kernel default - if (setdomainname(domainname, sizeof(domainname)) == -1) - throw SysError("cannot set domain name"); - - /* Make all filesystems private. This is necessary - because subtrees may have been mounted as "shared" - (MS_SHARED). (Systemd does this, for instance.) Even - though we have a private mount namespace, mounting - filesystems on top of a shared subtree still propagates - outside of the namespace. Making a subtree private is - local to the namespace, though, so setting MS_PRIVATE - does not affect the outside world. */ - if (mount(0, "/", 0, MS_PRIVATE | MS_REC, 0) == -1) - throw SysError("unable to make '/' private"); - - /* Bind-mount chroot directory to itself, to treat it as a - different filesystem from /, as needed for pivot_root. */ - if (mount(chrootRootDir.c_str(), chrootRootDir.c_str(), 0, MS_BIND, 0) == -1) - throw SysError("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 + 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 - bind-mount the host /dev. */ - Strings ss; - if (pathsInChroot.find("/dev") == pathsInChroot.end()) { - createDirs(chrootRootDir + "/dev/shm"); - createDirs(chrootRootDir + "/dev/pts"); - ss.push_back("/dev/full"); - if (store.systemFeatures.get().count("kvm") && pathExists("/dev/kvm")) - ss.push_back("/dev/kvm"); - ss.push_back("/dev/null"); - ss.push_back("/dev/random"); - ss.push_back("/dev/tty"); - ss.push_back("/dev/urandom"); - ss.push_back("/dev/zero"); - createSymlink("/proc/self/fd", chrootRootDir + "/dev/fd"); - createSymlink("/proc/self/fd/0", chrootRootDir + "/dev/stdin"); - createSymlink("/proc/self/fd/1", chrootRootDir + "/dev/stdout"); - createSymlink("/proc/self/fd/2", chrootRootDir + "/dev/stderr"); - } - - /* Fixed-output derivations typically need to access the - network, so give them access to /etc/resolv.conf and so - on. */ - if (!derivationType->isSandboxed()) { - // Only use nss functions to resolve hosts and - // services. Don’t use it for anything else that may - // be configured for this system. This limits the - // potential impurities introduced in fixed-outputs. - writeFile(chrootRootDir + "/etc/nsswitch.conf", "hosts: files dns\nservices: files\n"); - - /* N.B. it is realistic that these paths might not exist. It - happens when testing Nix building fixed-output derivations - within a pure derivation. */ - for (auto & path : { "/etc/resolv.conf", "/etc/services", "/etc/hosts" }) - if (pathExists(path)) - ss.push_back(path); - - if (settings.caFile != "" && pathExists(settings.caFile)) { - Path caFile = settings.caFile; - pathsInChroot.try_emplace("/etc/ssl/certs/ca-certificates.crt", canonPath(caFile, true), true); - } - } - - for (auto & i : ss) { - // For backwards-compatibiliy, resolve all the symlinks in the - // chroot paths - auto canonicalPath = canonPath(i, true); - pathsInChroot.emplace(i, canonicalPath); - } - - /* Bind-mount all the directories from the "host" - filesystem that we want in the chroot - environment. */ - for (auto & i : pathsInChroot) { - if (i.second.source == "/proc") continue; // backwards compatibility - - #if HAVE_EMBEDDED_SANDBOX_SHELL - if (i.second.source == "__embedded_sandbox_shell__") { - static unsigned char sh[] = { - #include "embedded-sandbox-shell.gen.hh" - }; - auto dst = chrootRootDir + i.first; - createDirs(dirOf(dst)); - writeFile(dst, std::string_view((const char *) sh, sizeof(sh))); - chmod_(dst, 0555); - } else - #endif - doBind(i.second.source, chrootRootDir + i.first, i.second.optional); - } - - /* Bind a new instance of procfs on /proc. */ - createDirs(chrootRootDir + "/proc"); - if (mount("none", (chrootRootDir + "/proc").c_str(), "proc", 0, 0) == -1) - throw SysError("mounting /proc"); - - /* Mount sysfs on /sys. */ - if (buildUser && buildUser->getUIDCount() != 1) { - 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, - fmt("size=%s", settings.sandboxShmSize).c_str()) == -1) - throw SysError("mounting /dev/shm"); - - /* Mount a new devpts on /dev/pts. Note that this - requires the kernel to be compiled with - CONFIG_DEVPTS_MULTIPLE_INSTANCES=y (which is the case - if /dev/ptx/ptmx exists). */ - if (pathExists("/dev/pts/ptmx") && - !pathExists(chrootRootDir + "/dev/ptmx") - && !pathsInChroot.count("/dev/pts")) - { - if (mount("none", (chrootRootDir + "/dev/pts").c_str(), "devpts", 0, "newinstance,mode=0620") == 0) - { - createSymlink("/dev/pts/ptmx", chrootRootDir + "/dev/ptmx"); - - /* Make sure /dev/pts/ptmx is world-writable. With some - Linux versions, it is created with permissions 0. */ - chmod_(chrootRootDir + "/dev/pts/ptmx", 0666); - } else { - if (errno != EINVAL) - throw SysError("mounting /dev/pts"); - doBind("/dev/pts", chrootRootDir + "/dev/pts"); - doBind("/dev/ptmx", chrootRootDir + "/dev/ptmx"); - } - } - - /* Make /etc unwritable */ - if (!drvOptions->useUidRange(*drv)) - chmod_(chrootRootDir + "/etc", 0555); - - /* 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"); - - /* 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 (cgroup && 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); - - if (mkdir("real-root", 0500) == -1) - throw SysError("cannot create real-root directory"); - - if (pivot_root(".", "real-root") == -1) - throw SysError("cannot pivot old root directory onto '%1%'", (chrootRootDir + "/real-root")); - - if (chroot(".") == -1) - throw SysError("cannot change root directory to '%1%'", chrootRootDir); - - if (umount2("real-root", MNT_DETACH) == -1) - throw SysError("cannot unmount real root filesystem"); - - if (rmdir("real-root") == -1) - throw SysError("cannot remove real-root directory"); - - /* Switch to the sandbox uid/gid in the user namespace, - which corresponds to the build user or calling user in - the parent namespace. */ - if (setgid(sandboxGid()) == -1) - throw SysError("setgid failed"); - if (setuid(sandboxUid()) == -1) - throw SysError("setuid failed"); - - setUser = false; - } -#endif - - if (chdir(tmpDirInSandbox.c_str()) == -1) - throw SysError("changing into '%1%'", tmpDir); - - /* Close all other file descriptors. */ - unix::closeExtraFDs(); - -#ifdef __linux__ - linux::setPersonality(drv->platform); -#endif - - /* Disable core dumps by default. */ - struct rlimit limit = { 0, RLIM_INFINITY }; - setrlimit(RLIMIT_CORE, &limit); - - // FIXME: set other limits to deterministic values? - - /* Fill in the environment. */ - Strings envStrs; - for (auto & i : env) - envStrs.push_back(rewriteStrings(i.first + "=" + i.second, inputRewrites)); - - /* If we are running in `build-users' mode, then switch to the - user we allocated above. Make sure that we drop all root - privileges. Note that above we have closed all file - descriptors except std*, so that's safe. Also note that - setuid() when run as root sets the real, effective and - saved UIDs. */ - if (setUser && buildUser) { - /* Preserve supplementary groups of the build user, to allow - admins to specify groups such as "kvm". */ - auto gids = buildUser->getSupplementaryGIDs(); - if (setgroups(gids.size(), gids.data()) == -1) - throw SysError("cannot set supplementary groups of build user"); - - if (setgid(buildUser->getGID()) == -1 || - getgid() != buildUser->getGID() || - getegid() != buildUser->getGID()) - throw SysError("setgid failed"); - - if (setuid(buildUser->getUID()) == -1 || - getuid() != buildUser->getUID() || - geteuid() != buildUser->getUID()) - throw SysError("setuid failed"); - } - -#ifdef __APPLE__ - /* This has to appear before import statements. */ - std::string sandboxProfile = "(version 1)\n"; - - if (useChroot) { - - /* Lots and lots and lots of file functions freak out if they can't stat their full ancestry */ - PathSet ancestry; - - /* 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 - particularly efficient... I doubt it'll be a bottleneck in practice */ - for (auto & i : pathsInChroot) { - Path cur = i.first; - while (cur.compare("/") != 0) { - cur = dirOf(cur); - ancestry.insert(cur); - } - } - - /* 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 cur = store.storeDir; - while (cur.compare("/") != 0) { - ancestry.insert(cur); - cur = dirOf(cur); - } - - /* Add all our input paths to the chroot */ - for (auto & i : inputPaths) { - auto p = store.printStorePath(i); - pathsInChroot[p] = p; - } - - /* Violations will go to the syslog if you set this. Unfortunately the destination does not appear to be configurable */ - if (settings.darwinLogSandboxViolations) { - sandboxProfile += "(deny default)\n"; - } else { - sandboxProfile += "(deny default (with no-log))\n"; - } - - sandboxProfile += - #include "sandbox-defaults.sb" - ; - - if (!derivationType->isSandboxed()) - sandboxProfile += - #include "sandbox-network.sb" - ; - - /* Add the output paths we'll use at build-time to the chroot */ - sandboxProfile += "(allow file-read* file-write* process-exec\n"; - for (auto & [_, path] : scratchOutputs) - sandboxProfile += fmt("\t(subpath \"%s\")\n", store.printStorePath(path)); - - sandboxProfile += ")\n"; - - /* Our inputs (transitive dependencies and any impurities computed above) - - without file-write* allowed, access() incorrectly returns EPERM - */ - sandboxProfile += "(allow file-read* file-write* process-exec\n"; - - // We create multiple allow lists, to avoid exceeding a limit in the darwin sandbox interpreter. - // See https://github.com/NixOS/nix/issues/4119 - // We split our allow groups approximately at half the actual limit, 1 << 16 - const size_t breakpoint = sandboxProfile.length() + (1 << 14); - for (auto & i : pathsInChroot) { - - if (sandboxProfile.length() >= breakpoint) { - debug("Sandbox break: %d %d", sandboxProfile.length(), breakpoint); - sandboxProfile += ")\n(allow file-read* file-write* process-exec\n"; - } - - if (i.first != i.second.source) - throw Error( - "can't map '%1%' to '%2%': mismatched impure paths not supported on Darwin", - i.first, i.second.source); - - std::string path = i.first; - auto optSt = maybeLstat(path.c_str()); - if (!optSt) { - if (i.second.optional) - continue; - throw SysError("getting attributes of required path '%s", path); - } - if (S_ISDIR(optSt->st_mode)) - sandboxProfile += fmt("\t(subpath \"%s\")\n", path); - else - sandboxProfile += fmt("\t(literal \"%s\")\n", path); - } - sandboxProfile += ")\n"; - - /* Allow file-read* on full directory hierarchy to self. Allows realpath() */ - sandboxProfile += "(allow file-read*\n"; - for (auto & i : ancestry) { - sandboxProfile += fmt("\t(literal \"%s\")\n", i); - } - sandboxProfile += ")\n"; - - sandboxProfile += drvOptions->additionalSandboxProfile; - } else - sandboxProfile += - #include "sandbox-minimal.sb" - ; - - debug("Generated sandbox profile:"); - debug(sandboxProfile); - - /* The tmpDir in scope points at the temporary build directory for our derivation. Some packages try different mechanisms - to find temporary directories, so we want to open up a broader place for them to put their files, if needed. */ - Path globalTmpDir = canonPath(defaultTempDir(), true); - - /* They don't like trailing slashes on subpath directives */ - while (!globalTmpDir.empty() && globalTmpDir.back() == '/') - globalTmpDir.pop_back(); - - if (getEnv("_NIX_TEST_NO_SANDBOX") != "1") { - Strings sandboxArgs; - sandboxArgs.push_back("_GLOBAL_TMP_DIR"); - sandboxArgs.push_back(globalTmpDir); - if (drvOptions->allowLocalNetworking) { - sandboxArgs.push_back("_ALLOW_LOCAL_NETWORKING"); - sandboxArgs.push_back("1"); - } - char * sandbox_errbuf = nullptr; - if (sandbox_init_with_parameters(sandboxProfile.c_str(), 0, stringsToCharPtrs(sandboxArgs).data(), &sandbox_errbuf)) { - writeFull(STDERR_FILENO, fmt("failed to configure sandbox: %s\n", sandbox_errbuf ? sandbox_errbuf : "(null)")); - _exit(1); - } - } -#endif - - /* Indicate that we managed to set up the build environment. */ - writeFull(STDERR_FILENO, std::string("\2\n")); - - sendException = false; - - /* Execute the program. This should not return. */ - if (drv->isBuiltin()) { - try { - logger = makeJSONLogger(getStandardError()); - - std::map outputs; - for (auto & e : drv->outputs) - outputs.insert_or_assign(e.first, - store.printStorePath(scratchOutputs.at(e.first))); - - if (drv->builder == "builtin:fetchurl") - builtinFetchurl(*drv, outputs, netrcData, caFileData); - else if (drv->builder == "builtin:buildenv") - builtinBuildenv(*drv, outputs); - else if (drv->builder == "builtin:unpack-channel") - builtinUnpackChannel(*drv, outputs); - else - throw Error("unsupported builtin builder '%1%'", drv->builder.substr(8)); - _exit(0); - } catch (std::exception & e) { - writeFull(STDERR_FILENO, e.what() + std::string("\n")); - _exit(1); - } - } - - // Now builder is not builtin - - Strings args; - args.push_back(std::string(baseNameOf(drv->builder))); - - for (auto & i : drv->args) - args.push_back(rewriteStrings(i, inputRewrites)); - -#ifdef __APPLE__ - posix_spawnattr_t attrp; - - if (posix_spawnattr_init(&attrp)) - throw SysError("failed to initialize builder"); - - if (posix_spawnattr_setflags(&attrp, POSIX_SPAWN_SETEXEC)) - throw SysError("failed to initialize builder"); - - if (drv->platform == "aarch64-darwin") { - // Unset kern.curproc_arch_affinity so we can escape Rosetta - int affinity = 0; - sysctlbyname("kern.curproc_arch_affinity", NULL, NULL, &affinity, sizeof(affinity)); - - cpu_type_t cpu = CPU_TYPE_ARM64; - posix_spawnattr_setbinpref_np(&attrp, 1, &cpu, NULL); - } else if (drv->platform == "x86_64-darwin") { - cpu_type_t cpu = CPU_TYPE_X86_64; - posix_spawnattr_setbinpref_np(&attrp, 1, &cpu, NULL); - } - - posix_spawn(NULL, drv->builder.c_str(), NULL, &attrp, stringsToCharPtrs(args).data(), stringsToCharPtrs(envStrs).data()); -#else - execve(drv->builder.c_str(), stringsToCharPtrs(args).data(), stringsToCharPtrs(envStrs).data()); -#endif - - throw SysError("executing '%1%'", drv->builder); - - } catch (...) { - handleChildException(sendException); - _exit(1); - } -} - - -SingleDrvOutputs DerivationBuilder::registerOutputs() -{ - std::map infos; - - /* Set of inodes seen during calls to canonicalisePathMetaData() - for this build's outputs. This needs to be shared between - outputs to allow hard links between outputs. */ - InodesSeen inodesSeen; - - Path checkSuffix = ".check"; - - 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. */ - StorePathSet referenceablePaths; - for (auto & p : inputPaths) referenceablePaths.insert(p); - for (auto & i : scratchOutputs) referenceablePaths.insert(i.second); - for (auto & p : addedPaths) referenceablePaths.insert(p); - - /* FIXME `needsHashRewrite` should probably be removed and we get to the - real reason why we aren't using the chroot dir */ - auto toRealPathChroot = [&](const Path & p) -> Path { - return useChroot && !needsHashRewrite() - ? chrootRootDir + p - : store.toRealPath(p); - }; - - /* Check whether the output paths were created, and make all - output paths read-only. Then get the references of each output (that we - might need to register), so we can topologically sort them. For the ones - that are most definitely already installed, we just store their final - name so we can also use it in rewrites. */ - StringSet outputsToSort; - struct AlreadyRegistered { StorePath path; }; - struct PerhapsNeedToRegister { StorePathSet refs; }; - std::map> outputReferencesIfUnregistered; - std::map outputStats; - for (auto & [outputName, _] : drv->outputs) { - auto scratchOutput = get(scratchOutputs, outputName); - if (!scratchOutput) - throw BuildError( - "builder for '%s' has no scratch output for '%s'", - store.printStorePath(drvPath), outputName); - auto actualPath = toRealPathChroot(store.printStorePath(*scratchOutput)); - - outputsToSort.insert(outputName); - - /* Updated wanted info to remove the outputs we definitely don't need to register */ - auto initialOutput = get(initialOutputs, outputName); - if (!initialOutput) - throw BuildError( - "builder for '%s' has no initial output for '%s'", - store.printStorePath(drvPath), outputName); - auto & initialInfo = *initialOutput; - - /* Don't register if already valid, and not checking */ - initialInfo.wanted = buildMode == bmCheck - || !(initialInfo.known && initialInfo.known->isValid()); - if (!initialInfo.wanted) { - outputReferencesIfUnregistered.insert_or_assign( - outputName, - AlreadyRegistered { .path = initialInfo.known->path }); - continue; - } - - auto optSt = maybeLstat(actualPath.c_str()); - if (!optSt) - throw BuildError( - "builder for '%s' failed to produce output path for output '%s' at '%s'", - store.printStorePath(drvPath), outputName, actualPath); - struct stat & st = *optSt; - -#ifndef __CYGWIN__ - /* Check that the output is not group or world writable, as - that means that someone else can have interfered with the - build. Also, the output should be owned by the build - user. */ - if ((!S_ISLNK(st.st_mode) && (st.st_mode & (S_IWGRP | S_IWOTH))) || - (buildUser && st.st_uid != buildUser->getUID())) - throw BuildError( - "suspicious ownership or permission on '%s' for output '%s'; rejecting this build output", - actualPath, outputName); -#endif - - /* 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 ? std::optional(buildUser->getUIDRange()) : std::nullopt, - inodesSeen); - - bool discardReferences = false; - if (auto udr = get(drvOptions->unsafeDiscardReferences, outputName)) { - discardReferences = *udr; - } - - StorePathSet references; - if (discardReferences) - debug("discarding references of output '%s'", outputName); - else { - debug("scanning for references for output '%s' in temp location '%s'", outputName, actualPath); - - /* Pass blank Sink as we are not ready to hash data at this stage. */ - NullSink blank; - references = scanForReferences(blank, actualPath, referenceablePaths); - } - - outputReferencesIfUnregistered.insert_or_assign( - outputName, - PerhapsNeedToRegister { .refs = references }); - outputStats.insert_or_assign(outputName, std::move(st)); - } - - auto sortedOutputNames = topoSort(outputsToSort, - {[&](const std::string & name) { - auto orifu = get(outputReferencesIfUnregistered, name); - if (!orifu) - throw BuildError( - "no output reference for '%s' in build of '%s'", - name, store.printStorePath(drvPath)); - return std::visit(overloaded { - /* Since we'll use the already installed versions of these, we - can treat them as leaves and ignore any references they - have. */ - [&](const AlreadyRegistered &) { return StringSet {}; }, - [&](const PerhapsNeedToRegister & refs) { - StringSet referencedOutputs; - /* FIXME build inverted map up front so no quadratic waste here */ - for (auto & r : refs.refs) - for (auto & [o, p] : scratchOutputs) - if (r == p) - referencedOutputs.insert(o); - return referencedOutputs; - }, - }, *orifu); - }}, - {[&](const std::string & path, const std::string & parent) { - // TODO with more -vvvv also show the temporary paths for manual inspection. - return BuildError( - "cycle detected in build of '%s' in the references of output '%s' from output '%s'", - store.printStorePath(drvPath), path, parent); - }}); - - std::reverse(sortedOutputNames.begin(), sortedOutputNames.end()); - - OutputPathMap finalOutputs; - - for (auto & outputName : sortedOutputNames) { - auto output = get(drv->outputs, outputName); - auto scratchPath = get(scratchOutputs, outputName); - assert(output && scratchPath); - auto actualPath = toRealPathChroot(store.printStorePath(*scratchPath)); - - auto finish = [&](StorePath finalStorePath) { - /* Store the final path */ - finalOutputs.insert_or_assign(outputName, finalStorePath); - /* The rewrite rule will be used in downstream outputs that refer to - use. This is why the topological sort is essential to do first - before this for loop. */ - if (*scratchPath != finalStorePath) - outputRewrites[std::string { scratchPath->hashPart() }] = std::string { finalStorePath.hashPart() }; - }; - - auto orifu = get(outputReferencesIfUnregistered, outputName); - assert(orifu); - - std::optional referencesOpt = std::visit(overloaded { - [&](const AlreadyRegistered & skippedFinalPath) -> std::optional { - finish(skippedFinalPath.path); - return std::nullopt; - }, - [&](const PerhapsNeedToRegister & r) -> std::optional { - return r.refs; - }, - }, *orifu); - - if (!referencesOpt) - continue; - auto references = *referencesOpt; - - auto rewriteOutput = [&](const StringMap & rewrites) { - /* Apply hash rewriting if necessary. */ - if (!rewrites.empty()) { - debug("rewriting hashes in '%1%'; cross fingers", actualPath); - - /* FIXME: Is this actually streaming? */ - auto source = sinkToSource([&](Sink & nextSink) { - RewritingSink rsink(rewrites, nextSink); - dumpPath(actualPath, rsink); - rsink.flush(); - }); - Path tmpPath = actualPath + ".tmp"; - restorePath(tmpPath, *source); - deletePath(actualPath); - movePath(tmpPath, actualPath); - - /* FIXME: set proper permissions in restorePath() so - we don't have to do another traversal. */ - canonicalisePathMetaData(actualPath, {}, inodesSeen); - } - }; - - auto rewriteRefs = [&]() -> StoreReferences { - /* In the CA case, we need the rewritten refs to calculate the - final path, therefore we look for a *non-rewritten - self-reference, and use a bool rather try to solve the - computationally intractable fixed point. */ - StoreReferences res { - .self = false, - }; - for (auto & r : references) { - auto name = r.name(); - auto origHash = std::string { r.hashPart() }; - if (r == *scratchPath) { - res.self = true; - } else if (auto outputRewrite = get(outputRewrites, origHash)) { - std::string newRef = *outputRewrite; - newRef += '-'; - newRef += name; - res.others.insert(StorePath { newRef }); - } else { - res.others.insert(r); - } - } - return res; - }; - - auto newInfoFromCA = [&](const DerivationOutput::CAFloating outputHash) -> ValidPathInfo { - auto st = get(outputStats, outputName); - if (!st) - throw BuildError( - "output path %1% without valid stats info", - actualPath); - if (outputHash.method.getFileIngestionMethod() == FileIngestionMethod::Flat) - { - /* The output path should be a regular file without execute permission. */ - if (!S_ISREG(st->st_mode) || (st->st_mode & S_IXUSR) != 0) - throw BuildError( - "output path '%1%' should be a non-executable regular file " - "since recursive hashing is not enabled (one of outputHashMode={flat,text} is true)", - actualPath); - } - rewriteOutput(outputRewrites); - /* FIXME optimize and deduplicate with addToStore */ - std::string oldHashPart { scratchPath->hashPart() }; - auto got = [&]{ - auto fim = outputHash.method.getFileIngestionMethod(); - switch (fim) { - case FileIngestionMethod::Flat: - case FileIngestionMethod::NixArchive: - { - HashModuloSink caSink { outputHash.hashAlgo, oldHashPart }; - auto fim = outputHash.method.getFileIngestionMethod(); - dumpPath( - {getFSSourceAccessor(), CanonPath(actualPath)}, - caSink, - (FileSerialisationMethod) fim); - return caSink.finish().first; - } - case FileIngestionMethod::Git: { - return git::dumpHash( - outputHash.hashAlgo, - {getFSSourceAccessor(), CanonPath(actualPath)}).hash; - } - } - assert(false); - }(); - - ValidPathInfo newInfo0 { - store, - outputPathName(drv->name, outputName), - ContentAddressWithReferences::fromParts( - outputHash.method, - std::move(got), - rewriteRefs()), - Hash::dummy, - }; - if (*scratchPath != newInfo0.path) { - // If the path has some self-references, we need to rewrite - // them. - // (note that this doesn't invalidate the ca hash we calculated - // above because it's computed *modulo the self-references*, so - // it already takes this rewrite into account). - rewriteOutput( - StringMap{{oldHashPart, - std::string(newInfo0.path.hashPart())}}); - } - - { - HashResult narHashAndSize = hashPath( - {getFSSourceAccessor(), CanonPath(actualPath)}, - FileSerialisationMethod::NixArchive, HashAlgorithm::SHA256); - newInfo0.narHash = narHashAndSize.first; - newInfo0.narSize = narHashAndSize.second; - } - - assert(newInfo0.ca); - return newInfo0; - }; - - ValidPathInfo newInfo = std::visit(overloaded { - - [&](const DerivationOutput::InputAddressed & output) { - /* input-addressed case */ - auto requiredFinalPath = output.path; - /* Preemptively add rewrite rule for final hash, as that is - what the NAR hash will use rather than normalized-self references */ - if (*scratchPath != requiredFinalPath) - outputRewrites.insert_or_assign( - std::string { scratchPath->hashPart() }, - std::string { requiredFinalPath.hashPart() }); - rewriteOutput(outputRewrites); - HashResult narHashAndSize = hashPath( - {getFSSourceAccessor(), CanonPath(actualPath)}, - FileSerialisationMethod::NixArchive, HashAlgorithm::SHA256); - ValidPathInfo newInfo0 { requiredFinalPath, narHashAndSize.first }; - newInfo0.narSize = narHashAndSize.second; - auto refs = rewriteRefs(); - newInfo0.references = std::move(refs.others); - if (refs.self) - newInfo0.references.insert(newInfo0.path); - return newInfo0; - }, - - [&](const DerivationOutput::CAFixed & dof) { - auto & wanted = dof.ca.hash; - - // Replace the output by a fresh copy of itself to make sure - // that there's no stale file descriptor pointing to it - Path tmpOutput = actualPath + ".tmp"; - copyFile( - std::filesystem::path(actualPath), - std::filesystem::path(tmpOutput), true); - - std::filesystem::rename(tmpOutput, actualPath); - - auto newInfo0 = newInfoFromCA(DerivationOutput::CAFloating { - .method = dof.ca.method, - .hashAlgo = wanted.algo, - }); - - /* Check wanted hash */ - assert(newInfo0.ca); - auto & got = newInfo0.ca->hash; - if (wanted != got) { - /* Throw an error after registering the path as - valid. */ - miscMethods.noteHashMismatch(); - delayedException = std::make_exception_ptr( - BuildError("hash mismatch in fixed-output derivation '%s':\n specified: %s\n got: %s", - store.printStorePath(drvPath), - wanted.to_string(HashFormat::SRI, true), - got.to_string(HashFormat::SRI, true))); - } - if (!newInfo0.references.empty()) { - auto numViolations = newInfo.references.size(); - delayedException = std::make_exception_ptr( - BuildError("fixed-output derivations must not reference store paths: '%s' references %d distinct paths, e.g. '%s'", - store.printStorePath(drvPath), - numViolations, - store.printStorePath(*newInfo.references.begin()))); - } - - return newInfo0; - }, - - [&](const DerivationOutput::CAFloating & dof) { - return newInfoFromCA(dof); - }, - - [&](const DerivationOutput::Deferred &) -> ValidPathInfo { - // No derivation should reach that point without having been - // rewritten first - assert(false); - }, - - [&](const DerivationOutput::Impure & doi) { - return newInfoFromCA(DerivationOutput::CAFloating { - .method = doi.method, - .hashAlgo = doi.hashAlgo, - }); - }, - - }, output->raw); - - /* FIXME: set proper permissions in restorePath() so - we don't have to do another traversal. */ - 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 - their usual "final destination" */ - auto finalDestPath = store.printStorePath(newInfo.path); - - /* Lock final output path, if not already locked. This happens with - floating CA derivations and hash-mismatching fixed-output - derivations. */ - PathLocks dynamicOutputLock; - dynamicOutputLock.setDeletion(true); - auto optFixedPath = output->path(store, drv->name, outputName); - if (!optFixedPath || - store.printStorePath(*optFixedPath) != finalDestPath) - { - assert(newInfo.ca); - dynamicOutputLock.lockPaths({store.toRealPath(finalDestPath)}); - } - - /* Move files, if needed */ - if (store.toRealPath(finalDestPath) != actualPath) { - if (buildMode == bmRepair) { - /* Path already exists, need to replace it */ - replaceValidPath(store.toRealPath(finalDestPath), actualPath); - actualPath = store.toRealPath(finalDestPath); - } else if (buildMode == bmCheck) { - /* Path already exists, and we want to compare, so we leave out - new path in place. */ - } else if (store.isValidPath(newInfo.path)) { - /* Path already exists because CA path produced by something - else. No moving needed. */ - assert(newInfo.ca); - } else { - auto destPath = store.toRealPath(finalDestPath); - deletePath(destPath); - movePath(actualPath, destPath); - actualPath = destPath; - } - } - - auto & localStore = getLocalStore(); - - if (buildMode == bmCheck) { - - if (!store.isValidPath(newInfo.path)) continue; - ValidPathInfo oldInfo(*store.queryPathInfo(newInfo.path)); - if (newInfo.narHash != oldInfo.narHash) { - miscMethods.noteCheckMismatch(); - if (settings.runDiffHook || settings.keepFailed) { - auto dst = store.toRealPath(finalDestPath + checkSuffix); - deletePath(dst); - movePath(actualPath, dst); - - handleDiffHook( - buildUser ? buildUser->getUID() : getuid(), - buildUser ? buildUser->getGID() : getgid(), - finalDestPath, dst, store.printStorePath(drvPath), tmpDir); - - throw NotDeterministic("derivation '%s' may not be deterministic: output '%s' differs from '%s'", - store.printStorePath(drvPath), store.toRealPath(finalDestPath), dst); - } else - throw NotDeterministic("derivation '%s' may not be deterministic: output '%s' differs", - store.printStorePath(drvPath), store.toRealPath(finalDestPath)); - } - - /* Since we verified the build, it's now ultimately trusted. */ - if (!oldInfo.ultimate) { - oldInfo.ultimate = true; - localStore.signPathInfo(oldInfo); - localStore.registerValidPaths({{oldInfo.path, oldInfo}}); - } - - continue; - } - - /* For debugging, print out the referenced and unreferenced paths. */ - for (auto & i : inputPaths) { - if (references.count(i)) - debug("referenced input: '%1%'", store.printStorePath(i)); - else - debug("unreferenced input: '%1%'", store.printStorePath(i)); - } - - localStore.optimisePath(actualPath, NoRepair); // FIXME: combine with scanForReferences() - miscMethods.markContentsGood(newInfo.path); - - newInfo.deriver = drvPath; - newInfo.ultimate = true; - localStore.signPathInfo(newInfo); - - finish(newInfo.path); - - /* If it's a CA path, register it right away. This is necessary if it - isn't statically known so that we can safely unlock the path before - the next iteration */ - if (newInfo.ca) - localStore.registerValidPaths({{newInfo.path, newInfo}}); - - infos.emplace(outputName, std::move(newInfo)); - } - - if (buildMode == bmCheck) { - /* In case of fixed-output derivations, if there are - mismatches on `--check` an error must be thrown as this is - also a source for non-determinism. */ - if (delayedException) - std::rethrow_exception(delayedException); - return miscMethods.assertPathValidity(); - } - - /* Apply output checks. */ - checkOutputs(infos); - - /* Register each output path as valid, and register the sets of - paths referenced by each of them. If there are cycles in the - outputs, this will fail. */ - { - auto & localStore = getLocalStore(); - - ValidPathInfos infos2; - for (auto & [outputName, newInfo] : infos) { - infos2.insert_or_assign(newInfo.path, newInfo); - } - localStore.registerValidPaths(infos2); - } - - /* In case of a fixed-output derivation hash mismatch, throw an - exception now that we have registered the output as valid. */ - if (delayedException) - std::rethrow_exception(delayedException); - - /* If we made it this far, we are sure the output matches the derivation - (since the delayedException would be a fixed output CA mismatch). That - means it's safe to link the derivation to the output hash. We must do - that for floating CA derivations, which otherwise couldn't be cached, - but it's fine to do in all cases. */ - SingleDrvOutputs builtOutputs; - - for (auto & [outputName, newInfo] : infos) { - auto oldinfo = get(initialOutputs, outputName); - assert(oldinfo); - auto thisRealisation = Realisation { - .id = DrvOutput { - oldinfo->outputHash, - outputName - }, - .outPath = newInfo.path - }; - if (experimentalFeatureSettings.isEnabled(Xp::CaDerivations) - && !drv->type().isImpure()) - { - store.signRealisation(thisRealisation); - store.registerDrvOutput(thisRealisation); - } - builtOutputs.emplace(outputName, thisRealisation); - } - - return builtOutputs; -} - - -void DerivationBuilder::checkOutputs(const std::map & outputs) -{ - std::map outputsByPath; - for (auto & output : outputs) - outputsByPath.emplace(store.printStorePath(output.second.path), output.second); - - for (auto & output : outputs) { - auto & outputName = output.first; - auto & info = output.second; - - /* Compute the closure and closure size of some output. This - is slightly tricky because some of its references (namely - other outputs) may not be valid yet. */ - auto getClosure = [&](const StorePath & path) - { - uint64_t closureSize = 0; - StorePathSet pathsDone; - std::queue pathsLeft; - pathsLeft.push(path); - - while (!pathsLeft.empty()) { - auto path = pathsLeft.front(); - pathsLeft.pop(); - if (!pathsDone.insert(path).second) continue; - - auto i = outputsByPath.find(store.printStorePath(path)); - if (i != outputsByPath.end()) { - closureSize += i->second.narSize; - for (auto & ref : i->second.references) - pathsLeft.push(ref); - } else { - auto info = store.queryPathInfo(path); - closureSize += info->narSize; - for (auto & ref : info->references) - pathsLeft.push(ref); - } - } - - return std::make_pair(std::move(pathsDone), closureSize); - }; - - auto applyChecks = [&](const DerivationOptions::OutputChecks & checks) - { - if (checks.maxSize && info.narSize > *checks.maxSize) - throw BuildError("path '%s' is too large at %d bytes; limit is %d bytes", - store.printStorePath(info.path), info.narSize, *checks.maxSize); - - if (checks.maxClosureSize) { - uint64_t closureSize = getClosure(info.path).second; - if (closureSize > *checks.maxClosureSize) - throw BuildError("closure of path '%s' is too large at %d bytes; limit is %d bytes", - store.printStorePath(info.path), closureSize, *checks.maxClosureSize); - } - - auto checkRefs = [&](const StringSet & value, bool allowed, bool recursive) - { - /* Parse a list of reference specifiers. Each element must - either be a store path, or the symbolic name of the output - of the derivation (such as `out'). */ - StorePathSet spec; - for (auto & i : value) { - if (store.isStorePath(i)) - spec.insert(store.parseStorePath(i)); - else if (auto output = get(outputs, i)) - spec.insert(output->path); - else { - std::string outputsListing = concatMapStringsSep(", ", outputs, [](auto & o) { return o.first; }); - throw BuildError("derivation '%s' output check for '%s' contains an illegal reference specifier '%s'," - " expected store path or output name (one of [%s])", - store.printStorePath(drvPath), outputName, i, outputsListing); - } - } - - auto used = recursive - ? getClosure(info.path).first - : info.references; - - if (recursive && checks.ignoreSelfRefs) - used.erase(info.path); - - StorePathSet badPaths; - - for (auto & i : used) - if (allowed) { - if (!spec.count(i)) - badPaths.insert(i); - } else { - if (spec.count(i)) - badPaths.insert(i); - } - - if (!badPaths.empty()) { - std::string badPathsStr; - for (auto & i : badPaths) { - badPathsStr += "\n "; - badPathsStr += store.printStorePath(i); - } - throw BuildError("output '%s' is not allowed to refer to the following paths:%s", - store.printStorePath(info.path), badPathsStr); - } - }; - - /* Mandatory check: absent whitelist, and present but empty - whitelist mean very different things. */ - if (auto & refs = checks.allowedReferences) { - checkRefs(*refs, true, false); - } - if (auto & refs = checks.allowedRequisites) { - checkRefs(*refs, true, true); - } - - /* Optimization: don't need to do anything when - disallowed and empty set. */ - if (!checks.disallowedReferences.empty()) { - checkRefs(checks.disallowedReferences, false, false); - } - if (!checks.disallowedRequisites.empty()) { - checkRefs(checks.disallowedRequisites, false, true); - } - }; - - std::visit(overloaded{ - [&](const DerivationOptions::OutputChecks & checks) { - applyChecks(checks); - }, - [&](const std::map & checksPerOutput) { - if (auto outputChecks = get(checksPerOutput, outputName)) - - applyChecks(*outputChecks); - }, - }, drvOptions->outputChecks); - } -} - - -void DerivationBuilder::deleteTmpDir(bool force) -{ - if (topTmpDir != "") { - /* Don't keep temporary directories for builtins because they - might have privileged stuff (like a copy of netrc). */ - if (settings.keepFailed && !force && !drv->isBuiltin()) { - printError("note: keeping build directory '%s'", tmpDir); - chmod(topTmpDir.c_str(), 0755); - chmod(tmpDir.c_str(), 0755); - } - else - deletePath(topTmpDir); - topTmpDir = ""; - tmpDir = ""; - } -} - - -bool LocalDerivationGoal::isReadDesc(int fd) -{ - return (hook && DerivationGoal::isReadDesc(fd)) || - (!hook && fd == builder.builderOut.get()); -} - - -StorePath DerivationBuilder::makeFallbackPath(OutputNameView outputName) -{ - // This is a bogus path type, constructed this way to ensure that it doesn't collide with any other store path - // See doc/manual/source/protocols/store-path.md for details - // TODO: We may want to separate the responsibilities of constructing the path fingerprint and of actually doing the hashing - auto pathType = "rewrite:" + std::string(drvPath.to_string()) + ":name:" + std::string(outputName); - return store.makeStorePath( - pathType, - // pass an all-zeroes hash - Hash(HashAlgorithm::SHA256), outputPathName(drv->name, outputName)); -} - - -StorePath DerivationBuilder::makeFallbackPath(const StorePath & path) -{ - // This is a bogus path type, constructed this way to ensure that it doesn't collide with any other store path - // See doc/manual/source/protocols/store-path.md for details - auto pathType = "rewrite:" + std::string(drvPath.to_string()) + ":" + std::string(path.to_string()); - return store.makeStorePath( - pathType, - // pass an all-zeroes hash - Hash(HashAlgorithm::SHA256), path.name()); -} - +std::unique_ptr makeDerivationBuilder( + Store & store, + DerivationBuilderCallbacks & miscMethods, + DerivationBuilderParams params); } diff --git a/src/libstore/unix/include/nix/store/meson.build b/src/libstore/unix/include/nix/store/meson.build index 9f12440cd..03101f4bb 100644 --- a/src/libstore/unix/include/nix/store/meson.build +++ b/src/libstore/unix/include/nix/store/meson.build @@ -2,6 +2,7 @@ include_dirs += include_directories('../..') headers += files( 'build/child.hh', + 'build/derivation-builder.hh', 'build/hook-instance.hh', 'build/local-derivation-goal.hh', 'user-lock.hh', diff --git a/src/libstore/unix/meson.build b/src/libstore/unix/meson.build index f06c9aa95..08d38742b 100644 --- a/src/libstore/unix/meson.build +++ b/src/libstore/unix/meson.build @@ -1,5 +1,6 @@ sources += files( 'build/child.cc', + 'build/derivation-builder.cc', 'build/hook-instance.cc', 'build/local-derivation-goal.cc', 'pathlocks.cc',