Possible native memory leak of the msgpack_sbuffer on the throw paths of pack
I found a possible alloc-before-validate leak in pack. The function obtains an
msgpack_sbuffer *sb (either popped from the reuse pool or freshly allocated with
msgpack_sbuffer_new()) and only transfers ownership of it at the very end, when it wraps
sb->data in a Node Buffer with the _free_sbuf finalizer. If any argument fails to serialize
— which is user-triggerable, e.g. an object containing a circular reference — the function
returns via Nan::ThrowError(...) before reaching that point, so sb (and any bytes already
accumulated in sb->data) is neither freed nor returned to the pool. Every failed
msgpack.pack(...) call leaks one msgpack_sbuffer.
File: src/msgpack.cc
Function: pack (NAN_METHOD(pack))
static NAN_METHOD(pack) {
Nan::HandleScope scope;
msgpack_packer pk;
MsgpackZone mz;
msgpack_sbuffer *sb;
if (!sbuffers.empty()) {
sb = sbuffers.top();
sbuffers.pop();
} else {
sb = msgpack_sbuffer_new(); // <-- per-call heap allocation
}
msgpack_packer_init(&pk, sb, msgpack_sbuffer_write);
for (int i = 0; i < info.Length(); i++) {
msgpack_object mo;
try {
v8_to_msgpack(info[i], &mo, &mz._mz, 0);
} catch (MsgpackException e) {
return Nan::ThrowError(e.getThrownException()); // <-- sb leaked
}
if (msgpack_pack_object(&pk, mo)) {
return Nan::ThrowError("Error serializaing object"); // <-- sb leaked
}
}
Local<Object> slowBuffer = Nan::NewBuffer(
sb->data, sb->size, _free_sbuf, (void *)sb // ownership handed off only here
).ToLocalChecked();
return info.GetReturnValue().Set(slowBuffer);
}
Path, line by line:
sb is acquired per call: reused from the sbuffers pool (in which case the pool relinquishes
that entry) or newly heap-allocated by msgpack_sbuffer_new().
- The only cleanup route for
sb is the success path, where Nan::NewBuffer(sb->data, sb->size, _free_sbuf, sb) transfers ownership; when the returned Buffer is GC'd, _free_sbuf either
frees sb or recycles it into the pool.
MsgpackZone mz is RAII, but it only owns the msgpack_zone (mz._mz) — it does not own
sb. sb is a raw pointer with no RAII wrapper.
- If
v8_to_msgpack throws MsgpackException (e.g. 512 < ++depth for a circular reference),
the catch returns Nan::ThrowError(...) and sb is dropped without being freed or pushed
back to the pool — a leak of the msgpack_sbuffer struct plus whatever sb->data had already
grown to from earlier successful arguments.
- The
msgpack_pack_object error path leaks identically.
This is a synchronous method (no async worker / after-work handoff), the leak is per operation,
and it is reachable with ordinary input.
JS trigger:
const msgpack = require('msgpack');
const o = {};
o.self = o; // circular reference -> v8_to_msgpack throws at depth > 512
for (let i = 0; i < 1e6; i++) {
try { msgpack.pack(o); } catch (e) {} // each call leaks one msgpack_sbuffer
}
Suggested fix: adopt sb in a RAII guard (or free/recycle it in the error paths). E.g. before
each return Nan::ThrowError(...), call _free_sbuf(sb->data, sb) (or reset and push sb back
onto sbuffers) so the buffer is reclaimed on every exit path, not only on success.
Possible native memory leak of the msgpack_sbuffer on the throw paths of
packI found a possible alloc-before-validate leak in
pack. The function obtains anmsgpack_sbuffer *sb(either popped from the reuse pool or freshly allocated withmsgpack_sbuffer_new()) and only transfers ownership of it at the very end, when it wrapssb->datain a Node Buffer with the_free_sbuffinalizer. If any argument fails to serialize— which is user-triggerable, e.g. an object containing a circular reference — the function
returns via
Nan::ThrowError(...)before reaching that point, sosb(and any bytes alreadyaccumulated in
sb->data) is neither freed nor returned to the pool. Every failedmsgpack.pack(...)call leaks onemsgpack_sbuffer.File:
src/msgpack.ccFunction:
pack(NAN_METHOD(pack))Path, line by line:
sbis acquired per call: reused from thesbufferspool (in which case the pool relinquishesthat entry) or newly heap-allocated by
msgpack_sbuffer_new().sbis the success path, whereNan::NewBuffer(sb->data, sb->size, _free_sbuf, sb)transfers ownership; when the returned Buffer is GC'd,_free_sbufeitherfrees
sbor recycles it into the pool.MsgpackZone mzis RAII, but it only owns themsgpack_zone(mz._mz) — it does not ownsb.sbis a raw pointer with no RAII wrapper.v8_to_msgpackthrowsMsgpackException(e.g.512 < ++depthfor a circular reference),the
catchreturnsNan::ThrowError(...)andsbis dropped without being freed or pushedback to the pool — a leak of the
msgpack_sbufferstruct plus whateversb->datahad alreadygrown to from earlier successful arguments.
msgpack_pack_objecterror path leaks identically.This is a synchronous method (no async worker / after-work handoff), the leak is per operation,
and it is reachable with ordinary input.
JS trigger:
Suggested fix: adopt
sbin a RAII guard (or free/recycle it in the error paths). E.g. beforeeach
return Nan::ThrowError(...), call_free_sbuf(sb->data, sb)(or reset and pushsbbackonto
sbuffers) so the buffer is reclaimed on every exit path, not only on success.