Possible memory leak of logCarrier payload in the libav logging callback
I found a possible per-message heap leak in the custom libav logging bridge. Each libav log line
allocates a logCarrier with new and hands it to the thread-safe function as the payload. The
JS-facing callback callJsCb receives that pointer as its data argument but never deletes it,
so one logCarrier (which owns a std::string) leaks for every log line emitted once a logging
callback has been installed via setLoggingCallback.
File: src/log.cc
Functions: av_log_custom_callback (producer) and callJsCb (consumer)
void av_log_custom_callback(void* ptr, int level, const char* fmt, va_list vl) {
...
logCarrier* c = new logCarrier; // allocated per log line
c->msg = line;
c->level = level;
napi_status status;
status = napi_call_threadsafe_function(threadSafeFunction, c, napi_tsfn_nonblocking);
return; // ownership handed to TSFN as `data`
}
static void callJsCb(napi_env env, napi_value jsCallback, void* context, void* data) {
logCarrier* c = (logCarrier*) data;
...
status = napi_call_function(env, jsThis, jsCallback, 1, &jsStr, &return_val);
CHECK_STATUS_VOID;
return; // never `delete c;`
}
av_log_custom_callback runs on whatever thread libav logs from; it does new logCarrier for
each message and passes the pointer as the TSFN payload.
callJsCb is the call_js_cb registered with the thread-safe function; it casts data back to
logCarrier*, uses c->msg, and returns. There is no delete c on any path (confirmed: the
only logCarrier references in log.cc are the new and this cast — no delete/free).
- Therefore every delivered log message leaks one
logCarrier and its embedded std::string.
Any early CHECK_STATUS_VOID return inside callJsCb leaks it too, but the leak also occurs on
the normal success path, so it is unconditional.
Note the TSFN is created with max_queue_size = 0 (unlimited), so the enqueue in
av_log_custom_callback will not fail with napi_queue_full; the reportable defect is the
missing free in callJsCb, not an unchecked bounded-queue enqueue.
JS trigger (if applicable):
const beamcoder = require('beamcoder');
beamcoder.setLoggingCallback(() => {});
beamcoder.logging('trace'); // verbose level → many log lines, each leaks a logCarrier
// any decode/demux/encode work now leaks a logCarrier per emitted log line
Suggested fix: delete c; at the end of callJsCb (after the callback has been invoked), on all
return paths — e.g. wrap the body so the payload is freed even when CHECK_STATUS_VOID returns
early, or adopt it into a std::unique_ptr<logCarrier> at the top of callJsCb.
Possible memory leak of
logCarrierpayload in the libav logging callbackI found a possible per-message heap leak in the custom libav logging bridge. Each libav log line
allocates a
logCarrierwithnewand hands it to the thread-safe function as the payload. TheJS-facing callback
callJsCbreceives that pointer as itsdataargument but neverdeletes it,so one
logCarrier(which owns astd::string) leaks for every log line emitted once a loggingcallback has been installed via
setLoggingCallback.File:
src/log.ccFunctions:
av_log_custom_callback(producer) andcallJsCb(consumer)av_log_custom_callbackruns on whatever thread libav logs from; it doesnew logCarrierforeach message and passes the pointer as the TSFN payload.
callJsCbis thecall_js_cbregistered with the thread-safe function; it castsdataback tologCarrier*, usesc->msg, and returns. There is nodelete con any path (confirmed: theonly
logCarrierreferences inlog.ccare thenewand this cast — nodelete/free).logCarrierand its embeddedstd::string.Any early
CHECK_STATUS_VOIDreturn insidecallJsCbleaks it too, but the leak also occurs onthe normal success path, so it is unconditional.
Note the TSFN is created with
max_queue_size = 0(unlimited), so the enqueue inav_log_custom_callbackwill not fail withnapi_queue_full; the reportable defect is themissing free in
callJsCb, not an unchecked bounded-queue enqueue.JS trigger (if applicable):
Suggested fix:
delete c;at the end ofcallJsCb(after the callback has been invoked), on allreturn paths — e.g. wrap the body so the payload is freed even when
CHECK_STATUS_VOIDreturnsearly, or adopt it into a
std::unique_ptr<logCarrier>at the top ofcallJsCb.