Skip to content

Fix #635: Segfault in async result via dangling pointer from optimized for loop#671

Merged
lefticus merged 3 commits intoChaiScript:developfrom
leftibot:fix/issue-635-async-segfault
Apr 12, 2026
Merged

Fix #635: Segfault in async result via dangling pointer from optimized for loop#671
lefticus merged 3 commits intoChaiScript:developfrom
leftibot:fix/issue-635-async-segfault

Conversation

@leftibot
Copy link
Copy Markdown
Contributor

Summary

  • Optimizer fix: The optimized for loop stored the loop counter as a stack-local int exposed via var(&i). When := copied this reference-type Boxed_Value, the returned value held a dangling pointer after the loop frame unwound. Now heap-allocates the counter via Boxed_Value(start_int).
  • := operator fix: Data::operator= performed a shallow clone (copying the reference_wrapper or shared_ptr, not the value), so ret := i aliased the same storage. Now deep-copies arithmetic values via Boxed_Number::clone, giving correct value semantics.
  • Tests: Added assign_no_aliasing.chai and async_return_value.chai covering both the aliasing fix and the async crash.

Root cause

The For_Loop optimizer in chaiscript_optimizer.hpp created a native C++ int i on the stack and registered it as a ChaiScript variable with var(&i) (a reference-type Boxed_Value). When the loop body executed ret := i, Boxed_Value::assign()Data::operator= cloned the Any (copying the reference_wrapper<int> — still pointing to the stack local) and copied the raw m_data_ptr/m_const_data_ptr from the rhs. After the optimizer lambda returned, the stack-local int was destroyed, leaving ret with a dangling pointer that crashed in Boxed_Number::get_as<int>().

Test plan

🤖 Generated with Claude Code

…m optimized for loop

Two bugs combined to cause the crash:

1. The optimized for loop (chaiscript_optimizer.hpp) stored the loop
   counter as a stack-local `int` and exposed it to ChaiScript via
   `var(&i)`, creating a reference-type Boxed_Value pointing to the
   stack frame.

2. The `:=` operator's `Data::operator=` performed a shallow clone of
   the Any (copying the reference_wrapper, not the value), so
   `ret := i` made `ret` alias the stack-local `int`. When the
   optimized loop's frame unwound, `ret` held a dangling pointer.

Fix 1: The optimizer now heap-allocates the loop counter via
`Boxed_Value(start_int)` and obtains a reference to the
heap-allocated int for the native C++ loop. The Boxed_Value's
shared_ptr ensures the storage outlives any copies.

Fix 2: The `:=` operator now deep-copies arithmetic values via
`Boxed_Number::clone` instead of the shallow `assign()`, ensuring
value semantics. This also fixes a pre-existing aliasing bug where
`a := b; ++b` would incorrectly mutate `a`.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@leftibot this will always fail when compiled without thread support - make sure this file is excluded from testing in those cases.

… thread support

Requested by @lefticus in PR ChaiScript#671 review.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@leftibot
Copy link
Copy Markdown
Contributor Author

Addressed review feedback from @lefticus. New commits:

f430afd4 Address review: exclude async_return_value.chai when compiled without thread support

Comment thread unittests/assign_no_aliasing.chai Outdated
// Verify that := performs a value copy, not an alias
var a = 10
var b = 20
a := b
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@leftibot is it an accident / mistake that := works on already created objects at all? Is it intended only for variable declarations?

@leftibot
Copy link
Copy Markdown
Contributor Author

Looking at the codebase, := working on already-created objects appears to be intentional, not accidental.

Evidence it's by design:

  1. The parser (chaiscript_parser.hpp:2712) treats := as a general equation operator alongside =, +=, etc. — no syntactic restriction to declarations.

  2. The eval code (chaiscript_eval.hpp:474-480) handles := for any LHS that is either undefined or type-matching, with no check for whether the LHS came from a Var_Decl.

  3. The prelude itself uses it on non-declarations:

    ri.get_var_attr("internal_obj") := r;   // chaiscript_prelude.hpp:153
    

    This assigns into an already-existing attribute, relying on :='s reference-assign semantics to alias r into the range's internal storage.

The semantic distinction appears to be:

  • = → goes through function dispatch, clones the value (value semantics)
  • := → calls Boxed_Value::assign() directly, shares the underlying data pointer (reference/alias semantics)

This is actually what makes the prelude's range wrapper work — the ri.get_var_attr("internal_obj") := r line intentionally aliases r so the range object holds a reference to the original container.

That said, this raises a concern about this PR's change. The PR adds deep-copy for arithmetic types in := (chaiscript_eval.hpp:476-478), which changes :='s semantics from "always alias" to "sometimes alias, sometimes copy" depending on whether the value is arithmetic. If := is the "reference assign" operator, should it remain consistent? The aliasing behavior for the original bug was caused by the optimizer's var(&i) creating a dangling stack reference — that's already fixed by the optimizer change to Boxed_Value(start_int). The := deep-copy might be an unnecessary second fix that muddies the operator's semantics.

Would you prefer to keep := as pure reference-assign and rely solely on the optimizer fix?

@lefticus
Copy link
Copy Markdown
Member

@leftibot yeah, remove your change for := let's keep the semantics the same. Make sure your tests thoroughly exercise the potential problems with the submitted issue.

…antics

Reverts the arithmetic deep-copy added to := in chaiscript_eval.hpp,
restoring its original reference-assign behavior as requested.
The optimizer fix (heap-allocated loop counter) remains and is
sufficient to prevent the ChaiScript#635 segfault.

Updated tests to use correct expected values for reference-assign
semantics and added more thorough coverage: nested loops, multiple
async calls, and value-assign comparison.

Requested by @lefticus in PR ChaiScript#671 review.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@leftibot
Copy link
Copy Markdown
Contributor Author

Addressed review feedback from @lefticus. New commits:

276fa9d1 Address review: revert := deep-copy change, keep reference-assign semantics

@lefticus lefticus merged commit 07d62aa into ChaiScript:develop Apr 12, 2026
22 checks passed
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.

2 participants