@stoprocent/noble
Advanced tools
| '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); |
@@ -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; | ||
| }; |
+1
-1
@@ -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
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
2414154
0.29%101
1%17012
0.3%