From f1a0c2ae4183fbefaf0427238073c0dc0bd7e180 Mon Sep 17 00:00:00 2001 From: leftibot Date: Sat, 11 Apr 2026 09:35:51 -0600 Subject: [PATCH] Fix #635: Segfault in optimized for-loop when := copies stack-local variable The optimized for-loop in chaiscript_optimizer.hpp stored the loop counter as a stack-local int and exposed it to ChaiScript via var(&i), creating a reference. When the := operator was used inside the loop body (e.g. ret := i), it copied the raw pointers to this stack variable into the target's Data. When the function returned via exception unwinding, the stack frame was destroyed, leaving dangling pointers in the return value. Accessing these pointers in the caller (e.g. via string interpolation) caused a segfault. The fix heap-allocates the loop counter via std::make_shared, so the data survives scope exit through reference counting. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../language/chaiscript_optimizer.hpp | 6 +++--- unittests/future_assign.chai | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 unittests/future_assign.chai diff --git a/include/chaiscript/language/chaiscript_optimizer.hpp b/include/chaiscript/language/chaiscript_optimizer.hpp index e8048eb3..e6d11544 100644 --- a/include/chaiscript/language/chaiscript_optimizer.hpp +++ b/include/chaiscript/language/chaiscript_optimizer.hpp @@ -397,11 +397,11 @@ namespace chaiscript { assert(children.size() == 1); chaiscript::eval::detail::Scope_Push_Pop spp(t_ss); - int i = start_int; - t_ss.add_object(id, var(&i)); + auto i = std::make_shared(start_int); + t_ss.add_object(id, var(i)); try { - for (; i < end_int; ++i) { + for (; *i < end_int; ++(*i)) { try { // Body of Loop children[0]->eval(t_ss); diff --git a/unittests/future_assign.chai b/unittests/future_assign.chai new file mode 100644 index 00000000..41152169 --- /dev/null +++ b/unittests/future_assign.chai @@ -0,0 +1,21 @@ +// Regression test for #635: Segfault when using := in async function +// with optimized for loop. The := operator copies raw pointers from the +// loop variable, which is stack-allocated in the optimized for loop. +// When the function returns, the stack is unwound and pointers dangle. + +var func = fun(){ + var ret = 0; + for (var i = 0; i < 50000; ++i) { + ret := i; + } + return ret; +} + +var fut1 = async(func); +var fut2 = async(func); + +// This triggers Boxed_Number::get_as on the future results +// which would crash if the pointers are dangling +// := creates an alias, so ret ends up as 50000 (loop exit value of i) +assert_equal(50000, fut1.get()) +assert_equal(50000, fut2.get())