ChaiScript/unittests/async_engine_lifetime_test.cpp
leftibot 96cae78242 Fix #636: Prevent heap-use-after-free in async threads during engine destruction
The Dispatch_Engine member destruction order destroyed m_state (containing
type maps) before m_stack_holder (containing async futures), allowing async
threads to access freed type map memory during eval_error formatting. The fix
moves async() registration from the stdlib into ChaiScript_Basic with thread
tracking, and adds a Dispatch_Engine destructor that joins all tracked async
threads before m_state is destroyed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 09:47:47 -06:00

52 lines
1.9 KiB
C++

// Regression test for issue #636:
// Heap-use-after-free when async threads access engine state during destruction.
//
// The root cause: Dispatch_Engine member destruction order destroys m_state (type maps)
// before m_stack_holder (which holds async futures). When async threads are still running
// and hit an error that triggers eval_error formatting, they access the already-freed
// type map via get_type_name().
#include <chaiscript/chaiscript.hpp>
#include <iostream>
int main() {
// Run multiple iterations to increase chance of triggering the race
for (int iter = 0; iter < 3; ++iter) {
// Create engine in a nested scope so it's destroyed while async tasks may still be running
{
chaiscript::ChaiScript chai;
try {
// This script launches async tasks that run a long-running loop.
// The futures are not retrieved - when the engine is destroyed, the
// async threads may still be accessing engine state (e.g. type maps
// during error formatting). Without the fix, m_state is destroyed
// before m_stack_holder (which holds the futures), causing the async
// threads to access freed memory.
//
// With ASan/TSan enabled, this reliably detects the heap-use-after-free.
chai.eval(R"(
var func = fun(){
var ret = 0;
for (var i = 0; i < 100000; ++i) {
ret += i;
}
return ret;
}
var fut1 = async(func);
var fut2 = async(func);
)");
} catch (const std::exception &) {
// Exceptions from eval are expected and fine
}
}
// If the fix is not applied, the engine destruction above may cause
// heap-use-after-free (detectable with ASan/TSan) because m_state
// is destroyed before async threads finish.
}
std::cout << "Async engine lifetime test passed\n";
return EXIT_SUCCESS;
}