Skip to content

Commit 9eb37b5

Browse files
Teo Koon PengMinggang Wang
authored andcommitted
fix: buffer overflows (#684)
Fix leak in CreateNode/ShadowNode For some reason, assigning the object wrapping `RclHandle` to a new persistent handle causes the weak callback to not get called, and the wrapped `RclHandle` is lost. My theory is that now that it has a strong persistent handle keeping it alive, v8 decides that the weak persistent handle is not needed anymore and disposes it. The fix is to simply remove the `handle` accessors, since the `RclHandle`'s life is now handled by v8, it should be automatically cleaned up when there is no more reference to the `ShadowNode`. * add cpp test artifacts to .gitignore * fix possible buffer overflows and use after stack scope * add helper scripts and instructions to run tests with asan * fix cpplint errors * make node.handle readonly; remove corrupted handle destruction test Fix #674
1 parent 35317bc commit 9eb37b5

File tree

13 files changed

+107
-115
lines changed

13 files changed

+107
-115
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,7 @@ install
2121
log
2222
.vscode
2323
.project
24+
25+
# test build artifacts
26+
/test/cpp/add_two_ints_client
27+
/test/cpp/listener

README.md

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
| develop | [![Build Status](https://travis-ci.org/RobotWebTools/rclnodejs.svg?branch=develop)](https://travis-ci.org/RobotWebTools/rclnodejs) | [![macOS Build Status](https://circleci.com/gh/RobotWebTools/rclnodejs/tree/develop.svg?style=shield)](https://circleci.com/gh/RobotWebTools/rclnodejs) | [![Build status](https://ci.appveyor.com/api/projects/status/upbc7tavdag1aa5e/branch/develop?svg=true)](https://ci.appveyor.com/project/minggangw/rclnodejs/branch/develop) |
66
| master | [![Build Status](https://travis-ci.org/RobotWebTools/rclnodejs.svg?branch=master)](https://travis-ci.org/RobotWebTools/rclnodejs) | [![macOS Build Status](https://circleci.com/gh/RobotWebTools/rclnodejs/tree/master.svg?style=shield)](https://circleci.com/gh/RobotWebTools/rclnodejs) | [![Build status](https://ci.appveyor.com/api/projects/status/upbc7tavdag1aa5e/branch/master?svg=true)](https://ci.appveyor.com/project/minggangw/rclnodejs/branch/master) |
77

8-
`rclnodejs` is a Node.js client for the Robot Operating System (ROS 2). It provides a simple and easy JavaScript API for ROS 2 programming. TypeScript declarations are included to support use of rclnodejs in TypeScript projects.
8+
`rclnodejs` is a Node.js client for the Robot Operating System (ROS 2). It provides a simple and easy JavaScript API for ROS 2 programming. TypeScript declarations are included to support use of rclnodejs in TypeScript projects.
99

10-
Here's an example for how to create a ROS 2 node that publishes a string message in a few lines of JavaScript.
10+
Here's an example for how to create a ROS 2 node that publishes a string message in a few lines of JavaScript.
1111

12-
``` JavaScript
12+
```JavaScript
1313
const rclnodejs = require('rclnodejs');
1414
rclnodejs.init().then(() => {
1515
const node = rclnodejs.createNode('publisher_example_node');
@@ -21,27 +21,30 @@ rclnodejs.init().then(() => {
2121

2222
## Prerequisites
2323

24-
**Node.js**
25-
* [Node.js](https://nodejs.org/en/) version between 8.12 - 12.x.
24+
**Node.js**
2625

27-
**ROS 2 SDK**
28-
* See the ROS 2 SDK [Installation Guide](https://index.ros.org/doc/ros2/Installation/) for details.
29-
* *** DON'T FORGET TO [SOURCE THE ROS 2 STARTUP FILES](https://index.ros.org/doc/ros2/Tutorials/Configuring-ROS2-Environment/#source-the-setup-files) ***
26+
- [Node.js](https://nodejs.org/en/) version between 8.12 - 12.x.
27+
28+
**ROS 2 SDK**
29+
30+
- See the ROS 2 SDK [Installation Guide](https://index.ros.org/doc/ros2/Installation/) for details.
31+
- **_ DON'T FORGET TO [SOURCE THE ROS 2 STARTUP FILES](https://index.ros.org/doc/ros2/Tutorials/Configuring-ROS2-Environment/#source-the-setup-files) _**
3032

3133
## Install rclnodejs
3234

33-
Install the rclnodejs version that is compatible with your version of ROS 2 (see table below).
34-
35+
Install the rclnodejs version that is compatible with your version of ROS 2 (see table below).
36+
3537
Run the following command for the most current version of rclnodejs
3638

37-
``` bash
39+
```bash
3840
npm i rclnodejs
3941
```
42+
4043
or to install a specific version of rclnodejs use
41-
``` bash
44+
45+
```bash
4246
4347
```
44-
#### RCLNODEJS - ROS 2 Version Compatibility
4548

4649
| RCLNODEJS Version | Compatible ROS 2 Release |
4750
| :---------------: | :-----------: |
@@ -97,7 +100,6 @@ The benefits of using TypeScript become evident when working with more complex u
97100
}
98101
```
99102

100-
101103
## Build from Scratch
102104

103105
### Get ready for ROS 2
@@ -201,6 +203,36 @@ This is caused by a bug in `ref` which happens when you `require` it multiple ti
201203

202204
If it is required to use Jest, a solution would be to fork `ref` and use npm shrinkwrap to installed a patched version.
203205

206+
### Running with ASAN
207+
208+
#### Linux
209+
210+
To run with google's AddressSanitizer tool, build with `-fsanitize=address` flag,
211+
212+
```sh
213+
CXXFLAGS=-fsanitize=address node-gyp build --debug
214+
```
215+
216+
ASAN needs to be loaded at the start of the process, since rclnodejs is a dynamically loaded library, it will not do so by default. To workaround this, run node with `LD_PRELOAD` to force ASAN to be loaded.
217+
218+
```sh
219+
LD_PRELOAD=$(g++ -print-file-name=libasan.so) node node_modules/.bin/mocha test/test-publisher.js
220+
```
221+
222+
Due to v8's garbage collector, there may be false positives in the leak test, to remove them as much as possible, there is a simple helper script to run gc on exit. To use it, the `--expose-gc` flag needs to be set in node, then run mocha with `-r test/gc-on-exit.js` e.g.
223+
224+
```sh
225+
LD_PRELOAD=$(g++ -print-file-name=libasan.so) node --expose-gc node_modules/.bin/mocha -r test/gc-on-exit.js test/test-publisher.js
226+
```
227+
228+
**Note**: Tests that forks the current process like `test-array.js` will not run gc when they exit. They may report many false positive leaks.
229+
230+
ASAN may report leaks in ref-napi and other modules, there is a suppression file you can use to hide them
231+
232+
```sh
233+
LSAN_OPTIONS=suppressions=suppr.txt node --expose-gc node_modules/.bin/mocha -r test/gc-on-exit.js test/test-publisher.js
234+
```
235+
204236
## Get Involved
205237

206238
### Contributing

index.js

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ const {
5252

5353
function inherits(target, source) {
5454
let properties = Object.getOwnPropertyNames(source.prototype);
55-
properties.forEach(property => {
55+
properties.forEach((property) => {
5656
target.prototype[property] = source.prototype[property];
5757
});
5858
}
@@ -86,7 +86,6 @@ function getCurrentGeneratorVersion() {
8686
* @exports rclnodejs
8787
*/
8888
let rcl = {
89-
9089
_rosVersionChecked: false,
9190

9291
// Map<Context,Array<Node>
@@ -198,13 +197,10 @@ let rcl = {
198197
let handle = rclnodejs.createNode(nodeName, namespace, context.handle());
199198
let node = new rclnodejs.ShadowNode();
200199
node.handle = handle;
200+
Object.defineProperty(node, 'handle', { configurable: false, writable: false }); // make read-only
201201
node.context = context;
202202
node.init(nodeName, namespace, context, options);
203-
debug(
204-
'Finish initializing node, name = %s and namespace = %s.',
205-
nodeName,
206-
namespace
207-
);
203+
debug('Finish initializing node, name = %s and namespace = %s.', nodeName, namespace);
208204

209205
this._contextToNodeArrayMap.get(context).push(node);
210206
return node;
@@ -217,12 +213,11 @@ let rcl = {
217213
* @return {Promise<undefined>} A Promise.
218214
*/
219215
init(context = Context.defaultContext(), argv = process.argv) {
220-
221216
return new Promise((resolve, reject) => {
222217
// check if context has already been initialized
223218
if (this._contextToNodeArrayMap.has(context)) {
224219
throw new Error('The context has already been initialized.');
225-
};
220+
}
226221

227222
// check argv for correct value and state
228223
if (!Array.isArray(argv)) {
@@ -242,14 +237,10 @@ let rcl = {
242237
}
243238

244239
getCurrentGeneratorVersion()
245-
.then(version => {
246-
let forced =
247-
version === null ||
248-
compareVersions(version, generator.version()) === -1;
240+
.then((version) => {
241+
let forced = version === null || compareVersions(version, generator.version()) === -1;
249242
if (forced) {
250-
debug(
251-
'The generator will begin to create JavaScript code from ROS IDL files...'
252-
);
243+
debug('The generator will begin to create JavaScript code from ROS IDL files...');
253244
}
254245

255246
generator
@@ -258,11 +249,11 @@ let rcl = {
258249
this._rosVersionChecked = true;
259250
resolve();
260251
})
261-
.catch(e => {
252+
.catch((e) => {
262253
reject(e);
263254
});
264255
})
265-
.catch(e => {
256+
.catch((e) => {
266257
reject(e);
267258
});
268259
});
@@ -311,20 +302,20 @@ let rcl = {
311302
}
312303

313304
// shutdown and remove all nodes assigned to context
314-
this._contextToNodeArrayMap.get(context).forEach(node => {
305+
this._contextToNodeArrayMap.get(context).forEach((node) => {
315306
node.stopSpinning();
316307
node.destroy();
317308
});
318309
this._contextToNodeArrayMap.delete(context);
319-
310+
320311
// shutdown context
321312
if (context === Context.defaultContext()) {
322313
Context.shutdownDefaultContext();
323314
} else {
324315
context.shutdown();
325316
}
326317
},
327-
318+
328319
/**
329320
* A predictate for testing if a context has been shutdown.
330321
* @param {Context} [context=defaultContext] - The context to inspect
@@ -361,7 +352,7 @@ let rcl = {
361352
debug('Finish regeneration.');
362353
resolve();
363354
})
364-
.catch(e => {
355+
.catch((e) => {
365356
reject(e);
366357
});
367358
});
@@ -424,7 +415,7 @@ process.on('SIGINT', () => {
424415

425416
module.exports = rcl;
426417

427-
// The following statements are located here to work around a
418+
// The following statements are located here to work around a
428419
// circular dependency issue occuring in rate.js
429420
const Node = require('./lib/node.js');
430421
const TimeSource = require('./lib/time_source.js');

src/addon.cpp

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,15 @@
2525
void InitModule(v8::Local<v8::Object> exports) {
2626
v8::Local<v8::Context> context = exports->GetIsolate()->GetCurrentContext();
2727

28-
for (uint32_t i = 0;
29-
i < rclnodejs::GetBindingMethodsCount(rclnodejs::binding_methods); i++) {
28+
for (uint32_t i = 0; i < rclnodejs::binding_methods.size(); i++) {
3029
Nan::Set(
3130
exports, Nan::New(rclnodejs::binding_methods[i].name).ToLocalChecked(),
3231
Nan::New<v8::FunctionTemplate>(rclnodejs::binding_methods[i].function)
3332
->GetFunction(context)
3433
.ToLocalChecked());
3534
}
3635

37-
for (uint32_t i = 0;
38-
i < rclnodejs::GetBindingMethodsCount(rclnodejs::action_binding_methods);
39-
i++) {
36+
for (uint32_t i = 0; i < rclnodejs::action_binding_methods.size(); i++) {
4037
Nan::Set(
4138
exports,
4239
Nan::New(rclnodejs::action_binding_methods[i].name).ToLocalChecked(),

src/rcl_action_bindings.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
#include <memory>
2424
#include <string>
25+
#include <vector>
2526

2627
#include "handle_manager.hpp"
2728
#include "macros.hpp"
@@ -761,7 +762,7 @@ NAN_METHOD(ActionGetNamesAndTypes) {
761762
info.GetReturnValue().Set(result_list);
762763
}
763764

764-
BindingMethod action_binding_methods[] = {
765+
std::vector<BindingMethod> action_binding_methods = {
765766
{"actionCreateClient", ActionCreateClient},
766767
{"actionCreateServer", ActionCreateServer},
767768
{"actionServerIsAvailable", ActionServerIsAvailable},

src/rcl_action_bindings.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@
1818
#include <nan.h>
1919
#include <rcl/rcl.h>
2020

21-
#include <string>
21+
#include <vector>
2222

2323
#include "rcl_bindings.hpp"
2424

2525
namespace rclnodejs {
2626

27-
extern BindingMethod action_binding_methods[];
27+
extern std::vector<BindingMethod> action_binding_methods;
2828

2929
} // namespace rclnodejs
3030

src/rcl_bindings.cpp

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838

3939
#include <memory>
4040
#include <string>
41+
#include <vector>
4142

4243
#include "handle_manager.hpp"
4344
#include "macros.hpp"
@@ -91,10 +92,10 @@ NAN_METHOD(Init) {
9192
argv = reinterpret_cast<char**>(malloc(argc * sizeof(char*)));
9293
for (int i = 0; i < argc; i++) {
9394
Nan::MaybeLocal<v8::Value> jsElement = Nan::Get(jsArgv, i);
94-
char* arg = *Nan::Utf8String(jsElement.ToLocalChecked());
95-
int len = std::strlen(arg) + 1;
95+
Nan::Utf8String utf8_arg(jsElement.ToLocalChecked());
96+
int len = utf8_arg.length() + 1;
9697
argv[i] = reinterpret_cast<char*>(malloc(len * sizeof(char*)));
97-
snprintf(argv[i], len, "%s", arg);
98+
snprintf(argv[i], len, "%s", *utf8_arg);
9899
}
99100
}
100101

@@ -1691,15 +1692,7 @@ NAN_METHOD(ServiceServerIsAvailable) {
16911692
info.GetReturnValue().Set(result);
16921693
}
16931694

1694-
uint32_t GetBindingMethodsCount(BindingMethod* methods) {
1695-
uint32_t count = 0;
1696-
while (methods[count].function) {
1697-
count++;
1698-
}
1699-
return count;
1700-
}
1701-
1702-
BindingMethod binding_methods[] = {
1695+
std::vector<BindingMethod> binding_methods = {
17031696
{"init", Init},
17041697
{"createNode", CreateNode},
17051698
{"getParameterOverrides", GetParameterOverrides},

src/rcl_bindings.hpp

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
#include <memory>
2323
#include <string>
24+
#include <vector>
2425

2526
namespace rclnodejs {
2627

@@ -36,11 +37,9 @@ extern rcl_guard_condition_t* g_sigint_gc;
3637
void ExtractNamesAndTypes(rcl_names_and_types_t names_and_types,
3738
v8::Local<v8::Array>* result_list);
3839

39-
uint32_t GetBindingMethodsCount(BindingMethod* methods);
40-
4140
std::unique_ptr<rmw_qos_profile_t> GetQoSProfile(v8::Local<v8::Value> qos);
4241

43-
extern BindingMethod binding_methods[];
42+
extern std::vector<BindingMethod> binding_methods;
4443

4544
} // namespace rclnodejs
4645

src/shadow_node.cpp

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,14 @@ namespace rclnodejs {
2525

2626
Nan::Persistent<v8::Function> ShadowNode::constructor;
2727

28-
ShadowNode::ShadowNode()
29-
: handle_manager_(std::make_unique<HandleManager>()), rcl_handle_(nullptr) {
28+
ShadowNode::ShadowNode() : handle_manager_(std::make_unique<HandleManager>()) {
3029
executor_ = std::make_unique<Executor>(handle_manager_.get(), this);
31-
rcl_handle_.reset(new Nan::Persistent<v8::Object>());
3230
}
3331

3432
ShadowNode::~ShadowNode() {
3533
Nan::HandleScope scope;
3634

3735
StopRunning();
38-
if (!rcl_handle_->IsEmpty()) {
39-
RclHandle* handle = RclHandle::Unwrap<RclHandle>(Nan::New(*rcl_handle_));
40-
handle->Reset();
41-
}
4236
}
4337

4438
void ShadowNode::Init(v8::Local<v8::Object> exports) {
@@ -49,8 +43,6 @@ void ShadowNode::Init(v8::Local<v8::Object> exports) {
4943
tpl->SetClassName(Nan::New("ShadowNode").ToLocalChecked());
5044
tpl->InstanceTemplate()->SetInternalFieldCount(1);
5145

52-
Nan::SetAccessor(tpl->InstanceTemplate(), Nan::New("handle").ToLocalChecked(),
53-
HandleGetter, HandleSetter);
5446
Nan::SetPrototypeMethod(tpl, "start", Start);
5547
Nan::SetPrototypeMethod(tpl, "stop", Stop);
5648
Nan::SetPrototypeMethod(tpl, "syncHandles", SyncHandles);
@@ -63,21 +55,6 @@ void ShadowNode::Init(v8::Local<v8::Object> exports) {
6355
tpl->GetFunction(context).ToLocalChecked());
6456
}
6557

66-
NAN_GETTER(ShadowNode::HandleGetter) {
67-
auto* me = ShadowNode::Unwrap<ShadowNode>(info.Holder());
68-
69-
if (!me->rcl_handle()->IsEmpty())
70-
info.GetReturnValue().Set(Nan::New(*(me->rcl_handle())));
71-
else
72-
info.GetReturnValue().Set(Nan::Undefined());
73-
}
74-
75-
NAN_SETTER(ShadowNode::HandleSetter) {
76-
auto* me = ShadowNode::Unwrap<ShadowNode>(info.Holder());
77-
auto obj = Nan::To<v8::Object>(value).ToLocalChecked();
78-
if (obj->InternalFieldCount() > 0) me->rcl_handle()->Reset(obj);
79-
}
80-
8158
void ShadowNode::StopRunning() {
8259
executor_->Stop();
8360
handle_manager_->ClearHandles();

0 commit comments

Comments
 (0)