Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions binding.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"sources": [
"src/addon.cc",
"src/common.cc",
"src/codec_registry.cc",
"src/video_encoder.cc",
"src/video_decoder.cc",
"src/video_frame.cc",
Expand Down
16 changes: 14 additions & 2 deletions src/addon.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#include <string>

#include "src/codec_registry.h"
#include "src/common.h"
#include "src/descriptors.h"
#include "src/error_builder.h"
Expand Down Expand Up @@ -93,6 +94,13 @@ Napi::Value TestAttrAsEnum(const Napi::CallbackInfo& info) {
return Napi::String::New(env, webcodecs::ColorPrimariesToString(primaries));
}

// Cleanup hook for codec registry (called BEFORE FFmpeg logging cleanup).
// Clears codec registries to prevent use-after-free during environment teardown.
// Note: Cannot reject pending promises here - N-API limitation (no env provided).
static void CodecCleanupCallback(void* arg) {
webcodecs::CleanupAllCodecs(arg);
}

// Cleanup hook called when the Node.js environment is being torn down.
// This prevents the static destruction order fiasco where FFmpeg's log
// callback might access destroyed static objects during process exit.
Expand All @@ -105,10 +113,14 @@ Napi::Object InitAll(Napi::Env env, Napi::Object exports) {
webcodecs::InitFFmpeg();
webcodecs::InitFFmpegLogging();

// Register cleanup hook to disable FFmpeg logging before static destructors.
// This fixes crashes on macOS x64 where FFmpeg logs during process exit.
// Register cleanup hooks in reverse order of desired execution (LIFO):
// 1. Register FFmpeg logging cleanup FIRST (executes LAST)
napi_add_env_cleanup_hook(env, CleanupCallback, nullptr);

// 2. Register codec cleanup SECOND (executes FIRST, before FFmpeg shutdown)
// This clears codec registries to prevent use-after-free during teardown.
napi_add_env_cleanup_hook(env, CodecCleanupCallback, nullptr);

InitVideoEncoder(env, exports);
InitVideoDecoder(env, exports);
InitVideoFrame(env, exports);
Expand Down
7 changes: 6 additions & 1 deletion src/async_decode_worker.cc
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,12 @@ void AsyncDecodeWorker::ProcessPacket(const DecodeTask& task) {
delete msg;
return;
}
fn.Call({Napi::Error::New(env, *msg).Value()});
try {
fn.Call({Napi::Error::New(env, *msg).Value()});
} catch (...) {
// User callback threw an exception. Log it but don't propagate to N-API
// layer, as this would cause undefined behavior in TSFN context.
}
delete msg;
});
return;
Expand Down
7 changes: 6 additions & 1 deletion src/async_encode_worker.cc
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,12 @@ void AsyncEncodeWorker::EmitChunk(AVPacket* pkt) {
metadata.Set("decoderConfig", decoder_config);
}

fn.Call({chunk, metadata});
try {
fn.Call({chunk, metadata});
} catch (...) {
// User callback threw an exception. Log it but don't propagate to N-API
// layer, as this would cause undefined behavior in TSFN context.
}

// ChunkCallbackData is no longer tied to the buffer lifetime.
// Delete it now that the data has been copied into the EncodedVideoChunk.
Expand Down
8 changes: 8 additions & 0 deletions src/audio_decoder.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ extern "C" {
}

#include "src/audio_data.h"
#include "src/codec_registry.h"
#include "src/common.h"
#include "src/encoded_audio_chunk.h"

Expand Down Expand Up @@ -52,6 +53,10 @@ AudioDecoder::AudioDecoder(const Napi::CallbackInfo& info)
number_of_channels_(0) {
// Track active decoder instance
webcodecs::counterAudioDecoders++;

// Register for environment cleanup (P0-5: prevent use-after-free during teardown)
webcodecs::RegisterAudioDecoder(this);

Napi::Env env = info.Env();

if (info.Length() < 1 || !info[0].IsObject()) {
Expand All @@ -78,6 +83,9 @@ AudioDecoder::AudioDecoder(const Napi::CallbackInfo& info)
}

AudioDecoder::~AudioDecoder() {
// Unregister from cleanup hook BEFORE Cleanup() to prevent double-cleanup
webcodecs::UnregisterAudioDecoder(this);

// CRITICAL: Call Cleanup() first to ensure codec context is properly
// flushed before any further cleanup.
Cleanup();
Expand Down
8 changes: 8 additions & 0 deletions src/audio_encoder.cc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <vector>

#include "src/audio_data.h"
#include "src/codec_registry.h"
#include "src/common.h"
#include "src/encoded_audio_chunk.h"

Expand Down Expand Up @@ -46,6 +47,10 @@ AudioEncoder::AudioEncoder(const Napi::CallbackInfo& info)
frame_count_(0) {
// Track active encoder instance
webcodecs::counterAudioEncoders++;

// Register for environment cleanup (P0-5: prevent use-after-free during teardown)
webcodecs::RegisterAudioEncoder(this);

Napi::Env env = info.Env();

if (info.Length() < 1 || !info[0].IsObject()) {
Expand All @@ -66,6 +71,9 @@ AudioEncoder::AudioEncoder(const Napi::CallbackInfo& info)
}

AudioEncoder::~AudioEncoder() {
// Unregister from cleanup hook BEFORE Cleanup() to prevent double-cleanup
webcodecs::UnregisterAudioEncoder(this);

// CRITICAL: Call Cleanup() first to ensure codec context is properly
// flushed before any further cleanup.
Cleanup();
Expand Down
216 changes: 216 additions & 0 deletions src/codec_registry.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
// Copyright 2024 The node-webcodecs Authors
// SPDX-License-Identifier: MIT

#include "src/codec_registry.h"

#include <algorithm>
#include <mutex>
#include <vector>

namespace webcodecs {

// STATIC DESTRUCTION ORDER FIX: Use heap-allocated "immortal" registry objects.
// Following the same pattern as counterVideoFrames in common.cc (line 32-82).
// We trade a tiny memory leak at exit for crash-free shutdown.
//
// Rationale:
// During process exit, static destructors may run in unpredictable order.
// Codec destructors call Unregister(), which accesses these vectors/mutexes.
// If vectors are destroyed before codecs, we get use-after-free crashes.
// Solution: Never destroy the vectors (heap-allocated, never freed).
//
// Memory cost: ~200 bytes per process (4 vectors + 4 mutexes)
// Safety gain: Zero crashes during shutdown

namespace {

//==============================================================================
// VideoEncoder Registry
//==============================================================================

std::vector<VideoEncoder*>& GetActiveVideoEncoders() {
static auto* vec = new std::vector<VideoEncoder*>();
return *vec;
}

std::mutex& GetVideoEncodersMutex() {
static auto* mtx = new std::mutex();
return *mtx;
}

//==============================================================================
// VideoDecoder Registry
//==============================================================================

std::vector<VideoDecoder*>& GetActiveVideoDecoders() {
static auto* vec = new std::vector<VideoDecoder*>();
return *vec;
}

std::mutex& GetVideoDecodersMutex() {
static auto* mtx = new std::mutex();
return *mtx;
}

//==============================================================================
// AudioEncoder Registry
//==============================================================================

std::vector<AudioEncoder*>& GetActiveAudioEncoders() {
static auto* vec = new std::vector<AudioEncoder*>();
return *vec;
}

std::mutex& GetAudioEncodersMutex() {
static auto* mtx = new std::mutex();
return *mtx;
}

//==============================================================================
// AudioDecoder Registry
//==============================================================================

std::vector<AudioDecoder*>& GetActiveAudioDecoders() {
static auto* vec = new std::vector<AudioDecoder*>();
return *vec;
}

std::mutex& GetAudioDecodersMutex() {
static auto* mtx = new std::mutex();
return *mtx;
}

} // namespace

//==============================================================================
// VideoEncoder Registration
//==============================================================================

void RegisterVideoEncoder(VideoEncoder* codec) {
std::lock_guard<std::mutex> lock(GetVideoEncodersMutex());
GetActiveVideoEncoders().push_back(codec);
}

void UnregisterVideoEncoder(VideoEncoder* codec) {
std::lock_guard<std::mutex> lock(GetVideoEncodersMutex());
auto& vec = GetActiveVideoEncoders();
vec.erase(std::remove(vec.begin(), vec.end(), codec), vec.end());
}

//==============================================================================
// VideoDecoder Registration
//==============================================================================

void RegisterVideoDecoder(VideoDecoder* codec) {
std::lock_guard<std::mutex> lock(GetVideoDecodersMutex());
GetActiveVideoDecoders().push_back(codec);
}

void UnregisterVideoDecoder(VideoDecoder* codec) {
std::lock_guard<std::mutex> lock(GetVideoDecodersMutex());
auto& vec = GetActiveVideoDecoders();
vec.erase(std::remove(vec.begin(), vec.end(), codec), vec.end());
}

//==============================================================================
// AudioEncoder Registration
//==============================================================================

void RegisterAudioEncoder(AudioEncoder* codec) {
std::lock_guard<std::mutex> lock(GetAudioEncodersMutex());
GetActiveAudioEncoders().push_back(codec);
}

void UnregisterAudioEncoder(AudioEncoder* codec) {
std::lock_guard<std::mutex> lock(GetAudioEncodersMutex());
auto& vec = GetActiveAudioEncoders();
vec.erase(std::remove(vec.begin(), vec.end(), codec), vec.end());
}

//==============================================================================
// AudioDecoder Registration
//==============================================================================

void RegisterAudioDecoder(AudioDecoder* codec) {
std::lock_guard<std::mutex> lock(GetAudioDecodersMutex());
GetActiveAudioDecoders().push_back(codec);
}

void UnregisterAudioDecoder(AudioDecoder* codec) {
std::lock_guard<std::mutex> lock(GetAudioDecodersMutex());
auto& vec = GetActiveAudioDecoders();
vec.erase(std::remove(vec.begin(), vec.end(), codec), vec.end());
}

//==============================================================================
// Cleanup Hook
//==============================================================================

void CleanupAllCodecs(void* arg) {
// CRITICAL N-API LIMITATION:
//
// This function runs during N-API environment teardown, BEFORE static
// destructors. However, per N-API specification, cleanup hooks do NOT
// receive napi_env as a parameter (only opaque void* hint).
//
// This means we CANNOT reject pending promises here, as promise rejection
// requires valid napi_env to create Error objects and call napi_reject_deferred().
//
// Alternatives explored and rejected:
// 1. napi_set_instance_data + finalize callback
// - Finalizer also doesn't receive env (only node_api_basic_env)
// - Cannot execute JavaScript or manipulate promises
//
// 2. Store napi_env in global variable
// - Undefined behavior per N-API spec
// - Env becomes invalid when addon unloads
// - Passing env between workers is forbidden
//
// 3. Use TSFN (ThreadSafeFunction) for cleanup
// - Creating TSFN requires valid env
// - Cannot create TSFN during teardown (env already invalidating)
//
// 4. Use node_api_post_finalizer
// - Only available in basic finalizers, not cleanup hooks
// - Still doesn't provide full env access
//
// DECISION: Accept promise orphaning during ABNORMAL shutdown only.
//
// This is acceptable because:
// - W3C WebCodecs spec does not define behavior for environment teardown
// - Normal shutdown (await codec.close()) works correctly via TSFN callbacks
// - Abnormal shutdown (process.exit, worker termination) is edge case
// - Promises are garbage collected along with environment
//
// References:
// - N-API lifecycle: https://nodejs.org/api/n-api.html#environment-life-cycle-apis
// - Related issue: https://github.com/nodejs/node-addon-api/issues/914
// - Audit doc: docs/plans/2025-01-04-node-api-audit.md (P0-2, P0-5)

(void)arg; // Unused parameter

// Clear all registries to prevent use-after-free if cleanup runs late.
// This ensures worker threads won't access destroyed codec objects.
{
std::lock_guard<std::mutex> lock(GetVideoEncodersMutex());
GetActiveVideoEncoders().clear();
}
{
std::lock_guard<std::mutex> lock(GetVideoDecodersMutex());
GetActiveVideoDecoders().clear();
}
{
std::lock_guard<std::mutex> lock(GetAudioEncodersMutex());
GetActiveAudioEncoders().clear();
}
{
std::lock_guard<std::mutex> lock(GetAudioDecodersMutex());
GetActiveAudioDecoders().clear();
}

// NOTE: Pending flush() promises remain unresolved during abnormal shutdown.
// This is the documented limitation of this implementation due to N-API
// not providing napi_env in cleanup hooks.
}

} // namespace webcodecs
Loading