Move restricted/pure-eval access control out of the evaluator and into the accessor

This commit is contained in:
Eelco Dolstra 2023-11-30 16:16:17 +01:00
parent 3bebaefcd0
commit ea95327e72
8 changed files with 115 additions and 171 deletions

View file

@ -260,9 +260,10 @@ void SourceExprCommand::completeInstallable(AddCompletions & completions, std::s
evalSettings.pureEval = false; evalSettings.pureEval = false;
auto state = getEvalState(); auto state = getEvalState();
Expr *e = state->parseExprFromFile( auto e =
resolveExprPath(state->checkSourcePath(lookupFileArg(*state, *file))) state->parseExprFromFile(
); resolveExprPath(
lookupFileArg(*state, *file)));
Value root; Value root;
state->eval(e, root); state->eval(e, root);

View file

@ -509,7 +509,18 @@ EvalState::EvalState(
, sOutputSpecified(symbols.create("outputSpecified")) , sOutputSpecified(symbols.create("outputSpecified"))
, repair(NoRepair) , repair(NoRepair)
, emptyBindings(0) , emptyBindings(0)
, rootFS(makeFSInputAccessor(CanonPath::root)) , rootFS(
makeFSInputAccessor(
CanonPath::root,
evalSettings.restrictEval || evalSettings.pureEval
? std::optional<std::set<CanonPath>>(std::set<CanonPath>())
: std::nullopt,
[](const CanonPath & path) -> RestrictedPathError {
auto modeInformation = evalSettings.pureEval
? "in pure evaluation mode (use '--impure' to override)"
: "in restricted mode";
throw RestrictedPathError("access to absolute path '%1%' is forbidden %2%", path, modeInformation);
}))
, corepkgsFS(makeMemoryInputAccessor()) , corepkgsFS(makeMemoryInputAccessor())
, internalFS(makeMemoryInputAccessor()) , internalFS(makeMemoryInputAccessor())
, derivationInternal{corepkgsFS->addFile( , derivationInternal{corepkgsFS->addFile(
@ -551,28 +562,10 @@ EvalState::EvalState(
searchPath.elements.emplace_back(SearchPath::Elem::parse(i)); searchPath.elements.emplace_back(SearchPath::Elem::parse(i));
} }
if (evalSettings.restrictEval || evalSettings.pureEval) { /* Allow access to all paths in the search path. */
allowedPaths = PathSet(); if (rootFS->hasAccessControl())
for (auto & i : searchPath.elements)
for (auto & i : searchPath.elements) { resolveSearchPathPath(i.path, true);
auto r = resolveSearchPathPath(i.path);
if (!r) continue;
auto path = std::move(*r);
if (store->isInStore(path)) {
try {
StorePathSet closure;
store->computeFSClosure(store->toStorePath(path).first, closure);
for (auto & path : closure)
allowPath(path);
} catch (InvalidPath &) {
allowPath(path);
}
} else
allowPath(path);
}
}
corepkgsFS->addFile( corepkgsFS->addFile(
CanonPath("fetchurl.nix"), CanonPath("fetchurl.nix"),
@ -590,14 +583,12 @@ EvalState::~EvalState()
void EvalState::allowPath(const Path & path) void EvalState::allowPath(const Path & path)
{ {
if (allowedPaths) rootFS->allowPath(CanonPath(path));
allowedPaths->insert(path);
} }
void EvalState::allowPath(const StorePath & storePath) void EvalState::allowPath(const StorePath & storePath)
{ {
if (allowedPaths) rootFS->allowPath(CanonPath(store->toRealPath(storePath)));
allowedPaths->insert(store->toRealPath(storePath));
} }
void EvalState::allowAndSetStorePathString(const StorePath & storePath, Value & v) void EvalState::allowAndSetStorePathString(const StorePath & storePath, Value & v)
@ -607,54 +598,6 @@ void EvalState::allowAndSetStorePathString(const StorePath & storePath, Value &
mkStorePathString(storePath, v); mkStorePathString(storePath, v);
} }
SourcePath EvalState::checkSourcePath(const SourcePath & path_)
{
// Don't check non-rootFS accessors, they're in a different namespace.
if (path_.accessor != ref<InputAccessor>(rootFS)) return path_;
if (!allowedPaths) return path_;
auto i = resolvedPaths.find(path_.path.abs());
if (i != resolvedPaths.end())
return i->second;
bool found = false;
/* First canonicalize the path without symlinks, so we make sure an
* attacker can't append ../../... to a path that would be in allowedPaths
* and thus leak symlink targets.
*/
Path abspath = canonPath(path_.path.abs());
for (auto & i : *allowedPaths) {
if (isDirOrInDir(abspath, i)) {
found = true;
break;
}
}
if (!found) {
auto modeInformation = evalSettings.pureEval
? "in pure eval mode (use '--impure' to override)"
: "in restricted mode";
throw RestrictedPathError("access to absolute path '%1%' is forbidden %2%", abspath, modeInformation);
}
/* Resolve symlinks. */
debug("checking access to '%s'", abspath);
SourcePath path = rootPath(CanonPath(canonPath(abspath, true)));
for (auto & i : *allowedPaths) {
if (isDirOrInDir(path.path.abs(), i)) {
resolvedPaths.insert_or_assign(path_.path.abs(), path);
return path;
}
}
throw RestrictedPathError("access to canonical path '%1%' is forbidden in restricted mode", path);
}
void EvalState::checkURI(const std::string & uri) void EvalState::checkURI(const std::string & uri)
{ {
if (!evalSettings.restrictEval) return; if (!evalSettings.restrictEval) return;
@ -674,12 +617,12 @@ void EvalState::checkURI(const std::string & uri)
/* If the URI is a path, then check it against allowedPaths as /* If the URI is a path, then check it against allowedPaths as
well. */ well. */
if (hasPrefix(uri, "/")) { if (hasPrefix(uri, "/")) {
checkSourcePath(rootPath(CanonPath(uri))); rootFS->checkAllowed(CanonPath(uri));
return; return;
} }
if (hasPrefix(uri, "file://")) { if (hasPrefix(uri, "file://")) {
checkSourcePath(rootPath(CanonPath(std::string(uri, 7)))); rootFS->checkAllowed(CanonPath(uri.substr(7)));
return; return;
} }
@ -1181,10 +1124,8 @@ Value * ExprPath::maybeThunk(EvalState & state, Env & env)
} }
void EvalState::evalFile(const SourcePath & path_, Value & v, bool mustBeTrivial) void EvalState::evalFile(const SourcePath & path, Value & v, bool mustBeTrivial)
{ {
auto path = checkSourcePath(path_);
FileEvalCache::iterator i; FileEvalCache::iterator i;
if ((i = fileEvalCache.find(path)) != fileEvalCache.end()) { if ((i = fileEvalCache.find(path)) != fileEvalCache.end()) {
v = i->second; v = i->second;
@ -1205,7 +1146,7 @@ void EvalState::evalFile(const SourcePath & path_, Value & v, bool mustBeTrivial
e = j->second; e = j->second;
if (!e) if (!e)
e = parseExprFromFile(checkSourcePath(resolvedPath)); e = parseExprFromFile(resolvedPath);
fileParseCache[resolvedPath] = e; fileParseCache[resolvedPath] = e;

View file

@ -217,12 +217,6 @@ public:
*/ */
RepairFlag repair; RepairFlag repair;
/**
* The allowed filesystem paths in restricted or pure evaluation
* mode.
*/
std::optional<PathSet> allowedPaths;
Bindings emptyBindings; Bindings emptyBindings;
/** /**
@ -396,12 +390,6 @@ public:
*/ */
void allowAndSetStorePathString(const StorePath & storePath, Value & v); void allowAndSetStorePathString(const StorePath & storePath, Value & v);
/**
* Check whether access to a path is allowed and throw an error if
* not. Otherwise return the canonicalised path.
*/
SourcePath checkSourcePath(const SourcePath & path);
void checkURI(const std::string & uri); void checkURI(const std::string & uri);
/** /**
@ -445,13 +433,15 @@ public:
SourcePath findFile(const SearchPath & searchPath, const std::string_view path, const PosIdx pos = noPos); SourcePath findFile(const SearchPath & searchPath, const std::string_view path, const PosIdx pos = noPos);
/** /**
* Try to resolve a search path value (not the optional key part) * Try to resolve a search path value (not the optional key part).
* *
* If the specified search path element is a URI, download it. * If the specified search path element is a URI, download it.
* *
* If it is not found, return `std::nullopt` * If it is not found, return `std::nullopt`
*/ */
std::optional<std::string> resolveSearchPathPath(const SearchPath::Path & path); std::optional<std::string> resolveSearchPathPath(
const SearchPath::Path & elem,
bool initAccessControl = false);
/** /**
* Evaluate an expression to normal form * Evaluate an expression to normal form
@ -756,6 +746,13 @@ public:
*/ */
[[nodiscard]] StringMap realiseContext(const NixStringContext & context); [[nodiscard]] StringMap realiseContext(const NixStringContext & context);
/* Call the binary path filter predicate used builtins.path etc. */
bool callPathFilter(
Value * filterFun,
const SourcePath & path,
std::string_view pathArg,
PosIdx pos);
private: private:
/** /**

View file

@ -783,7 +783,7 @@ SourcePath EvalState::findFile(const SearchPath & searchPath, const std::string_
} }
std::optional<std::string> EvalState::resolveSearchPathPath(const SearchPath::Path & value0) std::optional<std::string> EvalState::resolveSearchPathPath(const SearchPath::Path & value0, bool initAccessControl)
{ {
auto & value = value0.s; auto & value = value0.s;
auto i = searchPathResolved.find(value); auto i = searchPathResolved.find(value);
@ -800,7 +800,6 @@ std::optional<std::string> EvalState::resolveSearchPathPath(const SearchPath::Pa
logWarning({ logWarning({
.msg = hintfmt("Nix search path entry '%1%' cannot be downloaded, ignoring", value) .msg = hintfmt("Nix search path entry '%1%' cannot be downloaded, ignoring", value)
}); });
res = std::nullopt;
} }
} }
@ -814,6 +813,20 @@ std::optional<std::string> EvalState::resolveSearchPathPath(const SearchPath::Pa
else { else {
auto path = absPath(value); auto path = absPath(value);
/* Allow access to paths in the search path. */
if (initAccessControl) {
allowPath(path);
if (store->isInStore(path)) {
try {
StorePathSet closure;
store->computeFSClosure(store->toStorePath(path).first, closure);
for (auto & p : closure)
allowPath(p);
} catch (InvalidPath &) { }
}
}
if (pathExists(path)) if (pathExists(path))
res = { path }; res = { path };
else { else {
@ -829,7 +842,7 @@ std::optional<std::string> EvalState::resolveSearchPathPath(const SearchPath::Pa
else else
debug("failed to resolve search path element '%s'", value); debug("failed to resolve search path element '%s'", value);
searchPathResolved[value] = res; searchPathResolved.emplace(value, res);
return res; return res;
} }

View file

@ -15,6 +15,7 @@
#include "value-to-json.hh" #include "value-to-json.hh"
#include "value-to-xml.hh" #include "value-to-xml.hh"
#include "primops.hh" #include "primops.hh"
#include "fs-input-accessor.hh"
#include <boost/container/small_vector.hpp> #include <boost/container/small_vector.hpp>
#include <boost/regex.hpp> #include <boost/regex.hpp>
@ -90,8 +91,8 @@ StringMap EvalState::realiseContext(const NixStringContext & context)
for (auto & [outputName, outputPath] : outputs) { for (auto & [outputName, outputPath] : outputs) {
/* Add the output of this derivations to the allowed /* Add the output of this derivations to the allowed
paths. */ paths. */
if (allowedPaths) { if (rootFS->hasAccessControl()) {
allowPath(outputPath); allowPath(store->toRealPath(outputPath));
} }
/* Get all the output paths corresponding to the placeholders we had */ /* Get all the output paths corresponding to the placeholders we had */
if (experimentalFeatureSettings.isEnabled(Xp::CaDerivations)) { if (experimentalFeatureSettings.isEnabled(Xp::CaDerivations)) {
@ -110,27 +111,19 @@ StringMap EvalState::realiseContext(const NixStringContext & context)
return res; return res;
} }
struct RealisePathFlags { static SourcePath realisePath(EvalState & state, const PosIdx pos, Value & v)
// Whether to check that the path is allowed in pure eval mode
bool checkForPureEval = true;
};
static SourcePath realisePath(EvalState & state, const PosIdx pos, Value & v, const RealisePathFlags flags = {})
{ {
NixStringContext context; NixStringContext context;
auto path = state.coerceToPath(noPos, v, context, "while realising the context of a path"); auto path = state.coerceToPath(noPos, v, context, "while realising the context of a path");
try { try {
if (!context.empty()) { if (!context.empty() && path.accessor == state.rootFS) {
auto rewrites = state.realiseContext(context); auto rewrites = state.realiseContext(context);
auto realPath = state.toRealPath(rewriteStrings(path.path.abs(), rewrites), context); auto realPath = state.toRealPath(rewriteStrings(path.path.abs(), rewrites), context);
return {path.accessor, CanonPath(realPath)}; return {path.accessor, CanonPath(realPath)};
} } else
return path;
return flags.checkForPureEval
? state.checkSourcePath(path)
: path;
} catch (Error & e) { } catch (Error & e) {
e.addTrace(state.positions[pos], "while realising the context of path '%s'", path); e.addTrace(state.positions[pos], "while realising the context of path '%s'", path);
throw; throw;
@ -1493,7 +1486,7 @@ static void prim_storePath(EvalState & state, const PosIdx pos, Value * * args,
})); }));
NixStringContext context; NixStringContext context;
auto path = state.checkSourcePath(state.coerceToPath(pos, *args[0], context, "while evaluating the first argument passed to 'builtins.storePath'")).path; auto path = state.coerceToPath(pos, *args[0], context, "while evaluating the first argument passed to 'builtins.storePath'").path;
/* Resolve symlinks in path, unless path itself is a symlink /* Resolve symlinks in path, unless path itself is a symlink
directly in the store. The latter condition is necessary so directly in the store. The latter condition is necessary so
e.g. nix-push does the right thing. */ e.g. nix-push does the right thing. */
@ -1535,12 +1528,7 @@ static void prim_pathExists(EvalState & state, const PosIdx pos, Value * * args,
{ {
auto & arg = *args[0]; auto & arg = *args[0];
/* We dont check the path right now, because we dont want to auto path = realisePath(state, pos, arg);
throw if the path isnt allowed, but just return false (and we
cant just catch the exception here because we still want to
throw if something in the evaluation of `arg` tries to
access an unauthorized path). */
auto path = realisePath(state, pos, arg, { .checkForPureEval = false });
/* SourcePath doesn't know about trailing slash. */ /* SourcePath doesn't know about trailing slash. */
auto mustBeDir = arg.type() == nString auto mustBeDir = arg.type() == nString
@ -1548,14 +1536,9 @@ static void prim_pathExists(EvalState & state, const PosIdx pos, Value * * args,
|| arg.string_view().ends_with("/.")); || arg.string_view().ends_with("/."));
try { try {
auto checked = state.checkSourcePath(path); auto st = path.maybeLstat();
auto st = checked.maybeLstat();
auto exists = st && (!mustBeDir || st->type == SourceAccessor::tDirectory); auto exists = st && (!mustBeDir || st->type == SourceAccessor::tDirectory);
v.mkBool(exists); v.mkBool(exists);
} catch (SysError & e) {
/* Don't give away info from errors while canonicalising
path in restricted mode. */
v.mkBool(false);
} catch (RestrictedPathError & e) { } catch (RestrictedPathError & e) {
v.mkBool(false); v.mkBool(false);
} }
@ -1699,7 +1682,7 @@ static void prim_findFile(EvalState & state, const PosIdx pos, Value * * args, V
auto path = state.forceStringNoCtx(*args[1], pos, "while evaluating the second argument passed to builtins.findFile"); auto path = state.forceStringNoCtx(*args[1], pos, "while evaluating the second argument passed to builtins.findFile");
v.mkPath(state.checkSourcePath(state.findFile(searchPath, path, pos))); v.mkPath(state.findFile(searchPath, path, pos));
} }
static RegisterPrimOp primop_findFile(PrimOp { static RegisterPrimOp primop_findFile(PrimOp {
@ -2178,11 +2161,35 @@ static RegisterPrimOp primop_toFile({
.fun = prim_toFile, .fun = prim_toFile,
}); });
bool EvalState::callPathFilter(
Value * filterFun,
const SourcePath & path,
std::string_view pathArg,
PosIdx pos)
{
auto st = path.lstat();
/* Call the filter function. The first argument is the path, the
second is a string indicating the type of the file. */
Value arg1;
arg1.mkString(pathArg);
Value arg2;
// assert that type is not "unknown"
arg2.mkString(fileTypeToString(st.type));
Value * args []{&arg1, &arg2};
Value res;
callFunction(*filterFun, 2, args, res, pos);
return forceBool(res, pos, "while evaluating the return value of the path filter function");
}
static void addPath( static void addPath(
EvalState & state, EvalState & state,
const PosIdx pos, const PosIdx pos,
std::string_view name, std::string_view name,
Path path, SourcePath path,
Value * filterFun, Value * filterFun,
FileIngestionMethod method, FileIngestionMethod method,
const std::optional<Hash> expectedHash, const std::optional<Hash> expectedHash,
@ -2190,48 +2197,29 @@ static void addPath(
const NixStringContext & context) const NixStringContext & context)
{ {
try { try {
StorePathSet refs;
if (path.accessor == state.rootFS && state.store->isInStore(path.path.abs())) {
// FIXME: handle CA derivation outputs (where path needs to // FIXME: handle CA derivation outputs (where path needs to
// be rewritten to the actual output). // be rewritten to the actual output).
auto rewrites = state.realiseContext(context); auto rewrites = state.realiseContext(context);
path = state.toRealPath(rewriteStrings(path, rewrites), context); path = {state.rootFS, CanonPath(state.toRealPath(rewriteStrings(path.path.abs(), rewrites), context))};
StorePathSet refs;
if (state.store->isInStore(path)) {
try { try {
auto [storePath, subPath] = state.store->toStorePath(path); auto [storePath, subPath] = state.store->toStorePath(path.path.abs());
// FIXME: we should scanForReferences on the path before adding it // FIXME: we should scanForReferences on the path before adding it
refs = state.store->queryPathInfo(storePath)->references; refs = state.store->queryPathInfo(storePath)->references;
path = state.store->toRealPath(storePath) + subPath; path = {state.rootFS, CanonPath(state.store->toRealPath(storePath) + subPath)};
} catch (Error &) { // FIXME: should be InvalidPathError } catch (Error &) { // FIXME: should be InvalidPathError
} }
} }
path = evalSettings.pureEval && expectedHash std::unique_ptr<PathFilter> filter;
? path if (filterFun)
: state.checkSourcePath(state.rootPath(CanonPath(path))).path.abs(); filter = std::make_unique<PathFilter>([&](const Path & p) {
auto p2 = CanonPath(p);
PathFilter filter = filterFun ? ([&](const Path & path) { return state.callPathFilter(filterFun, {path.accessor, p2}, p2.abs(), pos);
auto st = lstat(path); });
/* Call the filter function. The first argument is the path,
the second is a string indicating the type of the file. */
Value arg1;
arg1.mkString(path);
Value arg2;
arg2.mkString(
S_ISREG(st.st_mode) ? "regular" :
S_ISDIR(st.st_mode) ? "directory" :
S_ISLNK(st.st_mode) ? "symlink" :
"unknown" /* not supported, will fail! */);
Value * args []{&arg1, &arg2};
Value res;
state.callFunction(*filterFun, 2, args, res, pos);
return state.forceBool(res, pos, "while evaluating the return value of the path filter function");
}) : defaultPathFilter;
std::optional<StorePath> expectedStorePath; std::optional<StorePath> expectedStorePath;
if (expectedHash) if (expectedHash)
@ -2242,7 +2230,7 @@ static void addPath(
}); });
if (!expectedHash || !state.store->isValidPath(*expectedStorePath)) { if (!expectedHash || !state.store->isValidPath(*expectedStorePath)) {
auto dstPath = state.rootPath(CanonPath(path)).fetchToStore(state.store, name, method, &filter, state.repair); auto dstPath = path.fetchToStore(state.store, name, method, filter.get(), state.repair);
if (expectedHash && expectedStorePath != dstPath) if (expectedHash && expectedStorePath != dstPath)
state.debugThrowLastTrace(Error("store path mismatch in (possibly filtered) path added from '%s'", path)); state.debugThrowLastTrace(Error("store path mismatch in (possibly filtered) path added from '%s'", path));
state.allowAndSetStorePathString(dstPath, v); state.allowAndSetStorePathString(dstPath, v);
@ -2261,7 +2249,8 @@ static void prim_filterSource(EvalState & state, const PosIdx pos, Value * * arg
auto path = state.coerceToPath(pos, *args[1], context, auto path = state.coerceToPath(pos, *args[1], context,
"while evaluating the second argument (the path to filter) passed to 'builtins.filterSource'"); "while evaluating the second argument (the path to filter) passed to 'builtins.filterSource'");
state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.filterSource"); state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.filterSource");
addPath(state, pos, path.baseName(), path.path.abs(), args[0], FileIngestionMethod::Recursive, std::nullopt, v, context);
addPath(state, pos, path.baseName(), path, args[0], FileIngestionMethod::Recursive, std::nullopt, v, context);
} }
static RegisterPrimOp primop_filterSource({ static RegisterPrimOp primop_filterSource({
@ -2356,7 +2345,7 @@ static void prim_path(EvalState & state, const PosIdx pos, Value * * args, Value
if (name.empty()) if (name.empty())
name = path->baseName(); name = path->baseName();
addPath(state, pos, name, path->path.abs(), filterFun, method, expectedHash, v, context); addPath(state, pos, name, *path, filterFun, method, expectedHash, v, context);
} }
static RegisterPrimOp primop_path({ static RegisterPrimOp primop_path({

View file

@ -310,8 +310,11 @@ static void main_nix_build(int argc, char * * argv)
else else
/* If we're in a #! script, interpret filenames /* If we're in a #! script, interpret filenames
relative to the script. */ relative to the script. */
exprs.push_back(state->parseExprFromFile(resolveExprPath(state->checkSourcePath(lookupFileArg(*state, exprs.push_back(
inShebang && !packages ? absPath(i, absPath(dirOf(script))) : i))))); state->parseExprFromFile(
resolveExprPath(
lookupFileArg(*state,
inShebang && !packages ? absPath(i, absPath(dirOf(script))) : i))));
} }
} }

View file

@ -183,7 +183,7 @@ static int main_nix_instantiate(int argc, char * * argv)
for (auto & i : files) { for (auto & i : files) {
Expr * e = fromArgs Expr * e = fromArgs
? state->parseExprFromString(i, state->rootPath(CanonPath::fromCwd())) ? state->parseExprFromString(i, state->rootPath(CanonPath::fromCwd()))
: state->parseExprFromFile(resolveExprPath(state->checkSourcePath(lookupFileArg(*state, i)))); : state->parseExprFromFile(resolveExprPath(lookupFileArg(*state, i)));
processExpr(*state, attrPaths, parseOnly, strict, autoArgs, processExpr(*state, attrPaths, parseOnly, strict, autoArgs,
evalOnly, outputKind, xmlOutputSourceLocation, e); evalOnly, outputKind, xmlOutputSourceLocation, e);
} }

View file

@ -14,8 +14,8 @@ nix-instantiate --restrict-eval --eval -E 'builtins.readFile ./simple.nix' -I sr
(! nix-instantiate --restrict-eval --eval -E 'builtins.readDir ../../src/nix-channel') (! nix-instantiate --restrict-eval --eval -E 'builtins.readDir ../../src/nix-channel')
nix-instantiate --restrict-eval --eval -E 'builtins.readDir ../../src/nix-channel' -I src=../../src nix-instantiate --restrict-eval --eval -E 'builtins.readDir ../../src/nix-channel' -I src=../../src
(! nix-instantiate --restrict-eval --eval -E 'let __nixPath = [ { prefix = "foo"; path = ./.; } ]; in <foo>') (! nix-instantiate --restrict-eval --eval -E 'let __nixPath = [ { prefix = "foo"; path = ./.; } ]; in builtins.readFile <foo/simple.nix>')
nix-instantiate --restrict-eval --eval -E 'let __nixPath = [ { prefix = "foo"; path = ./.; } ]; in <foo>' -I src=. nix-instantiate --restrict-eval --eval -E 'let __nixPath = [ { prefix = "foo"; path = ./.; } ]; in builtins.readFile <foo/simple.nix>' -I src=.
p=$(nix eval --raw --expr "builtins.fetchurl file://$(pwd)/restricted.sh" --impure --restrict-eval --allowed-uris "file://$(pwd)") p=$(nix eval --raw --expr "builtins.fetchurl file://$(pwd)/restricted.sh" --impure --restrict-eval --allowed-uris "file://$(pwd)")
cmp $p restricted.sh cmp $p restricted.sh