Skip to content

Commit

Permalink
Add pure evaluation mode
Browse files Browse the repository at this point in the history
In this mode, the following restrictions apply:

* The builtins currentTime, currentSystem and storePath throw an
  error.

* $NIX_PATH and -I are ignored.

* fetchGit and fetchMercurial require a revision hash.

* fetchurl and fetchTarball require a sha256 attribute.

* No file system access is allowed outside of the paths returned by
  fetch{Git,Mercurial,url,Tarball}. Thus 'nix build -f ./foo.nix' is
  not allowed.

Thus, the evaluation result is completely reproducible from the
command line arguments. E.g.

  nix build --pure-eval '(
    let
      nix = fetchGit { url = https://github.com/NixOS/nixpkgs.git; rev = "9c927de4b179a6dd210dd88d34bda8af4b575680"; };
      nixpkgs = fetchGit { url = https://github.com/NixOS/nixpkgs.git; ref = "release-17.09"; rev = "66b4de79e3841530e6d9c6baf98702aa1f7124e4"; };
    in (import (nix + "/release.nix") { inherit nix nixpkgs; }).build.x86_64-linux
  )'

The goal is to enable completely reproducible and traceable
evaluation. For example, a NixOS configuration could be fully
described by a single Git commit hash. 'nixos-rebuild' would do
something like

  nix build --pure-eval '(
    (import (fetchGit { url = file:///my-nixos-config; rev = "..."; })).system
  ')

where the Git repository /my-nixos-config would use further fetchGit
calls or Git externals to fetch Nixpkgs and whatever other
dependencies it has. Either way, the commit hash would uniquely
identify the NixOS configuration and allow it to reproduced.
  • Loading branch information
edolstra committed Jan 16, 2018
1 parent 23fa7e3 commit d4dcffd
Show file tree
Hide file tree
Showing 19 changed files with 159 additions and 53 deletions.
2 changes: 1 addition & 1 deletion mk/tests.mk
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ installcheck:
echo "$${red}$$failed out of $$total tests failed $$normal"; \
exit 1; \
else \
echo "$${green}All tests succeeded"; \
echo "$${green}All tests succeeded$$normal"; \
fi

.PHONY: check installcheck
2 changes: 1 addition & 1 deletion release.nix
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

let

pkgs = import nixpkgs {};
pkgs = import nixpkgs { system = "x86_64-linux"; };

jobs = rec {

Expand Down
65 changes: 39 additions & 26 deletions src/libexpr/eval.cc
Original file line number Diff line number Diff line change
Expand Up @@ -300,16 +300,25 @@ EvalState::EvalState(const Strings & _searchPath, ref<Store> store)
{
countCalls = getEnv("NIX_COUNT_CALLS", "0") != "0";

restricted = settings.restrictEval;

assert(gcInitialised);

/* Initialise the Nix expression search path. */
Strings paths = parseNixPath(getEnv("NIX_PATH", ""));
for (auto & i : _searchPath) addToSearchPath(i);
for (auto & i : paths) addToSearchPath(i);
if (!settings.pureEval) {
Strings paths = parseNixPath(getEnv("NIX_PATH", ""));
for (auto & i : _searchPath) addToSearchPath(i);
for (auto & i : paths) addToSearchPath(i);
}
addToSearchPath("nix=" + settings.nixDataDir + "/nix/corepkgs");

if (settings.restrictEval || settings.pureEval) {
allowedPaths = PathSet();
for (auto & i : searchPath) {
auto r = resolveSearchPathElem(i);
if (!r.first) continue;
allowedPaths->insert(r.second);
}
}

clearValue(vEmptySet);
vEmptySet.type = tAttrs;
vEmptySet.attrs = allocBindings(0);
Expand All @@ -326,38 +335,39 @@ EvalState::~EvalState()

Path EvalState::checkSourcePath(const Path & path_)
{
if (!restricted) return path_;
if (!allowedPaths) return path_;

auto doThrow = [&]() [[noreturn]] {
throw RestrictedPathError("access to path '%1%' is forbidden in restricted mode", path_);
};

bool found = false;

for (auto & i : *allowedPaths) {
if (isDirOrInDir(path_, i)) {
found = true;
break;
}
}

if (!found) doThrow();

/* Resolve symlinks. */
debug(format("checking access to '%s'") % path_);
Path path = canonPath(path_, true);

for (auto & i : searchPath) {
auto r = resolveSearchPathElem(i);
if (!r.first) continue;
if (path == r.second || isInDir(path, r.second))
for (auto & i : *allowedPaths) {
if (isDirOrInDir(path, i))
return path;
}

/* To support import-from-derivation, allow access to anything in
the store. FIXME: only allow access to paths that have been
constructed by this evaluation. */
if (store->isInStore(path)) return path;

#if 0
/* Hack to support the chroot dependencies of corepkgs (see
corepkgs/config.nix.in). */
if (path == settings.nixPrefix && isStorePath(settings.nixPrefix))
return path;
#endif

throw RestrictedPathError(format("access to path '%1%' is forbidden in restricted mode") % path_);
doThrow();
}


void EvalState::checkURI(const std::string & uri)
{
if (!restricted) return;
if (!settings.restrictEval) return;

/* 'uri' should be equal to a prefix, or in a subdirectory of a
prefix. Thus, the prefix https://github.co does not permit
Expand Down Expand Up @@ -396,7 +406,7 @@ void EvalState::addConstant(const string & name, Value & v)
}


void EvalState::addPrimOp(const string & name,
Value * EvalState::addPrimOp(const string & name,
unsigned int arity, PrimOpFun primOp)
{
Value * v = allocValue();
Expand All @@ -407,6 +417,7 @@ void EvalState::addPrimOp(const string & name,
staticBaseEnv.vars[symbols.create(name)] = baseEnvDispl;
baseEnv.values[baseEnvDispl++] = v;
baseEnv.values[0]->attrs->push_back(Attr(sym, v));
return v;
}


Expand Down Expand Up @@ -659,8 +670,10 @@ Value * ExprPath::maybeThunk(EvalState & state, Env & env)
}


void EvalState::evalFile(const Path & path, Value & v)
void EvalState::evalFile(const Path & path_, Value & v)
{
auto path = checkSourcePath(path_);

FileEvalCache::iterator i;
if ((i = fileEvalCache.find(path)) != fileEvalCache.end()) {
v = i->second;
Expand Down
8 changes: 4 additions & 4 deletions src/libexpr/eval.hh
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ public:
already exist there. */
RepairFlag repair;

/* If set, don't allow access to files outside of the Nix search
path or to environment variables. */
bool restricted;
/* The allowed filesystem paths in restricted or pure evaluation
mode. */
std::experimental::optional<PathSet> allowedPaths;

Value vEmptySet;

Expand Down Expand Up @@ -212,7 +212,7 @@ private:

void addConstant(const string & name, Value & v);

void addPrimOp(const string & name,
Value * addPrimOp(const string & name,
unsigned int arity, PrimOpFun primOp);

public:
Expand Down
44 changes: 35 additions & 9 deletions src/libexpr/primops.cc
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ static void prim_tryEval(EvalState & state, const Pos & pos, Value * * args, Val
static void prim_getEnv(EvalState & state, const Pos & pos, Value * * args, Value & v)
{
string name = state.forceStringNoCtx(*args[0], pos);
mkString(v, state.restricted ? "" : getEnv(name));
mkString(v, settings.restrictEval || settings.pureEval ? "" : getEnv(name));
}


Expand Down Expand Up @@ -1929,7 +1929,14 @@ void fetch(EvalState & state, const Pos & pos, Value * * args, Value & v,

state.checkURI(url);

if (settings.pureEval && !expectedHash)
throw Error("in pure evaluation mode, '%s' requires a 'sha256' argument", who);

Path res = getDownloader()->downloadCached(state.store, url, unpack, name, expectedHash);

if (state.allowedPaths)
state.allowedPaths->insert(res);

mkString(v, res, PathSet({res}));
}

Expand Down Expand Up @@ -1981,11 +1988,28 @@ void EvalState::createBaseEnv()
mkNull(v);
addConstant("null", v);

mkInt(v, time(0));
addConstant("__currentTime", v);
auto vThrow = addPrimOp("throw", 1, prim_throw);

mkString(v, settings.thisSystem);
addConstant("__currentSystem", v);
auto addPurityError = [&](const std::string & name) {
Value * v2 = allocValue();
mkString(*v2, fmt("'%s' is not allowed in pure evaluation mode", name));
mkApp(v, *vThrow, *v2);
addConstant(name, v);
};

if (settings.pureEval)
addPurityError("__currentTime");
else {
mkInt(v, time(0));
addConstant("__currentTime", v);
}

if (settings.pureEval)
addPurityError("__currentSystem");
else {
mkString(v, settings.thisSystem);
addConstant("__currentSystem", v);
}

mkString(v, nixVersion);
addConstant("__nixVersion", v);
Expand All @@ -2001,10 +2025,10 @@ void EvalState::createBaseEnv()
addConstant("__langVersion", v);

// Miscellaneous
addPrimOp("scopedImport", 2, prim_scopedImport);
auto vScopedImport = addPrimOp("scopedImport", 2, prim_scopedImport);
Value * v2 = allocValue();
mkAttrs(*v2, 0);
mkApp(v, *baseEnv.values[baseEnvDispl - 1], *v2);
mkApp(v, *vScopedImport, *v2);
forceValue(v);
addConstant("import", v);
if (settings.enableNativeCode) {
Expand All @@ -2020,7 +2044,6 @@ void EvalState::createBaseEnv()
addPrimOp("__isBool", 1, prim_isBool);
addPrimOp("__genericClosure", 1, prim_genericClosure);
addPrimOp("abort", 1, prim_abort);
addPrimOp("throw", 1, prim_throw);
addPrimOp("__addErrorContext", 2, prim_addErrorContext);
addPrimOp("__tryEval", 1, prim_tryEval);
addPrimOp("__getEnv", 1, prim_getEnv);
Expand All @@ -2035,7 +2058,10 @@ void EvalState::createBaseEnv()

// Paths
addPrimOp("__toPath", 1, prim_toPath);
addPrimOp("__storePath", 1, prim_storePath);
if (settings.pureEval)
addPurityError("__storePath");
else
addPrimOp("__storePath", 1, prim_storePath);
addPrimOp("__pathExists", 1, prim_pathExists);
addPrimOp("baseNameOf", 1, prim_baseNameOf);
addPrimOp("dirOf", 1, prim_dirOf);
Expand Down
15 changes: 10 additions & 5 deletions src/libexpr/primops/fetchGit.cc
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,15 @@ struct GitInfo
uint64_t revCount = 0;
};

std::regex revRegex("^[0-9a-fA-F]{40}$");

GitInfo exportGit(ref<Store> store, const std::string & uri,
std::experimental::optional<std::string> ref, std::string rev,
const std::string & name)
{
if (settings.pureEval && rev == "")
throw Error("in pure evaluation mode, 'fetchGit' requires a Git revision");

if (!ref && rev == "" && hasPrefix(uri, "/") && pathExists(uri + "/.git")) {

bool clean = true;
Expand Down Expand Up @@ -76,11 +81,8 @@ GitInfo exportGit(ref<Store> store, const std::string & uri,

if (!ref) ref = "master"s;

if (rev != "") {
std::regex revRegex("^[0-9a-fA-F]{40}$");
if (!std::regex_match(rev, revRegex))
throw Error("invalid Git revision '%s'", rev);
}
if (rev != "" && !std::regex_match(rev, revRegex))
throw Error("invalid Git revision '%s'", rev);

Path cacheDir = getCacheDir() + "/nix/git";

Expand Down Expand Up @@ -231,6 +233,9 @@ static void prim_fetchGit(EvalState & state, const Pos & pos, Value * * args, Va
mkString(*state.allocAttr(v, state.symbols.create("shortRev")), gitInfo.shortRev);
mkInt(*state.allocAttr(v, state.symbols.create("revCount")), gitInfo.revCount);
v.attrs->sort();

if (state.allowedPaths)
state.allowedPaths->insert(gitInfo.storePath);
}

static RegisterPrimOp r("fetchGit", 1, prim_fetchGit);
Expand Down
6 changes: 6 additions & 0 deletions src/libexpr/primops/fetchMercurial.cc
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ std::regex commitHashRegex("^[0-9a-fA-F]{40}$");
HgInfo exportMercurial(ref<Store> store, const std::string & uri,
std::string rev, const std::string & name)
{
if (settings.pureEval && rev == "")
throw Error("in pure evaluation mode, 'fetchMercurial' requires a Mercurial revision");

if (rev == "" && hasPrefix(uri, "/") && pathExists(uri + "/.hg")) {

bool clean = runProgram("hg", true, { "status", "-R", uri, "--modified", "--added", "--removed" }) == "";
Expand Down Expand Up @@ -196,6 +199,9 @@ static void prim_fetchMercurial(EvalState & state, const Pos & pos, Value * * ar
mkString(*state.allocAttr(v, state.symbols.create("shortRev")), std::string(hgInfo.rev, 0, 12));
mkInt(*state.allocAttr(v, state.symbols.create("revCount")), hgInfo.revCount);
v.attrs->sort();

if (state.allowedPaths)
state.allowedPaths->insert(hgInfo.storePath);
}

static RegisterPrimOp r("fetchMercurial", 1, prim_fetchMercurial);
Expand Down
3 changes: 3 additions & 0 deletions src/libstore/globals.hh
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,9 @@ public:
"Whether to restrict file system access to paths in $NIX_PATH, "
"and network access to the URI prefixes listed in 'allowed-uris'."};

Setting<bool> pureEval{this, false, "pure-eval",
"Whether to restrict file system and network access to files specified by cryptographic hash."};

Setting<size_t> buildRepeat{this, 0, "repeat",
"The number of times to repeat a build in order to verify determinism.",
{"build-repeat"}};
Expand Down
6 changes: 6 additions & 0 deletions src/libutil/util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,12 @@ bool isInDir(const Path & path, const Path & dir)
}


bool isDirOrInDir(const Path & path, const Path & dir)
{
return path == dir or isInDir(path, dir);
}


struct stat lstat(const Path & path)
{
struct stat st;
Expand Down
6 changes: 4 additions & 2 deletions src/libutil/util.hh
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,12 @@ Path dirOf(const Path & path);
following the final `/'. */
string baseNameOf(const Path & path);

/* Check whether a given path is a descendant of the given
directory. */
/* Check whether 'path' is a descendant of 'dir'. */
bool isInDir(const Path & path, const Path & dir);

/* Check whether 'path' is equal to 'dir' or a descendant of 'dir'. */
bool isDirOrInDir(const Path & path, const Path & dir);

/* Get status of `path'. */
struct stat lstat(const Path & path);

Expand Down
4 changes: 2 additions & 2 deletions src/nix-build/nix-build.cc
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,8 @@ void mainWrapped(int argc, char * * argv)
else
/* If we're in a #! script, interpret filenames
relative to the script. */
exprs.push_back(state.parseExprFromFile(resolveExprPath(lookupFileArg(state,
inShebang && !packages ? absPath(i, absPath(dirOf(script))) : i))));
exprs.push_back(state.parseExprFromFile(resolveExprPath(state.checkSourcePath(lookupFileArg(state,
inShebang && !packages ? absPath(i, absPath(dirOf(script))) : i)))));
}

/* Evaluate them into derivations. */
Expand Down
2 changes: 1 addition & 1 deletion src/nix-instantiate/nix-instantiate.cc
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ int main(int argc, char * * argv)
for (auto & i : files) {
Expr * e = fromArgs
? state.parseExprFromString(i, absPath("."))
: state.parseExprFromFile(resolveExprPath(lookupFileArg(state, i)));
: state.parseExprFromFile(resolveExprPath(state.checkSourcePath(lookupFileArg(state, i))));
processExpr(state, attrPaths, parseOnly, strict, autoArgs,
evalOnly, outputKind, xmlOutputSourceLocation, e);
}
Expand Down
7 changes: 7 additions & 0 deletions tests/fetchGit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,17 @@ rev2=$(git -C $repo rev-parse HEAD)
path=$(nix eval --raw "(builtins.fetchGit file://$repo).outPath")
[[ $(cat $path/hello) = world ]]

# In pure eval mode, fetchGit without a revision should fail.
[[ $(nix eval --raw "(builtins.readFile (fetchGit file://$repo + \"/hello\"))") = world ]]
(! nix eval --pure-eval --raw "(builtins.readFile (fetchGit file://$repo + \"/hello\"))")

# Fetch using an explicit revision hash.
path2=$(nix eval --raw "(builtins.fetchGit { url = file://$repo; rev = \"$rev2\"; }).outPath")
[[ $path = $path2 ]]

# In pure eval mode, fetchGit with a revision should succeed.
[[ $(nix eval --pure-eval --raw "(builtins.readFile (fetchGit { url = file://$repo; rev = \"$rev2\"; } + \"/hello\"))") = world ]]

# Fetch again. This should be cached.
mv $repo ${repo}-tmp
path2=$(nix eval --raw "(builtins.fetchGit file://$repo).outPath")
Expand Down
Loading

0 comments on commit d4dcffd

Please sign in to comment.