mafintosh / mafintosh/utp-native
SIGSEGV in napi_get_buffer_info: NAPI_MAKE_CALLBACK_AND_ALLOC uses an uninitialised napi_value
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 101
- Forks
- 36
- PR merge metrics
- No merged PRs in 30d
Description
### Summary
`UTP_NAPI_BUFFER_ALLOC` reads a `napi_value` that `NAPI_MAKE_CALLBACK_AND_ALLOC` may never have written, and passes it to `napi_get_buffer_info`. V8 then dereferences whatever was on the stack, and the process dies with SIGSEGV inside `v8::Value::IsArrayBufferView()`.
We hit this in production 13 times over three days (aarch64, Alpine, Node 24, utp-native 2.5.3 via webtorrent 2.8.5), always on incoming data, never at shutdown.
### The code
`binding.cc`:
```c
#define NAPI_MAKE_CALLBACK_AND_ALLOC(env, nil, ctx, cb, n, argv, res, nread) \
if (napi_make_callback(env, nil, ctx, cb, n, argv, &res) == napi_pending_exception) { \
... \
} else { \
UTP_NAPI_BUFFER_ALLOC(self, res, nread) \
}
#define UTP_NAPI_BUFFER_ALLOC(self, ret, nread) \
char *buf; \
size_t buf_len; \
napi_get_buffer_info(env, ret, (void **) &buf, &buf_len); \
...
```
`res` is declared uninitialised at each call site, e.g. in `on_utp_read`:
```c
UTP_NAPI_CALLBACK(self->on_read, {
napi_value ret; // uninitialised
napi_value argv[1];
napi_create_uint32(env, self->recv_packet_size, &(argv[0]));
NAPI_MAKE_CALLBACK_AND_ALLOC(env, NULL, ctx, callback, 1, argv, ret, self->recv_packet_size)
self->recv_packet_size = 0;
})
```
`napi_make_callback` has more failure modes than `napi_pending_exception` — `napi_closing` while the environment is tearing down, and `napi_invalid_arg` when `callback` is not a function, which is what `napi_get_reference_value` yields once the reference is gone. In both cases `res` is left as stack garbage and is then dereferenced.
### The stack
```
#0 v8::Value::IsArrayBufferView() const
#1 napi_get_buffer_info()
#2 on_utp_read(utp_callback_arguments*) utp_native.node
#3 utp_call_on_read(struct_utp_context*, UTPSocket*, unsigned char const*, unsigned long)
#4 utp_process_incoming(UTPSocket*, unsigned char const*, unsigned long, bool)
#5 utp_process_udp()
```
From the core dump, the main thread was idle in `epoll_pwait` inside `node::SpinEventLoopInternal` — nothing was shutting down. Only the worker thread running the uTP socket died.
### How the JS side gets torn down mid-callback
`Connection.prototype._onread` calls `this.push(buf)`, and a Node stream delivers to the consumer synchronously. Our consumer is BitTorrent wire-protocol parsing, which legitimately destroys the connection on that same data — a duplicate peer, a rejected handshake, a torrent being removed. `destroy()` runs inside the callback and calls `utp_napi_connection_close`, so by the time the C++ side resumes after `napi_make_callback`, the connection state it is about to use may be gone. libutp still holds the socket with `utp_get_userdata` pointing at it.
### Suggested fix
Check the status and bail out instead of using `res`:
```c
napi_status status = napi_make_callback(env, nil, ctx, cb, n, argv, &res);
if (status == napi_pending_exception) { ... }
else if (status == napi_ok) { UTP_NAPI_BUFFER_ALLOC(self, res, nread) }
/* otherwise: the environment or the callback is gone — nothing to allocate */
```
and initialise `napi_value ret = NULL;` at the call sites, so a missed path cannot dereference stack garbage. Checking the result of `napi_get_buffer_info` itself would make it safe against a non-buffer return value as well.
Happy to send a PR if that shape is acceptable.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in binding.cc at NAPI_MAKE_CALLBACK_AND_ALLOC and the on_utp_read call site, then trace the napi_make_callback status handling through the incoming-data path. Reproduce or inspect the reported SIGSEGV stack and verify that teardown or invalid callbacks do not reach buffer allocation, that ret is initialized, and that napi_get_buffer_info failures are handled.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, javascript, node.js
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 48/100