High watch ✔︎ verified CVSS 8.3 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in WebMIDI
DescriptionUse after free in WebMIDI
ComponentWebMIDI
Sub-componentMedia/midi
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable — upheld by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIConfirmed
Reviewer notes: Overstates the HasOpenSession change. The added test shows that ending the last pending client still deletes the manager mid-initialization. In that case the protection comes from taking GetTaskLock() first in ~MidiManagerWin, not from counting pending_clients_.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker500018484
Fix commite16291b3e888 (chromium/src) +34/-10
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-06 (per vendor advisory)
Fix landed2026-04-09 (public git)
Disclosed2026-04-28
Patch gap19 days — fix public in source before the advisory

Background

WebMIDI / `MidiManager`
the browser-process service backing the Web MIDI API; MidiManagerWin is the Windows platform implementation.
`clients_` vs `pending_clients_`
pending_clients_ holds sessions still waiting on asynchronous platform initialization, which move into clients_ once initialization completes.
`HasOpenSession` / `MidiService`
MidiService calls MidiManager::HasOpenSession to decide whether a manager is still needed; returning false lets MidiService destroy the manager.
`GetTaskLock` / `InitializeOnTaskRunner`
MidiManagerWin runs initialization on a background TaskRunner; GetTaskLock() serializes those tasks against other operations on the manager.

Root Cause Analysis

MidiManager::HasOpenSession reported clients_.size() != 0u, ignoring pending_clients_. A renderer that started a session and then closed it before platform initialization completed left a client only in pending_clients_, so HasOpenSession returned false and MidiService destroyed the MidiManagerWin while InitializeOnTaskRunner was still running on the background thread — a premature destruction and data race over the manager’s port vectors and pointers.

The fix makes HasOpenSession return !clients_.empty() || !pending_clients_.empty(), so a still-initializing session keeps the manager alive. Separately, ~MidiManagerWin now acquires GetTaskLock() at the very start of the destructor (instead of near the end), serializing teardown with InitializeOnTaskRunner and other bound tasks so finalization cannot run concurrently with a background task touching the same members. Together these close both the “deleted too early” and the “destructor races the task runner” halves of the lifecycle bug.

Key insight
The liveness check omitted the pending-initialization clients, letting the manager be freed mid-initialization; counting pending_clients_ in HasOpenSession and taking GetTaskLock() first in the destructor make lifetime reflect in-flight background work and serialize destruction against it.

Attack Path

  1. Open a MIDI session A renderer calls into WebMIDI, creating a client that is placed in pending_clients_ while MidiManagerWin runs asynchronous initialization on its TaskRunner.
  2. Close before init completes The renderer ends the session while the client is still only in pending_clients_; MidiService consults HasOpenSession, which (pre-fix) sees empty clients_ and reports no open session.
  3. Premature destruction MidiService destroys the MidiManagerWin even though InitializeOnTaskRunner is still executing on the background thread.
  4. Concurrent/dangling access The background initialization task (or the destructor running without holding GetTaskLock() early) accesses the manager’s port vectors and pointers during/after teardown, producing a data race and use-after-free (inferred: the precise faulting access and any further primitive are not shown in the diff).

Impact Assessment

An attacker controlling renderer behavior can time a WebMIDI session open/close to destroy MidiManagerWin while its initialization task is still running, causing a data race and premature-destruction use-after-free in the browser process on Windows; the advisory rates this high severity, “Use after free in WebMIDI.” Preconditions are the Windows platform path and winning the open-before-initialization-completes / close race. Escalation beyond the race/UAF condition is (inferred); the patch demonstrates only the lifecycle flaw and a regression test for the HasOpenSession accounting.

Changed Functions

FunctionChangeNotes
TEST_F
media/midi/midi_manager_unittest.cc
modified
PlatformMidiManagerTest
media/midi/midi_manager_unittest.cc
modified

Files Changed

  • media/midi/midi_manager.cc
  • media/midi/midi_manager_unittest.cc
  • media/midi/midi_manager_win.cc

Audit Directions

  • Liveness checks that ignore pending state
    Flag reference/liveness predicates that count only the “active” container while a separate “pending”/“in-flight” container can still hold work keeping the object needed.
  • Destructor vs background task ordering
    Audit destructors of objects with background TaskRunner work to confirm the serializing lock is acquired before any member is finalized, not partway through teardown.
  • Async-init lifecycle on platform managers
    Review other media/midi and platform-manager implementations for open/close-during-initialization races where service-level deletion decisions do not account for outstanding initialization tasks.
From e16291b3e8885b72320f49feb86573ce598daaf6 Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <apaseltiner@chromium.org>
Date: Thu, 09 Apr 2026 06:07:33 -0700
Subject: [PATCH] media/midi: Fix data race and premature destruction in MIDI manager

This change addresses a race condition in the Windows MIDI
implementation where MidiManagerWin could be destroyed while
initialization was still running on a background thread.

Key improvements:

1. Updated MidiManager::HasOpenSession to check both clients_ and
   pending_clients_. This prevents MidiService from deleting the
   manager when a renderer closes its session before initialization
   completes, correctly reflecting that a background task is still
   active.
2. Synchronized ~MidiManagerWin by acquiring GetTaskLock() at the
   start of the destructor. This ensures that destruction is serialized
   with InitializeOnTaskRunner and other background tasks, preventing
   concurrent access to port vectors and pointers.
3. Added a regression test ReproduceLifecycleRace in
   midi_manager_unittest.cc to verify that HasOpenSession correctly
   accounts for pending clients.

Fixed: 500018484
Change-Id: Ib7d5bc4deb5e00ac3d3f32ab561d7353d5699351
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7737022
Commit-Queue: Andrew Paseltiner <apaseltiner@chromium.org>
Reviewed-by: Takashi Toyoshima <toyoshim@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1612147}
---

diff --git a/media/midi/midi_manager.cc b/media/midi/midi_manager.cc
index 0c7c19a..5fca320 100644
--- a/media/midi/midi_manager.cc
+++ b/media/midi/midi_manager.cc
@@ -164,7 +164,7 @@
 
 bool MidiManager::HasOpenSession() {
   base::AutoLock auto_lock(lock_);
-  return clients_.size() != 0u;
+  return !clients_.empty() || !pending_clients_.empty();
 }
 
 void MidiManager::DispatchSendMidiData(MidiManagerClient* client,
diff --git a/media/midi/midi_manager_unittest.cc b/media/midi/midi_manager_unittest.cc
index 2be4a7b6..9eb48aa 100644
--- a/media/midi/midi_manager_unittest.cc
+++ b/media/midi/midi_manager_unittest.cc
@@ -219,6 +219,8 @@
 
   base::WeakPtr<FakeMidiManagerFactory> factory() { return factory_; }
 
+  MidiService* service() { return service_.get(); }
+
  private:
   base::test::TaskEnvironment env_;
   base::WeakPtr<FakeMidiManagerFactory> factory_;
@@ -337,6 +339,31 @@
   EXPECT_FALSE(test_future.IsReady());
 }
 
+TEST_F(MidiManagerTest, ReproduceLifecycleRace) {
+  base::test::TestFuture<void> test_future;
+  std::unique_ptr<FakeMidiManagerClient> client =
+      std::make_unique<FakeMidiManagerClient>(test_future.GetCallback());
+
+  // Start a session. This will put the client in pending_clients_.
+  StartSession(client.get());
+  ASSERT_TRUE(factory()->manager());
+  EXPECT_EQ(1U, factory()->manager()->GetPendingClientCount());
+  EXPECT_EQ(0U, factory()->manager()->GetClientCount());
+
+  // FIXED: HasOpenSession() now checks both clients_ and pending_clients_,
+  // so it should return true while initialization is ongoing.
+  EXPECT_TRUE(factory()->manager()->HasOpenSession());
+
+  // If we end the session now, EndSession calls HasOpenSession to decide if it
+  // should delete the manager. Since the client is still in pending_clients_
+  // until removed, EndSession correctly removes it.
+  EXPECT_TRUE(service()->EndSession(client.get()));
+
+  // Now that the last client is gone (even from pending_clients_),
+  // HasOpenSession should return false and the manager should be deleted.
+  EXPECT_FALSE(factory()->manager());
+}
+
 class PlatformMidiManagerTest : public ::testing::Test {
  public:
   PlatformMidiManagerTest()
diff --git a/media/midi/midi_manager_win.cc b/media/midi/midi_manager_win.cc
index d4d4550..2898226 100644
--- a/media/midi/midi_manager_win.cc
+++ b/media/midi/midi_manager_win.cc
@@ -723,6 +723,11 @@
   if (instance_id_ == kInvalidInstanceId)
     return;
 
+  // Behind the lock below, we can safely access all members for finalization
+  // even on the I/O thread. This also ensures that no bound task runs on
+  // TaskRunner concurrently while destructing the instance.
+  base::AutoLock lock(*GetTaskLock());
+
   // Unregisters on the I/O thread. OnDevicesChanged() won't be called any more.
   CHECK(thread_runner_->BelongsToCurrentThread());
   base::SystemMonitor::Get()->RemoveDevicesChangedObserver(this);
@@ -738,20 +743,12 @@
 
   // Invalidate instance bound tasks.
   {
-    base::AutoLock lock(*GetInstanceIdLock());
+    base::AutoLock lock_id(*GetInstanceIdLock());
     CHECK_EQ(instance_id_, g_active_instance_id);
     g_active_instance_id = kInvalidInstanceId;
     CHECK_EQ(this, g_manager_instance);
     g_manager_instance = nullptr;
   }
-
-  // Ensures that no bound task runs on TaskRunner so to destruct the instance
-  // safely.
-  // Tasks that did not started yet will do nothing after invalidate the
-  // instance ID above.
-  // Behind the lock below, we can safely access all members for finalization
-  // even on the I/O thread.
-  base::AutoLock lock(*GetTaskLock());
 }
 
 void MidiManagerWin::StartInitialization() {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/media/midi/midi_manager_unittest.cc b/media/midi/midi_manager_unittest.cc
index 2be4a7b6..9eb48aa 100644
--- a/media/midi/midi_manager_unittest.cc
+++ b/media/midi/midi_manager_unittest.cc
@@ -219,6 +219,8 @@
 
   base::WeakPtr<FakeMidiManagerFactory> factory() { return factory_; }
 
+  MidiService* service() { return service_.get(); }
+
  private:
   base::test::TaskEnvironment env_;
   base::WeakPtr<FakeMidiManagerFactory> factory_;
@@ -337,6 +339,31 @@
   EXPECT_FALSE(test_future.IsReady());
 }
 
+TEST_F(MidiManagerTest, ReproduceLifecycleRace) {
+  base::test::TestFuture<void> test_future;
+  std::unique_ptr<FakeMidiManagerClient> client =
+      std::make_unique<FakeMidiManagerClient>(test_future.GetCallback());
+
+  // Start a session. This will put the client in pending_clients_.
+  StartSession(client.get());
+  ASSERT_TRUE(factory()->manager());
+  EXPECT_EQ(1U, factory()->manager()->GetPendingClientCount());
+  EXPECT_EQ(0U, factory()->manager()->GetClientCount());
+
+  // FIXED: HasOpenSession() now checks both clients_ and pending_clients_,
+  // so it should return true while initialization is ongoing.
+  EXPECT_TRUE(factory()->manager()->HasOpenSession());
+
+  // If we end the session now, EndSession calls HasOpenSession to decide if it
+  // should delete the manager. Since the client is still in pending_clients_
+  // until removed, EndSession correctly removes it.
+  EXPECT_TRUE(service()->EndSession(client.get()));
+
+  // Now that the last client is gone (even from pending_clients_),
+  // HasOpenSession should return false and the manager should be deleted.
+  EXPECT_FALSE(factory()->manager());
+}
+
 class PlatformMidiManagerTest : public ::testing::Test {
  public:
   PlatformMidiManagerTest()
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Use-After-Free and Double-Free in MidiManagerWin via Race Condition

Project Fortify, 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 without the security team.

Overview: A data race in the Windows MIDI implementation allows the MidiManagerWin destructor to run concurrently with its initialization task on a background thread. This unsynchronized access leads to potential heap corruption primitives in the browser process, including vector backing-store use-after-free and double-frees. A compromised renderer could potentially exploit this race to achieve a sandbox escape.

Affected files:

  • media/midi/midi_manager_win.cc
  • media/midi/midi_manager.cc
  • media/midi/midi_service.cc
  • content/browser/media/midi_host.cc

Estimated timestamp from git blame: 2018-10-03

Description

A race condition exists in the lifecycle management of MidiManagerWin that can lead to severe heap corruption in the browser process. The vulnerability stems from two interconnected logic flaws:

  1. Premature Destruction: When a renderer requests a MIDI session, MidiService creates a MidiManagerWin which begins hardware initialization on a background thread (MidiServiceThread). The client is added to pending_clients_. If the renderer immediately closes its Mojo pipe, MidiHost::EndSession handles the disconnect and removes the client from pending_clients_. The service then calls MidiManager::HasOpenSession() to check if the manager should be destroyed. However, HasOpenSession() (in media/midi/midi_manager.cc) only checks clients_.size() != 0u and ignores pending_clients_. Because the count evaluates to 0, MidiService deletes the manager while the initialization task is actively running.

  2. Unsynchronized Destructor: The ~MidiManagerWin() destructor (in media/midi/midi_manager_win.cc) iterates over port_manager_->inputs() and outputs() to call Finalize() on each port. Crucially, it does so without holding GetTaskLock(). Concurrently, the background thread running InitializeOnTaskRunner holds the lock and invokes ReflectActiveDeviceList, which pushes new ports into these very same vectors.

Impact

This race condition results in multiple highly exploitable memory corruption primitives in the unsandboxed browser process:

  • Vector Use-After-Free: As the background thread pushes new ports, the std::vector backing array can reallocate. The concurrent iteration in the IO thread’s destructor will then operate on a freed backing array.
  • Double-Free: The background thread calls InPort::Open(), allocating a 32KB buffer into a std::unique_ptr (hdr_). Concurrently, the IO thread calls InPort::Finalize(), which moves that same unique_ptr. This torn state causes MIDIHDRDeleter to double-free the heap chunk.
  • Map Corruption: Concurrent reads and writes to hmidiin_to_index_map_ corrupt the underlying std::map Red-Black tree.

Because these structures are not protected by MiraclePtr, a compromised renderer could potentially groom the heap and weaponize these primitives to achieve a full Sandbox Escape (Browser Process RCE).

Potential Steps to Trigger

(Note: Our tooling does not execute code; these are suggested steps an attacker would follow based on static analysis)

  1. Attacker achieves initial code execution in a sandboxed renderer process.
  2. The compromised renderer binds the WebMIDI Mojo interface and calls StartSession.
  3. The browser process instantiates MidiManagerWin, places the client in pending_clients_, and spawns the initialization task on a background thread.
  4. Before initialization completes (which can be delayed due to slow Windows MIDI APIs like midiInGetDevCaps), the renderer deliberately closes the Mojo pipe.
  5. The browser process handles the disconnect, checks HasOpenSession(), receives false, and destructs MidiManagerWin.
  6. The ~MidiManagerWin destructor races with the still-running background initialization, causing the memory corruption primitives detailed above.

Suggested Fix

  1. Fix Lifecycle Check: Update MidiManager::HasOpenSession() in media/midi/midi_manager.cc to accurately reflect pending initialization states. It should check both clients_ and pending_clients_:
    bool MidiManager::HasOpenSession() {
      base::AutoLock auto_lock(lock_);
      return clients_.size() != 0u || pending_clients_.size() != 0u;
    }
    
  2. Synchronize Destructor: In media/midi/midi_manager_win.cc, ensure that GetTaskLock() is acquired before iterating over the port lists in ~MidiManagerWin(), preventing the data race entirely if premature destruction somehow occurs.

Evaluated with Chrome root at commit: f200f57a19490707ff8bc7aa5de3cbc443a3afad


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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