Skip to content

Commit 574cc35

Browse files
authored
Fix memory leaks, error handling, and type-safety issues in N-API bindings (#1422)
### Memory leaks (Critical) Added missing `free(header)` on `TAKE_FAILED` early-return paths where `malloc`'d `rmw_request_id_t*` was never freed. - `src/rcl_action_server_bindings.cpp` — `ActionTakeResultRequest`, `ActionTakeGoalRequest`, `ActionTakeCancelRequest` - `src/rcl_service_bindings.cpp` — `RclTakeRequest` ### Error handling misordering (High) Captured `rcl_get_error_string()` before calling `rcl_reset_error()` to avoid passing empty error messages to JavaScript. - `src/rcl_action_client_bindings.cpp` — `ActionTakeFeedback`, `ActionTakeStatus` - `src/rcl_action_server_bindings.cpp` — `ActionTakeGoalResponse`, `ActionTakeCancelResponse`, `ActionTakeResultResponse` ### Sequence number truncation (High) Changed `int32_t`/`uint32_t` → `double` casts for 64-bit sequence numbers to avoid silent truncation above 2^31. - `src/rcl_action_client_bindings.cpp`, `src/rcl_action_server_bindings.cpp`, `src/rcl_client_bindings.cpp` ### Exception-safe cleanup (Medium) Used `RCPPUTILS_SCOPE_EXIT` in `src/rcl_subscription_bindings.cpp` for `RclTakeRaw` and `GetContentFilter` to ensure resource finalization even if N-API calls throw. Both scope-exit blocks check the fini return value and call `rcl_reset_error()` on failure to prevent stale error state from leaking into subsequent calls. Reordered `free(subscription)` before `ThrowAsJavaScriptException()` in `CreateSubscription`. ### Context rollback on init failure (Medium) Wrapped post-init steps in `index.js` with `try/catch` that calls `context.tryShutdown()` if `generator.generateAll()` fails, preventing a half-initialized context from blocking re-initialization. The catch block normalizes non-Error exceptions before appending rollback failure details. Added corresponding test in `test/test-init-shutdown.js` with `try/finally` to guarantee sinon stub restoration even on assertion failure. Fix: #1420
1 parent 035ce9f commit 574cc35

7 files changed

Lines changed: 109 additions & 54 deletions

index.js

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -387,23 +387,35 @@ let rcl = {
387387

388388
rclnodejs.init(context.handle, argv, context._domainId);
389389

390-
if (_rosVersionChecked) {
391-
// no further processing required
392-
return;
393-
}
390+
try {
391+
if (_rosVersionChecked) {
392+
// no further processing required
393+
return;
394+
}
394395

395-
const version = await getCurrentGeneratorVersion();
396-
const forced =
397-
version === null || compareVersions(version, generator.version(), '<');
398-
if (forced) {
399-
debug(
400-
'The generator will begin to create JavaScript code from ROS IDL files...'
401-
);
402-
}
396+
const version = await getCurrentGeneratorVersion();
397+
const forced =
398+
version === null || compareVersions(version, generator.version(), '<');
399+
if (forced) {
400+
debug(
401+
'The generator will begin to create JavaScript code from ROS IDL files...'
402+
);
403+
}
403404

404-
await generator.generateAll(forced);
405-
// TODO determine if tsd generateAll() should be here
406-
_rosVersionChecked = true;
405+
await generator.generateAll(forced);
406+
// TODO determine if tsd generateAll() should be here
407+
_rosVersionChecked = true;
408+
} catch (error) {
409+
try {
410+
context.tryShutdown();
411+
} catch (shutdownError) {
412+
const initError =
413+
error instanceof Error ? error : new Error(String(error));
414+
initError.message += ` (rollback also failed: ${shutdownError.message})`;
415+
throw initError;
416+
}
417+
throw error;
418+
}
407419
},
408420

409421
/**

src/rcl_action_client_bindings.cpp

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ Napi::Value ActionSendGoalRequest(const Napi::CallbackInfo& info) {
132132
rcl_action_send_goal_request(action_client, buffer, &sequence_number),
133133
RCL_RET_OK, rcl_get_error_string().str);
134134

135-
return Napi::Number::New(env, static_cast<int32_t>(sequence_number));
135+
return Napi::Number::New(env, static_cast<double>(sequence_number));
136136
}
137137

138138
Napi::Value ActionSendResultRequest(const Napi::CallbackInfo& info) {
@@ -149,7 +149,7 @@ Napi::Value ActionSendResultRequest(const Napi::CallbackInfo& info) {
149149
rcl_action_send_result_request(action_client, buffer, &sequence_number),
150150
RCL_RET_OK, rcl_get_error_string().str);
151151

152-
return Napi::Number::New(env, static_cast<int32_t>(sequence_number));
152+
return Napi::Number::New(env, static_cast<double>(sequence_number));
153153
}
154154

155155
Napi::Value ActionTakeFeedback(const Napi::CallbackInfo& info) {
@@ -163,9 +163,9 @@ Napi::Value ActionTakeFeedback(const Napi::CallbackInfo& info) {
163163

164164
rcl_ret_t ret = rcl_action_take_feedback(action_client, buffer);
165165
if (ret != RCL_RET_OK && ret != RCL_RET_ACTION_CLIENT_TAKE_FAILED) {
166-
Napi::Error::New(env, rcl_get_error_string().str)
167-
.ThrowAsJavaScriptException();
166+
std::string error_msg = rcl_get_error_string().str;
168167
rcl_reset_error();
168+
Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
169169
return Napi::Boolean::New(env, false);
170170
}
171171

@@ -186,9 +186,9 @@ Napi::Value ActionTakeStatus(const Napi::CallbackInfo& info) {
186186

187187
rcl_ret_t ret = rcl_action_take_status(action_client, buffer);
188188
if (ret != RCL_RET_OK && ret != RCL_RET_ACTION_CLIENT_TAKE_FAILED) {
189+
std::string error_msg = rcl_get_error_string().str;
189190
rcl_reset_error();
190-
Napi::Error::New(env, rcl_get_error_string().str)
191-
.ThrowAsJavaScriptException();
191+
Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
192192
return Napi::Boolean::New(env, false);
193193
}
194194

@@ -247,7 +247,7 @@ Napi::Value ActionSendCancelRequest(const Napi::CallbackInfo& info) {
247247
rcl_action_send_cancel_request(action_client, buffer, &sequence_number),
248248
RCL_RET_OK, rcl_get_error_string().str);
249249

250-
return Napi::Number::New(env, static_cast<int32_t>(sequence_number));
250+
return Napi::Number::New(env, static_cast<double>(sequence_number));
251251
}
252252

253253
#if ROS_VERSION >= 2505 // ROS2 >= Kilted

src/rcl_action_server_bindings.cpp

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ Napi::Value ActionTakeResultRequest(const Napi::CallbackInfo& info) {
126126
return js_obj;
127127
}
128128

129+
free(header);
129130
return env.Undefined();
130131
}
131132

@@ -148,6 +149,7 @@ Napi::Value ActionTakeGoalRequest(const Napi::CallbackInfo& info) {
148149
return js_obj;
149150
}
150151

152+
free(header);
151153
return env.Undefined();
152154
}
153155

@@ -221,14 +223,14 @@ Napi::Value ActionTakeGoalResponse(const Napi::CallbackInfo& info) {
221223
free(header);
222224

223225
if (ret != RCL_RET_OK && ret != RCL_RET_ACTION_CLIENT_TAKE_FAILED) {
226+
std::string error_msg = rcl_get_error_string().str;
224227
rcl_reset_error();
225-
Napi::Error::New(env, rcl_get_error_string().str)
226-
.ThrowAsJavaScriptException();
228+
Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
227229
return env.Undefined();
228230
}
229231

230232
if (ret != RCL_RET_ACTION_CLIENT_TAKE_FAILED) {
231-
return Napi::Number::New(env, static_cast<int32_t>(sequence_number));
233+
return Napi::Number::New(env, static_cast<double>(sequence_number));
232234
}
233235
return env.Undefined();
234236
}
@@ -250,14 +252,14 @@ Napi::Value ActionTakeCancelResponse(const Napi::CallbackInfo& info) {
250252
free(header);
251253

252254
if (ret != RCL_RET_OK && ret != RCL_RET_ACTION_CLIENT_TAKE_FAILED) {
255+
std::string error_msg = rcl_get_error_string().str;
253256
rcl_reset_error();
254-
Napi::Error::New(env, rcl_get_error_string().str)
255-
.ThrowAsJavaScriptException();
257+
Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
256258
return env.Undefined();
257259
}
258260

259261
if (ret != RCL_RET_ACTION_CLIENT_TAKE_FAILED) {
260-
return Napi::Number::New(env, static_cast<int32_t>(sequence_number));
262+
return Napi::Number::New(env, static_cast<double>(sequence_number));
261263
}
262264
return env.Undefined();
263265
}
@@ -279,14 +281,14 @@ Napi::Value ActionTakeResultResponse(const Napi::CallbackInfo& info) {
279281
free(header);
280282

281283
if (ret != RCL_RET_OK && ret != RCL_RET_ACTION_CLIENT_TAKE_FAILED) {
284+
std::string error_msg = rcl_get_error_string().str;
282285
rcl_reset_error();
283-
Napi::Error::New(env, rcl_get_error_string().str)
284-
.ThrowAsJavaScriptException();
286+
Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
285287
return env.Undefined();
286288
}
287289

288290
if (ret != RCL_RET_ACTION_CLIENT_TAKE_FAILED) {
289-
return Napi::Number::New(env, static_cast<int32_t>(sequence_number));
291+
return Napi::Number::New(env, static_cast<double>(sequence_number));
290292
}
291293
return env.Undefined();
292294
}
@@ -463,6 +465,7 @@ Napi::Value ActionTakeCancelRequest(const Napi::CallbackInfo& info) {
463465
return js_obj;
464466
}
465467

468+
free(header);
466469
return env.Undefined();
467470
}
468471

src/rcl_client_bindings.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ Napi::Value SendRequest(const Napi::CallbackInfo& info) {
8888
THROW_ERROR_IF_NOT_EQUAL(rcl_send_request(client, buffer, &sequence_number),
8989
RCL_RET_OK, rcl_get_error_string().str);
9090

91-
return Napi::Number::New(env, static_cast<uint32_t>(sequence_number));
91+
return Napi::Number::New(env, static_cast<double>(sequence_number));
9292
}
9393

9494
Napi::Value RclTakeResponse(const Napi::CallbackInfo& info) {
@@ -104,7 +104,7 @@ Napi::Value RclTakeResponse(const Napi::CallbackInfo& info) {
104104
int64_t sequence_number = header.request_id.sequence_number;
105105

106106
if (ret == RCL_RET_OK) {
107-
return Napi::Number::New(env, static_cast<uint32_t>(sequence_number));
107+
return Napi::Number::New(env, static_cast<double>(sequence_number));
108108
}
109109

110110
rcl_reset_error();

src/rcl_service_bindings.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ Napi::Value RclTakeRequest(const Napi::CallbackInfo& info) {
9696
return js_obj;
9797
}
9898

99+
free(header);
99100
return env.Undefined();
100101
}
101102

src/rcl_subscription_bindings.cpp

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
#include <cstdio>
2121
#include <memory>
22+
#include <rcpputils/scope_exit.hpp>
23+
// NOLINTNEXTLINE
2224
#include <string>
2325

2426
#include "macros.h"
@@ -108,12 +110,6 @@ Napi::Value CreateSubscription(const Napi::CallbackInfo& info) {
108110
rcl_ret_t ret = rcl_subscription_options_set_content_filter_options(
109111
expression.c_str(), argc, (const char**)argv, &subscription_ops);
110112

111-
if (ret != RCL_RET_OK) {
112-
std::string error_string = rcl_get_error_string().str;
113-
rcl_reset_error();
114-
Napi::Error::New(env, error_string).ThrowAsJavaScriptException();
115-
}
116-
117113
if (argc) {
118114
for (int i = 0; i < argc; i++) {
119115
free(argv[i]);
@@ -122,7 +118,10 @@ Napi::Value CreateSubscription(const Napi::CallbackInfo& info) {
122118
}
123119

124120
if (ret != RCL_RET_OK) {
121+
std::string error_string = rcl_get_error_string().str;
122+
rcl_reset_error();
125123
free(subscription);
124+
Napi::Error::New(env, error_string).ThrowAsJavaScriptException();
126125
return env.Undefined();
127126
}
128127
}
@@ -137,8 +136,8 @@ Napi::Value CreateSubscription(const Napi::CallbackInfo& info) {
137136
if (ret != RCL_RET_OK) {
138137
std::string error_msg = rcl_get_error_string().str;
139138
rcl_reset_error();
140-
Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
141139
free(subscription);
140+
Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
142141
return env.Undefined();
143142
}
144143

@@ -157,9 +156,9 @@ Napi::Value CreateSubscription(const Napi::CallbackInfo& info) {
157156

158157
return js_obj;
159158
} else {
160-
Napi::Error::New(env, GetErrorMessageAndClear())
161-
.ThrowAsJavaScriptException();
159+
std::string error_msg = GetErrorMessageAndClear();
162160
free(subscription);
161+
Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
163162
return env.Undefined();
164163
}
165164
}
@@ -194,10 +193,16 @@ Napi::Value RclTakeRaw(const Napi::CallbackInfo& info) {
194193
return env.Undefined();
195194
}
196195

196+
RCPPUTILS_SCOPE_EXIT({
197+
rcl_ret_t fini_ret = rmw_serialized_message_fini(&msg);
198+
if (fini_ret != RCL_RET_OK) {
199+
rcl_reset_error();
200+
}
201+
});
202+
197203
Napi::Buffer<char> buffer = Napi::Buffer<char>::Copy(
198204
env, reinterpret_cast<char*>(msg.buffer), msg.buffer_length);
199-
THROW_ERROR_IF_NOT_EQUAL(rmw_serialized_message_fini(&msg), RCL_RET_OK,
200-
"Failed to deallocate message buffer");
205+
201206
return buffer;
202207
}
203208

@@ -369,6 +374,14 @@ Napi::Value GetContentFilter(const Napi::CallbackInfo& info) {
369374
return env.Undefined();
370375
}
371376

377+
RCPPUTILS_SCOPE_EXIT({
378+
rcl_ret_t fini_ret =
379+
rcl_subscription_content_filter_options_fini(subscription, &options);
380+
if (fini_ret != RCL_RET_OK) {
381+
rcl_reset_error();
382+
}
383+
});
384+
372385
// Create result object
373386
Napi::Object result = Napi::Object::New(env);
374387
result.Set(
@@ -387,16 +400,6 @@ Napi::Value GetContentFilter(const Napi::CallbackInfo& info) {
387400
}
388401
result.Set("parameters", parameters);
389402

390-
// Cleanup
391-
rcl_ret_t fini_ret =
392-
rcl_subscription_content_filter_options_fini(subscription, &options);
393-
if (fini_ret != RCL_RET_OK) {
394-
std::string error_msg = rcl_get_error_string().str;
395-
rcl_reset_error();
396-
Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
397-
return env.Undefined();
398-
}
399-
400403
return result;
401404
}
402405

test/test-init-shutdown.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
'use strict';
1616

1717
const assert = require('assert');
18+
const sinon = require('sinon');
1819
const rclnodejs = require('../index.js');
20+
const generator = require('../rosidl_gen/index.js');
1921

2022
describe('rclnodejs init and shutdown test suite', function () {
2123
this.timeout(60 * 1000);
@@ -99,6 +101,40 @@ describe('rclnodejs init and shutdown test suite', function () {
99101
}, 'shutting rclnodejs down twice should not cause an error to be thrown');
100102
});
101103

104+
it('rolls back context when generator init step fails', async function () {
105+
rclnodejs.shutdownAll();
106+
107+
const generateAllStub = sinon
108+
.stub(generator, 'generateAll')
109+
.rejects(new Error('generator failed'));
110+
111+
try {
112+
const indexPath = require.resolve('../index.js');
113+
delete require.cache[indexPath];
114+
const freshRclnodejs = require('../index.js');
115+
116+
const failedContext = new freshRclnodejs.Context();
117+
118+
await assert.rejects(async () => {
119+
await freshRclnodejs.init(failedContext);
120+
}, /generator failed/);
121+
assert.ok(freshRclnodejs.isShutdown(failedContext));
122+
123+
generateAllStub.restore();
124+
125+
const recoveredContext = new freshRclnodejs.Context();
126+
await assert.doesNotReject(async () => {
127+
await freshRclnodejs.init(recoveredContext);
128+
});
129+
freshRclnodejs.shutdown(recoveredContext);
130+
freshRclnodejs.removeSignalHandlers();
131+
} finally {
132+
if (generateAllStub.restore) {
133+
generateAllStub.restore();
134+
}
135+
}
136+
});
137+
102138
it('rclnodejs create node without init should fail', async function () {
103139
assert.throws(
104140
() => {

0 commit comments

Comments
 (0)