🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@stoprocent/noble

Package Overview
Dependencies
Maintainers
1
Versions
93
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@stoprocent/noble - npm Package Compare versions

Comparing version
2.5.8
to
2.5.9
+58
test/win-smoke/teardown.smoke.js
'use strict';
// Headless teardown smoke test for the Windows (WinRT) binding.
//
// Reproduces the crash scenarios from
// https://github.com/stoprocent/noble/issues/95 that do NOT require a real
// Bluetooth adapter, so they can run on a GitHub-hosted windows runner:
//
// 1. withBindings('default') then stop() WITHOUT ever starting. This used to
// `delete` an uninitialized `manager` pointer and crash the process with
// 0xC0000005 (ACCESS_VIOLATION).
// 2. start() (kicked without a powered-on radio) followed by an immediate
// stop(), repeated, which used to race the RadioWatcher fire_and_forget
// coroutine into a use-after-free during teardown.
//
// Pass condition: the process survives every teardown and exits with code 0.
// A clean exit also proves the native keep-alive (the referenced N-API
// ThreadSafeFunction) is released on stop(), so a probe can exit in-process.
//
// This only exercises the native module on win32; on other platforms it is a
// no-op so it is safe to invoke unconditionally from CI.
if (process.platform !== 'win32') {
console.log('SKIP: win teardown smoke test only runs on win32');
process.exit(0);
}
const assert = require('assert');
const { withBindings } = require('../..');
// Case 1: stop() before start() must be a safe, idempotent no-op.
{
const noble = withBindings('default');
noble.stop();
noble.stop(); // idempotent
}
// Case 2: stress start() -> immediate stop() to exercise the RadioWatcher
// coroutine teardown. Reading `.state` forces _initializeBindings() ->
// bindings.start() synchronously, without needing a powered-on adapter.
const ITERATIONS = 50;
for (let i = 0; i < ITERATIONS; i++) {
const noble = withBindings('default');
noble.on('stateChange', () => {});
assert.strictEqual(typeof noble.state, 'string');
noble.stop();
}
console.log(`win teardown smoke test: created and tore down ${ITERATIONS} instances`);
// Keep the event loop alive briefly so any in-flight WinRT coroutines resume
// AFTER their RadioWatcher has been destroyed. With the fix they observe the
// liveness flag and no-op; without it, this window is where the use-after-free
// crash would occur.
setTimeout(() => {
console.log('win teardown smoke test: process exited cleanly after stop()');
process.exit(0);
}, 2000);
+11
-3

@@ -61,5 +61,13 @@ #include "noble_winrt.h"

{
CHECK_MANAGER()
delete manager;
manager = nullptr;
// stop() must be a safe, idempotent teardown. Guard against a null manager
// (never started, or already stopped) instead of throwing, and never
// delete a pointer that was never assigned. `manager` is default
// initialized to nullptr (see noble_winrt.h), so this also protects the
// "withBindings() then stop() without ever powering on" path that
// previously deleted an uninitialized pointer and crashed with 0xC0000005.
if (manager)
{
delete manager;
manager = nullptr;
}
return info.Env().Undefined();

@@ -66,0 +74,0 @@ }

@@ -35,3 +35,3 @@ #pragma once

private:
BLEManager* manager;
BLEManager* manager = nullptr;
};

@@ -49,3 +49,5 @@ // Standard library includes

RadioWatcher::RadioWatcher()
: mRadio(nullptr), watcher(DeviceInformation::CreateWatcher(BluetoothAdapter::GetDeviceSelector()))
: mRadio(nullptr),
watcher(DeviceInformation::CreateWatcher(BluetoothAdapter::GetDeviceSelector())),
mAlive(std::make_shared<std::atomic<bool>>(true))
{

@@ -59,2 +61,40 @@ mAddedRevoker = watcher.Added(winrt::auto_revoke, bind2(this, &RadioWatcher::OnAdded));

RadioWatcher::~RadioWatcher()
{
// Signal any in-flight OnRadioChanged() coroutine that this object is gone
// so it does not dereference `this` after resuming from a co_await. Without
// this, tearing down the BLEManager (noble.stop()) while an enumeration /
// radio-state coroutine is still pending races into a use-after-free and
// crashes the process with 0xC0000005 (ACCESS_VIOLATION).
if (mAlive)
{
mAlive->store(false);
}
// Stop delivering further callbacks before the members they touch are
// destroyed.
mAddedRevoker.revoke();
mUpdatedRevoker.revoke();
mRemovedRevoker.revoke();
mCompletedRevoker.revoke();
mRadioStateChangedRevoker.revoke();
try
{
if (watcher)
{
auto status = watcher.Status();
if (status == DeviceWatcherStatus::Started ||
status == DeviceWatcherStatus::EnumerationCompleted)
{
watcher.Stop();
}
}
}
catch (...)
{
// Best effort during teardown; never let an exception escape a destructor.
}
}
void RadioWatcher::Start(std::function<void(Radio& radio, const AdapterCapabilities& capabilities)> on)

@@ -68,7 +108,14 @@ {

winrt::fire_and_forget RadioWatcher::OnRadioChanged() {
// Keep a private copy of the liveness flag alive for the whole coroutine so
// it stays valid even if the RadioWatcher is destroyed while we are
// suspended on a co_await. Re-check it after every co_await before touching
// `this` to avoid a use-after-free during teardown (noble.stop()).
auto alive = mAlive;
try {
auto adapter = co_await BluetoothAdapter::GetDefaultAsync();
if (!alive->load()) co_return;
if (adapter) {
auto radio = co_await adapter.GetRadioAsync();
if (!alive->load()) co_return;

@@ -137,2 +184,3 @@ AdapterCapabilities capabilities = {};

// so a disabled bthserv / unexpected WinRT failure can't take down the app.
if (!alive->load()) co_return;
mRadio = nullptr;

@@ -139,0 +187,0 @@ mRadioStateChangedRevoker.revoke();

@@ -11,3 +11,5 @@ //

// Standard library includes
#include <atomic>
#include <functional>
#include <memory>
#include <set>

@@ -59,2 +61,3 @@

RadioWatcher();
~RadioWatcher();

@@ -82,2 +85,8 @@ void Start(std::function<void(Radio& radio, const AdapterCapabilities& capabilities)> on);

winrt::event_revoker<IRadio> mRadioStateChangedRevoker;
// Shared liveness flag observed by the fire_and_forget OnRadioChanged()
// coroutine. The coroutine keeps its own copy of the shared_ptr, so the
// flag outlives the RadioWatcher and can be safely checked after each
// co_await to avoid touching a destroyed object during teardown.
std::shared_ptr<std::atomic<bool>> mAlive;
};

@@ -9,3 +9,3 @@ {

"description": "A Node.js BLE (Bluetooth Low Energy) central library.",
"version": "2.5.8",
"version": "2.5.9",
"repository": {

@@ -12,0 +12,0 @@ "type": "git",

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet