diff --git a/build_release_archive.py b/build_release_archive.py deleted file mode 100755 index e0683d1..0000000 --- a/build_release_archive.py +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- -import sys -from git_archive_all import main - -sys.exit(main()) - diff --git a/include/utilities.hpp b/include/utilities.hpp index d92e918..ac97829 100644 --- a/include/utilities.hpp +++ b/include/utilities.hpp @@ -459,7 +459,9 @@ std::vector from_base64_to_vector(std::string_view x); // Concept to match containers with a size() method template concept HasSize = requires(T t) { - { t.size() } -> std::convertible_to; + { + t.size() + } -> std::convertible_to; }; template diff --git a/prepare_release.sh b/prepare_release.sh index a3a157f..8e57726 100755 --- a/prepare_release.sh +++ b/prepare_release.sh @@ -12,7 +12,7 @@ read_char() { rm -f ./libsession_util_nodejs*.tar.gz python -m venv .venv . .venv/bin/activate -pip install git-archive-all +pip install -r requirements.txt --require-hashes PACKAGE_VERSION=$(node -p "require('./package.json').version") GIT_COMMIT=$(git rev-parse HEAD) @@ -42,7 +42,7 @@ esac echo "Continuing..." echo "Building tar archive of source..." -python3 build_release_archive.py libsession_util_nodejs-v$PACKAGE_VERSION.tar.gz --include src/version.h +git-archive-all libsession_util_nodejs-v$PACKAGE_VERSION.tar.gz --include src/version.h echo "tar archive size:" du -sh libsession_util_nodejs*.tar.gz diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..794d832 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +# Release-tooling Python dependencies, version- and hash-pinned so a +# compromised PyPI account / dependency-confusion upload can't inject a +# different artifact at release-tarball time. Installed via: +# pip install -r requirements.txt --require-hashes +# prepare_release.sh then invokes the `git-archive-all` console script +# from the venv directly, so a repo-root git_archive_all.py can't shadow +# this pinned wheel (the CWD is never on sys.path for a console script). +git-archive-all==1.23.1 \ + --hash=sha256:d9f9611f2df629de3df1d6f134955ced4c704cbc0e423d769a7c0e55a8484242 \ + --hash=sha256:a42fe0cedd2e0361250a4f3130f3d3543f640d4f1fe28530771df5972108729f diff --git a/src/addon.cpp b/src/addon.cpp index 15795ab..04bb65c 100644 --- a/src/addon.cpp +++ b/src/addon.cpp @@ -1,5 +1,6 @@ #include +#include #include #include "blinding/blinding.hpp" @@ -13,30 +14,74 @@ #include "user_groups_config.hpp" Napi::ThreadSafeFunction tsfn; +// Guards `tsfn` against the race between libsession's background log +// threads (which read it and BlockingCall through it) and N-API env +// teardown (which Releases and nulls it). The check + call in the +// logger and the check + release in the cleanup hook must each be +// atomic w.r.t. the other, or we risk a torn read / use-after-free on +// the wrapper. +std::mutex tsfn_mutex; Napi::Object InitAll(Napi::Env env, Napi::Object exports) { - tsfn = Napi::ThreadSafeFunction::New( - env, - Napi::Function::New(env, [](const Napi::CallbackInfo& info) {}), - "LoggerCallback", - 0, - 1); - - session::add_logger([](std::string_view msg) { - tsfn.BlockingCall( - new std::string(msg), - [](Napi::Env env, Napi::Function jsCallback, std::string* msg) { - Napi::HandleScope scope(env); - Napi::Function consoleLog = env.Global() - .Get("console") - .As() - .Get("log") - .As(); - Napi::String jsStr = Napi::String::New(env, "libsession: " + *msg); - consoleLog.Call({jsStr}); - delete msg; - }); + { + std::lock_guard lock(tsfn_mutex); + tsfn = Napi::ThreadSafeFunction::New( + env, + Napi::Function::New(env, [](const Napi::CallbackInfo& info) {}), + "LoggerCallback", + 0, + 1); + // The logger callback is fire-and-forget. Without Unref(), the + // TSFN keeps a strong ref on the loop and a `require()` from a + // short-lived CLI hangs forever waiting on the TSFN. + tsfn.Unref(env); + } + + // Release the TSFN when the N-API env tears down. Without this, a + // later libsession log from a background thread could BlockingCall + // into a destroyed env (abort / UAF). Taking the lock makes the + // check + release atomic w.r.t. the logger callback below. + env.AddCleanupHook([]() { + std::lock_guard lock(tsfn_mutex); + if (tsfn) { + tsfn.Release(); + tsfn = nullptr; + } + }); + + // Register the libsession -> console.log bridge exactly once per + // process. Re-running InitAll (multiple Workers / vm contexts) must + // not stack duplicate callbacks onto libsession's global logger + // registry — the single persistent callback always reads whatever + // the current `tsfn` is, under the lock. + static std::once_flag logger_once; + std::call_once(logger_once, [] { + session::add_logger([](std::string_view msg) { + // Hold the lock across the check + BlockingCall so env + // teardown can't Release/null `tsfn` underneath us. + std::lock_guard lock(tsfn_mutex); + if (!tsfn) + return; + auto* payload = new std::string(msg); + napi_status status = tsfn.BlockingCall( + payload, [](Napi::Env env, Napi::Function jsCallback, std::string* msg) { + Napi::HandleScope scope(env); + Napi::Function consoleLog = env.Global() + .Get("console") + .As() + .Get("log") + .As(); + Napi::String jsStr = Napi::String::New(env, "libsession: " + *msg); + consoleLog.Call({jsStr}); + delete msg; + }); + // On a rejected call (e.g. napi_closing if a Release slipped + // in) the finalize callback never runs, so the payload would + // leak — free it here instead. + if (status != napi_ok) + delete payload; + }); }); oxen::log::set_level_default(oxen::log::Level::info);