Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 81 additions & 53 deletions src/libexpr/eval.cc
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@

#include <nlohmann/json.hpp>
#include <boost/container/small_vector.hpp>
#include <boost/unordered/concurrent_flat_map.hpp>

#include "nix/util/strings-inline.hh"

Expand Down Expand Up @@ -264,6 +265,9 @@ EvalState::EvalState(
, debugRepl(nullptr)
, debugStop(false)
, trylevel(0)
, srcToStore(make_ref<decltype(srcToStore)::element_type>())
, importResolutionCache(make_ref<decltype(importResolutionCache)::element_type>())
, fileEvalCache(make_ref<decltype(fileEvalCache)::element_type>())
, regexCache(makeRegexCache())
#if NIX_USE_BOEHMGC
, valueAllocCache(std::allocate_shared<void *>(traceable_allocator<void *>(), nullptr))
Expand Down Expand Up @@ -1031,63 +1035,85 @@ Value * ExprPath::maybeThunk(EvalState & state, Env & env)
return &v;
}

void EvalState::evalFile(const SourcePath & path, Value & v, bool mustBeTrivial)
/**
* A helper `Expr` class to lets us parse and evaluate Nix expressions
* from a thunk, ensuring that every file is parsed/evaluated only
* once (via the thunk stored in `EvalState::fileEvalCache`).
*/
struct ExprParseFile : Expr
{
FileEvalCache::iterator i;
if ((i = fileEvalCache.find(path)) != fileEvalCache.end()) {
v = i->second;
return;
}
SourcePath & path;
bool mustBeTrivial;

auto resolvedPath = resolveExprPath(path);
if ((i = fileEvalCache.find(resolvedPath)) != fileEvalCache.end()) {
v = i->second;
return;
ExprParseFile(SourcePath & path, bool mustBeTrivial)
Comment on lines +1045 to +1048
Copy link

Copilot AI Sep 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Storing a reference to SourcePath in ExprParseFile is unsafe when the referenced object's lifetime is not guaranteed. Consider storing by value instead to avoid potential dangling references.

Copilot uses AI. Check for mistakes.
: path(path)
, mustBeTrivial(mustBeTrivial)
{
}

printTalkative("evaluating file '%1%'", resolvedPath);
Expr * e = nullptr;
void eval(EvalState & state, Env & env, Value & v) override
{
printTalkative("evaluating file '%s'", path);

auto j = fileParseCache.find(resolvedPath);
if (j != fileParseCache.end())
e = j->second;
auto e = state.parseExprFromFile(path);

if (!e)
e = parseExprFromFile(resolvedPath);
try {
auto dts =
state.debugRepl
? makeDebugTraceStacker(
state, *e, state.baseEnv, e->getPos(), "while evaluating the file '%s':", path.to_string())
: nullptr;

// Enforce that 'flake.nix' is a direct attrset, not a
// computation.
if (mustBeTrivial && !(dynamic_cast<ExprAttrs *>(e)))
state.error<EvalError>("file '%s' must be an attribute set", path).debugThrow();

state.eval(e, v);
} catch (Error & e) {
state.addErrorTrace(e, "while evaluating the file '%s':", path.to_string());
throw;
}
}
};

fileParseCache.emplace(resolvedPath, e);
void EvalState::evalFile(const SourcePath & path, Value & v, bool mustBeTrivial)
{
auto resolvedPath = getConcurrent(*importResolutionCache, path);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This leads to a dangling reference, because this variable is allocated on the stack, but it's later saved to the heap allocated in *vExpr.


try {
auto dts = debugRepl ? makeDebugTraceStacker(
*this,
*e,
this->baseEnv,
e->getPos(),
"while evaluating the file '%1%':",
resolvedPath.to_string())
: nullptr;

// Enforce that 'flake.nix' is a direct attrset, not a
// computation.
if (mustBeTrivial && !(dynamic_cast<ExprAttrs *>(e)))
error<EvalError>("file '%s' must be an attribute set", path).debugThrow();
eval(e, v);
} catch (Error & e) {
addErrorTrace(e, "while evaluating the file '%1%':", resolvedPath.to_string());
throw;
if (!resolvedPath) {
resolvedPath = resolveExprPath(path);
importResolutionCache->emplace(path, *resolvedPath);
}

if (auto v2 = getConcurrent(*fileEvalCache, *resolvedPath)) {
forceValue(**v2, noPos);
v = **v2;
return;
}

fileEvalCache.emplace(resolvedPath, v);
if (path != resolvedPath)
fileEvalCache.emplace(path, v);
Value * vExpr;
ExprParseFile expr{*resolvedPath, mustBeTrivial};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a bug. This expression is allocated on the stack and it the results in a dangling pointer. E.g this leads to a segfault in:

nix eval --expr '(builtins.getFlake "github:nixos/nixpkgs/25.05")' --impure

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, maybe there's something else at play here.


fileEvalCache->try_emplace_and_cvisit(
*resolvedPath,
nullptr,
[&](auto & i) {
vExpr = allocValue();
vExpr->mkThunk(&baseEnv, &expr);
Comment on lines +1102 to +1103
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any particular reason this has to be in a callback? Can't we just do this:

Value * vExpr = allocValue();
ExprParseFile expr{*resolvedPath, mustBeTrivial};
vExpr->mkThunk(&baseEnv, &expr);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It avoids an allocation in the case where the fileEvalCache entry already exists.

Comment on lines +1096 to +1103
Copy link

Copilot AI Sep 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ExprParseFile object is created on the stack but its address is passed to vExpr->mkThunk(). This creates a dangling reference when the stack object goes out of scope. The expression should be allocated on the heap or managed differently to ensure it remains valid for the lifetime of the thunk.

Suggested change
ExprParseFile expr{*resolvedPath, mustBeTrivial};
fileEvalCache->try_emplace_and_cvisit(
*resolvedPath,
nullptr,
[&](auto & i) {
vExpr = allocValue();
vExpr->mkThunk(&baseEnv, &expr);
auto expr = new ExprParseFile{*resolvedPath, mustBeTrivial};
fileEvalCache->try_emplace_and_cvisit(
*resolvedPath,
nullptr,
[&](auto & i) {
vExpr = allocValue();
vExpr->mkThunk(&baseEnv, expr);

Copilot uses AI. Check for mistakes.
i.second = vExpr;
},
[&](auto & i) { vExpr = i.second; });

forceValue(*vExpr, noPos);

v = *vExpr;
}

void EvalState::resetFileCache()
{
fileEvalCache.clear();
fileEvalCache.rehash(0);
fileParseCache.clear();
fileParseCache.rehash(0);
importResolutionCache->clear();
fileEvalCache->clear();
inputCache->clear();
}

Expand Down Expand Up @@ -2372,24 +2398,26 @@ StorePath EvalState::copyPathToStore(NixStringContext & context, const SourcePat
if (nix::isDerivation(path.path.abs()))
error<EvalError>("file names are not allowed to end in '%1%'", drvExtension).debugThrow();

std::optional<StorePath> dstPath;
if (!srcToStore.cvisit(path, [&dstPath](const auto & kv) { dstPath.emplace(kv.second); })) {
dstPath.emplace(fetchToStore(
auto dstPathCached = getConcurrent(*srcToStore, path);

auto dstPath = dstPathCached ? *dstPathCached : [&]() {
auto dstPath = fetchToStore(
fetchSettings,
*store,
path.resolveSymlinks(SymlinkResolution::Ancestors),
settings.readOnlyMode ? FetchMode::DryRun : FetchMode::Copy,
path.baseName(),
ContentAddressMethod::Raw::NixArchive,
nullptr,
repair));
allowPath(*dstPath);
srcToStore.try_emplace(path, *dstPath);
printMsg(lvlChatty, "copied source '%1%' -> '%2%'", path, store->printStorePath(*dstPath));
}
repair);
allowPath(dstPath);
srcToStore->try_emplace(path, dstPath);
printMsg(lvlChatty, "copied source '%1%' -> '%2%'", path, store->printStorePath(dstPath));
return dstPath;
}();

context.insert(NixStringContextElem::Opaque{.path = *dstPath});
return *dstPath;
context.insert(NixStringContextElem::Opaque{.path = dstPath});
return dstPath;
}

SourcePath EvalState::coerceToPath(const PosIdx pos, Value & v, NixStringContext & context, std::string_view errorCtx)
Expand Down
30 changes: 12 additions & 18 deletions src/libexpr/include/nix/expr/eval.hh
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
// For `NIX_USE_BOEHMGC`, and if that's set, `GC_THREADS`
#include "nix/expr/config.hh"

#include <boost/unordered/concurrent_flat_map.hpp>
#include <boost/unordered/unordered_flat_map.hpp>
#include <boost/unordered/concurrent_flat_map_fwd.hpp>

#include <map>
#include <optional>
#include <functional>
Expand Down Expand Up @@ -412,37 +413,30 @@ private:

/* Cache for calls to addToStore(); maps source paths to the store
paths. */
boost::concurrent_flat_map<SourcePath, StorePath, std::hash<SourcePath>> srcToStore;
ref<boost::concurrent_flat_map<SourcePath, StorePath>> srcToStore;

/**
* A cache from path names to parse trees.
* A cache that maps paths to "resolved" paths for importing Nix
* expressions, i.e. `/foo` to `/foo/default.nix`.
*/
typedef boost::unordered_flat_map<
SourcePath,
Expr *,
std::hash<SourcePath>,
std::equal_to<SourcePath>,
traceable_allocator<std::pair<const SourcePath, Expr *>>>
FileParseCache;
FileParseCache fileParseCache;
ref<boost::concurrent_flat_map<SourcePath, SourcePath>> importResolutionCache;

/**
* A cache from path names to values.
* A cache from resolved paths to values.
*/
typedef boost::unordered_flat_map<
ref<boost::concurrent_flat_map<
SourcePath,
Value,
Value *,
std::hash<SourcePath>,
std::equal_to<SourcePath>,
traceable_allocator<std::pair<const SourcePath, Value>>>
FileEvalCache;
FileEvalCache fileEvalCache;
traceable_allocator<std::pair<const SourcePath, Value *>>>>
fileEvalCache;

/**
* Associate source positions of certain AST nodes with their preceding doc comment, if they have one.
* Grouped by file.
*/
boost::unordered_flat_map<SourcePath, DocCommentMap, std::hash<SourcePath>> positionToDocComment;
boost::unordered_flat_map<SourcePath, DocCommentMap> positionToDocComment;

LookupPath lookupPath;

Expand Down
6 changes: 3 additions & 3 deletions src/libfetchers/filtering-source-accessor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,12 @@ void FilteringSourceAccessor::checkAccess(const CanonPath & path)
struct AllowListSourceAccessorImpl : AllowListSourceAccessor
{
std::set<CanonPath> allowedPrefixes;
boost::unordered_flat_set<CanonPath, std::hash<CanonPath>> allowedPaths;
boost::unordered_flat_set<CanonPath> allowedPaths;

AllowListSourceAccessorImpl(
ref<SourceAccessor> next,
std::set<CanonPath> && allowedPrefixes,
boost::unordered_flat_set<CanonPath, std::hash<CanonPath>> && allowedPaths,
boost::unordered_flat_set<CanonPath> && allowedPaths,
MakeNotAllowedError && makeNotAllowedError)
: AllowListSourceAccessor(SourcePath(next), std::move(makeNotAllowedError))
, allowedPrefixes(std::move(allowedPrefixes))
Expand All @@ -86,7 +86,7 @@ struct AllowListSourceAccessorImpl : AllowListSourceAccessor
ref<AllowListSourceAccessor> AllowListSourceAccessor::create(
ref<SourceAccessor> next,
std::set<CanonPath> && allowedPrefixes,
boost::unordered_flat_set<CanonPath, std::hash<CanonPath>> && allowedPaths,
boost::unordered_flat_set<CanonPath> && allowedPaths,
MakeNotAllowedError && makeNotAllowedError)
{
return make_ref<AllowListSourceAccessorImpl>(
Expand Down
4 changes: 2 additions & 2 deletions src/libfetchers/git-utils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -817,7 +817,7 @@ struct GitSourceAccessor : SourceAccessor
return toHash(*git_tree_entry_id(entry));
}

boost::unordered_flat_map<CanonPath, TreeEntry, std::hash<CanonPath>> lookupCache;
boost::unordered_flat_map<CanonPath, TreeEntry> lookupCache;

/* Recursively look up 'path' relative to the root. */
git_tree_entry * lookup(State & state, const CanonPath & path)
Expand Down Expand Up @@ -1254,7 +1254,7 @@ GitRepoImpl::getAccessor(const WorkdirInfo & wd, bool exportIgnore, MakeNotAllow
makeFSSourceAccessor(path),
std::set<CanonPath>{wd.files},
// Always allow access to the root, but not its children.
boost::unordered_flat_set<CanonPath, std::hash<CanonPath>>{CanonPath::root},
boost::unordered_flat_set<CanonPath>{CanonPath::root},
std::move(makeNotAllowedError))
.cast<SourceAccessor>();
if (exportIgnore)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ struct AllowListSourceAccessor : public FilteringSourceAccessor
static ref<AllowListSourceAccessor> create(
ref<SourceAccessor> next,
std::set<CanonPath> && allowedPrefixes,
boost::unordered_flat_set<CanonPath, std::hash<CanonPath>> && allowedPaths,
boost::unordered_flat_set<CanonPath> && allowedPaths,
MakeNotAllowedError && makeNotAllowedError);

using FilteringSourceAccessor::FilteringSourceAccessor;
Expand Down
14 changes: 11 additions & 3 deletions src/libutil/include/nix/util/canon-path.hh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
#include <set>
#include <vector>

#include <boost/container_hash/hash.hpp>

namespace nix {

/**
Expand Down Expand Up @@ -258,20 +260,26 @@ public:
*/
std::string makeRelative(const CanonPath & path) const;

friend struct std::hash<CanonPath>;
friend std::size_t hash_value(const CanonPath &);
};

std::ostream & operator<<(std::ostream & stream, const CanonPath & path);

inline std::size_t hash_value(const CanonPath & path)
{
boost::hash<std::string_view> hasher;
return hasher(path.path);
}

} // namespace nix

template<>
struct std::hash<nix::CanonPath>
{
using is_avalanching = std::true_type;

std::size_t operator()(const nix::CanonPath & s) const noexcept
std::size_t operator()(const nix::CanonPath & path) const noexcept
{
return std::hash<std::string>{}(s.path);
return nix::hash_value(path);
}
};
14 changes: 11 additions & 3 deletions src/libutil/include/nix/util/source-path.hh
Original file line number Diff line number Diff line change
Expand Up @@ -119,15 +119,23 @@ struct SourcePath

std::ostream & operator<<(std::ostream & str, const SourcePath & path);

inline std::size_t hash_value(const SourcePath & path)
{
std::size_t hash = 0;
boost::hash_combine(hash, path.accessor->number);
boost::hash_combine(hash, path.path);
return hash;
}

} // namespace nix

template<>
struct std::hash<nix::SourcePath>
{
using is_avalanching = std::true_type;

std::size_t operator()(const nix::SourcePath & s) const noexcept
{
std::size_t hash = 0;
hash_combine(hash, s.accessor->number, s.path);
return hash;
return nix::hash_value(s);
}
};
11 changes: 11 additions & 0 deletions src/libutil/include/nix/util/util.hh
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,17 @@ typename T::mapped_type * get(T & map, K & key)
template<class T>
typename T::mapped_type * get(T && map, const typename T::key_type & key) = delete;

/**
* Look up a value in a `boost::concurrent_flat_map`.
*/
template<class T>
std::optional<typename T::mapped_type> getConcurrent(const T & map, const typename T::key_type & key)
{
std::optional<typename T::mapped_type> res;
map.cvisit(key, [&](auto & x) { res = x.second; });
return res;
}

/**
* Get a value for the specified key from an associate container, or a default value if the key isn't present.
*/
Expand Down
4 changes: 1 addition & 3 deletions src/libutil/posix-source-accessor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,7 @@ std::optional<struct stat> PosixSourceAccessor::cachedLstat(const CanonPath & pa
// former is not hashable on libc++.
Path absPath = makeAbsPath(path).string();

std::optional<Cache::mapped_type> res;
cache.cvisit(absPath, [&](auto & x) { res.emplace(x.second); });
if (res)
if (auto res = getConcurrent(cache, absPath))
return *res;

auto st = nix::maybeLstat(absPath.c_str());
Expand Down
Loading