High watch CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in FileSystem
DescriptionUse after free in FileSystem
ComponentFileSystem
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.
Tracker497880137
Fix commitc0390bcd64ba (chromium/src) +151/-3
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
Creditedasjidkalam
Reported2026-03-31 (per vendor advisory)
Fix landed2026-04-07 (public git)
Disclosed2026-04-15
Patch gap8 days — fix public in source before the advisory

Background

`FileSystemAccessChangeSource`
a browser-process object that watches a file-system scope and notifies observers of changes; it is reference-counted/owned externally and can be destroyed from within its own callbacks.
`DidInitialize`
the method (named in the commit) that runs once initialization finishes: it records the result and invokes every queued initialization callback.
`initialization_result_` / `initialization_callbacks_`
member fields holding, respectively, the stored initialization result and the list of pending OnceCallbacks to run with that result.
Reentrant self-deletion
the pattern where running a callback can synchronously delete the object owning the loop, so any member access after the call touches freed memory.

Root Cause Analysis

In DidInitialize the result was stored in the member initialization_result_ = std::move(result);, and the pending callbacks were run in a loop that called std::move(callback).Run(initialization_result_->Clone());. The code already recognized that a callback can delete this — it moved initialization_callbacks_ to a stack local before iterating — but it still dereferenced the member initialization_result_ inside the loop body. Once any callback destroys this, the backing storage for initialization_result_ is freed, so the next loop iteration’s initialization_result_->Clone() reads through a dangling member pointer, a use-after-free the new ASAN test TestDestroyFromInitializeCallback reproduces by deleting the source from the first callback.

The fix keeps the result alive on the stack: it clones into the member once (initialization_result_ = result->Clone();) and then, inside the loop, clones from the still-owned stack parameter result->Clone() instead of the member, so no member is touched after a callback may have deleted this.

The restored invariant is that DidInitialize accesses only stack-based objects once it begins invoking callbacks that can free the object.

Key insight
The single mistake was guarding only initialization_callbacks_ against reentrant self-deletion while still dereferencing the member initialization_result_ inside the callback loop; the fix sources the per-callback clone from the stack parameter result so the loop no longer touches any member of a possibly-deleted this.

Attack Path

  1. Register multiple init callbacks Arrange for a FileSystemAccessChangeSource to have more than one pending callback queued in initialization_callbacks_ via EnsureInitialized.
  2. Have a callback destroy the source Cause the first invoked callback to synchronously delete the change source (the lifetime relationship the test models with delete source from the callback).
  3. Loop continues on freed object After this is deleted, DidInitialize’s loop advances and evaluates initialization_result_->Clone(), dereferencing the freed member.
  4. Use-after-free read The clone reads from released memory; the patch and ASAN test demonstrate the dangling read but no further primitive, so downstream escalation is (inferred).

Impact Assessment

An attacker who can drive the File System Access change-watching flow so that an initialization callback destroys the FileSystemAccessChangeSource triggers a use-after-free read in the browser process. The precondition is reaching this watcher path (File System Access API usage) with a callback whose completion tears down the source; such lifetimes are typically driven by renderer-initiated file-system watch requests. The advisory rates this high severity and reports no in-the-wild exploitation; escalation beyond the UAF read is (inferred).

Changed Functions

FunctionChangeNotes
for
content/browser/file_system_access/file_system_access_change_source.cc
modified
MockRawChangeObserver
content/browser/file_system_access/file_system_access_change_source_unittest.cc
modified
FakeChangeSource
content/browser/file_system_access/file_system_access_change_source_unittest.cc
modified
FileSystemAccessChangeSourceTest
content/browser/file_system_access/file_system_access_change_source_unittest.cc
modified
TEST_F
content/browser/file_system_access/file_system_access_change_source_unittest.cc
modified

Files Changed

  • content/browser/file_system_access/file_system_access_change_source.cc
  • content/browser/file_system_access/file_system_access_change_source_unittest.cc

Audit Directions

  • Member access after reentrant callbacks
    In any loop that runs user/observer callbacks capable of deleting this, verify that every post-call access (not just the callback container) uses stack-local copies.
  • Partial self-deletion guards
    Flag code that moves one member to the stack “because a callback may delete this” but still touches other members afterward; the guard must cover all of them.
From c0390bcd64ba1fd6594fbc9f6246a1649662d683 Mon Sep 17 00:00:00 2001
From: Fergal Daly <[email protected]>
Date: Mon, 06 Apr 2026 19:49:06 -0700
Subject: [PATCH] Fix UAF in FileSystemAccessChangeSource.

`DidInitialize` calls any outstanding initialization callbacks but a
callback can delete this. The code guards against this in its access
of `initialization_callbacks_` but not `initialization_result_`.

This fix keeps a copy of the result on the stack.

This also adds a test which fails with ASAN before the fix is applied
and passes after.

The basic test code was written by Gemini.

Fixed: 497880137
Change-Id: I046831db23cb4b8e41964910e2aede9b1be0db7f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7728464
Auto-Submit: Fergal Daly <[email protected]>
Reviewed-by: Ming-Ying Chung <[email protected]>
Commit-Queue: Ming-Ying Chung <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1610499}
---

diff --git a/content/browser/file_system_access/file_system_access_change_source.cc b/content/browser/file_system_access/file_system_access_change_source.cc
index 566dc1e..48bd867 100644
--- a/content/browser/file_system_access/file_system_access_change_source.cc
+++ b/content/browser/file_system_access/file_system_access_change_source.cc
@@ -71,13 +71,14 @@
   CHECK(!initialization_result_.has_value());
   CHECK(!initialization_callbacks_.empty());
 
-  initialization_result_ = std::move(result);
+  // The callbacks may cause |this| to be deleted, so we should only use
+  // stack-based objects below.
+  initialization_result_ = result->Clone();
 
-  // Move the callbacks to the stack since they may cause |this| to be deleted.
   auto initialization_callbacks = std::move(initialization_callbacks_);
   initialization_callbacks_.clear();
   for (auto& callback : initialization_callbacks) {
-    std::move(callback).Run(initialization_result_->Clone());
+    std::move(callback).Run(result->Clone());
   }
 }
 
diff --git a/content/browser/file_system_access/file_system_access_change_source_unittest.cc b/content/browser/file_system_access/file_system_access_change_source_unittest.cc
new file mode 100644
index 0000000..b0f15909
--- /dev/null
+++ b/content/browser/file_system_access/file_system_access_change_source_unittest.cc
@@ -0,0 +1,146 @@
+// 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 "content/browser/file_system_access/file_system_access_change_source.h"
+
+#include "base/files/scoped_temp_dir.h"
+#include "base/functional/bind.h"
+#include "base/memory/scoped_refptr.h"
+#include "base/task/sequenced_task_runner.h"
+#include "base/test/task_environment.h"
+#include "base/test/test_future.h"
+#include "content/browser/file_system_access/file_system_access_watch_scope.h"
+#include "storage/browser/file_system/file_system_context.h"
+#include "storage/browser/file_system/file_system_url.h"
+#include "storage/browser/quota/quota_manager_proxy.h"
+#include "storage/browser/test/test_file_system_context.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/mojom/file_system_access/file_system_access_error.mojom.h"
+
+namespace content {
+
+namespace {
+
+class MockRawChangeObserver
+    : public FileSystemAccessChangeSource::RawChangeObserver {
+ public:
+  MOCK_METHOD(void,
+              OnRawChange,
+              (const storage::FileSystemURL& changed_url,
+               bool error,
+               const FileSystemAccessChangeSource::ChangeInfo& change_info,
+               const FileSystemAccessWatchScope& scope),
+              (override));
+  MOCK_METHOD(void,
+              OnUsageChange,
+              (size_t old_usage,
+               size_t new_usage,
+               const FileSystemAccessWatchScope& scope),
+              (override));
+  MOCK_METHOD(void,
+              OnSourceBeingDestroyed,
+              (FileSystemAccessChangeSource * source),
+              (override));
+};
+
+class FakeChangeSource : public FileSystemAccessChangeSource {
+ public:
+  FakeChangeSource(
+      FileSystemAccessWatchScope scope,
+      scoped_refptr<storage::FileSystemContext> file_system_context)
+      : FileSystemAccessChangeSource(std::move(scope),
+                                     std::move(file_system_context)) {}
+  ~FakeChangeSource() override = default;
+
+  // FileSystemAccessChangeSource:
+  void Initialize(
+      base::OnceCallback<void(blink::mojom::FileSystemAccessErrorPtr)>
+          on_source_initialized) override {
+    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
+        FROM_HERE, base::BindOnce(std::move(on_source_initialized),
+                                  blink::mojom::FileSystemAccessError::New(
+                                      blink::mojom::FileSystemAccessStatus::kOk,
+                                      base::File::FILE_OK, "")));
+  }
+
+  void Signal(const storage::FileSystemURL& changed_url,
+              bool error = false,
+              ChangeInfo change_info = ChangeInfo()) {
+    NotifyOfChange(changed_url, error, change_info);
+  }
+};
+
+}  // namespace
+
+class FileSystemAccessChangeSourceTest : public testing::Test {
+ public:
+  FileSystemAccessChangeSourceTest()
+      : task_environment_(base::test::TaskEnvironment::MainThreadType::IO) {}
+
+  void SetUp() override {
+    ASSERT_TRUE(dir_.CreateUniqueTempDir());
+    file_system_context_ = storage::CreateFileSystemContextForTesting(
+        /*quota_manager_proxy=*/nullptr, dir_.GetPath());
+  }
+
+ protected:
+  base::test::TaskEnvironment task_environment_;
+  base::ScopedTempDir dir_;
+  scoped_refptr<storage::FileSystemContext> file_system_context_;
+};
+
+TEST_F(FileSystemAccessChangeSourceTest, CreateAndInitialize) {
+  auto file_path = dir_.GetPath().AppendASCII("file");
+  auto file_url = file_system_context_->CreateCrackedFileSystemURL(
+      blink::StorageKey(), storage::kFileSystemTypeLocal, file_path);
+
+  auto scope = FileSystemAccessWatchScope::GetScopeForFileWatch(file_url);
+  FakeChangeSource source(scope, file_system_context_);
+
+  base::test::TestFuture<blink::mojom::FileSystemAccessErrorPtr> future;
+  source.EnsureInitialized(future.GetCallback());
+  EXPECT_EQ(future.Get()->status, blink::mojom::FileSystemAccessStatus::kOk);
+}
+
+TEST_F(FileSystemAccessChangeSourceTest, NotifyOfChange) {
+  auto file_path = dir_.GetPath().AppendASCII("file");
+  auto file_url = file_system_context_->CreateCrackedFileSystemURL(
+      blink::StorageKey(), storage::kFileSystemTypeLocal, file_path);
+
+  auto scope = FileSystemAccessWatchScope::GetScopeForFileWatch(file_url);
+  FakeChangeSource source(scope, file_system_context_);
+
+  MockRawChangeObserver observer;
+  source.AddObserver(&observer);
+
+  EXPECT_CALL(observer, OnRawChange(testing::Eq(file_url), testing::IsFalse(),
+                                    testing::_, testing::Eq(scope)));
+  source.Signal(file_url);
+
+  source.RemoveObserver(&observer);
+}
+
+// A callback passed to `EnsureInitialized` may result in `this` being
+// destroyed. This tests that `DidInitialize` (which calls the callbacks) is
+// robust to that situation. See https://crbug.com/497880137.
+TEST_F(FileSystemAccessChangeSourceTest, TestDestroyFromInitializeCallback) {
+  auto file_path = dir_.GetPath().AppendASCII("file");
+  auto file_url = file_system_context_->CreateCrackedFileSystemURL(
+      blink::StorageKey(), storage::kFileSystemTypeLocal, file_path);
+
+  auto scope = FileSystemAccessWatchScope::GetScopeForFileWatch(file_url);
+  FakeChangeSource* source = new FakeChangeSource(scope, file_system_context_);
+
+  source->EnsureInitialized(base::BindOnce(
+      [](FakeChangeSource* source, blink::mojom::FileSystemAccessErrorPtr) {
+        delete source;
+      },
+      base::Unretained(source)));
+  base::test::TestFuture<blink::mojom::FileSystemAccessErrorPtr> future;
+  source->EnsureInitialized(future.GetCallback());
+  EXPECT_EQ(future.Get()->status, blink::mojom::FileSystemAccessStatus::kOk);
+}
+
+}  // namespace content
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/file_system_access/file_system_access_change_source_unittest.cc b/content/browser/file_system_access/file_system_access_change_source_unittest.cc
new file mode 100644
index 0000000..b0f15909
--- /dev/null
+++ b/content/browser/file_system_access/file_system_access_change_source_unittest.cc
@@ -0,0 +1,146 @@
+// 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 "content/browser/file_system_access/file_system_access_change_source.h"
+
+#include "base/files/scoped_temp_dir.h"
+#include "base/functional/bind.h"
+#include "base/memory/scoped_refptr.h"
+#include "base/task/sequenced_task_runner.h"
+#include "base/test/task_environment.h"
+#include "base/test/test_future.h"
+#include "content/browser/file_system_access/file_system_access_watch_scope.h"
+#include "storage/browser/file_system/file_system_context.h"
+#include "storage/browser/file_system/file_system_url.h"
+#include "storage/browser/quota/quota_manager_proxy.h"
+#include "storage/browser/test/test_file_system_context.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/mojom/file_system_access/file_system_access_error.mojom.h"
+
+namespace content {
+
+namespace {
+
+class MockRawChangeObserver
+    : public FileSystemAccessChangeSource::RawChangeObserver {
+ public:
+  MOCK_METHOD(void,
+              OnRawChange,
+              (const storage::FileSystemURL& changed_url,
+               bool error,
+               const FileSystemAccessChangeSource::ChangeInfo& change_info,
+               const FileSystemAccessWatchScope& scope),
+              (override));
+  MOCK_METHOD(void,
+              OnUsageChange,
+              (size_t old_usage,
+               size_t new_usage,
+               const FileSystemAccessWatchScope& scope),
+              (override));
+  MOCK_METHOD(void,
+              OnSourceBeingDestroyed,
+              (FileSystemAccessChangeSource * source),
+              (override));
+};
+
+class FakeChangeSource : public FileSystemAccessChangeSource {
+ public:
+  FakeChangeSource(
+      FileSystemAccessWatchScope scope,
+      scoped_refptr<storage::FileSystemContext> file_system_context)
+      : FileSystemAccessChangeSource(std::move(scope),
+                                     std::move(file_system_context)) {}
+  ~FakeChangeSource() override = default;
+
+  // FileSystemAccessChangeSource:
+  void Initialize(
+      base::OnceCallback<void(blink::mojom::FileSystemAccessErrorPtr)>
+          on_source_initialized) override {
+    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
+        FROM_HERE, base::BindOnce(std::move(on_source_initialized),
+                                  blink::mojom::FileSystemAccessError::New(
+                                      blink::mojom::FileSystemAccessStatus::kOk,
+                                      base::File::FILE_OK, "")));
+  }
+
+  void Signal(const storage::FileSystemURL& changed_url,
+              bool error = false,
+              ChangeInfo change_info = ChangeInfo()) {
+    NotifyOfChange(changed_url, error, change_info);
+  }
+};
+
+}  // namespace
+
+class FileSystemAccessChangeSourceTest : public testing::Test {
+ public:
+  FileSystemAccessChangeSourceTest()
+      : task_environment_(base::test::TaskEnvironment::MainThreadType::IO) {}
+
+  void SetUp() override {
+    ASSERT_TRUE(dir_.CreateUniqueTempDir());
+    file_system_context_ = storage::CreateFileSystemContextForTesting(
+        /*quota_manager_proxy=*/nullptr, dir_.GetPath());
+  }
+
+ protected:
+  base::test::TaskEnvironment task_environment_;
+  base::ScopedTempDir dir_;
+  scoped_refptr<storage::FileSystemContext> file_system_context_;
+};
+
+TEST_F(FileSystemAccessChangeSourceTest, CreateAndInitialize) {
+  auto file_path = dir_.GetPath().AppendASCII("file");
+  auto file_url = file_system_context_->CreateCrackedFileSystemURL(
+      blink::StorageKey(), storage::kFileSystemTypeLocal, file_path);
+
+  auto scope = FileSystemAccessWatchScope::GetScopeForFileWatch(file_url);
+  FakeChangeSource source(scope, file_system_context_);
+
+  base::test::TestFuture<blink::mojom::FileSystemAccessErrorPtr> future;
+  source.EnsureInitialized(future.GetCallback());
+  EXPECT_EQ(future.Get()->status, blink::mojom::FileSystemAccessStatus::kOk);
+}
+
+TEST_F(FileSystemAccessChangeSourceTest, NotifyOfChange) {
+  auto file_path = dir_.GetPath().AppendASCII("file");
+  auto file_url = file_system_context_->CreateCrackedFileSystemURL(
+      blink::StorageKey(), storage::kFileSystemTypeLocal, file_path);
+
+  auto scope = FileSystemAccessWatchScope::GetScopeForFileWatch(file_url);
+  FakeChangeSource source(scope, file_system_context_);
+
+  MockRawChangeObserver observer;
+  source.AddObserver(&observer);
+
+  EXPECT_CALL(observer, OnRawChange(testing::Eq(file_url), testing::IsFalse(),
+                                    testing::_, testing::Eq(scope)));
+  source.Signal(file_url);
+
+  source.RemoveObserver(&observer);
+}
+
+// A callback passed to `EnsureInitialized` may result in `this` being
+// destroyed. This tests that `DidInitialize` (which calls the callbacks) is
+// robust to that situation. See https://crbug.com/497880137.
+TEST_F(FileSystemAccessChangeSourceTest, TestDestroyFromInitializeCallback) {
+  auto file_path = dir_.GetPath().AppendASCII("file");
+  auto file_url = file_system_context_->CreateCrackedFileSystemURL(
+      blink::StorageKey(), storage::kFileSystemTypeLocal, file_path);
+
+  auto scope = FileSystemAccessWatchScope::GetScopeForFileWatch(file_url);
+  FakeChangeSource* source = new FakeChangeSource(scope, file_system_context_);
+
+  source->EnsureInitialized(base::BindOnce(
+      [](FakeChangeSource* source, blink::mojom::FileSystemAccessErrorPtr) {
+        delete source;
+      },
+      base::Unretained(source)));
+  base::test::TestFuture<blink::mojom::FileSystemAccessErrorPtr> future;
+  source->EnsureInitialized(future.GetCallback());
+  EXPECT_EQ(future.Get()->status, blink::mojom::FileSystemAccessStatus::kOk);
+}
+
+}  // namespace content
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index ec792aa..8071300 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -2676,6 +2676,7 @@
     "../browser/fenced_frame/redacted_fenced_frame_config_mojom_traits_unittest.cc",
     "../browser/file_system/browser_file_system_helper_unittest.cc",
     "../browser/file_system/file_system_operation_runner_unittest.cc",
+    "../browser/file_system_access/file_system_access_change_source_unittest.cc",
     "../browser/file_system_access/file_system_access_directory_handle_impl_unittest.cc",
     "../browser/file_system_access/file_system_access_file_handle_impl_unittest.cc",
     "../browser/file_system_access/file_system_access_file_modification_host_impl_unittest.cc",
Loading diff…

Original Bug Report

reported by [email protected]

REFILE: Browser-Process UAF in FileSystemAccessChangeSource::DidInitialize (Reliability-Improved PoC)

Reference: This is a highly-reliable refile of Issue https://issues.chromium.org/issues/497456775.

Why it was previously marked Not Reproducible: The previous PoC relied on a single-shot race condition (Worker Termination vs. IO Thread Execution). Because thread scheduling timings differ across hardware (e.g., M-series vs. Intel, core counts), it missed the race window on the triage machine.

The Fix: I have created a new PoC that acts as a “reliability sweep”. It programmatically sweeps through thousands of attempts using varying profiles of [attempts x fanout x concurrency] (e.g., 1024x24x8). This systematically exhausts the thread scheduler variations, effectively guaranteeing the Use-After-Free race window is hit across different hardware profiles.

A video demonstrating the reliable crash on the latest ASan build is attached.


VULNERABILITY DETAILS

Class: Browser-process heap-use-after-free / memory corruption.

Impact: A compromised renderer or a malicious web page can trigger a browser-process UAF. Because the vulnerability bypasses MiraclePtr (MiraclePtr Status: NOT PROTECTED), this memory corruption can be exploited for an arbitrary memory read or controlled write in the browser process, leading to sandbox escape/RCE.

Root Cause: The bug is in content/browser/file_system_access/file_system_access_change_source.cc, in FileSystemAccessChangeSource::DidInitialize(...).

DidInitialize() moves initialization_callbacks_ to a local stack variable because executing the callbacks may cause this to be deleted. However, it still reads this->initialization_result_ from the member variable inside the callback loop:

  initialization_result_ = std::move(result);

  // Move the callbacks to the stack since they may cause |this| to be deleted.
  auto initialization_callbacks = std::move(initialization_callbacks_);
  initialization_callbacks_.clear();
  for (auto& callback : initialization_callbacks) {
    // If the first callback deleted |this|, the next iteration UAFs here:
    std::move(callback).Run(initialization_result_->Clone()); 
  }

If the first callback tears down the watcher and deletes the source, the second iteration dereferences freed object state through this->initialization_result_. The implicit this pointer is not protected by BRP/MiraclePtr in this specific execution context.

VERSION

Chrome Version: Chromium 148.0.7761.0 canary (official mac-arm64 ASan prebuilt; reproduced March 31, 2026) Operating System: macOS 15.7.4 (24G517), arm64

REPRODUCTION CASE

Please see the attached individual files:

  • poc.html (The reliability sweep driver)
  • poc.js
  • worker.js
  • asan.log (ASan logs from the run)
  • FSA_UAF_POC.mov (Video evidence of reproduction)

The PoC is a minimized, pure-web reproduction utilizing a reliability sweep:

  1. Serve the PoC files from a local HTTP server (python3 -m http.server 8000).
  2. Launch the ASan browser.
  3. Click Start.
  4. In the directory picker, choose any writable local directory.
  5. The PoC will begin a staged sweep (Stage 1: 1024x24x8, Stage 2: 1024x32x8, Stage 3: 768x24x12).
  6. The browser will crash with a heap-use-after-free during one of these stages.

Exact Command Line from Repro (As recorded in asan.log):

profile_dir=$(mktemp -d /tmp/fsa_profile.XXXXXX)

ASAN_OPTIONS="detect_odr_violation=0:abort_on_error=1:symbolize=1:external_symbolizer_path=/Users/asjidkalam/fuzzing/chrome_research/chromium/src/third_party/llvm-build/Release+Asserts/bin/llvm-symbolizer:log_path=/tmp/fsa_local_context_arm64_default_symbolized_asan" \
open -n "/Users/asjidkalam/fuzzing/chrome_research/chromium/src/.asan_mac_arm64_run/Chromium.app" --args \
  --user-data-dir="$profile_dir" \
  --window-size=1280,900 \
  --window-position=0,0 \
  --enable-blink-features=FileSystemObserver \
  --test-type \
  --no-first-run \
  --no-default-browser-check \
  "http://127.0.0.1:8000/fsa_change_source_default_web_vrp_bundle/poc.html"

Notes:

  • No flags required: This repro does NOT use --disable-features=PartitionAllocBackupRefPtr.
  • Timing: If the crash does not occur in Stage 1, please allow the sweep to continue through Stage 3.

SUGGESTED PATCH

Move/Clone the initialization result to a local stack variable before entering the callback loop to decouple it from the object’s lifetime:

  auto initialization_callbacks = std::move(initialization_callbacks_);
  initialization_callbacks_.clear();
  auto result_clone = initialization_result_->Clone();
  for (auto& callback : initialization_callbacks) {
    std::move(callback).Run(result_clone->Clone());
  }

FOR CRASHES, PLEASE INCLUDE THE FOLLOWING ADDITIONAL INFORMATION

Type of crash: browser-process ASan heap-use-after-free

Crash State:

  • Use path: content::FileSystemAccessChangeSource::DidInitialize(...)
  • Callback path: content::FileSystemAccessLocalPathWatcher::Initialize(...)
  • Free path: content::FileSystemAccessWatcherManager::RemoveObserver(...)
  • Allocation path: content::FileSystemAccessWatcherManager::CreateOwnedSourceForScope(...)

Summary from attached ASan log:

  • ERROR: AddressSanitizer: heap-use-after-free
  • SUMMARY: AddressSanitizer: heap-use-after-free ... in content::FileSystemAccessChangeSource::DidInitialize(...)
  • MiraclePtr Status: NOT PROTECTED

CREDIT INFORMATION

Reporter credit: asjidkalam

View on issue tracker