Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 0 additions & 3 deletions include/chaiscript/chaiscript_stdlib.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
35 changes: 35 additions & 0 deletions include/chaiscript/dispatchkit/dispatchkit.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down
18 changes: 18 additions & 0 deletions include/chaiscript/language/chaiscript_engine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <exception>
#include <fstream>
#include <functional>
#include <future>
#include <map>
#include <memory>
#include <mutex>
Expand Down Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions unittests/async_engine_lifetime.chai
Original file line number Diff line number Diff line change
@@ -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 changes: 42 additions & 0 deletions unittests/async_lifetime_test.cpp
Original file line number Diff line number Diff line change
@@ -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;
}