Critical watch CVSS 8.3 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free WebMIDI
DescriptionUse after free WebMIDI
ComponentChromium
Sub-componentWebMIDI
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
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.
Tracker518108291
Fix commitc5162675305c (chromium/src) +247/-60
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-05-30 (per vendor advisory)
Fix landed2026-06-04 (public git)
Disclosed2026-06-11
Patch gap7 days — fix public in source before the advisory

Background

`MidiManagerWinrt` / `MidiPortManager`
the WinRT-based Web MIDI backend on Windows (media/midi/midi_manager_winrt.cc); a MidiManagerWinrt owns MidiPortManager objects that watch device arrivals/removals via asynchronous WinRT DeviceWatcher and async-operation callbacks.
`TaskService` instance binding
a MidiManagerWinrt calls BindInstance() to obtain an InstanceId, and PostBoundTask/PostBoundDelayedTask stamp posted tasks so TaskService can drop tasks whose bound InstanceId no longer matches the active instance after UnbindInstance().
WinRT completion callback
a WRL::Callback lambda (add_Added, add_EnumerationCompleted, add_Removed, put_Completed, add_MessageReceived) that fires asynchronously and re-posts work onto kComTaskRunner via base::Unretained(port_manager).
`base::Unretained`
binds a raw, non-owning pointer into a callback, so the callee (MidiPortManager::OnAdded, OnRemoved, etc.) is dereferenced with no lifetime guarantee, making correct InstanceId validation the sole thing preventing a use-after-free.

Root Cause Analysis

Each MidiPortManager WinRT callback lambda captured only [port_manager, task_service] and re-posted work with task_service->PostBoundTask(kComTaskRunner, ...), the overload that stamps the task with whatever InstanceId is currently active at post time rather than the ID that was active when the port manager was created. During rapid session re-binding, an old MidiPortManager could be torn down while its asynchronous WinRT callbacks were still outstanding; if a new MidiManagerWinrt had already called BindInstance() and become the active instance, a stale callback from the destroyed object would stamp its posted task with the new active InstanceId, so TaskService’s validation passed and the task ran MidiPortManager::OnAdded/OnRemoved/OnCompletedGetPortFromIdAsync on the freed port_manager via base::Unretained — a use-after-free.

The fix stores the construction-time ID in a new const TaskService::InstanceId instance_id_ member, captures a local const TaskService::InstanceId instance_id = instance_id_; by value in every callback lambda, and calls the new explicit-ID overload PostBoundTask(instance_id, kComTaskRunner, ...); a stale callback therefore stamps its task with the old ID, which fails validation and is dropped instead of dereferencing freed memory. The destructor additionally now iterates async_ops_ and calls async_op->Release() so outstanding async operations are cleaned up at teardown.

Key insight
The callbacks bound their re-posted tasks to the ambient active InstanceId at post time instead of the port manager’s own creation-time identity, so a stale callback from a freed instance was validated against — and executed on behalf of — the new live instance; the fix pins each callback to the construction-time instance_id_ captured by value so stale tasks fail TaskService validation.

Attack Path

  1. Trigger rapid re-binding of the WinRT MIDI backend repeatedly obtain and release Web MIDI access so an old MidiManagerWinrt/MidiPortManager is destroyed while a new MidiManagerWinrt calls BindInstance() and becomes the active instance before the old cleanup completes (the exact renderer-side sequence, e.g. repeated navigator.requestMIDIAccess(), is (inferred)).
  2. Leave an async WinRT callback outstanding on the old instance a DeviceWatcher (add_Added/add_Removed/add_EnumerationCompleted) or async-operation (put_Completed) callback belonging to the destroyed MidiPortManager is still pending; because it captured only [port_manager, task_service], it will re-post using the current-active-ID PostBoundTask(kComTaskRunner, ...).
  3. Stale callback stamps its task with the new active `InstanceId` when the pending callback fires, PostBoundTask binds the task to the now-active new instance’s ID, so TaskService’s ID check passes and the task is dispatched instead of dropped.
  4. Use-after-free on the freed port manager the dispatched task invokes MidiPortManager::OnAdded/OnRemoved/OnCompletedGetPortFromIdAsync on base::Unretained(port_manager), dereferencing the already-destroyed MidiPortManager.
  5. Escalation controlling the freed allocation’s contents to convert the UAF into a read/write primitive or code execution is (inferred); the diff proves only the use-after-free.

Impact Assessment

An attacker who can drive Web MIDI session re-binding on Windows can cause a use-after-free on a destroyed MidiPortManager, dereferenced on kComTaskRunner in the process hosting the MIDI backend (the browser or MIDI/audio service process; the exact process is (inferred)). Preconditions are the Windows WinRT MIDI backend and a timing window where a new instance binds before the old instance’s outstanding asynchronous callbacks are drained. The vendor advisory rates this critical severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.3. The advisory metadata classifies this as a WebMIDI use-after-free and does not state exploited-in-the-wild status or a severity rating in the provided material.

Changed Functions

FunctionChangeNotes
MidiManagerWinrt
media/midi/midi_manager_winrt.cc
modified
instance_id_
media/midi/midi_manager_winrt.cc
modified
for
media/midi/midi_manager_winrt.cc
modified
if
media/midi/midi_manager_winrt.cc
modified

Files Changed

  • media/midi/midi_manager_winrt.cc

Audit Directions

  • Ambient-ID task posting
    audit every PostBoundTask/PostBoundDelayedTask call site that omits an explicit InstanceId; any that runs from an asynchronous callback capturing base::Unretained(this) can stamp a stale task with a newer active instance and re-introduce the same UAF.
  • Async callbacks outliving their owner
    review WinRT/COM handlers (add_*, put_Completed) and other WRL::Callback/async lambdas that capture a raw owner pointer; confirm the owner’s identity (not just its address) is captured by value and re-validated before the callback dereferences it.
  • Teardown vs. outstanding operations
    check destructors of objects that register async operations (analogous to the new async_ops_ Release() loop) to ensure all in-flight operations are cancelled/released before the object is freed, closing the window where a callback fires post-destruction.
From c5162675305c91532d60c33f6f860d093c445557 Mon Sep 17 00:00:00 2001
From: Hongchan Choi <[email protected]>
Date: Wed, 03 Jun 2026 23:08:01 -0700
Subject: [PATCH] [WebMIDI] Resolve instance binding lifetime issue in MidiManagerWinrt

Address a lifetime issue in Chrome's WinRT-based MIDI manager
(`MidiManagerWinrt`) on Windows during rapid session re-binding.

The issue occurred due to stale asynchronous WinRT callbacks firing from
a destroyed `MidiPortManager` instance. If a new `MidiManagerWinrt`
instance bound to `TaskService` before the old port managers' cleanup
tasks were completed, the stale callbacks would stamp their posted tasks
with the new active `InstanceId`, bypassing validation and executing on
the outdated object.

Fix this by:
1. Modifying `TaskService::BindInstance()` to return
   `std::optional<InstanceId>`, which is `nullopt` on failure. This
   avoids breaking otherplatform managers.
2. Overloading `PostBoundTask` and `PostBoundDelayedTask` to accept an
   explicit `InstanceId`.
3. Storing the construction-time `InstanceId` in `MidiPortManager`,
   capturing it by value in all WinRT callback lambdas, and using the
   explicit-ID overloads.

Added unit tests in `media/midi/task_service_unittest.cc` to verify the
explicit-ID behavior.

CONV=ef5ca2e2-c51f-4594-a8bb-6055af840666
TAG=agy

Bug: 518108291
Change-Id: I16aa19425ea93afffec395697edfd7752c6db8fa
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7890706
Commit-Queue: Hongchan Choi <[email protected]>
Reviewed-by: Michael Wilson <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1641483}
---

diff --git a/media/midi/midi_manager_winrt.cc b/media/midi/midi_manager_winrt.cc
index 594c873f..0b3a4e63 100644
--- a/media/midi/midi_manager_winrt.cc
+++ b/media/midi/midi_manager_winrt.cc
@@ -25,6 +25,7 @@
 
 #include <iomanip>
 #include <memory>
+#include <optional>
 
 #include "base/check.h"
 #include "base/containers/heap_array.h"
@@ -219,11 +220,17 @@
 class MidiManagerWinrt::MidiPortManager {
  public:
   // MidiPortManager instances should be constructed on the kComTaskRunner.
-  MidiPortManager(MidiManagerWinrt* midi_manager)
-      : midi_service_(midi_manager->service()), midi_manager_(midi_manager) {}
+  MidiPortManager(MidiManagerWinrt* midi_manager,
+                  TaskService::InstanceId instance_id)
+      : midi_service_(midi_manager->service()),
+        midi_manager_(midi_manager),
+        instance_id_(instance_id) {}
 
   virtual ~MidiPortManager() {
     DCHECK(midi_service_->task_service()->IsOnTaskRunner(kComTaskRunner));
+    for (auto* async_op : async_ops_) {
+      async_op->Release();
+    }
   }
 
   bool StartWatcher() {
@@ -267,12 +274,13 @@
     // we can handle raw pointers safely in the following blocks.
     MidiPortManager* port_manager = this;
     TaskService* task_service = midi_service_->task_service();
+    const TaskService::InstanceId instance_id = instance_id_;
 
     hr = watcher_->add_Added(
         WRL::Callback<ITypedEventHandler<
             DeviceWatcher*, Win::Devices::Enumeration::DeviceInformation*>>(
-            [port_manager, task_service](IDeviceWatcher* watcher,
-                                         IDeviceInformation* info) {
+            [port_manager, task_service, instance_id](
+                IDeviceWatcher* watcher, IDeviceInformation* info) {
               if (!info) {
                 VLOG(1) << "DeviceWatcher.Added callback provides null "
                            "pointer, ignoring";
@@ -284,13 +292,14 @@
               if (IsMicrosoftSynthesizer(info))
                 return S_OK;
 
-              std::string dev_id = GetIdString(info),
-                          dev_name = GetNameString(info);
+              const std::string dev_id = GetIdString(info);
+              const std::string dev_name = GetNameString(info);
 
               task_service->PostBoundTask(
-                  kComTaskRunner, base::BindOnce(&MidiPortManager::OnAdded,
-                                                 base::Unretained(port_manager),
-                                                 dev_id, dev_name));
+                  instance_id, kComTaskRunner,
+                  base::BindOnce(&MidiPortManager::OnAdded,
+                                 base::Unretained(port_manager), dev_id,
+                                 dev_name));
 
               return S_OK;
             })
@@ -303,10 +312,10 @@
 
     hr = watcher_->add_EnumerationCompleted(
         WRL::Callback<ITypedEventHandler<DeviceWatcher*, IInspectable*>>(
-            [port_manager, task_service](IDeviceWatcher* watcher,
-                                         IInspectable* insp) {
+            [port_manager, task_service, instance_id](IDeviceWatcher* watcher,
+                                                      IInspectable* insp) {
               task_service->PostBoundTask(
-                  kComTaskRunner,
+                  instance_id, kComTaskRunner,
                   base::BindOnce(&MidiPortManager::OnEnumerationCompleted,
                                  base::Unretained(port_manager)));
 
@@ -322,8 +331,8 @@
     hr = watcher_->add_Removed(
         WRL::Callback<
             ITypedEventHandler<DeviceWatcher*, DeviceInformationUpdate*>>(
-            [port_manager, task_service](IDeviceWatcher* watcher,
-                                         IDeviceInformationUpdate* update) {
+            [port_manager, task_service, instance_id](
+                IDeviceWatcher* watcher, IDeviceInformationUpdate* update) {
               if (!update) {
                 VLOG(1) << "DeviceWatcher.Removed callback provides null "
                            "pointer, ignoring";
@@ -333,7 +342,7 @@
               std::string dev_id = GetIdString(update);
 
               task_service->PostBoundTask(
-                  kComTaskRunner,
+                  instance_id, kComTaskRunner,
                   base::BindOnce(&MidiPortManager::OnRemoved,
                                  base::Unretained(port_manager), dev_id));
 
@@ -451,6 +460,8 @@
   // from tasks that are invoked by TaskService.
   raw_ptr<MidiManagerWinrt> midi_manager_;
 
+  const TaskService::InstanceId instance_id_;
+
  private:
   // DeviceWatcher callbacks:
   void OnAdded(std::string dev_id, std::string dev_name) {
@@ -474,16 +485,17 @@
 
     MidiPortManager* port_manager = this;
     TaskService* task_service = midi_service_->task_service();
+    const TaskService::InstanceId instance_id = instance_id_;
 
     hr = async_op->put_Completed(
         WRL::Callback<
             Win::Foundation::IAsyncOperationCompletedHandler<RuntimeType*>>(
-            [port_manager, task_service](
+            [port_manager, task_service, instance_id](
                 IAsyncOperation<RuntimeType*>* async_op, AsyncStatus status) {
               // A reference to |async_op| is kept in |async_ops_|, safe to pass
               // outside.
               task_service->PostBoundTask(
-                  kComTaskRunner,
+                  instance_id, kComTaskRunner,
                   base::BindOnce(
                       &MidiPortManager::OnCompletedGetPortFromIdAsync,
                       base::Unretained(port_manager),
@@ -640,8 +652,9 @@
                              Win::Devices::Midi::IMidiInPortStatics,
                              RuntimeClass_Windows_Devices_Midi_MidiInPort> {
  public:
-  MidiInPortManager(MidiManagerWinrt* midi_manager)
-      : MidiPortManager(midi_manager) {}
+  MidiInPortManager(MidiManagerWinrt* midi_manager,
+                    TaskService::InstanceId instance_id)
+      : MidiPortManager(midi_manager, instance_id) {}
 
   MidiInPortManager(const MidiInPortManager&) = delete;
   MidiInPortManager& operator=(const MidiInPortManager&) = delete;
@@ -654,17 +667,18 @@
 
     MidiInPortManager* port_manager = this;
     TaskService* task_service = midi_service_->task_service();
+    const TaskService::InstanceId instance_id = instance_id_;
 
     HRESULT hr = handle->add_MessageReceived(
         WRL::Callback<ITypedEventHandler<
             Win::Devices::Midi::MidiInPort*,
             Win::Devices::Midi::MidiMessageReceivedEventArgs*>>(
-            [port_manager, task_service](
+            [port_manager, task_service, instance_id](
                 Win::Devices::Midi::IMidiInPort* handle,
                 Win::Devices::Midi::IMidiMessageReceivedEventArgs* args) {
               const base::TimeTicks now = base::TimeTicks::Now();
 
-              std::string dev_id = GetDeviceIdString(handle);
+              const std::string dev_id = GetDeviceIdString(handle);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/media/midi/task_service_unittest.cc b/media/midi/task_service_unittest.cc
index cc5eac0..fe780ac 100644
--- a/media/midi/task_service_unittest.cc
+++ b/media/midi/task_service_unittest.cc
@@ -5,6 +5,7 @@
 #include "media/midi/task_service.h"
 
 #include <memory>
+#include <optional>
 
 #include "base/functional/bind.h"
 #include "base/functional/callback.h"
@@ -61,9 +62,23 @@
   TaskServiceClient(const TaskServiceClient&) = delete;
   TaskServiceClient& operator=(const TaskServiceClient&) = delete;
 
-  bool Bind() { return task_service()->BindInstance(); }
+  bool Bind() {
+    std::optional<TaskService::InstanceId> instance_id =
+        task_service()->BindInstance();
+    if (instance_id) {
+      instance_id_ = *instance_id;
+      return true;
+    }
+    instance_id_ = TaskService::kInvalidInstanceId;
+    return false;
+  }
 
-  bool Unbind() { return task_service()->UnbindInstance(); }
+  bool Unbind() {
+    instance_id_ = TaskService::kInvalidInstanceId;
+    return task_service()->UnbindInstance();
+  }
+
+  TaskService::InstanceId instance_id() const { return instance_id_; }
 
   void PostBoundTask(TaskService::RunnerId runner_id) {
     task_service()->PostBoundTask(
@@ -71,6 +86,22 @@
                                   base::Unretained(this)));
   }
 
+  void PostBoundTaskWithExplicitId(TaskService::InstanceId instance_id,
+                                   TaskService::RunnerId runner_id) {
+    task_service()->PostBoundTask(
+        instance_id, runner_id,
+        base::BindOnce(&TaskServiceClient::IncrementCount,
+                       base::Unretained(this)));
+  }
+
+  void PostBoundSignalTaskWithExplicitId(TaskService::InstanceId instance_id,
+                                         TaskService::RunnerId runner_id) {
+    task_service()->PostBoundTask(
+        instance_id, runner_id,
+        base::BindOnce(&TaskServiceClient::SignalEvent,
+                       base::Unretained(this)));
+  }
+
   void PostBoundSignalTask(TaskService::RunnerId runner_id) {
     task_service()->PostBoundTask(
         runner_id, base::BindOnce(&TaskServiceClient::SignalEvent,
@@ -91,6 +122,27 @@
         base::Milliseconds(100));
   }
 
+  void PostBoundDelayedTaskWithExplicitId(TaskService::InstanceId instance_id,
+                                          TaskService::RunnerId runner_id,
+                                          base::TimeDelta delay) {
+    task_service()->PostBoundDelayedTask(
+        instance_id, runner_id,
+        base::BindOnce(&TaskServiceClient::IncrementCount,
+                       base::Unretained(this)),
+        delay);
+  }
+
+  void PostBoundDelayedSignalTaskWithExplicitId(
+      TaskService::InstanceId instance_id,
+      TaskService::RunnerId runner_id,
+      base::TimeDelta delay) {
+    task_service()->PostBoundDelayedTask(
+        instance_id, runner_id,
+        base::BindOnce(&TaskServiceClient::SignalEvent,
+                       base::Unretained(this)),
+        delay);
+  }
+
   void WaitTask() { wait_task_event_->Wait(); }
 
   size_t count() {
@@ -121,6 +173,7 @@
   raw_ptr<TaskService> task_service_;
   std::unique_ptr<base::WaitableEvent> wait_task_event_;
   size_t count_;
+  TaskService::InstanceId instance_id_ = TaskService::kInvalidInstanceId;
 };
 
 class MidiTaskServiceTest : public ::testing::Test {
@@ -310,6 +363,82 @@
   EXPECT_TRUE(client->Unbind());
 }
 
+// Tests if bound tasks with explicit InstanceId are handled correctly.
+TEST_F(MidiTaskServiceTest, RunBoundTaskWithExplicitInstanceId) {
+  std::unique_ptr<TaskServiceClient> client =
+      std::make_unique<TaskServiceClient>(task_service());
+
+  EXPECT_TRUE(client->Bind());
+  TaskService::InstanceId id1 = client->instance_id();
+  EXPECT_NE(TaskService::kInvalidInstanceId, id1);
+
+  // Task with correct ID should run.
+  EXPECT_EQ(0u, client->count());
+  client->PostBoundTaskWithExplicitId(id1, kFirstRunner);
+  client->PostBoundSignalTaskWithExplicitId(id1, kFirstRunner);
+  WaitEvent();
+  EXPECT_EQ(2u, client->count());
+
+  // Unbind.
+  EXPECT_TRUE(client->Unbind());
+
+  // Bind again to get a new ID.
+  ResetEvent();
+  EXPECT_TRUE(client->Bind());
+  TaskService::InstanceId id2 = client->instance_id();
+  EXPECT_NE(id1, id2);
+
+  // Task with old ID should be ignored.
+  client->PostBoundTaskWithExplicitId(id1, kFirstRunner);
+
+  // Task with new ID should run.
+  client->PostBoundSignalTaskWithExplicitId(id2, kFirstRunner);
+  WaitEvent();
+  // Count should only be incremented by the new task (2 -> 3).
+  EXPECT_EQ(3u, client->count());
+
+  EXPECT_TRUE(client->Unbind());
+}
+
+// Tests if bound delayed tasks with explicit InstanceId are handled correctly.
+TEST_F(MidiTaskServiceTest, RunBoundDelayedTaskWithExplicitInstanceId) {
+  std::unique_ptr<TaskServiceClient> client =
+      std::make_unique<TaskServiceClient>(task_service());
+
+  EXPECT_TRUE(client->Bind());
+  TaskService::InstanceId id1 = client->instance_id();
+  EXPECT_NE(TaskService::kInvalidInstanceId, id1);
+
+  // Task with correct ID should run.
+  EXPECT_EQ(0u, client->count());
+  client->PostBoundDelayedSignalTaskWithExplicitId(id1, kFirstRunner,
+                                                   base::Milliseconds(10));
+  WaitEvent();
+  EXPECT_EQ(1u, client->count());
+
+  // Unbind.
+  EXPECT_TRUE(client->Unbind());
+
+  // Bind again to get a new ID.
+  ResetEvent();
+  EXPECT_TRUE(client->Bind());
+  TaskService::InstanceId id2 = client->instance_id();
+  EXPECT_NE(id1, id2);
+
+  // Task with old ID should be ignored.
+  client->PostBoundDelayedTaskWithExplicitId(id1, kFirstRunner,
+                                             base::Milliseconds(10));
+
+  // Task with new ID should run.
+  client->PostBoundDelayedSignalTaskWithExplicitId(id2, kFirstRunner,
+                                                   base::Milliseconds(10));
+  WaitEvent();
+  // Count should only be incremented by the new task (1 -> 2).
+  EXPECT_EQ(2u, client->count());
+
+  EXPECT_TRUE(client->Unbind());
+}
+
 }  // namespace
 
 }  // namespace midi
Loading diff…

Original Bug Report

reported by [email protected]

Potential Use-after-free in MidiManagerWinrt via stale TaskService callback execution

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential use-after-free (UAF) vulnerability exists in Chrome’s Windows-specific MIDI implementation, MidiManagerWinrt, during rapid session re-binding. Stale WinRT callbacks can capture and run under a newly bound instance ID, defeating the TaskService validation gate and calling methods on a freed MidiPortManager. This could lead to potential heap corruption in the browser process.

Affected files:

  • media/midi/midi_manager_winrt.cc
  • media/midi/task_service.cc

Estimated timestamp from git blame: 2018-01-17

Root Cause Analysis

In media/midi/task_service.cc, TaskService::PostBoundTask() stamps posted tasks with the current bound_instance_id_ instead of the ID that was active when the caller’s callback was registered:

void TaskService::PostBoundTask(RunnerId runner_id, base::OnceClosure task) {
  InstanceId instance_id;
  {
    base::AutoLock lock(lock_);
    if (bound_instance_id_ == kInvalidInstanceId)
      return;
    instance_id = bound_instance_id_;
  }
  GetTaskRunner(runner_id)->PostTask(
      FROM_HERE, base::BindOnce(&TaskService::RunTask, base::Unretained(this),
                                instance_id, runner_id, std::move(task)));
}

MidiManagerWinrt registers WinRT callbacks that execute on WinRT-owned thread pool threads outside of Chrome’s immediate control. These callbacks capture a raw MidiPortManager* pointer and call PostBoundTask() directly when events (such as device addition/removal or message receipt) occur:

MidiPortManager* port_manager = this;
TaskService* task_service = midi_service_->task_service();
hr = watcher_->add_Added(
    WRL::Callback<...>
        [port_manager, task_service](IDeviceWatcher*, IDeviceInformation* info) {
          task_service->PostBoundTask(
              kComTaskRunner, base::BindOnce(&MidiPortManager::OnAdded,
                                             base::Unretained(port_manager),
                                             dev_id, dev_name));
          return S_OK;
        }).Get(), &token_Added_);

These callbacks are torn down in StopWatcher(), which is called inside FinalizeOnComRunner (a static task posted by the destructor on kComTaskRunner after UnbindInstance() completes).

If a new MIDI session is initiated before the deferred cleanup task FinalizeOnComRunner executes on kComTaskRunner, a new MidiManagerWinrt instance binds to the TaskService, setting bound_instance_id_ to a new ID.

If a stale WinRT callback from the first session fires before FinalizeOnComRunner runs, it calls PostBoundTask(). This call reads the newly bound instance ID, stamping the task with the active ID and successfully posting it to kComTaskRunner.

Because FinalizeOnComRunner is queued before this stale task, it executes first, unregistering the watcher and deleting the old MidiPortManager instance. When the stale task subsequently runs, it bypasses the IsInstanceIdStillBound check (since the active instance ID matches) and dereferences the freed MidiPortManager pointer via base::Unretained, causing a Use-After-Free (UAF).

Potential Steps to Reproduce

Note: The following steps are potential/suggested vectors of exploitation. Our security analysis tooling does not currently have the capability to execute live code or verify this via a working proof-of-concept.

  1. From a compromised renderer process, request a MIDI session using the Mojo interface midi::mojom::MidiSessionProvider. This constructs MidiManagerWinrt (Instance #1) and registers the WinRT callbacks.
  2. Rapidly terminate the session (e.g., by closing the Mojo pipe). This invokes the destructor, which calls UnbindInstance() and queues the cleanup task FinalizeOnComRunner on kComTaskRunner to stop the watchers and delete the port managers.
  3. Immediately initiate a new MIDI session. This constructs a second MidiManagerWinrt (Instance #2), which binds to TaskService and sets bound_instance_id_ to a new valid ID.
  4. If a stale WinRT callback from Instance #1 fires before FinalizeOnComRunner has executed on kComTaskRunner, it calls PostBoundTask(). This stamps the task with the ID of Instance #2 and posts it to the queue.
  5. On the kComTaskRunner thread:
    • FinalizeOnComRunner runs first, deleting the port managers of Instance #1.
    • The stale callback task runs next, bypassing validation because its stamped ID matches the active Instance #2 ID, and dereferences the deleted port manager of Instance #1.

Impact and Severity

This issue allows for a potential Use-After-Free (UAF) on a heap-allocated MidiInPortManager or MidiOutPortManager in the browser process (which is unsandboxed). An attacker with control over a compromised renderer could potentially leverage this heap corruption to escape the sandbox and achieve Remote Code Execution (RCE) with the privileges of the browser process. Because this is a high-severity browser-process memory corruption issue, it is classified as Severity 0 (Critical).

Suggested Fix

To resolve this issue, TaskService::PostBoundTask (and delayed tasks) should not rely on reading a dynamically changing global bound_instance_id_ at the time of posting. Instead, classes posting bound tasks should track and supply their own bound instance ID associated with their specific registration lifetime, or MidiPortManager should use weak pointers or ref-counted callbacks to ensure that tasks cannot execute on a freed instance. Alternatively, ensuring that WinRT watchers are stopped synchronously on the calling sequence during unbinding would prevent stale callbacks from firing altogether.

Evaluated with Chrome root at commit: fb72408a8493c46bc75fae1c70d03daec96b3040


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
Uploaded PoC files