Skip to content

fix: prevent stale scope slot reference in variable declarations - #263

Open
minh-anm-nguyen wants to merge 1 commit into
WerWolv:masterfrom
minh-anm-nguyen:fix/variable-slot-invalidation
Open

minh-anm-nguyen wants to merge 1 commit into
WerWolv:masterfrom
minh-anm-nguyen:fix/variable-slot-invalidation

Conversation

@minh-anm-nguyen

@minh-anm-nguyen minh-anm-nguyen commented Oct 24, 2026 •

Copy link
Copy Markdown

Fixes the crash reported in WerWolv/ImHex#2043.

ASTNodeVariableDecl::execute() binds a reference to the scope's last pattern:

evaluator->createVariable(this->getName(), evaluatedType.get(), { }, ...);
auto &variable = evaluator->getScope(0).scope->back();

and uses it again about thirty lines further down, after evaluatedType->createPatterns() has run. Scope::scope is a std::vector<std::shared_ptr<Pattern>>*, and creating the declared type's patterns appends to that same vector. The vector grows, relocates its storage, and the earlier reference is left pointing at memory the vector no longer owns.

Reproducer, a section placed inside a struct:

import std.mem;

struct Foo {
    std::mem::Section sec = std::mem::create_section("sec");
    u8 array[12] @ 0x00 in sec;
};

Foo foo;

Under a debugger the vector's data pointer moves while the reference stays behind:

after the binding   data=0x76b320  size=1 cap=1   &variable=0x76b320
at the later use    data=0x76fbe0  size=2 cap=2   &variable=0x76b320

Run through plcli this terminates on 19 of 20 runs. The runs that survive do so only because the previous bytes happen to still be readable.

The fix records the element's index and re-acquires the slot immediately before each later use. Copying the shared_ptr instead is not an option: Evaluator::setVariable(std::shared_ptr<Pattern>&, ...) assigns through its reference parameter, so a copy would stop updating the scope slot and would silently change the behaviour of reference variables.

The regression test covers the same path without needing an include path, by calling builtin::std::mem::create_section directly. Because a pattern-local declaration produces no entries in getPatterns(), there is no resulting value to assert on, so the test asserts emptiness as a structural guard: that fails if the reproducer is ever changed to a placed declaration, which takes the other branch and would stop covering this defect. The evaluation is repeated so that one lucky run cannot hide a regression.

ASTNodeVariableDecl::execute() bound a reference to the scope's last
pattern and used it again after creating the declared type's patterns.
Creating those patterns appends to the very same vector, which grows and
relocates its storage, leaving the earlier reference pointing at memory
the vector no longer owns.

Remember the element's index instead and re-acquire the slot immediately
before each later use. A copied shared_ptr is not an alternative here:
Evaluator::setVariable() assigns through its reference parameter, so a
copy would stop updating the scope slot for reference variables.

Add a regression test covering the pattern-local declaration path that
reaches this code. The reproducer fails on 19 of 20 unrepaired runs, so
the test repeats the evaluation to keep a single clean run from hiding a
regression. Pattern-local declarations produce no entries in
getPatterns(), so it asserts emptiness as a structural guard against the
source being changed to a placed declaration.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant