Skip to content

Possible memory leak of logCarrier payload in the libav logging callback #120

Description

@OvOhao

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;`
}
  1. 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.
  2. 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).
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions