Segfault when comparing infinite data structures #330
Labels
No labels
Area/build-packaging
Area/cli
Area/evaluator
Area/fetching
Area/flakes
Area/language
Area/profiles
Area/protocol
Area/releng
Area/remote-builds
Area/repl
Area/store
bug
crash 💥
Cross Compilation
devx
docs
Downstream Dependents
E/easy
E/hard
E/help wanted
E/reproducible
E/requires rearchitecture
imported
Needs Langver
OS/Linux
OS/macOS
performance
regression
release-blocker
RFD
stability
Status
blocked
Status
invalid
Status
postponed
Status
wontfix
testing
testing/flakey
ux
No milestone
No project
No assignees
4 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lix-project/lix#330
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Describe the bug
When comparing two infinitely nested data structures (and pointer equality doesn't suffice), Lix segfaults.
Steps To Reproduce
Causes Lix to segfault.
Expected behavior
If possible, define a result (I have no idea if there's a sensible argument to be made for either true or false, nor whether there's any way to efficiently determine this result). Otherwise, fail with a nice error instead of segfaulting.
nix --version
outputnix (Lix, like Nix) 2.90.0-beta.1-lixpre20240506-b6799ab
Additional context
Inherited from Nix, present since at least 2022.
defining a result won't really be possible, but we should definitely limit recursion depth on comparisons (lix already does this for the evaluator itself, but comparisons themselves aren't subject to this yet). reusing the same stack depth limit we already have for eval should be fine, and give an obvious place to tune if anyfew run into this trapping.
When I run either
nix
orlix
with:or do the same in
nix repl
, I get:Is this what you mean by segfault?
ah, Darwin not having the stack overflow detector strikes again. so yeah it's segfaulting due to infinite recursion, but it would be reasonably plausible to decide that after 1000 or 100 depth or something, the values are not going to become equal all of a sudden and return false.
as mentioned there is a recursion limit implemented in the evaluator. if you want to find the code i would suggest checking the 2.90 release notes in the manual (https://docs.lix.systems or in rl-2.90.md in the code in doc/manual/release-notes or so), it's mentioned in there with a link to the code change.
big oof, no, if anything we should throw. the recursion limit currently only applies to function calls, not value comparisons; if we applied the recursion limit here too we'd naturally get an exception instead of a crash.
I read through the code that was mentioned by @jade and found this Nix PR and this CL.
I would like to share my thoughts about how to approach this issue with regards to infinitely nested attrsets and ask for your feedback:
This issue affects the evaluation of
==
and!=
. I'm guessing that it's becauseEvalState::eqValues
calls itself recursively ifv1
andv2
are both non-derivation attrsets for each attribute that has the same name (line 2510):case nAttrs: {
/* If both sets denote a derivation (type = "derivation"),
then compare their outPaths. */
if (isDerivation(v1) && isDerivation(v2)) {
Bindings::iterator i = v1.attrs->find(sOutPath);
Bindings::iterator j = v2.attrs->find(sOutPath);
if (i != v1.attrs->end() && j != v2.attrs->end())
return eqValues(*i->value, *j->value, pos, errorCtx);
}
if (v1.attrs->size() != v2.attrs->size()) return false;
/* Otherwise, compare the attributes one by one. */
Bindings::iterator i, j;
for (i = v1.attrs->begin(), j = v2.attrs->begin(); i != v1.attrs->end(); ++i, ++j)
if (i->name != j->name || !eqValues(*i->value, *j->value, pos, errorCtx))
return false;
return true;
}
I tried to see if any other attrset operation (like
//
) has a similar effect and it doesn't seem to be the case.So to follow the same approach of handling infinite recursion in function calls, we should probably have a depth counter in
EvalState
, maybeattrEqDepth
? Then the counter should be incremented every time we reach the case of evaluating the equality of non-derivation attrsets quoted above. Before incrementing it, we can check ifattrEqDepth
exceededevalSettings.maxAttrEqDepth
and throw an exception ("stack overflow: max-attr-eq-depth exceeded") with a stack trace.This can then be tested with a functional test, just like
tests/functional/lang/eval-fail-infinite-recursion-lambda.nix
.What do you think?
hmmm. is there a reason why we would have to keep this as part of
EvalState
rather than as a parameter to eqValues? If it's that it has to conform to some signature we can make it a wrapper over a function that takes it as a parameter. I don't like non-reentrant state where avoidable.I don't think that equality comparison should ever hit any other parts of the evaluator such that it would have to be passed elsewhere, so it should be possible to make a parameter.
Good idea! :)
I'm all for it. I'll add a parameter with a default value of 0 for the external callers.
It's probably still good to have an
evalSettings.maxAttrEqDepth
for setting the boundary, right?Another question is whether I should check the boundary at the beginning of the function or right before the recursive call.
I think that performance-wise, it would be better to make the check before the recursive call so that in most cases it can be skipped.
Yup, was my thought. Don't even need any fancy state, just add one to it when you recurse.
I think it's probably reasonable that it uses the same depth limit setting as we use for the rest of the language so there's just one knob.
As to when to check it, it should not cause a perf problem to do it right at the start because branch predictors exist and will ignore it. If you're worried you can probably put an unlikely annotation on the if statement, but I don't think this code is terribly hot to begin with. I think we recurse in a couple of places so it's going to be slightly easier to maintain if we only have one check.
I've just tried it with a low hard-coded
callDepth
boundary of10
:and while it works with:
it doesn't work with infinitely nested recursive attribute sets, like the example at the top of the issue.
It simply aborts with:
I need to investigate further why it's not working with infinitely nested recursive attribute sets.
Ok, this was silly. I figured it out. I only incremented
callDepth
for nested attributes sets, but not for nested lists... The example hadrec { foo = [foo]; }
so it's nesting attrset -> list -> attrset -> list -> ...