mirror of
https://github.com/ChaiScript/ChaiScript.git
synced 2026-07-31 00:37:02 +08:00
Fix #632: Join async threads before engine destruction to prevent use-after-free
The heap-use-after-free was caused by a race condition where async threads continued accessing the engine's shared state (global objects map) after the main thread began destroying the Dispatch_Engine. The fix moves the async function registration from the stdlib module into ChaiScript_Basic, where it can track spawned threads via Dispatch_Engine. The engine's destructor now joins all outstanding async threads before destroying shared data structures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7b45fe19fe
commit
c5b81b880f
@ -403,6 +403,10 @@ if(BUILD_TESTING)
|
||||
"CHAI_USE_PATH=${CMAKE_CURRENT_SOURCE_DIR}/unittests/"
|
||||
"CHAI_MODULE_PATH=${CMAKE_CURRENT_BINARY_DIR}/"
|
||||
)
|
||||
|
||||
add_executable(async_lifetime_test unittests/async_lifetime_test.cpp)
|
||||
target_link_libraries(async_lifetime_test ${LIBS})
|
||||
add_test(NAME Async_Lifetime_Test COMMAND async_lifetime_test)
|
||||
endif()
|
||||
|
||||
add_executable(multifile_test
|
||||
|
||||
@ -49,9 +49,6 @@ namespace chaiscript {
|
||||
|
||||
#ifndef CHAISCRIPT_NO_THREADS
|
||||
bootstrap::standard_library::future_type<std::future<chaiscript::Boxed_Value>>("future", *lib);
|
||||
lib->add(chaiscript::fun(
|
||||
[](const std::function<chaiscript::Boxed_Value()> &t_func) { return std::async(std::launch::async, t_func); }),
|
||||
"async");
|
||||
#endif
|
||||
|
||||
json_wrap::library(*lib);
|
||||
|
||||
@ -370,6 +370,21 @@ namespace chaiscript {
|
||||
, m_parser(parser) {
|
||||
}
|
||||
|
||||
~Dispatch_Engine() {
|
||||
join_async_threads();
|
||||
}
|
||||
|
||||
Dispatch_Engine(const Dispatch_Engine &) = delete;
|
||||
Dispatch_Engine &operator=(const Dispatch_Engine &) = delete;
|
||||
|
||||
#ifndef CHAISCRIPT_NO_THREADS
|
||||
/// Track a thread created by async so we can join it before destruction
|
||||
void track_async_thread(std::thread &&t_thread) {
|
||||
chaiscript::detail::threading::unique_lock<chaiscript::detail::threading::shared_mutex> l(m_async_mutex);
|
||||
m_async_threads.push_back(std::move(t_thread));
|
||||
}
|
||||
#endif
|
||||
|
||||
/// \brief casts an object while applying any Dynamic_Conversion available
|
||||
template<typename Type>
|
||||
decltype(auto) boxed_cast(const Boxed_Value &bv) const {
|
||||
@ -1165,8 +1180,28 @@ namespace chaiscript {
|
||||
get_function_objects_int().insert_or_assign(t_name, std::move(new_func));
|
||||
}
|
||||
|
||||
void join_async_threads() {
|
||||
#ifndef CHAISCRIPT_NO_THREADS
|
||||
std::vector<std::thread> threads;
|
||||
{
|
||||
chaiscript::detail::threading::unique_lock<chaiscript::detail::threading::shared_mutex> l(m_async_mutex);
|
||||
threads = std::move(m_async_threads);
|
||||
}
|
||||
for (auto &t : threads) {
|
||||
if (t.joinable()) {
|
||||
t.join();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
mutable chaiscript::detail::threading::shared_mutex m_mutex;
|
||||
|
||||
#ifndef CHAISCRIPT_NO_THREADS
|
||||
mutable chaiscript::detail::threading::shared_mutex m_async_mutex;
|
||||
std::vector<std::thread> m_async_threads;
|
||||
#endif
|
||||
|
||||
Type_Conversions m_conversions;
|
||||
chaiscript::detail::threading::Thread_Storage<Stack_Holder> m_stack_holder;
|
||||
std::reference_wrapper<parser::ChaiScript_Parser_Base> m_parser;
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
#include <exception>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@ -179,6 +180,23 @@ namespace chaiscript {
|
||||
}),
|
||||
"namespace");
|
||||
m_engine.add(fun([this](const std::string &t_namespace_name) { import(t_namespace_name); }), "import");
|
||||
|
||||
#ifndef CHAISCRIPT_NO_THREADS
|
||||
m_engine.add(chaiscript::fun(
|
||||
[this](const std::function<chaiscript::Boxed_Value()> &t_func) {
|
||||
auto promise_ptr = std::make_shared<std::promise<chaiscript::Boxed_Value>>();
|
||||
auto future = promise_ptr->get_future();
|
||||
m_engine.track_async_thread(std::thread([promise_ptr, t_func]() {
|
||||
try {
|
||||
promise_ptr->set_value(t_func());
|
||||
} catch (...) {
|
||||
promise_ptr->set_exception(std::current_exception());
|
||||
}
|
||||
}));
|
||||
return future;
|
||||
}),
|
||||
"async");
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Skip BOM at the beginning of file
|
||||
|
||||
18
unittests/async_engine_lifetime.chai
Normal file
18
unittests/async_engine_lifetime.chai
Normal file
@ -0,0 +1,18 @@
|
||||
// Regression test for #632: Heap-use-after-free in async threads
|
||||
// Async threads must complete before the engine is destroyed.
|
||||
// Without the fix, this crashes with ASAN (use-after-free).
|
||||
|
||||
var func = fun(){
|
||||
var ret = 0;
|
||||
for (var i = 0; i < 1000; ++i) {
|
||||
ret += i;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
var fut1 = async(func);
|
||||
var fut2 = async(func);
|
||||
|
||||
// Wait for results to verify correctness
|
||||
assert_equal(fut1.get(), 499500);
|
||||
assert_equal(fut2.get(), 499500);
|
||||
42
unittests/async_lifetime_test.cpp
Normal file
42
unittests/async_lifetime_test.cpp
Normal file
@ -0,0 +1,42 @@
|
||||
// Regression test for #632: Heap-use-after-free in async threads during engine shutdown.
|
||||
// The engine must wait for outstanding async threads before destroying shared state.
|
||||
|
||||
#include <chaiscript/chaiscript.hpp>
|
||||
#include <chaiscript/chaiscript_basic.hpp>
|
||||
#include <chaiscript/language/chaiscript_parser.hpp>
|
||||
|
||||
int main() {
|
||||
// Test 1: Async threads that are still running when the engine is destroyed.
|
||||
// Without the fix, this triggers a heap-use-after-free under ASAN/TSAN.
|
||||
{
|
||||
chaiscript::ChaiScript chai;
|
||||
chai.eval(R"(
|
||||
var func = fun(){
|
||||
var ret = 0;
|
||||
for (var i = 0; i < 5000; ++i) {
|
||||
ret += i;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
var fut1 = async(func);
|
||||
var fut2 = async(func);
|
||||
)");
|
||||
// Engine is destroyed here without explicitly waiting for futures.
|
||||
// The fix ensures the engine joins all async threads before destruction.
|
||||
}
|
||||
|
||||
// Test 2: Verify async still works correctly (results are accessible).
|
||||
{
|
||||
chaiscript::ChaiScript chai;
|
||||
auto result = chai.eval<int>(R"(
|
||||
var f = async(fun() { return 42; });
|
||||
f.get();
|
||||
)");
|
||||
if (result != 42) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user