diff --git a/src/libstore/meson.build b/src/libstore/meson.build index 496c5b10d..7b38bafe0 100644 --- a/src/libstore/meson.build +++ b/src/libstore/meson.build @@ -234,6 +234,7 @@ sources = files( 'realisation.cc', 'remote-fs-accessor.cc', 'remote-store.cc', + 'restricted-store.cc', 's3-binary-cache-store.cc', 'serve-protocol-connection.cc', 'serve-protocol.cc', @@ -305,6 +306,7 @@ headers = [config_h] + files( 'remote-fs-accessor.hh', 'remote-store-connection.hh', 'remote-store.hh', + 'restricted-store.hh', 's3-binary-cache-store.hh', 's3.hh', 'ssh-store.hh', diff --git a/src/libstore/restricted-store.cc b/src/libstore/restricted-store.cc new file mode 100644 index 000000000..42e81765d --- /dev/null +++ b/src/libstore/restricted-store.cc @@ -0,0 +1,341 @@ +#include "restricted-store.hh" +#include "build-result.hh" +#include "callback.hh" +#include "realisation.hh" + +namespace nix { + +static StorePath pathPartOfReq(const SingleDerivedPath & req) +{ + return std::visit( + overloaded{ + [&](const SingleDerivedPath::Opaque & bo) { return bo.path; }, + [&](const SingleDerivedPath::Built & bfd) { return pathPartOfReq(*bfd.drvPath); }, + }, + req.raw()); +} + +static StorePath pathPartOfReq(const DerivedPath & req) +{ + return std::visit( + overloaded{ + [&](const DerivedPath::Opaque & bo) { return bo.path; }, + [&](const DerivedPath::Built & bfd) { return pathPartOfReq(*bfd.drvPath); }, + }, + req.raw()); +} + +bool RestrictionContext::isAllowed(const DerivedPath & req) +{ + return isAllowed(pathPartOfReq(req)); +} + +struct RestrictedStoreConfig : virtual LocalFSStoreConfig +{ + using LocalFSStoreConfig::LocalFSStoreConfig; + const std::string name() override + { + return "Restricted Store"; + } +}; + +/** + * A wrapper around LocalStore that only allows building/querying of + * paths that are in the input closures of the build or were added via + * recursive Nix calls. + */ +struct RestrictedStore : public virtual RestrictedStoreConfig, public virtual IndirectRootStore, public virtual GcStore +{ + ref next; + + RestrictionContext & goal; + + RestrictedStore(const Params & params, ref next, RestrictionContext & goal) + : StoreConfig(params) + , LocalFSStoreConfig(params) + , RestrictedStoreConfig(params) + , Store(params) + , LocalFSStore(params) + , next(next) + , goal(goal) + { + } + + Path getRealStoreDir() override + { + return next->realStoreDir; + } + + std::string getUri() override + { + return next->getUri(); + } + + StorePathSet queryAllValidPaths() override; + + void queryPathInfoUncached( + const StorePath & path, Callback> callback) noexcept override; + + void queryReferrers(const StorePath & path, StorePathSet & referrers) override; + + std::map> + queryPartialDerivationOutputMap(const StorePath & path, Store * evalStore = nullptr) override; + + std::optional queryPathFromHashPart(const std::string & hashPart) override + { + throw Error("queryPathFromHashPart"); + } + + StorePath addToStore( + std::string_view name, + const SourcePath & srcPath, + ContentAddressMethod method, + HashAlgorithm hashAlgo, + const StorePathSet & references, + PathFilter & filter, + RepairFlag repair) override + { + throw Error("addToStore"); + } + + void addToStore( + const ValidPathInfo & info, + Source & narSource, + RepairFlag repair = NoRepair, + CheckSigsFlag checkSigs = CheckSigs) override; + + StorePath addToStoreFromDump( + Source & dump, + std::string_view name, + FileSerialisationMethod dumpMethod, + ContentAddressMethod hashMethod, + HashAlgorithm hashAlgo, + const StorePathSet & references, + RepairFlag repair) override; + + void narFromPath(const StorePath & path, Sink & sink) override; + + void ensurePath(const StorePath & path) override; + + void registerDrvOutput(const Realisation & info) override; + + void queryRealisationUncached( + const DrvOutput & id, Callback> callback) noexcept override; + + void + buildPaths(const std::vector & paths, BuildMode buildMode, std::shared_ptr evalStore) override; + + std::vector buildPathsWithResults( + const std::vector & paths, + BuildMode buildMode = bmNormal, + std::shared_ptr evalStore = nullptr) override; + + BuildResult + buildDerivation(const StorePath & drvPath, const BasicDerivation & drv, BuildMode buildMode = bmNormal) override + { + unsupported("buildDerivation"); + } + + void addTempRoot(const StorePath & path) override {} + + void addIndirectRoot(const Path & path) override {} + + Roots findRoots(bool censor) override + { + return Roots(); + } + + void collectGarbage(const GCOptions & options, GCResults & results) override {} + + void addSignatures(const StorePath & storePath, const StringSet & sigs) override + { + unsupported("addSignatures"); + } + + void queryMissing( + const std::vector & targets, + StorePathSet & willBuild, + StorePathSet & willSubstitute, + StorePathSet & unknown, + uint64_t & downloadSize, + uint64_t & narSize) override; + + virtual std::optional getBuildLogExact(const StorePath & path) override + { + return std::nullopt; + } + + virtual void addBuildLog(const StorePath & path, std::string_view log) override + { + unsupported("addBuildLog"); + } + + std::optional isTrustedClient() override + { + return NotTrusted; + } +}; + +ref makeRestrictedStore(const Store::Params & params, ref next, RestrictionContext & context) +{ + return make_ref(params, next, context); +} + +StorePathSet RestrictedStore::queryAllValidPaths() +{ + StorePathSet paths; + for (auto & p : goal.originalPaths()) + paths.insert(p); + for (auto & p : goal.addedPaths) + paths.insert(p); + return paths; +} + +void RestrictedStore::queryPathInfoUncached( + const StorePath & path, Callback> callback) noexcept +{ + if (goal.isAllowed(path)) { + try { + /* Censor impure information. */ + auto info = std::make_shared(*next->queryPathInfo(path)); + info->deriver.reset(); + info->registrationTime = 0; + info->ultimate = false; + info->sigs.clear(); + callback(info); + } catch (InvalidPath &) { + callback(nullptr); + } + } else + callback(nullptr); +}; + +void RestrictedStore::queryReferrers(const StorePath & path, StorePathSet & referrers) {} + +std::map> +RestrictedStore::queryPartialDerivationOutputMap(const StorePath & path, Store * evalStore) +{ + if (!goal.isAllowed(path)) + throw InvalidPath("cannot query output map for unknown path '%s' in recursive Nix", printStorePath(path)); + return next->queryPartialDerivationOutputMap(path, evalStore); +} + +void RestrictedStore::addToStore( + const ValidPathInfo & info, Source & narSource, RepairFlag repair, CheckSigsFlag checkSigs) +{ + next->addToStore(info, narSource, repair, checkSigs); + goal.addDependency(info.path); +} + +StorePath RestrictedStore::addToStoreFromDump( + Source & dump, + std::string_view name, + FileSerialisationMethod dumpMethod, + ContentAddressMethod hashMethod, + HashAlgorithm hashAlgo, + const StorePathSet & references, + RepairFlag repair) +{ + auto path = next->addToStoreFromDump(dump, name, dumpMethod, hashMethod, hashAlgo, references, repair); + goal.addDependency(path); + return path; +} + +void RestrictedStore::narFromPath(const StorePath & path, Sink & sink) +{ + if (!goal.isAllowed(path)) + throw InvalidPath("cannot dump unknown path '%s' in recursive Nix", printStorePath(path)); + LocalFSStore::narFromPath(path, sink); +} + +void RestrictedStore::ensurePath(const StorePath & path) +{ + if (!goal.isAllowed(path)) + throw InvalidPath("cannot substitute unknown path '%s' in recursive Nix", printStorePath(path)); + /* Nothing to be done; 'path' must already be valid. */ +} + +void RestrictedStore::registerDrvOutput(const Realisation & info) +// XXX: This should probably be allowed as a no-op if the realisation +// corresponds to an allowed derivation +{ + throw Error("registerDrvOutput"); +} + +void RestrictedStore::queryRealisationUncached( + const DrvOutput & id, Callback> callback) noexcept +// XXX: This should probably be allowed if the realisation corresponds to +// an allowed derivation +{ + if (!goal.isAllowed(id)) + callback(nullptr); + next->queryRealisation(id, std::move(callback)); +} + +void RestrictedStore::buildPaths( + const std::vector & paths, BuildMode buildMode, std::shared_ptr evalStore) +{ + for (auto & result : buildPathsWithResults(paths, buildMode, evalStore)) + if (!result.success()) + result.rethrow(); +} + +std::vector RestrictedStore::buildPathsWithResults( + const std::vector & paths, BuildMode buildMode, std::shared_ptr evalStore) +{ + assert(!evalStore); + + if (buildMode != bmNormal) + throw Error("unsupported build mode"); + + StorePathSet newPaths; + std::set newRealisations; + + for (auto & req : paths) { + if (!goal.isAllowed(req)) + throw InvalidPath("cannot build '%s' in recursive Nix because path is unknown", req.to_string(*next)); + } + + auto results = next->buildPathsWithResults(paths, buildMode); + + for (auto & result : results) { + for (auto & [outputName, output] : result.builtOutputs) { + newPaths.insert(output.outPath); + newRealisations.insert(output); + } + } + + StorePathSet closure; + next->computeFSClosure(newPaths, closure); + for (auto & path : closure) + goal.addDependency(path); + for (auto & real : Realisation::closure(*next, newRealisations)) + goal.addedDrvOutputs.insert(real.id); + + return results; +} + +void RestrictedStore::queryMissing( + const std::vector & targets, + StorePathSet & willBuild, + StorePathSet & willSubstitute, + StorePathSet & unknown, + uint64_t & downloadSize, + uint64_t & narSize) +{ + /* This is slightly impure since it leaks information to the + client about what paths will be built/substituted or are + already present. Probably not a big deal. */ + + std::vector allowed; + for (auto & req : targets) { + if (goal.isAllowed(req)) + allowed.emplace_back(req); + else + unknown.insert(pathPartOfReq(req)); + } + + next->queryMissing(allowed, willBuild, willSubstitute, unknown, downloadSize, narSize); +} + +} diff --git a/src/libstore/restricted-store.hh b/src/libstore/restricted-store.hh new file mode 100644 index 000000000..84b455456 --- /dev/null +++ b/src/libstore/restricted-store.hh @@ -0,0 +1,60 @@ +#pragma once +///@file + +#include "local-store.hh" + +namespace nix { + +/** + * A restricted store has a pointer to one of these, which manages the + * restrictions that are in place. + * + * This is a separate data type so the whitelists can be mutated before + * the restricted store is created: put differently, someones we don't + * know whether we will in fact create a restricted store, but we need + * to prepare the whitelists just in case. + * + * It is possible there are other ways to solve this problem. This was + * just the easiest place to begin, when this was extracted from + * `LocalDerivationGoal`. + */ +struct RestrictionContext +{ + /** + * Paths that are already allowed to begin with + */ + virtual const StorePathSet & originalPaths() = 0; + + /** + * Paths that were added via recursive Nix calls. + */ + StorePathSet addedPaths; + + /** + * Realisations that were added via recursive Nix calls. + */ + std::set addedDrvOutputs; + + /** + * Recursive Nix calls are only allowed to build or realize paths + * in the original input closure or added via a recursive Nix call + * (so e.g. you can't do 'nix-store -r /nix/store/' where + * /nix/store/ is some arbitrary path in a binary cache). + */ + virtual bool isAllowed(const StorePath &) = 0; + virtual bool isAllowed(const DrvOutput & id) = 0; + bool isAllowed(const DerivedPath & id); + + /** + * Add 'path' to the set of paths that may be referenced by the + * outputs, and make it appear in the sandbox. + */ + virtual void addDependency(const StorePath & path) = 0; +}; + +/** + * Create a shared pointer to a restricted store. + */ +ref makeRestrictedStore(const Store::Params & params, ref next, RestrictionContext & context); + +} diff --git a/src/libstore/unix/build/local-derivation-goal.cc b/src/libstore/unix/build/local-derivation-goal.cc index fac83b0c1..371d94b95 100644 --- a/src/libstore/unix/build/local-derivation-goal.cc +++ b/src/libstore/unix/build/local-derivation-goal.cc @@ -21,6 +21,7 @@ #include "unix-domain-socket.hh" #include "posix-fs-canonicalise.hh" #include "posix-source-accessor.hh" +#include "restricted-store.hh" #include #include @@ -75,7 +76,7 @@ extern "C" int sandbox_init_with_parameters(const char *profile, uint64_t flags, namespace nix { -struct LocalDerivationGoal : public DerivationGoal +struct LocalDerivationGoal : DerivationGoal, RestrictionContext { LocalStore & getLocalStore(); @@ -208,27 +209,16 @@ struct LocalDerivationGoal : public DerivationGoal */ std::vector daemonWorkerThreads; - /** - * Paths that were added via recursive Nix calls. - */ - StorePathSet addedPaths; + const StorePathSet & originalPaths() override + { + return inputPaths; + } - /** - * Realisations that were added via recursive Nix calls. - */ - std::set addedDrvOutputs; - - /** - * Recursive Nix calls are only allowed to build or realize paths - * in the original input closure or added via a recursive Nix call - * (so e.g. you can't do 'nix-store -r /nix/store/' where - * /nix/store/ is some arbitrary path in a binary cache). - */ - bool isAllowed(const StorePath & path) + bool isAllowed(const StorePath & path) override { return inputPaths.count(path) || addedPaths.count(path); } - bool isAllowed(const DrvOutput & id) + bool isAllowed(const DrvOutput & id) override { return addedDrvOutputs.count(id); } @@ -287,11 +277,7 @@ struct LocalDerivationGoal : public DerivationGoal */ void stopDaemon(); - /** - * Add 'path' to the set of paths that may be referenced by the - * outputs, and make it appear in the sandbox. - */ - void addDependency(const StorePath & path); + void addDependency(const StorePath & path) override; /** * Make a file owned by the builder. @@ -1618,263 +1604,6 @@ void LocalDerivationGoal::writeStructuredAttrs() } -static StorePath pathPartOfReq(const SingleDerivedPath & req) -{ - return std::visit(overloaded { - [&](const SingleDerivedPath::Opaque & bo) { - return bo.path; - }, - [&](const SingleDerivedPath::Built & bfd) { - return pathPartOfReq(*bfd.drvPath); - }, - }, req.raw()); -} - - -static StorePath pathPartOfReq(const DerivedPath & req) -{ - return std::visit(overloaded { - [&](const DerivedPath::Opaque & bo) { - return bo.path; - }, - [&](const DerivedPath::Built & bfd) { - return pathPartOfReq(*bfd.drvPath); - }, - }, req.raw()); -} - - -bool LocalDerivationGoal::isAllowed(const DerivedPath & req) -{ - return this->isAllowed(pathPartOfReq(req)); -} - - -struct RestrictedStoreConfig : virtual LocalFSStoreConfig -{ - using LocalFSStoreConfig::LocalFSStoreConfig; - const std::string name() override { return "Restricted Store"; } -}; - -/* A wrapper around LocalStore that only allows building/querying of - paths that are in the input closures of the build or were added via - recursive Nix calls. */ -struct RestrictedStore : public virtual RestrictedStoreConfig, public virtual IndirectRootStore, public virtual GcStore -{ - ref next; - - LocalDerivationGoal & goal; - - RestrictedStore(const Params & params, ref next, LocalDerivationGoal & goal) - : StoreConfig(params) - , LocalFSStoreConfig(params) - , RestrictedStoreConfig(params) - , Store(params) - , LocalFSStore(params) - , next(next), goal(goal) - { } - - Path getRealStoreDir() override - { return next->realStoreDir; } - - std::string getUri() override - { return next->getUri(); } - - StorePathSet queryAllValidPaths() override - { - StorePathSet paths; - for (auto & p : goal.inputPaths) paths.insert(p); - for (auto & p : goal.addedPaths) paths.insert(p); - return paths; - } - - void queryPathInfoUncached(const StorePath & path, - Callback> callback) noexcept override - { - if (goal.isAllowed(path)) { - try { - /* Censor impure information. */ - auto info = std::make_shared(*next->queryPathInfo(path)); - info->deriver.reset(); - info->registrationTime = 0; - info->ultimate = false; - info->sigs.clear(); - callback(info); - } catch (InvalidPath &) { - callback(nullptr); - } - } else - callback(nullptr); - }; - - void queryReferrers(const StorePath & path, StorePathSet & referrers) override - { } - - std::map> queryPartialDerivationOutputMap( - const StorePath & path, - Store * evalStore = nullptr) override - { - if (!goal.isAllowed(path)) - throw InvalidPath("cannot query output map for unknown path '%s' in recursive Nix", printStorePath(path)); - return next->queryPartialDerivationOutputMap(path, evalStore); - } - - std::optional queryPathFromHashPart(const std::string & hashPart) override - { throw Error("queryPathFromHashPart"); } - - StorePath addToStore( - std::string_view name, - const SourcePath & srcPath, - ContentAddressMethod method, - HashAlgorithm hashAlgo, - const StorePathSet & references, - PathFilter & filter, - RepairFlag repair) override - { throw Error("addToStore"); } - - void addToStore(const ValidPathInfo & info, Source & narSource, - RepairFlag repair = NoRepair, CheckSigsFlag checkSigs = CheckSigs) override - { - next->addToStore(info, narSource, repair, checkSigs); - goal.addDependency(info.path); - } - - StorePath addToStoreFromDump( - Source & dump, - std::string_view name, - FileSerialisationMethod dumpMethod, - ContentAddressMethod hashMethod, - HashAlgorithm hashAlgo, - const StorePathSet & references, - RepairFlag repair) override - { - auto path = next->addToStoreFromDump(dump, name, dumpMethod, hashMethod, hashAlgo, references, repair); - goal.addDependency(path); - return path; - } - - void narFromPath(const StorePath & path, Sink & sink) override - { - if (!goal.isAllowed(path)) - throw InvalidPath("cannot dump unknown path '%s' in recursive Nix", printStorePath(path)); - LocalFSStore::narFromPath(path, sink); - } - - void ensurePath(const StorePath & path) override - { - if (!goal.isAllowed(path)) - throw InvalidPath("cannot substitute unknown path '%s' in recursive Nix", printStorePath(path)); - /* Nothing to be done; 'path' must already be valid. */ - } - - void registerDrvOutput(const Realisation & info) override - // XXX: This should probably be allowed as a no-op if the realisation - // corresponds to an allowed derivation - { throw Error("registerDrvOutput"); } - - void queryRealisationUncached(const DrvOutput & id, - Callback> callback) noexcept override - // XXX: This should probably be allowed if the realisation corresponds to - // an allowed derivation - { - if (!goal.isAllowed(id)) - callback(nullptr); - next->queryRealisation(id, std::move(callback)); - } - - void buildPaths(const std::vector & paths, BuildMode buildMode, std::shared_ptr evalStore) override - { - for (auto & result : buildPathsWithResults(paths, buildMode, evalStore)) - if (!result.success()) - result.rethrow(); - } - - std::vector buildPathsWithResults( - const std::vector & paths, - BuildMode buildMode = bmNormal, - std::shared_ptr evalStore = nullptr) override - { - assert(!evalStore); - - if (buildMode != bmNormal) throw Error("unsupported build mode"); - - StorePathSet newPaths; - std::set newRealisations; - - for (auto & req : paths) { - if (!goal.isAllowed(req)) - throw InvalidPath("cannot build '%s' in recursive Nix because path is unknown", req.to_string(*next)); - } - - auto results = next->buildPathsWithResults(paths, buildMode); - - for (auto & result : results) { - for (auto & [outputName, output] : result.builtOutputs) { - newPaths.insert(output.outPath); - newRealisations.insert(output); - } - } - - StorePathSet closure; - next->computeFSClosure(newPaths, closure); - for (auto & path : closure) - goal.addDependency(path); - for (auto & real : Realisation::closure(*next, newRealisations)) - goal.addedDrvOutputs.insert(real.id); - - return results; - } - - BuildResult buildDerivation(const StorePath & drvPath, const BasicDerivation & drv, - BuildMode buildMode = bmNormal) override - { unsupported("buildDerivation"); } - - void addTempRoot(const StorePath & path) override - { } - - void addIndirectRoot(const Path & path) override - { } - - Roots findRoots(bool censor) override - { return Roots(); } - - void collectGarbage(const GCOptions & options, GCResults & results) override - { } - - void addSignatures(const StorePath & storePath, const StringSet & sigs) override - { unsupported("addSignatures"); } - - void queryMissing(const std::vector & targets, - StorePathSet & willBuild, StorePathSet & willSubstitute, StorePathSet & unknown, - uint64_t & downloadSize, uint64_t & narSize) override - { - /* This is slightly impure since it leaks information to the - client about what paths will be built/substituted or are - already present. Probably not a big deal. */ - - std::vector allowed; - for (auto & req : targets) { - if (goal.isAllowed(req)) - allowed.emplace_back(req); - else - unknown.insert(pathPartOfReq(req)); - } - - next->queryMissing(allowed, willBuild, willSubstitute, - unknown, downloadSize, narSize); - } - - virtual std::optional getBuildLogExact(const StorePath & path) override - { return std::nullopt; } - - virtual void addBuildLog(const StorePath & path, std::string_view log) override - { unsupported("addBuildLog"); } - - std::optional isTrustedClient() override - { return NotTrusted; } -}; - - void LocalDerivationGoal::startDaemon() { experimentalFeatureSettings.require(Xp::RecursiveNix); @@ -1886,7 +1615,7 @@ void LocalDerivationGoal::startDaemon() params["root"] = *optRoot; params["state"] = "/no-such-path"; params["log"] = "/no-such-path"; - auto store = make_ref(params, + auto store = makeRestrictedStore(params, ref(std::dynamic_pointer_cast(worker.store.shared_from_this())), *this);