mafintosh / mafintosh/utp-native

SIGSEGV in napi_get_buffer_info: NAPI_MAKE_CALLBACK_AND_ALLOC uses an uninitialised napi_value

Open
#57 1 comment 0 reactions 0 assignees View on GitHub

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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.