Skip to content

Commit 08f9e0f

Browse files
committed
Fix: resolve memory leaks, use-after-free, and correctness issues in … (#1408)
Comprehensive audit and fix of all C++ source files under `src/` that implement N-API bindings for the ROS2 rcl library. Five categories of issues were identified and resolved. ### 1. Use-after-free in context deleter lambda **File:** `src/rcl_context_bindings.cpp` Changed `[&env]` capture to `[env]` in the lambda deleter passed to `RclHandle`. Since `Napi::Env` is a lightweight pointer wrapper, capturing by value copies the `napi_env` pointer safely. Capturing by reference created a dangling reference because the lambda outlives the stack frame where `env` was declared — the deleter runs during garbage collection, long after `CreateContext` returns. ### 2. Memory leaks on early-return error paths **Files:** 9 binding files When `rcl_*_init()` fails, the previously `malloc`'d handle was not freed before throwing and returning. Added `free()` calls before the error throw in: - `src/rcl_node_bindings.cpp` — `CreateNode` - `src/rcl_publisher_bindings.cpp` — `CreatePublisher` - `src/rcl_timer_bindings.cpp` — `CreateTimer` (both `ROS_VERSION > 2305` branches) - `src/rcl_guard_condition_bindings.cpp` — `CreateGuardCondition` - `src/rcl_client_bindings.cpp` — `CreateClient` - `src/rcl_service_bindings.cpp` — `CreateService` - `src/rcl_action_client_bindings.cpp` — `ActionCreateClient` - `src/rcl_action_server_bindings.cpp` — `ActionCreateServer` - `src/rcl_lifecycle_bindings.cpp` — `CreateLifecycleStateMachine` (all 3 version branches) ### 3. Missing return after throw **File:** `src/rcl_type_description_service_bindings.cpp` Added `free(service)` and `return env.Undefined()` after `ThrowAsJavaScriptException()` on init failure. Without the return, execution continued and wrapped the uninitialized handle into an `RclHandle`, which would later attempt to finalize invalid memory. ### 4. Static variable not reset between calls **File:** `src/executor.cpp` Added `handle_closed = false;` immediately after the `static bool handle_closed` declaration in `Executor::Stop()`. Static local variables are initialized only once; on a second call to `Stop()`, `handle_closed` would still be `true` from the previous invocation, causing the `while (!handle_closed)` loop to be skipped entirely — the `uv_close` callback would never run and the async handle would leak. Fix: #1407
1 parent f1626e2 commit 08f9e0f

12 files changed

Lines changed: 155 additions & 57 deletions

src/executor.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ void Executor::Stop() {
110110

111111
if (uv_is_active(reinterpret_cast<uv_handle_t*>(async_))) {
112112
static bool handle_closed = false;
113+
handle_closed = false;
113114
uv_close(reinterpret_cast<uv_handle_t*>(async_),
114115
[](uv_handle_t* async) -> void {
115116
// Important Notice:

src/rcl_action_client_bindings.cpp

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,17 @@ Napi::Value ActionCreateClient(const Napi::CallbackInfo& info) {
7070
malloc(sizeof(rcl_action_client_t)));
7171
*action_client = rcl_action_get_zero_initialized_client();
7272

73-
THROW_ERROR_IF_NOT_EQUAL(
74-
rcl_action_client_init(action_client, node, ts, action_name.c_str(),
75-
&action_client_ops),
76-
RCL_RET_OK, rcl_get_error_string().str);
73+
{
74+
rcl_ret_t ret = rcl_action_client_init(
75+
action_client, node, ts, action_name.c_str(), &action_client_ops);
76+
if (RCL_RET_OK != ret) {
77+
std::string error_msg = rcl_get_error_string().str;
78+
rcl_reset_error();
79+
free(action_client);
80+
Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
81+
return env.Undefined();
82+
}
83+
}
7784
auto js_obj = RclHandle::NewInstance(
7885
env, action_client, node_handle, [node, env](void* ptr) {
7986
rcl_action_client_t* action_client =

src/rcl_action_server_bindings.cpp

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,18 @@ Napi::Value ActionCreateServer(const Napi::CallbackInfo& info) {
7676
malloc(sizeof(rcl_action_server_t)));
7777
*action_server = rcl_action_get_zero_initialized_server();
7878

79-
THROW_ERROR_IF_NOT_EQUAL(
80-
rcl_action_server_init(action_server, node, clock, ts,
81-
action_name.c_str(), &action_server_ops),
82-
RCL_RET_OK, rcl_get_error_string().str);
79+
{
80+
rcl_ret_t ret =
81+
rcl_action_server_init(action_server, node, clock, ts,
82+
action_name.c_str(), &action_server_ops);
83+
if (RCL_RET_OK != ret) {
84+
std::string error_msg = rcl_get_error_string().str;
85+
rcl_reset_error();
86+
free(action_server);
87+
Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
88+
return env.Undefined();
89+
}
90+
}
8391
auto js_obj = RclHandle::NewInstance(
8492
env, action_server, node_handle, [node, env](void* ptr) {
8593
rcl_action_server_t* action_server =

src/rcl_client_bindings.cpp

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,17 @@ Napi::Value CreateClient(const Napi::CallbackInfo& info) {
4949
client_ops.qos = *qos_profile;
5050
}
5151

52-
THROW_ERROR_IF_NOT_EQUAL(
53-
rcl_client_init(client, node, ts, service_name.c_str(), &client_ops),
54-
RCL_RET_OK, rcl_get_error_string().str);
52+
{
53+
rcl_ret_t ret =
54+
rcl_client_init(client, node, ts, service_name.c_str(), &client_ops);
55+
if (RCL_RET_OK != ret) {
56+
std::string error_msg = rcl_get_error_string().str;
57+
rcl_reset_error();
58+
free(client);
59+
Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
60+
return env.Undefined();
61+
}
62+
}
5563

5664
auto js_obj = RclHandle::NewInstance(
5765
env, client, node_handle, [node, env](void* ptr) {

src/rcl_context_bindings.cpp

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,13 @@ Napi::Value CreateContext(const Napi::CallbackInfo& info) {
120120
rcl_context_t* context =
121121
reinterpret_cast<rcl_context_t*>(malloc(sizeof(rcl_context_t)));
122122
*context = rcl_get_zero_initialized_context();
123-
auto js_obj =
124-
RclHandle::NewInstance(env, context, nullptr, [&env](void* ptr) {
125-
rcl_context_t* context = reinterpret_cast<rcl_context_t*>(ptr);
126-
rcl_ret_t ret = DestroyContext(env, context);
127-
free(ptr);
128-
THROW_ERROR_IF_NOT_EQUAL_NO_RETURN(RCL_RET_OK, ret,
129-
rcl_get_error_string().str);
130-
});
123+
auto js_obj = RclHandle::NewInstance(env, context, nullptr, [env](void* ptr) {
124+
rcl_context_t* context = reinterpret_cast<rcl_context_t*>(ptr);
125+
rcl_ret_t ret = DestroyContext(env, context);
126+
free(ptr);
127+
THROW_ERROR_IF_NOT_EQUAL_NO_RETURN(RCL_RET_OK, ret,
128+
rcl_get_error_string().str);
129+
});
131130

132131
return js_obj;
133132
}

src/rcl_guard_condition_bindings.cpp

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
#include <rcl/error_handling.h>
1818
#include <rcl/rcl.h>
1919

20+
#include <string>
21+
2022
#include "macros.h"
2123
#include "rcl_handle.h"
2224
#include "rcl_utilities.h"
@@ -37,9 +39,16 @@ Napi::Value CreateGuardCondition(const Napi::CallbackInfo& info) {
3739
rcl_guard_condition_options_t gc_options =
3840
rcl_guard_condition_get_default_options();
3941

40-
THROW_ERROR_IF_NOT_EQUAL(RCL_RET_OK,
41-
rcl_guard_condition_init(gc, context, gc_options),
42-
rcl_get_error_string().str);
42+
{
43+
rcl_ret_t ret = rcl_guard_condition_init(gc, context, gc_options);
44+
if (RCL_RET_OK != ret) {
45+
std::string error_msg = rcl_get_error_string().str;
46+
rcl_reset_error();
47+
free(gc);
48+
Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
49+
return env.Undefined();
50+
}
51+
}
4352

4453
auto handle = RclHandle::NewInstance(env, gc, nullptr, [env](void* ptr) {
4554
rcl_guard_condition_t* gc = reinterpret_cast<rcl_guard_condition_t*>(ptr);

src/rcl_lifecycle_bindings.cpp

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,17 @@ Napi::Value CreateLifecycleStateMachine(const Napi::CallbackInfo& info) {
7979
RclHandle* clock_handle = RclHandle::Unwrap(info[2].As<Napi::Object>());
8080
rcl_clock_t* clock = reinterpret_cast<rcl_clock_t*>(clock_handle->ptr());
8181

82-
THROW_ERROR_IF_NOT_EQUAL(
83-
RCL_RET_OK,
84-
rcl_lifecycle_state_machine_init(state_machine, node, clock, pn, cs, gs,
85-
gas, gat, gtg, &options),
86-
rcl_get_error_string().str);
82+
{
83+
rcl_ret_t ret = rcl_lifecycle_state_machine_init(
84+
state_machine, node, clock, pn, cs, gs, gas, gat, gtg, &options);
85+
if (RCL_RET_OK != ret) {
86+
std::string error_msg = rcl_get_error_string().str;
87+
rcl_reset_error();
88+
free(state_machine);
89+
Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
90+
return env.Undefined();
91+
}
92+
}
8793

8894
auto js_obj = RclHandle::NewInstance(
8995
env, state_machine, node_handle, [node, env](void* ptr) {
@@ -99,11 +105,17 @@ Napi::Value CreateLifecycleStateMachine(const Napi::CallbackInfo& info) {
99105
rcl_lifecycle_get_default_state_machine_options();
100106
options.enable_com_interface = info[1].As<Napi::Boolean>().Value();
101107

102-
THROW_ERROR_IF_NOT_EQUAL(
103-
RCL_RET_OK,
104-
rcl_lifecycle_state_machine_init(state_machine, node, pn, cs, gs, gas,
105-
gat, gtg, &options),
106-
rcl_get_error_string().str);
108+
{
109+
rcl_ret_t ret = rcl_lifecycle_state_machine_init(
110+
state_machine, node, pn, cs, gs, gas, gat, gtg, &options);
111+
if (RCL_RET_OK != ret) {
112+
std::string error_msg = rcl_get_error_string().str;
113+
rcl_reset_error();
114+
free(state_machine);
115+
Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
116+
return env.Undefined();
117+
}
118+
}
107119

108120
auto js_obj = RclHandle::NewInstance(
109121
env, state_machine, node_handle, [node, env](void* ptr) {
@@ -118,11 +130,18 @@ Napi::Value CreateLifecycleStateMachine(const Napi::CallbackInfo& info) {
118130
const rcl_node_options_t* node_options =
119131
reinterpret_cast<const rcl_node_options_t*>(rcl_node_get_options(node));
120132

121-
THROW_ERROR_IF_NOT_EQUAL(RCL_RET_OK,
122-
rcl_lifecycle_state_machine_init(
123-
state_machine, node, pn, cs, gs, gas, gat, gtg,
124-
true, &node_options->allocator),
125-
rcl_get_error_string().str);
133+
{
134+
rcl_ret_t ret = rcl_lifecycle_state_machine_init(
135+
state_machine, node, pn, cs, gs, gas, gat, gtg, true,
136+
&node_options->allocator);
137+
if (RCL_RET_OK != ret) {
138+
std::string error_msg = rcl_get_error_string().str;
139+
rcl_reset_error();
140+
free(state_machine);
141+
Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
142+
return env.Undefined();
143+
}
144+
}
126145

127146
auto js_obj = RclHandle::NewInstance(
128147
env, state_machine, node_handle, [node, node_options, env](void* ptr) {

src/rcl_node_bindings.cpp

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -224,10 +224,17 @@ Napi::Value CreateNode(const Napi::CallbackInfo& info) {
224224
options.rosout_qos = *qos_profile;
225225
}
226226

227-
THROW_ERROR_IF_NOT_EQUAL(RCL_RET_OK,
228-
rcl_node_init(node, node_name.c_str(),
229-
name_space.c_str(), context, &options),
230-
rcl_get_error_string().str);
227+
{
228+
rcl_ret_t ret = rcl_node_init(node, node_name.c_str(), name_space.c_str(),
229+
context, &options);
230+
if (RCL_RET_OK != ret) {
231+
std::string error_msg = rcl_get_error_string().str;
232+
rcl_reset_error();
233+
free(node);
234+
Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
235+
return env.Undefined();
236+
}
237+
}
231238

232239
auto handle = RclHandle::NewInstance(env, node, nullptr, [env](void* ptr) {
233240
rcl_node_t* node = reinterpret_cast<rcl_node_t*>(ptr);

src/rcl_publisher_bindings.cpp

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,17 @@ Napi::Value CreatePublisher(const Napi::CallbackInfo& info) {
5151
publisher_ops.qos = *qos_profile;
5252
}
5353

54-
THROW_ERROR_IF_NOT_EQUAL(
55-
rcl_publisher_init(publisher, node, ts, topic.c_str(), &publisher_ops),
56-
RCL_RET_OK, rcl_get_error_string().str);
54+
{
55+
rcl_ret_t ret = rcl_publisher_init(publisher, node, ts, topic.c_str(),
56+
&publisher_ops);
57+
if (RCL_RET_OK != ret) {
58+
std::string error_msg = rcl_get_error_string().str;
59+
rcl_reset_error();
60+
free(publisher);
61+
Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
62+
return env.Undefined();
63+
}
64+
}
5765

5866
auto js_obj = RclHandle::NewInstance(
5967
env, publisher, node_handle, [node, env](void* ptr) {
@@ -66,6 +74,7 @@ Napi::Value CreatePublisher(const Napi::CallbackInfo& info) {
6674

6775
return js_obj;
6876
} else {
77+
free(publisher);
6978
Napi::Error::New(env, GetErrorMessageAndClear())
7079
.ThrowAsJavaScriptException();
7180
return env.Undefined();

src/rcl_service_bindings.cpp

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,17 @@ Napi::Value CreateService(const Napi::CallbackInfo& info) {
5252
service_ops.qos = *qos_profile;
5353
}
5454

55-
THROW_ERROR_IF_NOT_EQUAL(
56-
rcl_service_init(service, node, ts, service_name.c_str(), &service_ops),
57-
RCL_RET_OK, rcl_get_error_string().str);
55+
{
56+
rcl_ret_t ret = rcl_service_init(service, node, ts, service_name.c_str(),
57+
&service_ops);
58+
if (RCL_RET_OK != ret) {
59+
std::string error_msg = rcl_get_error_string().str;
60+
rcl_reset_error();
61+
free(service);
62+
Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
63+
return env.Undefined();
64+
}
65+
}
5866
auto js_obj = RclHandle::NewInstance(
5967
env, service, node_handle, [node, env](void* ptr) {
6068
rcl_service_t* service = reinterpret_cast<rcl_service_t*>(ptr);

0 commit comments

Comments
 (0)