Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free WebMIDI
DescriptionUse after free WebMIDI
ComponentChromium
Bug ClassUAF
Tracker518108291
Fix commitc5162675305c (chromium/src) +247/-60
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-11

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
From c5162675305c91532d60c33f6f860d093c445557 Mon Sep 17 00:00:00 2001
From: Hongchan Choi <hongchan@google.com>
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 <hongchan@chromium.org>
Reviewed-by: Michael Wilson <mjwilson@chromium.org>
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 vm...@google.com

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