@@ -25,8 +25,10 @@ std::string extract_error_from_napi_value(const Napi::CallbackInfo& cb_info)
2525 return " Unknown error from TypeScript" ;
2626}
2727
28- Napi::Function create_buffer_resolve_handler (Napi::Env env, CallbackResults* cb_results)
28+ Napi::Function create_buffer_resolve_handler (Napi::Env env, std::shared_ptr< CallbackResults> cb_results)
2929{
30+ // Capture shared_ptr by value to ensure CallbackResults outlives the Promise handler.
31+ // This prevents use-after-free when timeouts occur before the Promise resolves.
3032 return Napi::Function::New (
3133 env,
3234 [cb_results](const Napi::CallbackInfo& cb_info) -> Napi::Value {
@@ -56,8 +58,9 @@ Napi::Function create_buffer_resolve_handler(Napi::Env env, CallbackResults* cb_
5658 " resolveHandler" );
5759}
5860
59- Napi::Function create_string_resolve_handler (Napi::Env env, CallbackResults* cb_results)
61+ Napi::Function create_string_resolve_handler (Napi::Env env, std::shared_ptr< CallbackResults> cb_results)
6062{
63+ // Capture shared_ptr by value to ensure CallbackResults outlives the Promise handler.
6164 return Napi::Function::New (
6265 env,
6366 [cb_results](const Napi::CallbackInfo& cb_info) -> Napi::Value {
@@ -87,8 +90,9 @@ Napi::Function create_string_resolve_handler(Napi::Env env, CallbackResults* cb_
8790 " resolveHandler" );
8891}
8992
90- Napi::Function create_void_resolve_handler (Napi::Env env, CallbackResults* cb_results)
93+ Napi::Function create_void_resolve_handler (Napi::Env env, std::shared_ptr< CallbackResults> cb_results)
9194{
95+ // Capture shared_ptr by value to ensure CallbackResults outlives the Promise handler.
9296 return Napi::Function::New (
9397 env,
9498 [cb_results](const Napi::CallbackInfo& cb_info) -> Napi::Value {
@@ -98,8 +102,9 @@ Napi::Function create_void_resolve_handler(Napi::Env env, CallbackResults* cb_re
98102 " resolveHandler" );
99103}
100104
101- Napi::Function create_reject_handler (Napi::Env env, CallbackResults* cb_results)
105+ Napi::Function create_reject_handler (Napi::Env env, std::shared_ptr< CallbackResults> cb_results)
102106{
107+ // Capture shared_ptr by value to ensure CallbackResults outlives the Promise handler.
103108 return Napi::Function::New (
104109 env,
105110 [cb_results](const Napi::CallbackInfo& cb_info) -> Napi::Value {
@@ -110,16 +115,11 @@ Napi::Function create_reject_handler(Napi::Env env, CallbackResults* cb_results)
110115 " rejectHandler" );
111116}
112117
113- void attach_promise_handlers (Napi::Promise promise,
114- Napi::Function resolve_handler,
115- Napi::Function reject_handler,
116- CallbackResults* cb_results)
118+ void attach_promise_handlers (Napi::Promise promise, Napi::Function resolve_handler, Napi::Function reject_handler)
117119{
118120 auto then_prop = promise.Get (" then" );
119121 if (!then_prop.IsFunction ()) {
120- cb_results->error_message = " Promise does not have .then() method" ;
121- cb_results->result_promise .set_value (std::nullopt );
122- return ;
122+ throw std::runtime_error (" Promise does not have .then() method" );
123123 }
124124
125125 auto then_fn = then_prop.As <Napi::Function>();
@@ -149,32 +149,37 @@ template <typename T> T deserialize_from_msgpack(const std::vector<uint8_t>& dat
149149std::optional<std::vector<uint8_t >> invoke_ts_callback_with_promise (
150150 const Napi::ThreadSafeFunction& callback,
151151 const std::string& operation_name,
152- std::function<void (Napi::Env, Napi::Function, CallbackResults* )> call_js_function,
152+ std::function<void (Napi::Env, Napi::Function, std::shared_ptr< CallbackResults> )> call_js_function,
153153 std::chrono::seconds timeout)
154154{
155- // Create promise/future pair for synchronization
155+ // Create promise/future pair for synchronization.
156+ // The shared_ptr is passed to call_js_function which MUST capture it in Promise handlers.
157+ // This ensures CallbackResults outlives the Promise, even if we timeout and return early.
156158 auto callback_data = std::make_shared<CallbackResults>();
157159 auto future = callback_data->result_promise .get_future ();
158160
159- // Call TypeScript callback on the JS main thread
161+ // Call TypeScript callback on the JS main thread.
162+ // We pass the shared_ptr to the call_js_function so it can be captured by Promise handlers.
160163 auto status = callback.BlockingCall (
161164 callback_data.get (),
162- [call_js_function](Napi::Env env, Napi::Function js_callback, CallbackResults* cb_results) {
165+ [call_js_function, callback_data ](Napi::Env env, Napi::Function js_callback, CallbackResults* /* cb_results*/ ) {
163166 try {
164- // Call the TypeScript function with appropriate arguments
165- call_js_function (env, js_callback, cb_results);
167+ // Call the TypeScript function with the shared_ptr (not raw pointer).
168+ // The call_js_function MUST capture this shared_ptr in Promise handlers.
169+ call_js_function (env, js_callback, callback_data);
166170
167171 } catch (const std::exception& e) {
168- cb_results ->error_message = std::string (" Exception calling TypeScript: " ) + e.what ();
169- cb_results ->result_promise .set_value (std::nullopt );
172+ callback_data ->error_message = std::string (" Exception calling TypeScript: " ) + e.what ();
173+ callback_data ->result_promise .set_value (std::nullopt );
170174 }
171175 });
172176
173177 if (status != napi_ok) {
174178 throw std::runtime_error (" Failed to invoke TypeScript callback for " + operation_name);
175179 }
176180
177- // Wait for the promise to be fulfilled (with timeout)
181+ // Wait for the promise to be fulfilled (with timeout).
182+ // If timeout occurs, we throw but callback_data stays alive via shared_ptr in Promise handlers.
178183 auto wait_status = future.wait_for (timeout);
179184 if (wait_status == std::future_status::timeout) {
180185 throw std::runtime_error (" Timeout waiting for TypeScript callback for " + operation_name);
@@ -196,7 +201,9 @@ std::optional<std::vector<uint8_t>> invoke_single_string_callback(const Napi::Th
196201 const std::string& operation_name)
197202{
198203 return invoke_ts_callback_with_promise (
199- callback, operation_name, [input_str](Napi::Env env, Napi::Function js_callback, CallbackResults* cb_results) {
204+ callback,
205+ operation_name,
206+ [input_str](Napi::Env env, Napi::Function js_callback, std::shared_ptr<CallbackResults> cb_results) {
200207 auto js_input = Napi::String::New (env, input_str);
201208 auto js_result = js_callback.Call ({ js_input });
202209
@@ -207,9 +214,10 @@ std::optional<std::vector<uint8_t>> invoke_single_string_callback(const Napi::Th
207214 }
208215
209216 auto promise = js_result.As <Napi::Promise>();
217+ // Pass shared_ptr to handlers so CallbackResults outlives the Promise
210218 auto resolve_handler = create_buffer_resolve_handler (env, cb_results);
211219 auto reject_handler = create_reject_handler (env, cb_results);
212- attach_promise_handlers (promise, resolve_handler, reject_handler, cb_results );
220+ attach_promise_handlers (promise, resolve_handler, reject_handler);
213221 });
214222}
215223
@@ -221,7 +229,8 @@ std::optional<std::vector<uint8_t>> invoke_double_string_callback(const Napi::Th
221229 return invoke_ts_callback_with_promise (
222230 callback,
223231 operation_name,
224- [input_str1, input_str2](Napi::Env env, Napi::Function js_callback, CallbackResults* cb_results) {
232+ [input_str1,
233+ input_str2](Napi::Env env, Napi::Function js_callback, std::shared_ptr<CallbackResults> cb_results) {
225234 auto js_input1 = Napi::String::New (env, input_str1);
226235 auto js_input2 = Napi::String::New (env, input_str2);
227236 auto js_result = js_callback.Call ({ js_input1, js_input2 });
@@ -233,9 +242,10 @@ std::optional<std::vector<uint8_t>> invoke_double_string_callback(const Napi::Th
233242 }
234243
235244 auto promise = js_result.As <Napi::Promise>();
245+ // Pass shared_ptr to handlers so CallbackResults outlives the Promise
236246 auto resolve_handler = create_string_resolve_handler (env, cb_results);
237247 auto reject_handler = create_reject_handler (env, cb_results);
238- attach_promise_handlers (promise, resolve_handler, reject_handler, cb_results );
248+ attach_promise_handlers (promise, resolve_handler, reject_handler);
239249 });
240250}
241251
@@ -246,7 +256,8 @@ void invoke_buffer_void_callback(const Napi::ThreadSafeFunction& callback,
246256 auto result = invoke_ts_callback_with_promise (
247257 callback,
248258 operation_name,
249- [buffer_data = std::move (buffer_data)](Napi::Env env, Napi::Function js_callback, CallbackResults* cb_results) {
259+ [buffer_data = std::move (buffer_data)](
260+ Napi::Env env, Napi::Function js_callback, std::shared_ptr<CallbackResults> cb_results) {
250261 auto js_buffer = Napi::Buffer<uint8_t >::Copy (env, buffer_data.data (), buffer_data.size ());
251262 auto js_result = js_callback.Call ({ js_buffer });
252263
@@ -257,9 +268,10 @@ void invoke_buffer_void_callback(const Napi::ThreadSafeFunction& callback,
257268 }
258269
259270 auto promise = js_result.As <Napi::Promise>();
271+ // Pass shared_ptr to handlers so CallbackResults outlives the Promise
260272 auto resolve_handler = create_void_resolve_handler (env, cb_results);
261273 auto reject_handler = create_reject_handler (env, cb_results);
262- attach_promise_handlers (promise, resolve_handler, reject_handler, cb_results );
274+ attach_promise_handlers (promise, resolve_handler, reject_handler);
263275 });
264276
265277 // For void callbacks, we just need to ensure no errors occurred
0 commit comments