Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Network
DescriptionUse after free in Network
ComponentNetwork
Bug ClassUAF
Tracker522550059
Fix commitb9cd09174145 (chromium/src) +90/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
source_set
components/wifi/BUILD.gn
modified
if
components/wifi/BUILD.gn
modified
TEST_F
components/wifi/wifi_service_mac_unittest.mm
modified

Files Changed

  • components/BUILD.gn
  • components/wifi/BUILD.gn
  • components/wifi/wifi_service_mac.mm
  • components/wifi/wifi_service_mac_unittest.mm
From b9cd09174145163e90936140ca3485c1b6ae5a78 Mon Sep 17 00:00:00 2001
From: Keita Suzuki <suzukikeita@chromium.org>
Date: Thu, 02 Jul 2026 23:56:32 -0700
Subject: [PATCH] [wifi] Bind WeakPtr from WiFiServiceMac SSID-change observer

WiFiServiceMac registers an NSNotificationCenter block for CoreWLAN
SSID-change notifications with `queue:nil`, so the block runs on the
posting (CoreWLAN) thread. The block was referencing the `task_runner_`
member, implicitly capturing `this`, and posting
OnWlanObserverNotification bound with base::Unretained. Because the
service is destroyed on the same worker sequence the block posts to, a
notification delivered around teardown can leave a queued task that runs
after the service is gone.

Capture the worker task runner by value and bind a
base::WeakPtr<WiFiServiceMac> instead of base::Unretained so
late-arriving notifications are dropped once the service has been
destroyed.

Adds //components/wifi:unit_tests with a regression test that posts the
SSID-change notification, destroys the service, then drains the run
loop.

TAG=agy
CONV=13ec7a8f-4d73-4c01-b1b7-4d465e984eb3

Bug: 522550059
Change-Id: I60fb685c1ca7785829b0ffc3a31bef08099e8d3e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8018224
Commit-Queue: Keita Suzuki <suzukikeita@chromium.org>
Reviewed-by: Kenichi Ishibashi <bashi@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1656384}
---

diff --git a/components/BUILD.gn b/components/BUILD.gn
index f238dd0c..f4d9aa03 100644
--- a/components/BUILD.gn
+++ b/components/BUILD.gn
@@ -974,6 +974,7 @@
       "//components/enterprise/platform_auth:unit_tests",
       "//components/remote_cocoa/app_shim:unit_tests",
       "//components/remote_cocoa/common:unit_tests",
+      "//components/wifi:unit_tests",
     ]
   }
 
diff --git a/components/wifi/BUILD.gn b/components/wifi/BUILD.gn
index c6c7a5b..4c3dcce 100644
--- a/components/wifi/BUILD.gn
+++ b/components/wifi/BUILD.gn
@@ -68,3 +68,19 @@
     "//components/onc",
   ]
 }
+
+source_set("unit_tests") {
+  testonly = true
+  sources = []
+  deps = [
+    ":wifi",
+    "//base",
+    "//base/test:test_support",
+    "//testing/gtest",
+  ]
+
+  if (is_mac) {
+    sources += [ "wifi_service_mac_unittest.mm" ]
+    frameworks = [ "Foundation.framework" ]
+  }
+}
diff --git a/components/wifi/wifi_service_mac.mm b/components/wifi/wifi_service_mac.mm
index 216f1fd..789c7ec5 100644
--- a/components/wifi/wifi_service_mac.mm
+++ b/components/wifi/wifi_service_mac.mm
@@ -17,6 +17,7 @@
 #include "base/apple/scoped_cftyperef.h"
 #include "base/compiler_specific.h"
 #include "base/functional/bind.h"
+#include "base/memory/weak_ptr.h"
 #include "base/strings/string_view_util.h"
 #include "base/strings/sys_string_conversions.h"
 #import "base/task/sequenced_task_runner.h"
@@ -164,6 +165,8 @@
   std::string connected_network_guid_;
   // Temporary storage of network properties indexed by |network_guid|.
   base::DictValue network_properties_;
+
+  base::WeakPtrFactory<WiFiServiceMac> weak_factory_{this};
 };
 
 WiFiServiceMac::WiFiServiceMac() = default;
@@ -389,12 +392,18 @@
 
   // Subscribe to OS notifications.
   if (!networks_changed_observer_.is_null()) {
+    // CoreWLAN delivers SSID-change notifications on its own dispatch queue,
+    // so the block must not reference `this` directly. Capture the task runner
+    // by value and bind a weak pointer so any task that lands after this
+    // object has been destroyed on the worker sequence is dropped.
+    scoped_refptr<base::SequencedTaskRunner> worker_task_runner = task_runner_;
+    base::WeakPtr<WiFiServiceMac> weak_this = weak_factory_.GetWeakPtr();
     void (^ns_observer)(NSNotification* notification) = ^(
         NSNotification* notification) {
       DVLOG(1) << "Received CoreWLAN notification that the SSID changed";
-      task_runner_->PostTask(
+      worker_task_runner->PostTask(
           FROM_HERE, base::BindOnce(&WiFiServiceMac::OnWlanObserverNotification,
-                                    base::Unretained(this)));
+                                    weak_this));
     };
 
     // A notification with the symbol kCWSSIDDidChangeNotification started being
diff --git a/components/wifi/wifi_service_mac_unittest.mm b/components/wifi/wifi_service_mac_unittest.mm
new file mode 100644
index 0000000..6732bd4f
--- /dev/null
+++ b/components/wifi/wifi_service_mac_unittest.mm
@@ -0,0 +1,62 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/wifi/wifi_service.h"
+
+#import <Foundation/Foundation.h>
+
+#include <memory>
+
+#include "base/functional/callback_helpers.h"
+#include "base/memory/scoped_refptr.h"
+#include "base/run_loop.h"
+#include "base/task/sequenced_task_runner.h"
+#include "base/task/single_thread_task_runner.h"
+#include "base/test/task_environment.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace wifi {
+
+namespace {
+
+// Matches the notification name observed by WiFiServiceMac::SetEventObservers.
+NSNotificationName const kCoreWLANSSIDChangedNotification =
+    @"com.apple.coreWLAN.notification.ssid.legacy";
+
+}  // namespace
+
+using WiFiServiceMacTest = testing::Test;
+
+// CoreWLAN posts SSID-change notifications from a background dispatch queue,
+// so the observer block can run concurrently with the WiFiService being torn
+// down on its worker sequence. Any task the block posts to the worker sequence
+// may therefore run after the service has been destroyed and must not touch
+// it.
+TEST_F(WiFiServiceMacTest, NotificationDeliveredAfterServiceDestroyed) {
+  base::test::SingleThreadTaskEnvironment task_environment;
+
+  std::unique_ptr<WiFiService> wifi_service(WiFiService::Create());
+  wifi_service->Initialize(base::SequencedTaskRunner::GetCurrentDefault());
+  wifi_service->SetEventObservers(
+      base::SingleThreadTaskRunner::GetCurrentDefault(),
+      /*networks_changed_observer=*/base::DoNothing(),
+      /*network_list_changed_observer=*/base::DoNothing());
+
+  // The observer is registered with `queue:nil`, so the block is invoked
+  // synchronously on the posting thread and queues its work on the worker
+  // sequence before destruction below.
+  [NSNotificationCenter.defaultCenter
+      postNotificationName:kCoreWLANSSIDChangedNotification
+                    object:nil];
+
+  wifi_service.reset();
+
+  // Draining the queued work must not touch the destroyed service.
+  base::RunLoop run_loop;
+  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+      FROM_HERE, run_loop.QuitClosure());
+  run_loop.Run();
+}
+
+}  // namespace wifi
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/wifi/wifi_service_mac_unittest.mm b/components/wifi/wifi_service_mac_unittest.mm
new file mode 100644
index 0000000..6732bd4f
--- /dev/null
+++ b/components/wifi/wifi_service_mac_unittest.mm
@@ -0,0 +1,62 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/wifi/wifi_service.h"
+
+#import <Foundation/Foundation.h>
+
+#include <memory>
+
+#include "base/functional/callback_helpers.h"
+#include "base/memory/scoped_refptr.h"
+#include "base/run_loop.h"
+#include "base/task/sequenced_task_runner.h"
+#include "base/task/single_thread_task_runner.h"
+#include "base/test/task_environment.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace wifi {
+
+namespace {
+
+// Matches the notification name observed by WiFiServiceMac::SetEventObservers.
+NSNotificationName const kCoreWLANSSIDChangedNotification =
+    @"com.apple.coreWLAN.notification.ssid.legacy";
+
+}  // namespace
+
+using WiFiServiceMacTest = testing::Test;
+
+// CoreWLAN posts SSID-change notifications from a background dispatch queue,
+// so the observer block can run concurrently with the WiFiService being torn
+// down on its worker sequence. Any task the block posts to the worker sequence
+// may therefore run after the service has been destroyed and must not touch
+// it.
+TEST_F(WiFiServiceMacTest, NotificationDeliveredAfterServiceDestroyed) {
+  base::test::SingleThreadTaskEnvironment task_environment;
+
+  std::unique_ptr<WiFiService> wifi_service(WiFiService::Create());
+  wifi_service->Initialize(base::SequencedTaskRunner::GetCurrentDefault());
+  wifi_service->SetEventObservers(
+      base::SingleThreadTaskRunner::GetCurrentDefault(),
+      /*networks_changed_observer=*/base::DoNothing(),
+      /*network_list_changed_observer=*/base::DoNothing());
+
+  // The observer is registered with `queue:nil`, so the block is invoked
+  // synchronously on the posting thread and queues its work on the worker
+  // sequence before destruction below.
+  [NSNotificationCenter.defaultCenter
+      postNotificationName:kCoreWLANSSIDChangedNotification
+                    object:nil];
+
+  wifi_service.reset();
+
+  // Draining the queued work must not touch the destroyed service.
+  base::RunLoop run_loop;
+  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+      FROM_HERE, run_loop.QuitClosure());
+  run_loop.Run();
+}
+
+}  // namespace wifi
Loading diff…

Original Bug Report

reported by rj...@google.com

Potential Use-After-Free in WiFiServiceMac via NSNotificationCenter block

Flapjack, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A Use-After-Free (UAF) vulnerability exists in WiFiServiceMac on macOS due to an Objective-C block capturing a raw this pointer, bypassing MiraclePtr protections. An attacker with the networkingPrivate extension permission can reliably trigger this by delaying task execution on a worker thread, leading to arbitrary code execution in the browser process.

Affected files:

  • components/wifi/wifi_service_mac.mm

Estimated timestamp from git blame: 2014-01-29

Summary

A potential Use-After-Free (UAF) vulnerability exists in the WiFiServiceMac class in components/wifi/wifi_service_mac.mm. The issue arises from the unsafe capture of the raw this pointer within an Objective-C block used as an NSNotificationCenter observer. Because the block captures this as a raw C++ pointer, it entirely bypasses MiraclePtr protections. By manipulating task queues, an attacker can reliably turn this into a deterministic UAF leading to Remote Code Execution (RCE) in the browser process.

Technical Details

In WiFiServiceMac::SetEventObservers, a block is registered to handle notifications for macOS CoreWLAN SSID changes:

    void (^ns_observer)(NSNotification* notification) = ^(
        NSNotification* notification) {
      DVLOG(1) << "Received CoreWLAN notification that the SSID changed";
      task_runner_->PostTask(
          FROM_HERE, base::BindOnce(&WiFiServiceMac::OnWlanObserverNotification,
                                    base::Unretained(this)));
    };

    wlan_observer_ = [NSNotificationCenter.defaultCenter
        addObserverForName:@"com.apple.coreWLAN.notification.ssid.legacy"
                    object:nil
                     queue:nil
                usingBlock:ns_observer];

In Objective-C++, referencing a member variable (such as task_runner_) inside a block implicitly captures the this pointer by value. Because this is a raw C++ pointer, the block does not safely track the lifetime of the WiFiServiceMac instance.

NetworkingPrivateServiceClient manages WiFiServiceMac and executes all interactions, including destruction, on a single SequencedTaskRunner (g_sequenced_task_runner). When WiFiServiceMac is destroyed, its destructor calls UnInitialize(), which unregisters the observer using removeObserver:.

However, removeObserver: does not block blocks that are already executing on other threads. If a notification is fired concurrently from an OS background thread while WiFiServiceMac is being destroyed, the block will still execute. The block will post OnWlanObserverNotification (bound with base::Unretained(this)) to the sequenced task runner.

Exploit Scenario (Suggested Steps)

Because both the destruction task and the notification task are posted to the same SequencedTaskRunner, an attacker can achieve 100% reliability:

  1. A compromised extension with networkingPrivate permission spams API calls (e.g., getNetworks) to flood and stall the g_sequenced_task_runner.
  2. The attacker triggers Profile destruction (e.g., closing the extension), queueing the WiFiServiceMac destruction task on the stalled runner.
  3. The attacker immediately triggers a WiFi SSID change (e.g., via startConnect), causing macOS to broadcast a notification.
  4. The ns_observer block executes synchronously on an OS thread. It posts OnWlanObserverNotification (using base::Unretained(this)) to the stalled g_sequenced_task_runner.
  5. Because the runner is stalled, the notification task is queued strictly behind the destruction task.
  6. The worker thread catches up. It executes the destruction task first, freeing the WiFiServiceMac object.
  7. The worker thread then executes the OnWlanObserverNotification task on the freed this pointer.
  8. Inside OnWlanObserverNotification, the code calls [interface_ ssid]. interface_ is read from the freed memory. If the attacker has sprayed the heap, this results in an objc_msgSend call to an attacker-controlled pointer, yielding RCE in the browser process.

Suggested Fix

To fix this issue, avoid capturing the raw this pointer in the Objective-C block. Instead, capture a base::WeakPtr<WiFiServiceMac> and check it before posting the task.

    base::WeakPtr<WiFiServiceMac> weak_this = weak_factory_.GetWeakPtr();
    scoped_refptr<base::SequencedTaskRunner> task_runner = task_runner_;

    void (^ns_observer)(NSNotification* notification) = ^(
        NSNotification* notification) {
      task_runner->PostTask(
          FROM_HERE, base::BindOnce(
              [](base::WeakPtr<WiFiServiceMac> weak_self) {
                if (weak_self)
                  weak_self->OnWlanObserverNotification();
              }, weak_this));
    };

Note that WeakPtr must be checked on the thread it was created on (the worker thread), hence why it should be passed to the posted task rather than checked inside the block (which runs on an OS thread).

Evaluated with Chrome root at commit: 2155cb00003ec35716a76ed3246eae995f87b7ff


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker