Merge remote-tracking branch 'pennae/path-access' into HEAD
This fixes a bug where flakes do not actually do purity path checks correctly. Tested-By: Jade Lovelace <lix@jade.fyi> Change-Id: If7d131a8e73a5874fb15cfaa0dea3b8811ba35d2
This commit is contained in:
@@ -253,8 +253,12 @@ static void main_nix_build(std::string programName, Strings argv)
|
||||
else
|
||||
/* If we're in a #! script, interpret filenames
|
||||
relative to the script. */
|
||||
exprs.push_back(evaluator->parseExprFromFile(resolveExprPath(evaluator->paths.checkSourcePath(lookupFileArg(*evaluator,
|
||||
inShebang && !packages ? absPath(i, absPath(dirOf(script))) : i)))));
|
||||
exprs.push_back(evaluator->parseExprFromFile(
|
||||
evaluator->paths.resolveExprPath(lookupFileArg(
|
||||
*evaluator,
|
||||
inShebang && !packages ? absPath(i, absPath(dirOf(script))) : i
|
||||
))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+19
-13
@@ -96,11 +96,16 @@ static bool parseInstallSourceOptions(Globals & globals,
|
||||
}
|
||||
|
||||
|
||||
static bool isNixExpr(const SourcePath & path, struct InputAccessor::Stat & st)
|
||||
static bool isNixExpr(EvalPaths & paths, const CheckedSourcePath & path, struct InputAccessor::Stat & st)
|
||||
{
|
||||
return
|
||||
st.type == InputAccessor::tRegular
|
||||
|| (st.type == InputAccessor::tDirectory && (path + "default.nix").pathExists());
|
||||
if (st.type == InputAccessor::tRegular) {
|
||||
return true;
|
||||
} else if (st.type != InputAccessor::tDirectory) {
|
||||
return false;
|
||||
} else {
|
||||
auto defaultNix = paths.checkSourcePath(path + "default.nix");
|
||||
return defaultNix.pathExists();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -108,7 +113,7 @@ static constexpr size_t maxAttrs = 1024;
|
||||
|
||||
|
||||
static void getAllExprs(Evaluator & state,
|
||||
const SourcePath & path, StringSet & seen, BindingsBuilder & attrs)
|
||||
const CheckedSourcePath & path, StringSet & seen, BindingsBuilder & attrs)
|
||||
{
|
||||
StringSet namesSorted;
|
||||
for (auto & [name, _] : path.readDirectory()) namesSorted.insert(name);
|
||||
@@ -119,16 +124,16 @@ static void getAllExprs(Evaluator & state,
|
||||
are implemented using profiles). */
|
||||
if (i == "manifest.nix") continue;
|
||||
|
||||
SourcePath path2 = path + i;
|
||||
auto path2 = state.paths.checkSourcePath(path + i);
|
||||
|
||||
InputAccessor::Stat st;
|
||||
try {
|
||||
st = path2.resolveSymlinks().lstat();
|
||||
st = path2.stat();
|
||||
} catch (Error &) {
|
||||
continue; // ignore dangling symlinks in ~/.nix-defexpr
|
||||
}
|
||||
|
||||
if (isNixExpr(path2, st) && (st.type != InputAccessor::tRegular || path2.baseName().ends_with(".nix"))) {
|
||||
if (isNixExpr(state.paths, path2, st) && (st.type != InputAccessor::tRegular || path2.baseName().ends_with(".nix"))) {
|
||||
/* Strip off the `.nix' filename suffix (if applicable),
|
||||
otherwise the attribute cannot be selected with the
|
||||
`-A' option. Useful if you want to stick a Nix
|
||||
@@ -138,7 +143,7 @@ static void getAllExprs(Evaluator & state,
|
||||
attrName = std::string(attrName, 0, attrName.size() - 4);
|
||||
if (!seen.insert(attrName).second) {
|
||||
std::string suggestionMessage = "";
|
||||
if (path2.path.abs().find("channels") != std::string::npos && path.path.abs().find("channels") != std::string::npos)
|
||||
if (path2.to_string().find("channels") != std::string::npos && path.to_string().find("channels") != std::string::npos)
|
||||
suggestionMessage = fmt("\nsuggestion: remove '%s' from either the root channels or the user channels", attrName);
|
||||
printError("warning: name collision in input Nix expressions, skipping '%1%'"
|
||||
"%2%", path2, suggestionMessage);
|
||||
@@ -146,7 +151,7 @@ static void getAllExprs(Evaluator & state,
|
||||
}
|
||||
/* Load the expression on demand. */
|
||||
auto vArg = state.mem.allocValue();
|
||||
vArg->mkString(path2.path.abs());
|
||||
vArg->mkString(path2.canonical().abs());
|
||||
if (seen.size() == maxAttrs)
|
||||
throw Error("too many Nix expressions in directory '%1%'", path);
|
||||
attrs.alloc(attrName).mkApp(&state.builtins.get("import"), vArg);
|
||||
@@ -160,11 +165,12 @@ static void getAllExprs(Evaluator & state,
|
||||
|
||||
|
||||
|
||||
static void loadSourceExpr(EvalState & state, const SourcePath & path, Value & v)
|
||||
static void loadSourceExpr(EvalState & state, const SourcePath & path_, Value & v)
|
||||
{
|
||||
auto st = path.resolveSymlinks().lstat();
|
||||
auto path = state.ctx.paths.checkSourcePath(path_);
|
||||
auto st = path.stat();
|
||||
|
||||
if (isNixExpr(path, st))
|
||||
if (isNixExpr(state.ctx.paths, path, st))
|
||||
state.evalFile(path, v);
|
||||
|
||||
/* The path is a directory. Put the Nix expressions in the
|
||||
|
||||
@@ -168,10 +168,7 @@ static int main_nix_instantiate(std::string programName, Strings argv)
|
||||
if (findFile) {
|
||||
for (auto & i : files) {
|
||||
auto p = evaluator->paths.findFile(i);
|
||||
if (auto fn = p.getPhysicalPath())
|
||||
std::cout << fn->abs() << std::endl;
|
||||
else
|
||||
throw Error("'%s' has no physical path", p);
|
||||
std::cout << p.canonical().abs() << std::endl;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -186,7 +183,7 @@ static int main_nix_instantiate(std::string programName, Strings argv)
|
||||
for (auto & i : files) {
|
||||
Expr & e = fromArgs
|
||||
? evaluator->parseExprFromString(i, CanonPath::fromCwd())
|
||||
: evaluator->parseExprFromFile(resolveExprPath(evaluator->paths.checkSourcePath(lookupFileArg(*evaluator, i))));
|
||||
: evaluator->parseExprFromFile(evaluator->paths.resolveExprPath(lookupFileArg(*evaluator, i)));
|
||||
processExpr(*state, attrPaths, parseOnly, strict, autoArgs,
|
||||
evalOnly, outputKind, xmlOutputSourceLocation, e);
|
||||
}
|
||||
|
||||
@@ -7,9 +7,6 @@ namespace nix {
|
||||
|
||||
Strings editorFor(const SourcePath & file, uint32_t line)
|
||||
{
|
||||
auto path = file.getPhysicalPath();
|
||||
if (!path)
|
||||
throw Error("cannot open '%s' in an editor because it has no physical path", file);
|
||||
auto editor = getEnv("EDITOR").value_or("cat");
|
||||
auto args = tokenizeString<Strings>(editor);
|
||||
if (line > 0 && (
|
||||
@@ -18,7 +15,7 @@ Strings editorFor(const SourcePath & file, uint32_t line)
|
||||
editor.find("vim") != std::string::npos ||
|
||||
editor.find("kak") != std::string::npos))
|
||||
args.push_back(fmt("+%d", line));
|
||||
args.push_back(path->abs());
|
||||
args.push_back(file.canonical().abs());
|
||||
return args;
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ std::optional<DerivedPathWithInfo> InstallableValue::trySinglePathToDerivedPaths
|
||||
)
|
||||
{
|
||||
if (v.type() == nPath) {
|
||||
auto storePath = fetchToStore(*evaluator->store, v.path());
|
||||
auto storePath = fetchToStore(*evaluator->store, state.ctx.paths.checkSourcePath(v.path()));
|
||||
return {{
|
||||
.path = DerivedPath::Opaque {
|
||||
.path = std::move(storePath),
|
||||
|
||||
@@ -215,7 +215,7 @@ void SourceExprCommand::completeInstallable(EvalState & state, AddCompletions &
|
||||
auto evaluator = getEvaluator();
|
||||
|
||||
Expr & e = evaluator->parseExprFromFile(
|
||||
resolveExprPath(evaluator->paths.checkSourcePath(lookupFileArg(*evaluator, *file)))
|
||||
state.ctx.paths.resolveExprPath(lookupFileArg(*evaluator, *file))
|
||||
);
|
||||
|
||||
Value root;
|
||||
|
||||
+4
-4
@@ -655,7 +655,7 @@ ProcessLineResult NixRepl::processLine(std::string line)
|
||||
return {path, 0};
|
||||
} else if (v.isLambda()) {
|
||||
auto pos = evaluator.positions[v.lambda.fun->pos];
|
||||
if (auto path = std::get_if<SourcePath>(&pos.origin))
|
||||
if (auto path = std::get_if<CheckedSourcePath>(&pos.origin))
|
||||
return {*path, pos.line};
|
||||
else
|
||||
throw Error("'%s' cannot be shown in an editor", pos);
|
||||
@@ -676,7 +676,7 @@ ProcessLineResult NixRepl::processLine(std::string line)
|
||||
|
||||
// Reload right after exiting the editor if path is not in store
|
||||
// Store is immutable, so there could be no changes, so there's no need to reload
|
||||
if (!evaluator.store->isInStore(path.resolveSymlinks().path.abs())) {
|
||||
if (!evaluator.store->isInStore(canonPath(path.canonical().abs(), true))) {
|
||||
state.resetFileCache();
|
||||
reloadFiles();
|
||||
}
|
||||
@@ -820,7 +820,7 @@ ProcessLineResult NixRepl::processLine(std::string line)
|
||||
logger->cout(trim(renderMarkdownToTerminal(markdown)));
|
||||
} else if (v.isLambda()) {
|
||||
auto pos = evaluator.positions[v.lambda.fun->pos];
|
||||
if (auto path = std::get_if<SourcePath>(&pos.origin)) {
|
||||
if (auto path = std::get_if<CheckedSourcePath>(&pos.origin)) {
|
||||
// Path and position have now been obtained, feed to nix-doc library to get data.
|
||||
auto docComment = lambdaDocsForPos(*path, pos);
|
||||
if (!docComment) {
|
||||
@@ -1109,7 +1109,7 @@ void NixRepl::evalString(std::string s, Value & v)
|
||||
|
||||
Value * NixRepl::evalFile(SourcePath & path)
|
||||
{
|
||||
auto & expr = evaluator.parseExprFromFile(path, staticEnv);
|
||||
auto & expr = evaluator.parseExprFromFile(evaluator.paths.checkSourcePath(path), staticEnv);
|
||||
Value * result(evaluator.mem.allocValue());
|
||||
expr.eval(state, *env, *result);
|
||||
state.forceValue(*result, result->determinePos(noPos));
|
||||
|
||||
@@ -178,7 +178,7 @@ std::pair<SourcePath, uint32_t> findPackageFilename(EvalState & state, Value & v
|
||||
NixStringContext context;
|
||||
auto path = state.coerceToPath(noPos, *v2, context, "while evaluating the 'meta.position' attribute of a derivation");
|
||||
|
||||
auto fn = path.path.abs();
|
||||
auto fn = path.canonical().abs();
|
||||
|
||||
auto fail = [fn]() {
|
||||
throw ParseError("cannot parse 'meta.position' attribute '%s'", fn);
|
||||
|
||||
@@ -439,8 +439,8 @@ Value & AttrCursor::forceValue(EvalState & state)
|
||||
cachedValue = {root->db->setString(getKey(), v.string.s, v.string.context),
|
||||
string_t{v.string.s, {}}};
|
||||
else if (v.type() == nPath) {
|
||||
auto path = v.path().path;
|
||||
cachedValue = {root->db->setString(getKey(), path.abs()), string_t{path.abs(), {}}};
|
||||
auto path = v.path().canonical().abs();
|
||||
cachedValue = {root->db->setString(getKey(), path), string_t{path, {}}};
|
||||
}
|
||||
else if (v.type() == nBool)
|
||||
cachedValue = {root->db->setBool(getKey(), v.boolean), v.boolean};
|
||||
|
||||
+93
-45
@@ -282,7 +282,7 @@ EvalPaths::EvalPaths(
|
||||
, errors(errors)
|
||||
{
|
||||
if (evalSettings.restrictEval || evalSettings.pureEval) {
|
||||
allowedPaths = std::optional(PathSet());
|
||||
allowedPaths = AllowedPath{.allowAllChildren = false};
|
||||
|
||||
for (auto & i : searchPath_.elements) {
|
||||
auto r = resolveSearchPathPath(i.path);
|
||||
@@ -362,14 +362,23 @@ EvalState::~EvalState()
|
||||
|
||||
void EvalPaths::allowPath(const Path & path)
|
||||
{
|
||||
if (allowedPaths)
|
||||
allowedPaths->insert(path);
|
||||
if (!allowedPaths) {
|
||||
return;
|
||||
}
|
||||
|
||||
CanonPath p(path);
|
||||
auto * level = &*allowedPaths;
|
||||
for (const auto & entry : p) {
|
||||
level = &level->children.emplace(std::piecewise_construct, std::tuple(entry), std::tuple())
|
||||
.first->second;
|
||||
}
|
||||
level->allowAllChildren = true;
|
||||
}
|
||||
|
||||
void EvalPaths::allowPath(const StorePath & storePath)
|
||||
{
|
||||
if (allowedPaths)
|
||||
allowedPaths->insert(store->toRealPath(storePath));
|
||||
allowPath(store->toRealPath(storePath));
|
||||
}
|
||||
|
||||
void EvalPaths::allowAndSetStorePathString(const StorePath & storePath, Value & v)
|
||||
@@ -379,50 +388,86 @@ void EvalPaths::allowAndSetStorePathString(const StorePath & storePath, Value &
|
||||
mkStorePathString(storePath, v);
|
||||
}
|
||||
|
||||
SourcePath EvalPaths::checkSourcePath(const SourcePath & path_)
|
||||
CheckedSourcePath EvalPaths::checkSourcePath(const SourcePath & path_)
|
||||
{
|
||||
if (!allowedPaths) return path_;
|
||||
if (!allowedPaths) return auto(path_).unsafeIntoChecked();
|
||||
|
||||
auto i = resolvedPaths.find(path_.path.abs());
|
||||
auto i = resolvedPaths.find(path_.canonical().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());
|
||||
const CanonPath abspath{path_.canonical().abs()};
|
||||
|
||||
if (abspath.starts_with(corepkgsPrefix)) return CanonPath(abspath);
|
||||
|
||||
for (auto & i : *allowedPaths) {
|
||||
if (isDirOrInDir(abspath, i)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
if (abspath.abs().starts_with(corepkgsPrefix)) {
|
||||
return SourcePath(std::move(abspath)).unsafeIntoChecked();
|
||||
}
|
||||
|
||||
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. */
|
||||
/* Resolve symlinks. This is mostly restricted copy of canonPath with
|
||||
resolveSymlinks=true, because we need access to intermediat paths. */
|
||||
debug("checking access to '%s'", abspath);
|
||||
SourcePath path = CanonPath(canonPath(abspath, true));
|
||||
|
||||
for (auto & i : *allowedPaths) {
|
||||
if (isDirOrInDir(path.path.abs(), i)) {
|
||||
resolvedPaths.insert_or_assign(path_.path.abs(), path);
|
||||
return path;
|
||||
}
|
||||
/* Count the number of times we follow a symlink and stop at some
|
||||
arbitrary (but high) limit to prevent infinite loops. */
|
||||
unsigned int followCount = 0, maxFollow = 1024;
|
||||
|
||||
std::optional<CanonPath> componentsBacking;
|
||||
std::vector<std::string_view> components(abspath.begin(), abspath.end());
|
||||
|
||||
retry:
|
||||
|
||||
if (++followCount >= maxFollow) {
|
||||
throw Error("infinite symlink recursion in path '%1%'", path_);
|
||||
}
|
||||
|
||||
throw RestrictedPathError("access to canonical path '%1%' is forbidden in restricted mode", path);
|
||||
// TODO: tests for this stuff
|
||||
const auto * level = &*allowedPaths;
|
||||
CheckedSourcePath current = SourcePath(CanonPath::root).unsafeIntoChecked();
|
||||
for (auto ct = components.begin(); ct != components.end(); ct++) {
|
||||
auto & p = *ct;
|
||||
// an empty level means all subpaths are allowed, propagate this forwards
|
||||
// by setting level=nullptr for the subsequent checks. a symlink will set
|
||||
// level to the "VFS" root and restart the check with a the resolved path
|
||||
if (level) {
|
||||
if (level->allowAllChildren) {
|
||||
level = nullptr;
|
||||
} else if (auto it = level->children.find(p); it != level->children.end()) {
|
||||
level = &it->second;
|
||||
} else {
|
||||
goto failed;
|
||||
}
|
||||
}
|
||||
auto next = (current + p).unsafeIntoChecked();
|
||||
auto st = next.maybeLstat();
|
||||
// resolve symlinks, treating nonexistant components like regular directories.
|
||||
// this mirrors canonPath behavior and is necessary for `builtins.pathExists`.
|
||||
if (st && st->type == InputAccessor::tSymlink) {
|
||||
auto target = next.readLink();
|
||||
auto levelResolved = target.starts_with("/")
|
||||
? CanonPath(target)
|
||||
: CanonPath(current.canonical().abs() + "/" + target);
|
||||
for (ct++; ct != components.end(); ct++) {
|
||||
levelResolved.push(*ct);
|
||||
}
|
||||
components = {levelResolved.begin(), levelResolved.end()};
|
||||
componentsBacking = std::move(levelResolved);
|
||||
followCount += 1;
|
||||
goto retry;
|
||||
}
|
||||
current = std::move(next);
|
||||
}
|
||||
|
||||
resolvedPaths.insert_or_assign(path_.canonical().abs(), current);
|
||||
return current;
|
||||
|
||||
failed:
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -803,9 +848,9 @@ void Evaluator::evalLazily(Expr & e, Value & v)
|
||||
void EvalState::mkPos(Value & v, PosIdx p)
|
||||
{
|
||||
auto origin = ctx.positions.originOf(p);
|
||||
if (auto path = std::get_if<SourcePath>(&origin)) {
|
||||
if (auto path = std::get_if<CheckedSourcePath>(&origin)) {
|
||||
auto attrs = ctx.buildBindings(3);
|
||||
attrs.alloc(ctx.s.file).mkString(path->path.abs());
|
||||
attrs.alloc(ctx.s.file).mkString(path->to_string());
|
||||
makePositionThunks(*this, p, attrs.alloc(ctx.s.line), attrs.alloc(ctx.s.column));
|
||||
v.mkAttrs(attrs);
|
||||
} else
|
||||
@@ -951,7 +996,7 @@ void EvalState::evalFile(const SourcePath & path_, Value & v)
|
||||
return;
|
||||
}
|
||||
|
||||
auto resolvedPath = resolveExprPath(path);
|
||||
auto resolvedPath = ctx.paths.resolveExprPath(path);
|
||||
if (auto i = ctx.caches.fileEval.find(resolvedPath); i != ctx.caches.fileEval.end()) {
|
||||
v = i->second->result;
|
||||
return;
|
||||
@@ -2269,7 +2314,7 @@ BackedStringView EvalState::coerceToString(
|
||||
v._path
|
||||
: copyToStore
|
||||
? ctx.store->printStorePath(ctx.paths.copyPathToStore(context, v.path(), ctx.repair))
|
||||
: std::string(v.path().path.abs());
|
||||
: v.path().to_string();
|
||||
}
|
||||
|
||||
if (v.type() == nAttrs) {
|
||||
@@ -2339,7 +2384,7 @@ BackedStringView EvalState::coerceToString(
|
||||
|
||||
StorePath EvalPaths::copyPathToStore(NixStringContext & context, const SourcePath & path, RepairFlag repair)
|
||||
{
|
||||
if (nix::isDerivation(path.path.abs()))
|
||||
if (nix::isDerivation(path.canonical().abs()))
|
||||
errors.make<EvalError>("file names are not allowed to end in '%1%'", drvExtension).debugThrow();
|
||||
|
||||
auto i = srcToStore.find(path);
|
||||
@@ -2347,7 +2392,7 @@ StorePath EvalPaths::copyPathToStore(NixStringContext & context, const SourcePat
|
||||
auto dstPath = i != srcToStore.end()
|
||||
? i->second
|
||||
: [&]() {
|
||||
auto dstPath = fetchToStore(*store, path, path.baseName(), FileIngestionMethod::Recursive, nullptr, repair);
|
||||
auto dstPath = fetchToStore(*store, checkSourcePath(path), path.baseName(), FileIngestionMethod::Recursive, nullptr, repair);
|
||||
allowPath(dstPath);
|
||||
srcToStore.insert_or_assign(path, dstPath);
|
||||
printMsg(lvlChatty, "copied source '%1%' -> '%2%'", path, store->printStorePath(dstPath));
|
||||
@@ -2621,7 +2666,7 @@ void Evaluator::printStatistics()
|
||||
else
|
||||
obj["name"] = nullptr;
|
||||
if (auto pos = positions[fun->pos]) {
|
||||
if (auto path = std::get_if<SourcePath>(&pos.origin))
|
||||
if (auto path = std::get_if<CheckedSourcePath>(&pos.origin))
|
||||
obj["file"] = path->to_string();
|
||||
obj["line"] = pos.line;
|
||||
obj["column"] = pos.column;
|
||||
@@ -2636,7 +2681,7 @@ void Evaluator::printStatistics()
|
||||
for (auto & i : stats.attrSelects) {
|
||||
json obj = json::object();
|
||||
if (auto pos = positions[i.first]) {
|
||||
if (auto path = std::get_if<SourcePath>(&pos.origin))
|
||||
if (auto path = std::get_if<CheckedSourcePath>(&pos.origin))
|
||||
obj["file"] = path->to_string();
|
||||
obj["line"] = pos.line;
|
||||
obj["column"] = pos.column;
|
||||
@@ -2661,8 +2706,9 @@ void Evaluator::printStatistics()
|
||||
}
|
||||
|
||||
|
||||
SourcePath resolveExprPath(SourcePath path)
|
||||
CheckedSourcePath EvalPaths::resolveExprPath(SourcePath path_)
|
||||
{
|
||||
auto path = checkSourcePath(path_);
|
||||
unsigned int followCount = 0, maxFollow = 1024;
|
||||
|
||||
/* If `path' is a symlink, follow it. This is so that relative
|
||||
@@ -2672,24 +2718,26 @@ SourcePath resolveExprPath(SourcePath path)
|
||||
if (++followCount >= maxFollow)
|
||||
throw Error("too many symbolic links encountered while traversing the path '%s'", path);
|
||||
if (path.lstat().type != InputAccessor::tSymlink) break;
|
||||
path = {CanonPath(path.readLink(), path.path.parent().value_or(CanonPath::root))};
|
||||
path = checkSourcePath(
|
||||
CanonPath(path.readLink(), path.canonical().parent().value_or(CanonPath::root))
|
||||
);
|
||||
}
|
||||
|
||||
/* If `path' refers to a directory, append `/default.nix'. */
|
||||
if (path.lstat().type == InputAccessor::tDirectory)
|
||||
return path + "default.nix";
|
||||
return checkSourcePath(path + "default.nix");
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
|
||||
Expr & Evaluator::parseExprFromFile(const SourcePath & path)
|
||||
Expr & Evaluator::parseExprFromFile(const CheckedSourcePath & path)
|
||||
{
|
||||
return parseExprFromFile(path, builtins.staticEnv);
|
||||
}
|
||||
|
||||
|
||||
Expr & Evaluator::parseExprFromFile(const SourcePath & path, std::shared_ptr<StaticEnv> & staticEnv)
|
||||
Expr & Evaluator::parseExprFromFile(const CheckedSourcePath & path, std::shared_ptr<StaticEnv> & staticEnv)
|
||||
{
|
||||
auto buffer = path.readFile();
|
||||
return *parse(buffer.data(), buffer.size(), Pos::Origin(path), path.parent(), staticEnv);
|
||||
|
||||
+24
-11
@@ -376,13 +376,26 @@ public:
|
||||
|
||||
const SearchPath & searchPath() const { return searchPath_; }
|
||||
|
||||
private:
|
||||
struct AllowedPath
|
||||
{
|
||||
struct ComponentLess : std::less<>
|
||||
{
|
||||
// we'll only use this for string-likes, it's fine. trust me sis.
|
||||
using is_transparent = void;
|
||||
};
|
||||
|
||||
std::map<std::string, AllowedPath, ComponentLess> children;
|
||||
|
||||
bool allowAllChildren = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* The allowed filesystem paths in restricted or pure evaluation
|
||||
* mode.
|
||||
*/
|
||||
std::optional<PathSet> allowedPaths;
|
||||
std::optional<AllowedPath> allowedPaths;
|
||||
|
||||
private:
|
||||
|
||||
/* Cache for calls to addToStore(); maps source paths to the store
|
||||
paths. */
|
||||
@@ -393,7 +406,7 @@ private:
|
||||
/**
|
||||
* Cache used by checkSourcePath().
|
||||
*/
|
||||
std::unordered_map<Path, SourcePath> resolvedPaths;
|
||||
std::unordered_map<Path, CheckedSourcePath> resolvedPaths;
|
||||
|
||||
public:
|
||||
/**
|
||||
@@ -416,7 +429,12 @@ public:
|
||||
* Check whether access to a path is allowed and throw an error if
|
||||
* not. Otherwise return the canonicalised path.
|
||||
*/
|
||||
SourcePath checkSourcePath(const SourcePath & path);
|
||||
CheckedSourcePath checkSourcePath(const SourcePath & path);
|
||||
|
||||
/**
|
||||
* If `path` refers to a directory, then append "/default.nix".
|
||||
*/
|
||||
CheckedSourcePath resolveExprPath(SourcePath path);
|
||||
|
||||
void checkURI(const std::string & uri);
|
||||
|
||||
@@ -531,8 +549,8 @@ public:
|
||||
/**
|
||||
* Parse a Nix expression from the specified file.
|
||||
*/
|
||||
Expr & parseExprFromFile(const SourcePath & path);
|
||||
Expr & parseExprFromFile(const SourcePath & path, std::shared_ptr<StaticEnv> & staticEnv);
|
||||
Expr & parseExprFromFile(const CheckedSourcePath & path);
|
||||
Expr & parseExprFromFile(const CheckedSourcePath & path, std::shared_ptr<StaticEnv> & staticEnv);
|
||||
|
||||
/**
|
||||
* Parse a Nix expression from the specified string.
|
||||
@@ -848,11 +866,6 @@ private:
|
||||
std::string_view showType(ValueType type, bool withArticle = true);
|
||||
std::string showType(const Value & v);
|
||||
|
||||
/**
|
||||
* If `path` refers to a directory, then append "/default.nix".
|
||||
*/
|
||||
SourcePath resolveExprPath(SourcePath path);
|
||||
|
||||
static constexpr std::string_view corepkgsPrefix{"/__corepkgs__/"};
|
||||
|
||||
|
||||
|
||||
@@ -242,8 +242,8 @@ static Flake getFlake(
|
||||
};
|
||||
|
||||
// FIXME: symlink attack
|
||||
auto resolvedFlakeFile = resolveExprPath(state.ctx.paths.checkSourcePath(CanonPath(flakeFile)));
|
||||
Expr & flakeExpr = state.ctx.parseExprFromFile(state.ctx.paths.checkSourcePath(resolvedFlakeFile));
|
||||
auto resolvedFlakeFile = state.ctx.paths.resolveExprPath(CanonPath(flakeFile));
|
||||
Expr & flakeExpr = state.ctx.parseExprFromFile(resolvedFlakeFile);
|
||||
|
||||
// Enforce that 'flake.nix' is a direct attrset, not a computation.
|
||||
if (!(dynamic_cast<ExprAttrs *>(&flakeExpr))) {
|
||||
|
||||
@@ -605,7 +605,7 @@ template<> struct BuildAST<grammar::v1::path::interpolation> : BuildAST<grammar:
|
||||
|
||||
template<> struct BuildAST<grammar::v1::path::anchor> {
|
||||
static void apply(const auto & in, StringState & s, State & ps) {
|
||||
Path path(absPath(in.string(), ps.basePath.path.abs()));
|
||||
Path path(absPath(in.string(), ps.basePath.canonical().abs()));
|
||||
/* add back in the trailing '/' to the first segment */
|
||||
if (in.string_view().ends_with('/') && in.size() > 1)
|
||||
path += "/";
|
||||
|
||||
+34
-29
@@ -117,12 +117,7 @@ StringMap EvalPaths::realiseContext(const NixStringContext & context)
|
||||
return res;
|
||||
}
|
||||
|
||||
struct RealisePathFlags {
|
||||
// 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 = {})
|
||||
static auto realisePath(EvalState & state, const PosIdx pos, Value & v, auto checkFn)
|
||||
{
|
||||
NixStringContext context;
|
||||
|
||||
@@ -131,17 +126,20 @@ static SourcePath realisePath(EvalState & state, const PosIdx pos, Value & v, co
|
||||
try {
|
||||
StringMap rewrites = state.ctx.paths.realiseContext(context);
|
||||
|
||||
auto realPath = CanonPath(state.ctx.paths.toRealPath(rewriteStrings(path.path.abs(), rewrites), context));
|
||||
|
||||
return flags.checkForPureEval
|
||||
? state.ctx.paths.checkSourcePath(realPath)
|
||||
: realPath;
|
||||
return checkFn(SourcePath(CanonPath(
|
||||
state.ctx.paths.toRealPath(rewriteStrings(path.canonical().abs(), rewrites), context)
|
||||
)));
|
||||
} catch (Error & e) {
|
||||
e.addTrace(state.ctx.positions[pos], "while realising the context of path '%s'", path);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
static CheckedSourcePath realisePath(EvalState & state, const PosIdx pos, Value & v)
|
||||
{
|
||||
return realisePath(state, pos, v, [&](auto p) { return state.ctx.paths.checkSourcePath(p); });
|
||||
}
|
||||
|
||||
/**
|
||||
* Add and attribute to the given attribute map from the output name to
|
||||
* the output path, or a placeholder.
|
||||
@@ -176,7 +174,7 @@ static void mkOutputString(
|
||||
static void import(EvalState & state, const PosIdx pos, Value & vPath, Value * vScope, Value & v)
|
||||
{
|
||||
auto path = realisePath(state, pos, vPath);
|
||||
auto path2 = path.path.abs();
|
||||
auto path2 = path.canonical().abs();
|
||||
|
||||
// FIXME
|
||||
auto isValidDerivationInStore = [&]() -> std::optional<StorePath> {
|
||||
@@ -251,7 +249,7 @@ static void import(EvalState & state, const PosIdx pos, Value & vPath, Value * v
|
||||
// args[0]->attrs is already sorted.
|
||||
|
||||
debug("evaluating file '%1%'", path);
|
||||
Expr & e = state.ctx.parseExprFromFile(resolveExprPath(path), staticEnv);
|
||||
Expr & e = state.ctx.parseExprFromFile(state.ctx.paths.resolveExprPath(path), staticEnv);
|
||||
|
||||
e.eval(state, *env, v);
|
||||
}
|
||||
@@ -283,7 +281,7 @@ void prim_importNative(EvalState & state, const PosIdx pos, Value * * args, Valu
|
||||
|
||||
std::string sym(state.forceStringNoCtx(*args[1], pos, "while evaluating the second argument passed to builtins.importNative"));
|
||||
|
||||
void *handle = dlopen(path.path.c_str(), RTLD_LAZY | RTLD_LOCAL);
|
||||
void *handle = dlopen(path.canonical().c_str(), RTLD_LAZY | RTLD_LOCAL);
|
||||
if (!handle)
|
||||
state.ctx.errors.make<EvalError>("could not open '%1%': %2%", path, dlerror()).debugThrow();
|
||||
|
||||
@@ -1153,7 +1151,7 @@ static void prim_toPath(EvalState & state, const PosIdx pos, Value * * args, Val
|
||||
{
|
||||
NixStringContext context;
|
||||
auto path = state.coerceToPath(pos, *args[0], context, "while evaluating the first argument passed to builtins.toPath");
|
||||
v.mkString(path.path.abs(), context);
|
||||
v.mkString(path.to_string(), context);
|
||||
}
|
||||
|
||||
/* Allow a valid store path to be used in an expression. This is
|
||||
@@ -1173,7 +1171,7 @@ static void prim_storePath(EvalState & state, const PosIdx pos, Value * * args,
|
||||
).atPos(pos).debugThrow();
|
||||
|
||||
NixStringContext context;
|
||||
auto path = state.ctx.paths.checkSourcePath(state.coerceToPath(pos, *args[0], context, "while evaluating the first argument passed to builtins.storePath")).path;
|
||||
auto path = state.ctx.paths.checkSourcePath(state.coerceToPath(pos, *args[0], context, "while evaluating the first argument passed to builtins.storePath")).canonical();
|
||||
/* Resolve symlinks in ‘path’, unless ‘path’ itself is a symlink
|
||||
directly in the store. The latter condition is necessary so
|
||||
e.g. nix-push does the right thing. */
|
||||
@@ -1198,7 +1196,7 @@ static void prim_pathExists(EvalState & state, const PosIdx pos, Value * * args,
|
||||
can’t 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 });
|
||||
auto path = realisePath(state, pos, arg, std::identity{});
|
||||
|
||||
/* SourcePath doesn't know about trailing slash. */
|
||||
auto mustBeDir = arg.type() == nString
|
||||
@@ -1206,12 +1204,13 @@ static void prim_pathExists(EvalState & state, const PosIdx pos, Value * * args,
|
||||
|| arg.str().ends_with("/."));
|
||||
|
||||
try {
|
||||
auto checked = state
|
||||
.ctx.paths
|
||||
.checkSourcePath(path)
|
||||
.resolveSymlinks(mustBeDir ? SymlinkResolution::Full : SymlinkResolution::Ancestors);
|
||||
auto checked = state.ctx.paths.checkSourcePath(path);
|
||||
|
||||
auto st = checked.maybeLstat();
|
||||
// previously we fully resolved symlinks in the mustBeDir case or in pure eval
|
||||
// mode (by accident, since checkSourcePath does this in that case), and up to
|
||||
// the last component otherwise. this is equivalent to calling stat and lstat,
|
||||
// respectively. (in neither case do intermediate symlinks affect the result.)
|
||||
auto st = mustBeDir ? checked.maybeStat() : checked.maybeLstat();
|
||||
auto exists = st && (!mustBeDir || st->type == InputAccessor::tDirectory);
|
||||
v.mkBool(exists);
|
||||
} catch (SysError & e) {
|
||||
@@ -1241,7 +1240,7 @@ static void prim_dirOf(EvalState & state, const PosIdx pos, Value * * args, Valu
|
||||
state.forceValue(*args[0], pos);
|
||||
if (args[0]->type() == nPath) {
|
||||
auto path = args[0]->path();
|
||||
v.mkPath(path.path.isRoot() ? path : path.parent());
|
||||
v.mkPath(path.canonical().isRoot() ? path : path.parent());
|
||||
} else {
|
||||
NixStringContext context;
|
||||
auto path = state.coerceToString(pos, *args[0], context,
|
||||
@@ -1263,9 +1262,9 @@ static void prim_readFile(EvalState & state, const PosIdx pos, Value * * args, V
|
||||
path
|
||||
).atPos(pos).debugThrow();
|
||||
StorePathSet refs;
|
||||
if (state.ctx.store->isInStore(path.path.abs())) {
|
||||
if (state.ctx.store->isInStore(path.canonical().abs())) {
|
||||
try {
|
||||
refs = state.ctx.store->queryPathInfo(state.ctx.store->toStorePath(path.path.abs()).first)->references;
|
||||
refs = state.ctx.store->queryPathInfo(state.ctx.store->toStorePath(path.canonical().abs()).first)->references;
|
||||
} catch (Error &) { // FIXME: should be InvalidPathError
|
||||
}
|
||||
// Re-scan references to filter down to just the ones that actually occur in the file.
|
||||
@@ -1513,7 +1512,7 @@ static void addPath(
|
||||
|
||||
path = evalSettings.pureEval && expectedHash
|
||||
? path
|
||||
: state.ctx.paths.checkSourcePath(CanonPath(path)).path.abs();
|
||||
: state.ctx.paths.checkSourcePath(CanonPath(path)).canonical().abs();
|
||||
|
||||
PathFilter filter = filterFun ? ([&](const Path & path) {
|
||||
auto st = lstat(path);
|
||||
@@ -1547,7 +1546,13 @@ static void addPath(
|
||||
|
||||
if (!expectedHash || !state.ctx.store->isValidPath(*expectedStorePath)) {
|
||||
auto dstPath = fetchToStore(
|
||||
*state.ctx.store, CanonPath(path), name, method, &filter, state.ctx.repair);
|
||||
*state.ctx.store,
|
||||
state.ctx.paths.checkSourcePath(CanonPath(path)),
|
||||
name,
|
||||
method,
|
||||
&filter,
|
||||
state.ctx.repair
|
||||
);
|
||||
if (expectedHash && expectedStorePath != dstPath)
|
||||
state.ctx.errors.make<EvalError>(
|
||||
"store path mismatch in (possibly filtered) path added from '%s'",
|
||||
@@ -1569,7 +1574,7 @@ static void prim_filterSource(EvalState & state, const PosIdx pos, Value * * arg
|
||||
auto path = state.coerceToPath(pos, *args[1], context,
|
||||
"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");
|
||||
addPath(state, pos, path.baseName(), path.path.abs(), args[0], FileIngestionMethod::Recursive, std::nullopt, v, context);
|
||||
addPath(state, pos, path.baseName(), path.canonical().abs(), args[0], FileIngestionMethod::Recursive, std::nullopt, v, context);
|
||||
}
|
||||
|
||||
static void prim_path(EvalState & state, const PosIdx pos, Value * * args, Value & v)
|
||||
@@ -1608,7 +1613,7 @@ static void prim_path(EvalState & state, const PosIdx pos, Value * * args, Value
|
||||
if (name.empty())
|
||||
name = path->baseName();
|
||||
|
||||
addPath(state, pos, name, path->path.abs(), filterFun, method, expectedHash, v, context);
|
||||
addPath(state, pos, name, path->canonical().abs(), filterFun, method, expectedHash, v, context);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ json printValueAsJSON(EvalState & state, bool strict,
|
||||
out = state.ctx.store->printStorePath(
|
||||
state.ctx.paths.copyPathToStore(context, v.path(), state.ctx.repair));
|
||||
else
|
||||
out = v.path().path.abs();
|
||||
out = v.path().to_string();
|
||||
break;
|
||||
|
||||
case nNull:
|
||||
|
||||
@@ -21,8 +21,8 @@ static void printValueAsXML(EvalState & state, bool strict, bool location,
|
||||
|
||||
static void posToXML(EvalState & state, XMLAttrs & xmlAttrs, const Pos & pos)
|
||||
{
|
||||
if (auto path = std::get_if<SourcePath>(&pos.origin))
|
||||
xmlAttrs["path"] = path->path.abs();
|
||||
if (auto path = std::get_if<CheckedSourcePath>(&pos.origin))
|
||||
xmlAttrs["path"] = path->to_string();
|
||||
xmlAttrs["line"] = fmt("%1%", pos.line);
|
||||
xmlAttrs["column"] = fmt("%1%", pos.column);
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ void Value::mkStringMove(const char * s, const NixStringContext & context)
|
||||
|
||||
void Value::mkPath(const SourcePath & path)
|
||||
{
|
||||
mkPath(gcCopyStringIfNeeded(path.path.abs()));
|
||||
*this = Value(NewValueAs::path, path);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -347,7 +347,7 @@ public:
|
||||
/// dynamic (GC) allocation to do so.
|
||||
Value(path_t, SourcePath const & path)
|
||||
: internalType(tPath)
|
||||
, _path(gcCopyStringIfNeeded(path.path.abs()))
|
||||
, _path(gcCopyStringIfNeeded(path.canonical().abs()))
|
||||
, _path_pad(0)
|
||||
{ }
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace nix {
|
||||
|
||||
StorePath fetchToStore(
|
||||
Store & store,
|
||||
const SourcePath & path,
|
||||
const CheckedSourcePath & path,
|
||||
std::string_view name,
|
||||
FileIngestionMethod method,
|
||||
PathFilter * filter,
|
||||
@@ -18,8 +18,8 @@ StorePath fetchToStore(
|
||||
|
||||
return
|
||||
settings.readOnlyMode
|
||||
? store.computeStorePathForPath(name, path.path.abs(), method, HashType::SHA256, filter2).first
|
||||
: store.addToStore(name, path.path.abs(), method, HashType::SHA256, filter2, repair);
|
||||
? store.computeStorePathForPath(name, path.canonical().abs(), method, HashType::SHA256, filter2).first
|
||||
: store.addToStore(name, path.canonical().abs(), method, HashType::SHA256, filter2, repair);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace nix {
|
||||
*/
|
||||
StorePath fetchToStore(
|
||||
Store & store,
|
||||
const SourcePath & path,
|
||||
const CheckedSourcePath & path,
|
||||
std::string_view name = "source",
|
||||
FileIngestionMethod method = FileIngestionMethod::Recursive,
|
||||
PathFilter * filter = nullptr,
|
||||
|
||||
@@ -84,6 +84,11 @@ public:
|
||||
|
||||
struct Iterator
|
||||
{
|
||||
using difference_type = void;
|
||||
using value_type = std::string_view;
|
||||
using reference = std::string_view;
|
||||
using iterator_category = std::input_iterator_tag;
|
||||
|
||||
std::string_view remaining;
|
||||
size_t slash;
|
||||
|
||||
@@ -101,7 +106,7 @@ public:
|
||||
const std::string_view operator * () const
|
||||
{ return remaining.substr(0, slash); }
|
||||
|
||||
void operator ++ ()
|
||||
Iterator & operator ++ ()
|
||||
{
|
||||
if (slash == remaining.npos)
|
||||
remaining = remaining.substr(remaining.size());
|
||||
@@ -109,6 +114,14 @@ public:
|
||||
remaining = remaining.substr(slash + 1);
|
||||
slash = remaining.find('/');
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
Iterator operator++(int)
|
||||
{
|
||||
auto result = *this;
|
||||
++*this;
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ std::optional<std::string> Pos::getSource() const
|
||||
// Get rid of the null terminators added by the parser.
|
||||
return std::string(s.source->c_str());
|
||||
},
|
||||
[](const SourcePath & path) -> std::optional<std::string> {
|
||||
[](const CheckedSourcePath & path) -> std::optional<std::string> {
|
||||
try {
|
||||
return path.readFile();
|
||||
} catch (Error &) {
|
||||
|
||||
@@ -42,7 +42,7 @@ struct Pos
|
||||
auto operator<=>(const Hidden &) const = default;
|
||||
};
|
||||
|
||||
typedef std::variant<std::monostate, Stdin, String, SourcePath, Hidden> Origin;
|
||||
typedef std::variant<std::monostate, Stdin, String, CheckedSourcePath, Hidden> Origin;
|
||||
|
||||
Origin origin = std::monostate();
|
||||
|
||||
|
||||
+28
-46
@@ -1,4 +1,5 @@
|
||||
#include "lix/libutil/source-path.hh"
|
||||
#include "file-system.hh"
|
||||
#include "lix/libutil/strings.hh"
|
||||
|
||||
namespace nix {
|
||||
@@ -21,9 +22,8 @@ SourcePath SourcePath::parent() const
|
||||
return std::move(*p);
|
||||
}
|
||||
|
||||
InputAccessor::Stat SourcePath::lstat() const
|
||||
static InputAccessor::Stat convertStat(const struct stat & st)
|
||||
{
|
||||
auto st = nix::lstat(path.abs());
|
||||
return InputAccessor::Stat {
|
||||
.type =
|
||||
S_ISREG(st.st_mode) ? InputAccessor::tRegular :
|
||||
@@ -34,15 +34,35 @@ InputAccessor::Stat SourcePath::lstat() const
|
||||
};
|
||||
}
|
||||
|
||||
std::optional<InputAccessor::Stat> SourcePath::maybeLstat() const
|
||||
InputAccessor::Stat CheckedSourcePath::lstat() const
|
||||
{
|
||||
// FIXME: merge these into one operation.
|
||||
if (!pathExists())
|
||||
return {};
|
||||
return lstat();
|
||||
return convertStat(nix::lstat(path.abs()));
|
||||
}
|
||||
|
||||
InputAccessor::DirEntries SourcePath::readDirectory() const
|
||||
std::optional<InputAccessor::Stat> CheckedSourcePath::maybeLstat() const
|
||||
{
|
||||
if (auto st = nix::maybeLstat(path.abs())) {
|
||||
return convertStat(*st);
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
InputAccessor::Stat CheckedSourcePath::stat() const
|
||||
{
|
||||
return convertStat(nix::stat(path.abs()));
|
||||
}
|
||||
|
||||
std::optional<InputAccessor::Stat> CheckedSourcePath::maybeStat() const
|
||||
{
|
||||
if (auto st = nix::maybeStat(path.abs())) {
|
||||
return convertStat(*st);
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
InputAccessor::DirEntries CheckedSourcePath::readDirectory() const
|
||||
{
|
||||
InputAccessor::DirEntries res;
|
||||
for (auto & entry : nix::readDirectory(path.abs())) {
|
||||
@@ -58,42 +78,4 @@ InputAccessor::DirEntries SourcePath::readDirectory() const
|
||||
return res;
|
||||
}
|
||||
|
||||
SourcePath SourcePath::resolveSymlinks(SymlinkResolution mode) const
|
||||
{
|
||||
SourcePath res(CanonPath::root);
|
||||
|
||||
int linksAllowed = 1024;
|
||||
|
||||
std::list<std::string> todo;
|
||||
for (auto & c : path)
|
||||
todo.push_back(std::string(c));
|
||||
|
||||
bool resolve_last = mode == SymlinkResolution::Full;
|
||||
|
||||
while (!todo.empty()) {
|
||||
auto c = *todo.begin();
|
||||
todo.pop_front();
|
||||
if (c == "" || c == ".")
|
||||
;
|
||||
else if (c == "..")
|
||||
res.path.pop();
|
||||
else {
|
||||
res.path.push(c);
|
||||
if (resolve_last || !todo.empty()) {
|
||||
if (auto st = res.maybeLstat(); st && st->type == InputAccessor::tSymlink) {
|
||||
if (!linksAllowed--)
|
||||
throw Error("infinite symlink recursion in path '%s'", path);
|
||||
auto target = res.readLink();
|
||||
res.path.pop();
|
||||
if (target.starts_with("/"))
|
||||
res.path = CanonPath::root;
|
||||
todo.splice(todo.begin(), tokenizeString<std::list<std::string>>(target, "/"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+88
-86
@@ -14,36 +14,17 @@
|
||||
|
||||
namespace nix {
|
||||
|
||||
/**
|
||||
* Note there is a decent chance this type soon goes away because the problem is solved another way.
|
||||
* See the discussion in https://github.com/NixOS/nix/pull/9985.
|
||||
*/
|
||||
enum class SymlinkResolution {
|
||||
/**
|
||||
* Resolve symlinks in the ancestors only.
|
||||
*
|
||||
* Only the last component of the result is possibly a symlink.
|
||||
*/
|
||||
Ancestors,
|
||||
|
||||
/**
|
||||
* Resolve symlinks fully, realpath(3)-style.
|
||||
*
|
||||
* No component of the result will be a symlink.
|
||||
*/
|
||||
Full,
|
||||
};
|
||||
class CheckedSourcePath;
|
||||
|
||||
/**
|
||||
* An abstraction for accessing source files during
|
||||
* evaluation. Currently, it's just a wrapper around `CanonPath` that
|
||||
* accesses files in the regular filesystem, but in the future it will
|
||||
* support fetching files in other ways.
|
||||
* An abstraction for manipulating path names during evaluation.
|
||||
*/
|
||||
struct SourcePath
|
||||
{
|
||||
protected:
|
||||
CanonPath path;
|
||||
|
||||
public:
|
||||
SourcePath(CanonPath path)
|
||||
: path(std::move(path))
|
||||
{ }
|
||||
@@ -56,63 +37,16 @@ struct SourcePath
|
||||
*/
|
||||
SourcePath parent() const;
|
||||
|
||||
/**
|
||||
* If this `SourcePath` denotes a regular file (not a symlink),
|
||||
* return its contents; otherwise throw an error.
|
||||
*/
|
||||
std::string readFile() const
|
||||
{ return nix::readFile(path.abs()); }
|
||||
|
||||
/**
|
||||
* Return whether this `SourcePath` denotes a file (of any type)
|
||||
* that exists
|
||||
*/
|
||||
bool pathExists() const
|
||||
{ return nix::pathExists(path.abs()); }
|
||||
|
||||
/**
|
||||
* Return stats about this `SourcePath`, or throw an exception if
|
||||
* it doesn't exist.
|
||||
*/
|
||||
InputAccessor::Stat lstat() const;
|
||||
|
||||
/**
|
||||
* Return stats about this `SourcePath`, or std::nullopt if it
|
||||
* doesn't exist.
|
||||
*/
|
||||
std::optional<InputAccessor::Stat> maybeLstat() const;
|
||||
|
||||
/**
|
||||
* If this `SourcePath` denotes a directory (not a symlink),
|
||||
* return its directory entries; otherwise throw an error.
|
||||
*/
|
||||
InputAccessor::DirEntries readDirectory() const;
|
||||
|
||||
/**
|
||||
* If this `SourcePath` denotes a symlink, return its target;
|
||||
* otherwise throw an error.
|
||||
*/
|
||||
std::string readLink() const
|
||||
{ return nix::readLink(path.abs()); }
|
||||
|
||||
/**
|
||||
* Dump this `SourcePath` to `sink` as a NAR archive.
|
||||
*/
|
||||
void dumpPath(
|
||||
Sink & sink,
|
||||
PathFilter & filter = defaultPathFilter) const
|
||||
{ sink << nix::dumpPath(path.abs(), filter); }
|
||||
|
||||
/**
|
||||
* Return the location of this path in the "real" filesystem, if
|
||||
* it has a physical location.
|
||||
*/
|
||||
std::optional<CanonPath> getPhysicalPath() const
|
||||
{ return path; }
|
||||
const CanonPath & canonical() const { return path; }
|
||||
|
||||
std::string to_string() const
|
||||
{ return path.abs(); }
|
||||
|
||||
/**
|
||||
* Converts this `SourcePath` into a checked `SourcePath`, consuming it.
|
||||
*/
|
||||
CheckedSourcePath unsafeIntoChecked();
|
||||
|
||||
/**
|
||||
* Append a `CanonPath` to this path.
|
||||
*/
|
||||
@@ -141,18 +75,86 @@ struct SourcePath
|
||||
{
|
||||
return path < x.path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve any symlinks in this `SourcePath` according to the
|
||||
* given resolution mode.
|
||||
*
|
||||
* @param mode might only be a temporary solution for this.
|
||||
* See the discussion in https://github.com/NixOS/nix/pull/9985.
|
||||
*/
|
||||
SourcePath resolveSymlinks(
|
||||
SymlinkResolution mode = SymlinkResolution::Full) const;
|
||||
};
|
||||
|
||||
std::ostream & operator << (std::ostream & str, const SourcePath & path);
|
||||
|
||||
/**
|
||||
* An abstraction for accessing source files during
|
||||
* evaluation. Currently, it's just a wrapper around `CanonPath` that
|
||||
* accesses files in the regular filesystem, but in the future it will
|
||||
* support fetching files in other ways.
|
||||
*/
|
||||
class CheckedSourcePath : public SourcePath
|
||||
{
|
||||
friend struct SourcePath;
|
||||
|
||||
CheckedSourcePath(CanonPath path): SourcePath(std::move(path)) {}
|
||||
|
||||
public:
|
||||
/**
|
||||
* If this `SourcePath` denotes a regular file (not a symlink),
|
||||
* return its contents; otherwise throw an error.
|
||||
*/
|
||||
std::string readFile() const
|
||||
{ return nix::readFile(path.abs()); }
|
||||
|
||||
/**
|
||||
* Return whether this `SourcePath` denotes a file (of any type)
|
||||
* that exists
|
||||
*/
|
||||
bool pathExists() const
|
||||
{ return nix::pathExists(path.abs()); }
|
||||
|
||||
/**
|
||||
* Return stats about this `SourcePath`, or throw an exception if
|
||||
* it doesn't exist.
|
||||
*/
|
||||
InputAccessor::Stat lstat() const;
|
||||
|
||||
/**
|
||||
* Return stats about this `SourcePath`, or std::nullopt if it
|
||||
* doesn't exist.
|
||||
*/
|
||||
std::optional<InputAccessor::Stat> maybeLstat() const;
|
||||
|
||||
/**
|
||||
* Return stats about this `SourcePath`, or throw an exception if
|
||||
* it doesn't exist. Symlinks are resolved by this function.
|
||||
*/
|
||||
InputAccessor::Stat stat() const;
|
||||
|
||||
/**
|
||||
* Return stats about this `SourcePath`, or std::nullopt if it
|
||||
* doesn't exist. Symlinks are resolved by this function.
|
||||
*/
|
||||
std::optional<InputAccessor::Stat> maybeStat() const;
|
||||
|
||||
/**
|
||||
* If this `SourcePath` denotes a directory (not a symlink),
|
||||
* return its directory entries; otherwise throw an error.
|
||||
*/
|
||||
InputAccessor::DirEntries readDirectory() const;
|
||||
|
||||
/**
|
||||
* If this `SourcePath` denotes a symlink, return its target;
|
||||
* otherwise throw an error.
|
||||
*/
|
||||
std::string readLink() const
|
||||
{ return nix::readLink(path.abs()); }
|
||||
|
||||
/**
|
||||
* Dump this `SourcePath` to `sink` as a NAR archive.
|
||||
*/
|
||||
void dumpPath(
|
||||
Sink & sink,
|
||||
PathFilter & filter = defaultPathFilter) const
|
||||
{ sink << nix::dumpPath(path.abs(), filter); }
|
||||
};
|
||||
|
||||
inline CheckedSourcePath SourcePath::unsafeIntoChecked()
|
||||
{
|
||||
return CheckedSourcePath(std::move(path));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
-1
@@ -542,7 +542,9 @@ struct CmdFlakeCheck : FlakeCommand
|
||||
if (auto attr = v.attrs->get(evaluator->symbols.create("path"))) {
|
||||
if (attr->name == evaluator->symbols.create("path")) {
|
||||
NixStringContext context;
|
||||
auto path = state->coerceToPath(attr->pos, *attr->value, context, "");
|
||||
auto path = state->ctx.paths.checkSourcePath(
|
||||
state->coerceToPath(attr->pos, *attr->value, context, "")
|
||||
);
|
||||
if (!path.pathExists())
|
||||
throw Error("template '%s' refers to a non-existent path '%s'", attrPath, path);
|
||||
// TODO: recursively check the flake in 'path'.
|
||||
|
||||
+1
-1
@@ -201,7 +201,7 @@ static int main_nix_prefetch_url(std::string programName, Strings argv)
|
||||
} else {
|
||||
Value vRoot;
|
||||
state->evalFile(
|
||||
resolveExprPath(
|
||||
evaluator->paths.resolveExprPath(
|
||||
lookupFileArg(*evaluator, args.empty() ? "." : args[0])),
|
||||
vRoot);
|
||||
Value & v(*findAlongAttrPath(*state, attrPath, autoArgs, vRoot).first);
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
from functional2.testlib.fixtures import Nix
|
||||
import re
|
||||
|
||||
|
||||
def test_purity_traversal(nix: Nix, tmp_path: Path):
|
||||
ERROR_RE = re.compile(r"error: access to absolute path '.+' is forbidden in pure eval mode")
|
||||
|
||||
flake_dir = tmp_path / 'flake'
|
||||
flake_dir.mkdir()
|
||||
|
||||
evilpath = tmp_path / 'sekrit.txt'
|
||||
evilpath.write_text('kitty kitty')
|
||||
|
||||
evilnix = tmp_path / 'default.nix'
|
||||
evilnix.write_text('"woof"')
|
||||
|
||||
goodnix = flake_dir / 'good.nix'
|
||||
goodnix.write_text("1")
|
||||
|
||||
nested_dir = flake_dir / 'nested' / 'nested2'
|
||||
nested_dir.mkdir(parents=True)
|
||||
(nested_dir / 'good.nix').write_text("1")
|
||||
|
||||
(flake_dir / 'evil-link').symlink_to(evilpath)
|
||||
(flake_dir / 'evil-default-nix').symlink_to(tmp_path)
|
||||
(flake_dir / 'less-evil-link').symlink_to(tmp_path / 'link-to-flake')
|
||||
(tmp_path / 'link-to-flake').symlink_to(flake_dir / 'flake.nix')
|
||||
|
||||
(flake_dir / 'nested-good.nix').symlink_to('nested/nested2/good.nix')
|
||||
(flake_dir / 'nested-bad.nix').symlink_to('nested/../../nested2/good.nix')
|
||||
|
||||
(flake_dir / 'flake.nix').write_text(dedent("""
|
||||
{
|
||||
inputs = {};
|
||||
outputs = inputs: {
|
||||
bad1 = "${@ABSPATH@}";
|
||||
bad2 = builtins.readFile "${@ABSPATH@}";
|
||||
bad3 = builtins.readFile @ABSPATH@;
|
||||
bad4 = builtins.readFile ./evil-link;
|
||||
bad5 = builtins.readFile "${./evil-link}";
|
||||
bad6 = builtins.readFile ./less-evil-link;
|
||||
bad7 = builtins.readFile "${./less-evil-link}";
|
||||
bad8 = import ./evil-default-nix;
|
||||
bad9 = import ./nested-bad.nix;
|
||||
|
||||
good1 = builtins.readFile "${inputs.self.outPath}/good.nix";
|
||||
good2 = builtins.readFile ./good.nix;
|
||||
good3 = builtins.readFile "${./good.nix}";
|
||||
good4 = toString (import ./good.nix);
|
||||
good5 = toString (import ./nested-good.nix);
|
||||
};
|
||||
}
|
||||
""").replace('@ABSPATH@', str(evilpath.absolute())))
|
||||
|
||||
for idx in range(1, 10):
|
||||
cmd = nix.nix(['eval', f'.#bad{idx}'], flake=True)
|
||||
cmd.cwd = flake_dir
|
||||
res = cmd.run().expect(1)
|
||||
print(res.stderr_plain)
|
||||
assert ERROR_RE.search(res.stderr_plain)
|
||||
for idx in range(1, 6):
|
||||
cmd = nix.nix(['eval', f'.#good{idx}'], flake=True)
|
||||
cmd.cwd = flake_dir
|
||||
res = cmd.run().expect(0)
|
||||
assert res.stdout_plain == '"1"'
|
||||
@@ -3,7 +3,7 @@ import json
|
||||
import subprocess
|
||||
from typing import Any
|
||||
from pathlib import Path
|
||||
from functools import partial, partialmethod
|
||||
from functools import partialmethod
|
||||
from functional2.testlib.terminal_code_eater import eat_terminal_codes
|
||||
import dataclasses
|
||||
|
||||
@@ -198,17 +198,21 @@ class Nix:
|
||||
settings.store = str(store_path)
|
||||
return settings
|
||||
|
||||
def nix_cmd(self, argv: list[str], allow_builds: bool = False):
|
||||
def nix_cmd(self, argv: list[str], flake: bool = False):
|
||||
"""
|
||||
Constructs a NixCommand with the appropriate settings.
|
||||
"""
|
||||
settings = self.settings()
|
||||
if flake:
|
||||
settings.feature('nix-command', 'flakes')
|
||||
|
||||
return NixCommand(argv=argv,
|
||||
cwd=self.test_root,
|
||||
env=self.make_env(),
|
||||
settings=self.settings())
|
||||
settings=settings)
|
||||
|
||||
def nix(self, cmd: list[str], nix_exe: str = 'nix') -> NixCommand:
|
||||
return self.nix_cmd([nix_exe, *cmd])
|
||||
def nix(self, cmd: list[str], nix_exe: str = 'nix', flake: bool = False) -> NixCommand:
|
||||
return self.nix_cmd([nix_exe, *cmd], flake=flake)
|
||||
|
||||
nix_build = partialmethod(nix, nix_exe='nix-build')
|
||||
nix_shell = partialmethod(nix, nix_exe='nix-shell')
|
||||
|
||||
@@ -34,12 +34,12 @@ TEST(Arguments, lookupFileArg) {
|
||||
auto state = std::make_shared<Evaluator>(searchPath, store, store);
|
||||
|
||||
SourcePath const foundUnitData = lookupFileArg(*state, "<example>");
|
||||
EXPECT_EQ(foundUnitData.path, canonDataPath);
|
||||
EXPECT_EQ(foundUnitData.canonical(), canonDataPath);
|
||||
|
||||
// lookupFileArg should not resolve <search paths> if anything else is before or after it.
|
||||
SourcePath const yepEvenSpaces = lookupFileArg(*state, " <example>");
|
||||
EXPECT_EQ(yepEvenSpaces.path, CanonPath::fromCwd(" <example>"));
|
||||
EXPECT_EQ(lookupFileArg(*state, "<example>/nixos").path, CanonPath::fromCwd("<example>/nixos"));
|
||||
EXPECT_EQ(yepEvenSpaces.canonical(), CanonPath::fromCwd(" <example>"));
|
||||
EXPECT_EQ(lookupFileArg(*state, "<example>/nixos").canonical(), CanonPath::fromCwd("<example>/nixos"));
|
||||
|
||||
try {
|
||||
lookupFileArg(*state, INVALID_CHANNEL);
|
||||
@@ -49,7 +49,7 @@ TEST(Arguments, lookupFileArg) {
|
||||
}
|
||||
|
||||
SourcePath const normalFile = lookupFileArg(*state, unitDataPath);
|
||||
EXPECT_EQ(normalFile.path, canonDataPath);
|
||||
EXPECT_EQ(normalFile.canonical(), canonDataPath);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user