Repository: electron/electron
Pull request #46745: fix: bluetooth crash in `select-bluetooth-device` event
Author: login: codebytere
Labels: name: semver/patch
description: backwards-compatible bug fixes
name: merged/34-x-y
description: PR was merged to the "34-x-y" branch.
name: merged/35-x-y
description: PR was merged to the "35-x-y" branch.
name: merged/36-x-y
description: PR was merged to the "36-x-y" branch.
Created: 2025-04-23T16:43:39Z | Merged: 2025-04-25T15:26:19Z

#### Description of Change

Closes https://github.com/electron/electron/issues/46707

Fixes a possible crash when using `navigator.bluetooth.requestDevice` and the `select-bluetooth-device` event. This happened in the case where Bluetooth was turned off and `navigator.bluetooth.requestDevice` was called and subsequently cancelled via the `select-bluetooth-device` event. Also a bit of related cleanup.

#### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] PR description included and stakeholders cc'd
- [x] `npm test` passes
- [x] [PR release notes](https://github.com/electron/clerk/blob/main/README.md) describe the change in a way relevant to app developers, and are [capitalized, punctuated, and past tense](https://github.com/electron/clerk/blob/main/README.md#examples).

#### Release Notes

Notes: Fixed a possible crash when using `navigator.bluetooth.requestDevice` and the `select-bluetooth-device` event.



Diff:
diff --git a/shell/browser/lib/bluetooth_chooser.cc b/shell/browser/lib/bluetooth_chooser.cc
index 0c8708a91a484..23f39e1a3160f 100644
--- a/shell/browser/lib/bluetooth_chooser.cc
+++ b/shell/browser/lib/bluetooth_chooser.cc
@@ -25,19 +25,6 @@ struct Converter<electron::BluetoothChooser::DeviceInfo> {
 
 namespace electron {
 
-namespace {
-
-void OnDeviceChosen(const content::BluetoothChooser::EventHandler& handler,
-                    const std::string& device_id) {
-  if (device_id.empty()) {
-    handler.Run(content::BluetoothChooserEvent::CANCELLED, device_id);
-  } else {
-    handler.Run(content::BluetoothChooserEvent::SELECTED, device_id);
-  }
-}
-
-}  // namespace
-
 BluetoothChooser::BluetoothChooser(api::WebContents* contents,
                                    const EventHandler& event_handler)
     : api_web_contents_(contents), event_handler_(event_handler) {}
@@ -49,14 +36,13 @@ BluetoothChooser::~BluetoothChooser() {
 void BluetoothChooser::SetAdapterPresence(AdapterPresence presence) {
   switch (presence) {
     case AdapterPresence::ABSENT:
+      NOTREACHED();
     case AdapterPresence::POWERED_OFF:
-    // Chrome currently directs the user to system preferences
-    // to grant bluetooth permission for this case, should we
-    // do something similar ?
-    // https://chromium-review.googlesource.com/c/chromium/src/+/2617129
-    case AdapterPresence::UNAUTHORIZED:
       event_handler_.Run(content::BluetoothChooserEvent::CANCELLED, "");
       break;
+    case AdapterPresence::UNAUTHORIZED:
+      event_handler_.Run(content::BluetoothChooserEvent::DENIED_PERMISSION, "");
+      break;
     case AdapterPresence::POWERED_ON:
       rescan_ = true;
       break;
@@ -74,25 +60,27 @@ void BluetoothChooser::ShowDiscoveryState(DiscoveryState state) {
       refreshing_ = false;
       idle_state = true;
       break;
+    // The first time this state fires is due to a rescan triggering so we
+    // set a flag to ignore devices - the second time this state fires
+    // we are now safe to pick a device.
     case DiscoveryState::DISCOVERING:
-      // The first time this state fires is due to a rescan triggering so set a
-      // flag to ignore devices
       if (rescan_ && !refreshing_) {
         refreshing_ = true;
       } else {
-        // The second time this state fires we are now safe to pick a device
         refreshing_ = false;
       }
       break;
   }
+
   bool prevent_default =
       api_web_contents_->Emit("select-bluetooth-device", GetDeviceList(),
-                              base::BindOnce(&OnDeviceChosen, event_handler_));
+                              base::BindOnce(&BluetoothChooser::OnDeviceChosen,
+                                             weak_ptr_factory_.GetWeakPtr()));
   if (!prevent_default && idle_state) {
-    if (device_map_.empty()) {
+    if (device_id_to_name_map_.empty()) {
       event_handler_.Run(content::BluetoothChooserEvent::CANCELLED, "");
     } else {
-      auto it = device_map_.begin();
+      auto it = device_id_to_name_map_.begin();
       auto device_id = it->first;
       event_handler_.Run(content::BluetoothChooserEvent::SELECTED, device_id);
     }
@@ -105,38 +93,47 @@ void BluetoothChooser::AddOrUpdateDevice(const std::string& device_id,
                                          bool is_gatt_connected,
                                          bool is_paired,
                                          int signal_strength_level) {
-  if (refreshing_) {
-    // If the list of bluetooth devices is currently being generated don't fire
-    // an event
+  // Don't fire an event during refresh.
+  if (refreshing_)
     return;
-  }
 
-  auto [iter, changed] = device_map_.try_emplace(device_id, device_name);
+  // Emit a select-bluetooth-device handler to allow for user to listen for
+  // bluetooth device found. If there's no listener in place, then select the
+  // first device that matches the filters provided.
+  auto [iter, changed] =
+      device_id_to_name_map_.try_emplace(device_id, device_name);
   if (!changed && should_update_name) {
     iter->second = device_name;
     changed = true;
   }
 
   if (changed) {
-    // Emit a select-bluetooth-device handler to allow for user to listen for
-    // bluetooth device found.
     bool prevent_default = api_web_contents_->Emit(
         "select-bluetooth-device", GetDeviceList(),
-        base::BindOnce(&OnDeviceChosen, event_handler_));
+        base::BindOnce(&BluetoothChooser::OnDeviceChosen,
+                       weak_ptr_factory_.GetWeakPtr()));
 
-    // If emit not implemented select first device that matches the filters
-    //  provided.
-    if (!prevent_default) {
+    if (!prevent_default)
       event_handler_.Run(content::BluetoothChooserEvent::SELECTED, device_id);
-    }
+  }
+}
+
+void BluetoothChooser::OnDeviceChosen(const std::string& device_id) {
+  if (event_handler_.is_null())
+    return;
+
+  if (device_id.empty()) {
+    event_handler_.Run(content::BluetoothChooserEvent::CANCELLED, device_id);
+  } else {
+    event_handler_.Run(content::BluetoothChooserEvent::SELECTED, device_id);
   }
 }
 
 std::vector<electron::BluetoothChooser::DeviceInfo>
 BluetoothChooser::GetDeviceList() {
   std::vector<electron::BluetoothChooser::DeviceInfo> vec;
-  vec.reserve(device_map_.size());
-  for (const auto& [device_id, device_name] : device_map_)
+  vec.reserve(device_id_to_name_map_.size());
+  for (const auto& [device_id, device_name] : device_id_to_name_map_)
     vec.emplace_back(device_id, device_name);
   return vec;
 }
diff --git a/shell/browser/lib/bluetooth_chooser.h b/shell/browser/lib/bluetooth_chooser.h
index 19f7eac838911..5b377e5e1b696 100644
--- a/shell/browser/lib/bluetooth_chooser.h
+++ b/shell/browser/lib/bluetooth_chooser.h
@@ -5,13 +5,14 @@
 #ifndef ELECTRON_SHELL_BROWSER_LIB_BLUETOOTH_CHOOSER_H_
 #define ELECTRON_SHELL_BROWSER_LIB_BLUETOOTH_CHOOSER_H_
 
-#include <map>
 #include <string>
 #include <vector>
 
 #include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
 #include "content/public/browser/bluetooth_chooser.h"
 #include "shell/browser/api/electron_api_web_contents.h"
+#include "third_party/abseil-cpp/absl/container/flat_hash_map.h"
 
 namespace electron {
 
@@ -39,14 +40,18 @@ class BluetoothChooser : public content::BluetoothChooser {
                          bool is_gatt_connected,
                          bool is_paired,
                          int signal_strength_level) override;
+
+  void OnDeviceChosen(const std::string& device_id);
   std::vector<DeviceInfo> GetDeviceList();
 
  private:
-  std::map<std::string, std::u16string> device_map_;
+  absl::flat_hash_map<std::string, std::u16string> device_id_to_name_map_;
   raw_ptr<api::WebContents> api_web_contents_;
   EventHandler event_handler_;
   bool refreshing_ = false;
   bool rescan_ = false;
+
+  base::WeakPtrFactory<BluetoothChooser> weak_ptr_factory_{this};
 };
 
 }  // namespace electron


Reviews:
user: login: ckerr
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-24T14:53:04Z
user: login: codebytere
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T16:00:41Z
user: login: ckerr
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-25T15:26:13Z

Review comments:
diff_hunk: @@ -39,14 +40,18 @@ class BluetoothChooser : public content::BluetoothChooser {
                          bool is_gatt_connected,
                          bool is_paired,
                          int signal_strength_level) override;
+
+  void OnDeviceChosen(const std::string& device_id);
   std::vector<DeviceInfo> GetDeviceList();
 
  private:
-  std::map<std::string, std::u16string> device_map_;
+  std::unordered_map<std::string, std::u16string> device_id_to_name_map_;
path: shell/browser/lib/bluetooth_chooser.h
user: login: ckerr
body: 1. I like the new name, it's much easier to read :smile_cat: 

2. This field is a good candidate for `absl::flat_hash_map`.

    Xref: https://chromium.googlesource.com/chromium/src/+/main/base/containers/README.md#map-and-set-selection-usage-advice
    > Note that this advice never suggests the use of std::unordered_map and std::unordered_set. These containers provides similar features to the Abseil flat hash containers but with worse performance. They should only be used if absolutely required for compatibility with third-party code.
created_at: 2025-04-24T14:41:27Z
author_association: MEMBER
diff_hunk: @@ -105,38 +93,42 @@ void BluetoothChooser::AddOrUpdateDevice(const std::string& device_id,
                                          bool is_gatt_connected,
                                          bool is_paired,
                                          int signal_strength_level) {
-  if (refreshing_) {
-    // If the list of bluetooth devices is currently being generated don't fire
-    // an event
+  // Don't fire an event during refresh.
+  if (refreshing_)
     return;
-  }
-
-  auto [iter, changed] = device_map_.try_emplace(device_id, device_name);
-  if (!changed && should_update_name) {
-    iter->second = device_name;
-    changed = true;
-  }
 
+  // Emit a select-bluetooth-device handler to allow for user to listen for
+  // bluetooth device found. If there's no listener in place, then select the
+  // first device that matches the filters provided.
+  auto [iter, changed] =
+      device_id_to_name_map_.try_emplace(device_id, device_name);
path: shell/browser/lib/bluetooth_chooser.cc
user: login: ckerr
body: Looks like the `if (!changed && should_update_name)` got lost here, which seems like a bug?
created_at: 2025-04-24T14:50:59Z
author_association: MEMBER
diff_hunk: @@ -105,38 +93,42 @@ void BluetoothChooser::AddOrUpdateDevice(const std::string& device_id,
                                          bool is_gatt_connected,
                                          bool is_paired,
                                          int signal_strength_level) {
-  if (refreshing_) {
-    // If the list of bluetooth devices is currently being generated don't fire
-    // an event
+  // Don't fire an event during refresh.
+  if (refreshing_)
     return;
-  }
-
-  auto [iter, changed] = device_map_.try_emplace(device_id, device_name);
-  if (!changed && should_update_name) {
-    iter->second = device_name;
-    changed = true;
-  }
 
+  // Emit a select-bluetooth-device handler to allow for user to listen for
+  // bluetooth device found. If there's no listener in place, then select the
+  // first device that matches the filters provided.
+  auto [iter, changed] =
+      device_id_to_name_map_.try_emplace(device_id, device_name);
path: shell/browser/lib/bluetooth_chooser.cc
user: login: codebytere
body: yep, fixed! lost in the shuffle
created_at: 2025-04-24T16:00:41Z
author_association: MEMBER

PR comments:
user: login: ckerr
created_at: 2025-04-24T14:53:26Z
body: > fix-dangly-boi

:rofl: 
author_association: MEMBER
user: login: release-clerk[bot]
created_at: 2025-04-25T15:26:22Z
body: **Release Notes Persisted**

> Fixed a possible crash when using `navigator.bluetooth.requestDevice` and the `select-bluetooth-device` event.
author_association: NONE
user: login: trop[bot]
created_at: 2025-04-25T15:26:46Z
body: I have automatically backported this PR to "36-x-y",     please check out #46782
author_association: CONTRIBUTOR
user: login: trop[bot]
created_at: 2025-04-25T15:26:56Z
body: I have automatically backported this PR to "34-x-y",     please check out #46783
author_association: CONTRIBUTOR
user: login: trop[bot]
created_at: 2025-04-25T15:26:58Z
body: I have automatically backported this PR to "35-x-y",     please check out #46784
author_association: CONTRIBUTOR