Skip to content

Commit

Permalink
Add 'poison' context
Browse files Browse the repository at this point in the history
This can be added to strings to prevent them from being used in
derivations. This lets us implement "unstable" functions without fear!
  • Loading branch information
9999years committed Mar 10, 2024
1 parent ac73062 commit 0737401
Show file tree
Hide file tree
Showing 11 changed files with 127 additions and 27 deletions.
3 changes: 3 additions & 0 deletions src/libexpr/eval-cache.cc
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,9 @@ string_t AttrCursor::getStringWithContext()
[&](const NixStringContextElem::Opaque & o) -> const StorePath & {
return o.path;
},
[&](const NixStringContextElem::Poison & p) -> const StorePath & {
root->state.error<PoisonContextError>().debugThrow();
},
}, c.raw);
if (!root->state.store->isValidPath(path)) {
valid = false;
Expand Down
4 changes: 2 additions & 2 deletions src/libexpr/eval-cache.hh
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,6 @@ class AttrCursor : public std::enable_shared_from_this<AttrCursor>

AttrKey getKey();

Value & getValue();

public:

AttrCursor(
Expand Down Expand Up @@ -130,6 +128,8 @@ public:

bool isDerivation();

Value & getValue();

Value & forceValue();

/**
Expand Down
8 changes: 8 additions & 0 deletions src/libexpr/eval-error.cc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "eval-error.hh"
#include "eval.hh"
#include "print.hh"
#include "value.hh"

namespace nix {
Expand Down Expand Up @@ -101,5 +102,12 @@ template class EvalErrorBuilder<MissingArgumentError>;
template class EvalErrorBuilder<InfiniteRecursionError>;
template class EvalErrorBuilder<CachedEvalError>;
template class EvalErrorBuilder<InvalidPathError>;
template class EvalErrorBuilder<PoisonContextError>;

PoisonContextError::PoisonContextError(EvalState & state)
: EvalError(state, "Found 'poison' context that may not be built or included in derivations")
, value(*state.allocValue())
{
}

}
17 changes: 16 additions & 1 deletion src/libexpr/eval-error.hh
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

#include "error.hh"
#include "pos-idx.hh"
#include "print-options.hh"
#include "print.hh"

namespace nix {

Expand Down Expand Up @@ -56,6 +58,19 @@ public:
}
};

struct PoisonContextError : public EvalError
{
public:
Value & value;
PoisonContextError(EvalState & state, Value & value)
: EvalError(state, "Value contains 'poison' context that may not be built or included in derivations: %1%", ValuePrinter(state, value, errorPrintOptions))
, value(value)
{
}

PoisonContextError(EvalState & state);
};

/**
* `EvalErrorBuilder`s may only be constructed by `EvalState`. The `debugThrow`
* method must be the final method in any such `EvalErrorBuilder` usage, and it
Expand All @@ -67,7 +82,7 @@ class EvalErrorBuilder final
friend class EvalState;

template<typename... Args>
explicit EvalErrorBuilder(EvalState & state, const Args &... args)
explicit EvalErrorBuilder(EvalState & state, Args &&... args)
: error(T(state, args...))
{
}
Expand Down
3 changes: 3 additions & 0 deletions src/libexpr/eval.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2473,6 +2473,9 @@ std::pair<SingleDerivedPath, std::string_view> EvalState::coerceToSingleDerivedP
[&](NixStringContextElem::Built && b) -> SingleDerivedPath {
return std::move(b);
},
[&](NixStringContextElem::Poison && p) -> SingleDerivedPath {
error<PoisonContextError>(v).debugThrow();
},
}, ((NixStringContextElem &&) *context.begin()).raw);
return {
std::move(derivedPath),
Expand Down
4 changes: 2 additions & 2 deletions src/libexpr/eval.hh
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ public:

template<class T, typename... Args>
[[nodiscard, gnu::noinline]]
EvalErrorBuilder<T> & error(const Args & ... args) {
EvalErrorBuilder<T> & error(Args && ... args) {
// `EvalErrorBuilder::debugThrow` performs the corresponding `delete`.
return *new EvalErrorBuilder<T>(*this, args...);
}
Expand Down Expand Up @@ -689,7 +689,7 @@ public:
* Realise the given context, and return a mapping from the placeholders
* used to construct the associated value to their final store path
*/
[[nodiscard]] StringMap realiseContext(const NixStringContext & context);
[[nodiscard]] StringMap realiseContext(Value & v, const NixStringContext & context);

/* Call the binary path filter predicate used builtins.path etc. */
bool callPathFilter(
Expand Down
64 changes: 49 additions & 15 deletions src/libexpr/primops.cc
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ namespace nix {
* Miscellaneous
*************************************************************/

StringMap EvalState::realiseContext(const NixStringContext & context)
StringMap EvalState::realiseContext(Value & v, const NixStringContext & context)
{
std::vector<DerivedPath::Built> drvs;
StringMap res;
Expand Down Expand Up @@ -68,6 +68,9 @@ StringMap EvalState::realiseContext(const NixStringContext & context)
res.insert_or_assign(ctxS, ctxS);
ensureValid(d.drvPath);
},
[&](const NixStringContextElem::Poison & p) {
error<PoisonContextError>(v).debugThrow();
},
}, c.raw);
}

Expand Down Expand Up @@ -123,7 +126,7 @@ static SourcePath realisePath(EvalState & state, const PosIdx pos, Value & v, st

try {
if (!context.empty() && path.accessor == state.rootFS) {
auto rewrites = state.realiseContext(context);
auto rewrites = state.realiseContext(v, context);
auto realPath = state.toRealPath(rewriteStrings(path.path.abs(), rewrites), context);
path = {path.accessor, CanonPath(realPath)};
}
Expand Down Expand Up @@ -375,7 +378,7 @@ void prim_exec(EvalState & state, const PosIdx pos, Value * * args, Value & v)
false, false).toOwned());
}
try {
auto _ = state.realiseContext(context); // FIXME: Handle CA derivations
auto _ = state.realiseContext(v, context); // FIXME: Handle CA derivations
} catch (InvalidPathError & e) {
state.error<EvalError>("cannot execute '%1%', since path '%2%' is not valid", program, e.path).atPos(pos).debugThrow();
}
Expand Down Expand Up @@ -1040,7 +1043,7 @@ static void prim_second(EvalState & state, const PosIdx pos, Value * * args, Val
* Derivations
*************************************************************/

static void derivationStrictInternal(EvalState & state, const std::string & name, Bindings * attrs, Value & v);
static void derivationStrictInternal(EvalState & state, const std::string & name, Bindings * attrs, Value & input, Value & v);

/* Construct (as a unobservable side effect) a Nix derivation
expression that performs the derivation described by the argument
Expand All @@ -1067,7 +1070,7 @@ static void prim_derivationStrict(EvalState & state, const PosIdx pos, Value * *
}

try {
derivationStrictInternal(state, drvName, attrs, v);
derivationStrictInternal(state, drvName, attrs, *args[0], v);
} catch (Error & e) {
Pos pos = state.positions[nameAttr->pos];
/*
Expand Down Expand Up @@ -1096,7 +1099,7 @@ static void prim_derivationStrict(EvalState & state, const PosIdx pos, Value * *
}

static void derivationStrictInternal(EvalState & state, const std::string &
drvName, Bindings * attrs, Value & v)
drvName, Bindings * attrs, Value & input, Value & v)
{
/* Check whether attributes should be passed as a JSON file. */
using nlohmann::json;
Expand Down Expand Up @@ -1288,6 +1291,10 @@ drvName, Bindings * attrs, Value & v)
[&](const NixStringContextElem::Opaque & o) {
drv.inputSrcs.insert(o.path);
},
[&](const NixStringContextElem::Poison & p) {
state.error<PoisonContextError>(input)
.debugThrow();
},
}, c.raw);
}

Expand Down Expand Up @@ -1681,7 +1688,7 @@ static void prim_findFile(EvalState & state, const PosIdx pos, Value * * args, V
false, false).toOwned();

try {
auto rewrites = state.realiseContext(context);
auto rewrites = state.realiseContext(*i->value, context);
path = rewriteStrings(path, rewrites);
} catch (InvalidPathError & e) {
state.error<EvalError>(
Expand Down Expand Up @@ -2209,6 +2216,7 @@ static void addPath(
const PosIdx pos,
std::string_view name,
SourcePath path,
Value & pathValue,
Value * filterFun,
FileIngestionMethod method,
const std::optional<Hash> expectedHash,
Expand All @@ -2221,7 +2229,7 @@ static void addPath(
if (path.accessor == state.rootFS && state.store->isInStore(path.path.abs())) {
// FIXME: handle CA derivation outputs (where path needs to
// be rewritten to the actual output).
auto rewrites = state.realiseContext(context);
auto rewrites = state.realiseContext(pathValue, context);
path = {state.rootFS, CanonPath(state.toRealPath(rewriteStrings(path.path.abs(), rewrites), context))};

try {
Expand Down Expand Up @@ -2279,7 +2287,18 @@ static void prim_filterSource(EvalState & state, const PosIdx pos, Value * * arg
"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, args[0], FileIngestionMethod::Recursive, std::nullopt, v, context);
addPath(
state,
pos,
path.baseName(),
path,
*args[1],
args[0],
FileIngestionMethod::Recursive,
std::nullopt,
v,
context
);
}

static RegisterPrimOp primop_filterSource({
Expand Down Expand Up @@ -2339,7 +2358,7 @@ static RegisterPrimOp primop_filterSource({

static void prim_path(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
std::optional<SourcePath> path;
std::optional<std::pair<SourcePath, Value &>> path;
std::string name;
Value * filterFun = nullptr;
auto method = FileIngestionMethod::Recursive;
Expand All @@ -2350,9 +2369,13 @@ static void prim_path(EvalState & state, const PosIdx pos, Value * * args, Value

for (auto & attr : *args[0]->attrs) {
auto n = state.symbols[attr.name];
if (n == "path")
path.emplace(state.coerceToPath(attr.pos, *attr.value, context, "while evaluating the 'path' attribute passed to 'builtins.path'"));
else if (attr.name == state.sName)
if (n == "path") {
auto pair = std::pair(
state.coerceToPath(attr.pos, *attr.value, context, "while evaluating the 'path' attribute passed to 'builtins.path'"),
*attr.value
);
path.emplace(pair);
} else if (attr.name == state.sName)
name = state.forceStringNoCtx(*attr.value, attr.pos, "while evaluating the `name` attribute passed to builtins.path");
else if (n == "filter")
state.forceFunction(*(filterFun = attr.value), attr.pos, "while evaluating the `filter` parameter passed to builtins.path");
Expand All @@ -2371,9 +2394,20 @@ static void prim_path(EvalState & state, const PosIdx pos, Value * * args, Value
"missing required 'path' attribute in the first argument to builtins.path"
).atPos(pos).debugThrow();
if (name.empty())
name = path->baseName();
name = path->first.baseName();

addPath(state, pos, name, *path, filterFun, method, expectedHash, v, context);
addPath(
state,
pos,
name,
path->first,
path->second,
filterFun,
method,
expectedHash,
v,
context
);
}

static RegisterPrimOp primop_path({
Expand Down
21 changes: 16 additions & 5 deletions src/libexpr/primops/context.cc
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ static void prim_addDrvOutputDependencies(EvalState & state, const PosIdx pos, V
/* Reuse original item because we want this to be idempotent. */
return std::move(c);
},
[&](const NixStringContextElem::Poison & p) -> NixStringContextElem::DrvDeep {
state.error<PoisonContextError>(*args[0]).debugThrow();
},
}, context.begin()->raw) }),
};

Expand Down Expand Up @@ -178,20 +181,23 @@ static void prim_getContext(EvalState & state, const PosIdx pos, Value * * args,
};
NixStringContext context;
state.forceString(*args[0], context, pos, "while evaluating the argument passed to builtins.getContext");
auto contextInfos = std::map<StorePath, ContextInfo>();
auto contextInfos = std::map<std::string, ContextInfo>();
for (auto && i : context) {
std::visit(overloaded {
[&](NixStringContextElem::DrvDeep && d) {
contextInfos[std::move(d.drvPath)].allOutputs = true;
contextInfos[state.store->printStorePath(d.drvPath)].allOutputs = true;
},
[&](NixStringContextElem::Built && b) {
// FIXME should eventually show string context as is, no
// resolving here.
auto drvPath = resolveDerivedPath(*state.store, *b.drvPath);
contextInfos[std::move(drvPath)].outputs.emplace_back(std::move(b.output));
contextInfos[state.store->printStorePath(drvPath)].outputs.emplace_back(std::move(b.output));
},
[&](NixStringContextElem::Opaque && o) {
contextInfos[std::move(o.path)].path = true;
contextInfos[state.store->printStorePath(o.path)].path = true;
},
[&](NixStringContextElem::Poison && p) {
contextInfos["poison"] = {};
},
}, ((NixStringContextElem &&) i).raw);
}
Expand All @@ -212,7 +218,7 @@ static void prim_getContext(EvalState & state, const PosIdx pos, Value * * args,
for (const auto & [i, output] : enumerate(info.second.outputs))
(outputsVal.listElems()[i] = state.allocValue())->mkString(output);
}
attrs.alloc(state.store->printStorePath(info.first)).mkAttrs(infoAttrs);
attrs.alloc(info.first).mkAttrs(infoAttrs);
}

v.mkAttrs(attrs);
Expand All @@ -239,6 +245,11 @@ static RegisterPrimOp primop_getContext({
```
{ "/nix/store/arhvjaf6zmlyn8vh8fgn55rpwnxq0n7l-a.drv" = { outputs = [ "out" ]; }; }
```
There also exists a special "poison" context added by functions like
[`builtins.toStringDebug`](#builtins-toStringDebug) which will prevent
the string from being included in derivations. If the poison context is
present, an attribute named `poison` will be present in the output.
)",
.fun = prim_getContext
});
Expand Down
6 changes: 6 additions & 0 deletions src/libexpr/value/context.cc
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ NixStringContextElem NixStringContextElem::parse(
.drvPath = StorePath { s.substr(1) },
};
}
case '%': {
return Poison();
}
default: {
// Ensure no '!'
if (s.find("!") != std::string_view::npos) {
Expand Down Expand Up @@ -100,6 +103,9 @@ std::string NixStringContextElem::to_string() const
res += '=';
res += d.drvPath.to_string();
},
[&](const Poison & p) {
res += "%poison";
},
}, raw);

return res;
Expand Down
18 changes: 17 additions & 1 deletion src/libexpr/value/context.hh
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,25 @@ struct NixStringContextElem {
*/
using Built = SingleDerivedPath::Built;

/**
* "Poison pill" output that is rejected by `builtins.derivation`.
*
* Used to ensure the implementation of functions like
* `builtins.toStringDebug` do not get hashed into derivations.
*
* Encoded as ‘%poison’.
*/
struct Poison {
bool operator ==(const Poison & other) const { return true; }
bool operator <(const Poison & other) const { return false; }
bool operator >(const Poison & other) const { return false; }
};

using Raw = std::variant<
Opaque,
DrvDeep,
Built
Built,
Poison
>;

Raw raw;
Expand All @@ -71,6 +86,7 @@ struct NixStringContextElem {
* - ‘<path>’
* - ‘=<path>’
* - ‘!<name>!<path>’
* - ‘%poison’
*
* @param xpSettings Stop-gap to avoid globals during unit tests.
*/
Expand Down
Loading

0 comments on commit 0737401

Please sign in to comment.