CVE-2026-110142
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/file_system_access/file_system_access_lock_manager.cc |
modified |
Files Changed
content/browser/file_system_access/file_system_access_lock_manager.cccontent/browser/file_system_access/file_system_access_lock_manager_unittest.cc
Patch
From aa6c477ded0f2d8ad56bdabce2820462f9a9f30c Mon Sep 17 00:00:00 2001 From: François Doray <[email protected]> Date: Tue, 29 Sep 2026 10:59:37 -0700 Subject: [PATCH] [FSA] Fix use-after-free in FileSystemAccessLockManager Problem: Consider three locks on the same path: - Lock A: Active lock held by a BFCached page. - Lock B: Pending lock requested by another BFCached page. Lock A begins eviction, and Lock B's callback holds the only surviving handle to Lock A (`Lock B->evicting_subroot_lock_ = Lock A`). - Lock C: New pending lock requested by an active page, superseding Lock B in its parent's `child_locks_`. In `Lock C->SetEvictingSubrootLock(Lock B)`, Lock A was extracted into a stack local `next_evicting_subroot_lock` while calling `evicting_subroot_lock->EvictPendingSubtree()` on Lock B. Evicting Lock B dropped its callbacks, releasing the last handle to Lock A and invoking `Lock A->LockHandleDestroyed()`. Because Lock C had not yet adopted Lock A as its `evicting_subroot_lock_`, `Lock A->IsEvictingSubroot()` returned false. Believing it was an active lock, Lock A invoked `DestroySelf()`, which called `ReleaseChild()` on its parent (`root_lock_`). This erased Lock C from `root_lock_->child_locks_` while Lock C's `SetEvictingSubrootLock()` was still running on the stack. When `EvictPendingSubtree()` returned, Lock C accessed its now-freed memory to assign `evicting_subroot_lock_`, causing a heap use-after-free. Additionally, two related lifecycle bugs existed: 1. `LockHandleDestroyed()` unconditionally called `DestroySelf()` for locks with empty handles, even if they were detached/superseded (`parent->GetChild(path) != this`) or still anchored child locks in the path hierarchy (`!child_locks_.empty()`). 2. An unhandled null pointer occurred in `IsEvictingSubroot()` when `parent_lock_->GetChild(path_)` was null. Fix: 1. In `SetEvictingSubrootLock()`: Adopt `next_evicting_subroot_lock` (Lock A) into `evicting_subroot_lock_` before calling `EvictPendingSubtree()` on Lock B. During eviction, Lock A's `IsEvictingSubroot()` evaluates to true, so Lock A destroys itself safely via `pending_lock->RemoveEvictingSubrootLock()` and promotes Lock C, without invoking `ReleaseChild()`. 2. In `LockHandleDestroyed()`: - Do not destroy a lock if `!child_locks_.empty() || evicting_subroot_lock_`. Intermediate path nodes must be preserved until their children are gone to avoid dangling parent references. - Verify parent ownership (`parent_lock_->GetChild(path_) == this`) before calling `DestroySelf()`. Detached or superseded locks cannot delete unrelated children from former parents. 3. In `ReleaseChild()`: Prune ancestor locks whose children are now empty if they have no remaining handles or active evictions. 4. In `IsEvictingSubroot()`: Add a null check for `parent_lock_->GetChild(path_)` to handle cases where `path_` has already been released or detached. 5. In `IteratePendingSubtreeCallbacks()`: Guard callback execution with `!pending_callbacks_.empty()` to support locks promoted reentrantly before callbacks are attached. AGY was used to help write this patch. Fixed: 553525515 Change-Id: I9f1c8f0d3310d992823dfebc84b6a8bbe32196ef Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8389638 Reviewed-by: Mingyu Lei <[email protected]> Commit-Queue: Francois Pierre Doray <[email protected]> Cr-Commit-Position: refs/heads/main@{#1707268} --- diff --git a/content/browser/file_system_access/file_system_access_lock_manager.cc b/content/browser/file_system_access/file_system_access_lock_manager.cc index 350d682..0425e4d 100644 --- a/content/browser/file_system_access/file_system_access_lock_manager.cc +++ b/content/browser/file_system_access/file_system_access_lock_manager.cc @@ -196,6 +196,37 @@ size_t count_removed = child_locks_.erase(path); CHECK_EQ(1u, count_removed); + + // If this lock was kept alive solely as an ancestor of children that are + // now gone, destroy it if possible. + // + // This is not expected to happen: `ReleaseChild()` is only called from a + // child's `LockHandleDestroyed()`, while the child's `LockHandle` still + // holds a `parent_lock_handle_` to `this`. `this` is instead destroyed when + // that parent handle is released. + // TODO(crbug.com/553525515): Remove the `DestroySelf()` fallback below if + // there are no reports of this CHECK failing. + CHECK(!CanDestroySelf(), base::NotFatalUntil::M160); + if (CanDestroySelf()) { + DestroySelf(); + } + } + + bool CanDestroySelf() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + // A lock can only destroy itself through its parent if: + // - No active handles or pending callbacks reference it. + // - It does not anchor any child locks in the path hierarchy (it will be + // pruned in `ReleaseChild` once its children are gone). + // - It is neither waiting on an underlying eviction nor being evicted as + // another lock's `evicting_subroot_lock_`. + // - Its parent still owns it at `path_` (an evicted or superseded lock must + // not call `ReleaseChild()`, which would erroneously remove whatever new + // lock replaced it). + return frame_id_lock_handles_.empty() && child_locks_.empty() && + pending_callbacks_.empty() && !evicting_subroot_lock_ && + !IsEvictingSubroot() && + (!parent_lock_.has_value() || parent_lock_->GetChild(path_) == this); } bool IsHeldOnlyByInactivePages() { @@ -241,41 +272,42 @@ // If nothing is holding this lock, release it. if (frame_id_lock_handles_.empty()) { - // If we're not an Evicting subroot, then our parent owns us, and we can - // destroy ourselves through our parent. - if (!IsEvictingSubroot()) { + if (IsEvictingSubroot()) { + // The root cannot be an Evicting subroot. + CHECK(parent_lock_.has_value()); + + // If we are an Evicting subroot, then we must destroy ourselves through + // the `pending_lock` that owns us. + Lock* pending_lock = parent_lock_->GetChild(path_); + CHECK(pending_lock); + bool in_pending_subtree = InPendingSubtree(); + CHECK(!in_pending_subtree || !IsPendingSubroot()); + + // `RemoveEvictingSubrootLock` will destroy `this`. + pending_lock->RemoveEvictingSubrootLock(); + + if (in_pending_subtree) { + return; + } + + if (pending_lock->InEvictingSubtree()) { + // If the pending lock is in an Evicting subtree, then it should be + // evicted. + pending_lock->EvictPendingSubtree(); + return; + } + + // This was the last Evicting Lock that needed to be destroyed to + // promote the `pending_lock`. + pending_lock->PromotePendingToTaken(); + return; + } + + if (CanDestroySelf()) { // `DestroySelf` will destroy `this`. DestroySelf(); return; } - - // The root cannot be an Evicting subroot. - CHECK(parent_lock_.has_value()); - - // If we are an Evicting subroot, then we must destroy ourselves through - // the `pending_lock` that owns us. - Lock* pending_lock = parent_lock_->GetChild(path_); - bool in_pending_subtree = InPendingSubtree(); - CHECK(!in_pending_subtree || !IsPendingSubroot()); - - // `RemoveEvictingSubrootLock` will destroy `this`. - pending_lock->RemoveEvictingSubrootLock(); - - if (in_pending_subtree) { - return; - } - - if (pending_lock->InEvictingSubtree()) { - // If the pending lock is in an Evicting subtree, then it should be - // evicted. - - pending_lock->EvictPendingSubtree(); - return; - } - - // This was the last Evicting Lock that needed to be destroyed to promote - // the `pending_lock`. - pending_lock->PromotePendingToTaken(); } } @@ -315,11 +347,13 @@ return; } - // This is a leaf of a Pending subtree, so it should have Pending callbacks. - CHECK(pending_callbacks_.size() > 0); - - // `this` may be destroyed. - callback.Run(std::move(pending_callbacks_)); + // This is a leaf of a Pending subtree. If it has pending callbacks, run + // them. It may not have any yet if it was promoted before callbacks were + // attached. + if (!pending_callbacks_.empty()) { + // `this` may be destroyed. + callback.Run(std::move(pending_callbacks_)); + } } void EvictPendingSubtree() { @@ -371,8 +405,13 @@ // Returns if we're the subroot of an Evicting subtree. See class comment. bool IsEvictingSubroot() { - return parent_lock_.has_value() && - parent_lock_->GetChild(path_)->evicting_subroot_lock_.get() == this; + if (!parent_lock_.has_value()) { + return false; + } + // `child` can be null if `path_` in the parent has already been released + // or if `this` has been detached. + Lock* child = parent_lock_->GetChild(path_); + return child && child->evicting_subroot_lock_.get() == this; } // Makes `this` a Pending subtree that is waiting on the destruction of the @@ -380,17 +419,23 @@ // comment. void SetEvictingSubrootLock(std::unique_ptr<Lock> evicting_subroot_lock) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + CHECK(evicting_subroot_lock); is_pending_ = true; - evicting_subroot_lock_ = std::move(evicting_subroot_lock); - // If our `evicting_subroot_lock_` has its own `evicting_subroot_lock_`, - // then replace the former with the latter. + // If `evicting_subroot_lock` was itself pending on an evicting subroot, + // then that underlying evicting subroot is what `this` must wait on. auto next_evicting_subroot_lock = - evicting_subroot_lock_->TakeEvictingSubrootLock(); + evicting_subroot_lock->TakeEvictingSubrootLock(); if (next_evicting_subroot_lock) { - // Destroys `evicting_subroot_lock_`. - evicting_subroot_lock_->EvictPendingSubtree(); + // Must adopt `next_evicting_subroot_lock` BEFORE calling + // `EvictPendingSubtree()`. Dropping callbacks in `EvictPendingSubtree()` + // may release handles to the underlying evicting lock, which relies on + // `this->evicting_subroot_lock_` to recognize that it is an evicting + // subroot rather than an active lock. evicting_subroot_lock_ = std::move(next_evicting_subroot_lock); + evicting_subroot_lock->EvictPendingSubtree(); + } else { + evicting_subroot_lock_ = std::move(evicting_subroot_lock); } } diff --git a/content/browser/file_system_access/file_system_access_lock_manager_unittest.cc b/content/browser/file_system_access/file_system_access_lock_manager_unittest.cc index c1749ad6..55be25a 100644 --- a/content/browser/file_system_access/file_system_access_lock_manager_unittest.cc +++ b/content/browser/file_system_access/file_system_access_lock_manager_unittest.cc @@ -1317,6 +1317,59 @@ } TEST_F(FileSystemAccessLockManagerTest, + BFCacheEvictPendingLockReentrantDestroySelf) { + RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(main_rfh()); + auto active_context = kBindingContext; + auto bf_cache_context = FileSystemAccessManagerImpl::BindingContext( + kTestStorageKey, kTestURL, rfh->GetGlobalId()); + rfh->SetLifecycleState( + RenderFrameHostImpl::LifecycleStateImpl::kInBackForwardCache); + + base::FilePath parent = dir_.GetPath().AppendASCII("parent"); + auto parent_url = CreateLocalUrl(parent); + LockType exclusive_lock_type = manager_->GetExclusiveLockType(); + + // 1. Take Lock A on `parent_url` in BFCache. + auto lock_a = TakeLockSync(bf_cache_context, parent_url, exclusive_lock_type); + ASSERT_TRUE(lock_a); + + // 2. Take Lock B on `parent_url` in BFCache, holding Lock A in the callback. + base::test::TestFuture<scoped_refptr<FileSystemAccessLockManager::LockHandle>> + lock_b_future; + auto holding_callback = base::BindOnce( + [](scoped_refptr<FileSystemAccessLockManager::LockHandle> held_lock, + base::OnceCallback<void( + scoped_refptr<FileSystemAccessLockManager::LockHandle>)> cb, + scoped_refptr<FileSystemAccessLockManager::LockHandle> lock) { + std::move(cb).Run(std::move(lock)); + }, + std::move(lock_a), lock_b_future.GetCallback()); + + manager_->TakeLock(bf_cache_context, parent_url, exclusive_lock_type, + std::move(holding_callback)); + + // 3. Take Lock C on `parent_url` in active_context. + // This triggers eviction of Lock B, which has Lock A parked as its + // evicting subroot. When B's pending subtree is evicted, the holding + // callback is dropped, destroying the last handle to Lock A. + auto lock_c_future = + TakeLockAsync(active_context, parent_url, exclusive_lock_type); + + // When Lock B was evicted, the holding callback must be dropped. + EXPECT_FALSE(lock_b_future.IsReady()); + // Lock C must be reentrantly promoted and granted. + ASSERT_TRUE(lock_c_future->IsReady()); + auto lock_c = lock_c_future->Take(); + ASSERT_TRUE(lock_c); + + // Verify the lock is actively held. + EXPECT_FALSE(TakeLockSync(active_context, parent_url, exclusive_lock_type)); + // Verify clean release when Lock C is destroyed. + lock_c.reset(); + EXPECT_TRUE(TakeLockSync(active_context, parent_url, exclusive_lock_type)); +} + +TEST_F(FileSystemAccessLockManagerTest, LocksCanExistAfterFileSystemAccessManagerIsDestroyed) { base::FilePath path = dir_.GetPath().AppendASCII("foo"); auto url = manager_->CreateFileSystemURLFromPath(PathInfo(path));
Regression Test / PoC
diff --git a/content/browser/file_system_access/file_system_access_lock_manager_unittest.cc b/content/browser/file_system_access/file_system_access_lock_manager_unittest.cc
index c1749ad6..55be25a 100644
--- a/content/browser/file_system_access/file_system_access_lock_manager_unittest.cc
+++ b/content/browser/file_system_access/file_system_access_lock_manager_unittest.cc
@@ -1317,6 +1317,59 @@
}
TEST_F(FileSystemAccessLockManagerTest,
+ BFCacheEvictPendingLockReentrantDestroySelf) {
+ RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(main_rfh());
+ auto active_context = kBindingContext;
+ auto bf_cache_context = FileSystemAccessManagerImpl::BindingContext(
+ kTestStorageKey, kTestURL, rfh->GetGlobalId());
+ rfh->SetLifecycleState(
+ RenderFrameHostImpl::LifecycleStateImpl::kInBackForwardCache);
+
+ base::FilePath parent = dir_.GetPath().AppendASCII("parent");
+ auto parent_url = CreateLocalUrl(parent);
+ LockType exclusive_lock_type = manager_->GetExclusiveLockType();
+
+ // 1. Take Lock A on `parent_url` in BFCache.
+ auto lock_a = TakeLockSync(bf_cache_context, parent_url, exclusive_lock_type);
+ ASSERT_TRUE(lock_a);
+
+ // 2. Take Lock B on `parent_url` in BFCache, holding Lock A in the callback.
+ base::test::TestFuture<scoped_refptr<FileSystemAccessLockManager::LockHandle>>
+ lock_b_future;
+ auto holding_callback = base::BindOnce(
+ [](scoped_refptr<FileSystemAccessLockManager::LockHandle> held_lock,
+ base::OnceCallback<void(
+ scoped_refptr<FileSystemAccessLockManager::LockHandle>)> cb,
+ scoped_refptr<FileSystemAccessLockManager::LockHandle> lock) {
+ std::move(cb).Run(std::move(lock));
+ },
+ std::move(lock_a), lock_b_future.GetCallback());
+
+ manager_->TakeLock(bf_cache_context, parent_url, exclusive_lock_type,
+ std::move(holding_callback));
+
+ // 3. Take Lock C on `parent_url` in active_context.
+ // This triggers eviction of Lock B, which has Lock A parked as its
+ // evicting subroot. When B's pending subtree is evicted, the holding
+ // callback is dropped, destroying the last handle to Lock A.
+ auto lock_c_future =
+ TakeLockAsync(active_context, parent_url, exclusive_lock_type);
+
+ // When Lock B was evicted, the holding callback must be dropped.
+ EXPECT_FALSE(lock_b_future.IsReady());
+ // Lock C must be reentrantly promoted and granted.
+ ASSERT_TRUE(lock_c_future->IsReady());
+ auto lock_c = lock_c_future->Take();
+ ASSERT_TRUE(lock_c);
+
+ // Verify the lock is actively held.
+ EXPECT_FALSE(TakeLockSync(active_context, parent_url, exclusive_lock_type));
+ // Verify clean release when Lock C is destroyed.
+ lock_c.reset();
+ EXPECT_TRUE(TakeLockSync(active_context, parent_url, exclusive_lock_type));
+}
+
+TEST_F(FileSystemAccessLockManagerTest,
LocksCanExistAfterFileSystemAccessManagerIsDestroyed) {
base::FilePath path = dir_.GetPath().AppendASCII("foo");
auto url = manager_->CreateFileSystemURLFromPath(PathInfo(path));
Original Bug Report
UAF in Browser process in Lock::GetOrCreateChild
Report description
UAF in Browser process in Lock::GetOrCreateChild
Bug location
Where do you want to report your vulnerability?
Chrome VRP – Report security issues affecting the Chrome browser. See program rules
The problem
Please describe the technical details of the vulnerability
there’s a browser process UAF in Lock::GetOrCreateChild
details
Component: content/browser/file_system_access/file_system_access_lock_manager.cc. Heap use-after-free on the browser process main thread, an 8 byte read followed immediately by an 8 byte write into the same freed slot.
Reproduced on 154.0.8029.0 (asan-linux-release-1687295) and on 154.0.8011.0, both Linux ASAN builds. Both are .0 trunk builds, not released stable binaries. The code path sits behind no flag and no origin trial, and the cited lines are unchanged on main.
SetEvictingSubrootLock moves the previous evicting subroot into a stack local at line 389, calls EvictPendingSubtree() at line 392, then assigns evicting_subroot_lock_ at line 393. For the duration of that line 392 call the parked lock is owned by neither its parent’s child_locks_ map nor any evicting_subroot_lock_, only by the stack temporary.
EvictPendingSubtree runs the pending callbacks, which drops the last LockHandle and re-enters LockHandleDestroyed on the parked lock. That function picks a destruction path using IsEvictingSubroot(), which asks the parent which lock currently sits at this path. During the window the parent’s map holds the newly created pending lock instead, so the test reads false and line 248 takes DestroySelf(), which calls parent_lock_->ReleaseChild(path_) and erases the map entry for that path.
The erased entry is not the parked lock. It is the pending lock that GetOrCreateChild allocated moments earlier at line 179, and whose SetEvictingSubrootLock frame is still live on the stack. Control returns to line 393, which reads that freed object’s evicting_subroot_lock_ and then stores through the same address.
The trace agrees. sizeof(Lock) is 152 and evicting_subroot_lock_ sits at offset 104, matching “104 bytes inside of 152-byte region”. The allocation stack for the freed region is GetOrCreateChild reached from FileSystemAccessDirectoryHandleImpl::Remove, and the free stack is EvictPendingSubtree to the barrier callback to ~LockHandle to LockHandleDestroyed to DestroySelf to the map erase. asan_154.0.8029.0_browser_uaf.log is the full report. SetEvictingSubrootLock is inlined into its caller in this build, so frame 0 and the SUMMARY both name GetOrCreateChild, and unique_ptr.h:261 is the read of the old pointer inside unique_ptr::reset() that the line 393 move-assignment expands to. The same inlining shows in the free stack, where GetOrCreateChild carries line 392.
Two things have to overlap. A frame the browser has already marked inactive issues FileSystemFileHandle.move() from its pagehide handler, creating the evicting subtree whose barrier callback holds the last handle. A second active page on the same origin, in its own renderer process, calls FileSystemDirectoryHandle.remove() on that directory while the first frame is still unloading. mover.html does the first, hammer.html the second, driver.html times them.
The state discriminator is whether the mover’s pagehide handler ran. unload is disabled by default in Chrome 154, so an unload-based variant of this PoC arms nothing and looks clean. The runner prints mover pagehide fired and mover move issued st=sent once the first half has armed, and a hammer round=N fired=... settled=... line for the second. A round that never armed says mover SETUP FAILED or mover TIMED OUT instead. The hammer line is emitted about two seconds into a round, so when the report fires in round 0 it is normal for that line not to appear at all.
reproduction steps
- Unpack a Linux ASAN build of Chrome. Tested with 154.0.8029.0, asan-linux-release-1687295.
- Put
run_poc.py,driver.html,mover.html,hammer.htmlandblank.htmlin one directory. - Run one command from that directory:
python3 run_poc.py --chrome /path/to/asan/chrome
Both a server and a page are required, and run_poc.py supplies both. It serves the four HTML files over loopback with Python, launches Chrome directly as a subprocess with two URLs on the command line, and captures stderr until ASAN prints its SUMMARY. It supplies a display itself, using $DISPLAY when one is set and otherwise xvfb-run, and it prints which one it chose. Headless is not offered, because Chrome rejects two startup URLs with “Multiple targets are not supported in headless mode”. It aborts with a message if a PoC file is missing, if no display can be arranged, or if Chrome exits early. No CDP, no remote debugging port, no automation framework.
run_poc.py passes Chrome three switches and the two page URLs, nothing more: --user-data-dir pointed at a throwaway profile so OPFS starts empty, --no-first-run and --no-default-browser-check. Dropping the latter two leaves Chrome sitting on its first-run flow and the pages never load. The command line recorded in the trace shows four more, --ozone-platform=x11, an empty --flag-switches-begin/--flag-switches-end pair and --file-url-path-alias, all of which Chrome appends itself at startup. There are no feature flags and no logging switches. ASAN writes to stderr on its own.
Measured with these exact files and this exact command from a clean directory, 154.0.8029.0 gave 6 reports from 6 runs, the slowest taking 26.0 seconds. The same files also report on 154.0.8011.0. The script stops at the first report and gives up after 14 rounds, which left spare rounds in every run.
=================================================================
==1415819==ERROR: AddressSanitizer: heap-use-after-free on address 0x6fd05f9fc688 at pc 0x5984f2edf5b7 bp 0x7fff2d141c70 sp 0x7fff2d141c68
READ of size 8 at 0x6fd05f9fc688 thread T0 (chrome)
SCARINESS: 51 (8-byte-read-heap-use-after-free)
#0 0x5984f2edf5b6 in content::Lock::GetOrCreateChild(base::FilePath const&, base::IdType<content::LockTypeTag, int, 0, 1>) third_party/libc++/src/include/__memory/unique_ptr.h:261:21
#1 0x5984f2ede28a in content::FileSystemAccessLockManager::TakeLock(content::GlobalRenderFrameHostId const&, storage::FileSystemURL const&, base::IdType<content::LockTypeTag, int, 0, 1>, base::OnceCallback<void (scoped_refptr<content::FileSystemAccessLockManager::LockHandle>)>) content/browser/file_system_access/file_system_access_lock_manager.cc:594:18
#2 0x5984f2f05023 in content::FileSystemAccessManagerImpl::TakeLock(content::FileSystemAccessEntryFactory::BindingContext const&, storage::FileSystemURL const&, base::IdType<content::LockTypeTag, int, 0, 1>, base::OnceCallback<void (scoped_refptr<content::FileSystemAccessLockManager::LockHandle>)>) content/browser/file_system_access/file_system_access_manager_impl.cc:1413:18
#3 0x5984f2ed2383 in content::FileSystemAccessHandleBase::DoRemove(storage::FileSystemURL const&, bool, base::OnceCallback<void (mojo::InlinedStructPtr<blink::mojom::FileSystemAccessError>)>) content/browser/file_system_access/file_system_access_handle_base.cc:730:14
...
SUMMARY: AddressSanitizer: heap-use-after-free third_party/libc++/src/include/__memory/unique_ptr.h:261:21 in content::Lock::GetOrCreateChild(base::FilePath const&, base::IdType<content::LockTypeTag, int, 0, 1>)
Shadow bytes around the buggy address:
0x6fd05f9fc400: fd fd fd fd fa fa fa fa fa fa f7 fa fd fd fd fd
0x6fd05f9fc480: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
0x6fd05f9fc500: fa fa fa fa fa fa f7 fa fd fd fd fd fd fd fd fd
0x6fd05f9fc580: fd fd fd fd fd fd fd fd fd fd fd fd fa fa fa fa
0x6fd05f9fc600: fa fa f7 fa fd fd fd fd fd fd fd fd fd fd fd fd
=>0x6fd05f9fc680: fd[fd]fd fd fd fd fd fa fa fa fa fa fa fa fa fa
0x6fd05f9fc700: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x6fd05f9fc780: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x6fd05f9fc800: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x6fd05f9fc880: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x6fd05f9fc900: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==1415819==ADDITIONAL INFO
==1415819==Note: Please include this section with the ASan report.
Task trace:
#0 0x59850202b963 in mojo::SimpleWatcher::Context::Notify(unsigned int, MojoHandleSignalsState, unsigned int) mojo/public/cpp/system/simple_watcher.cc:103:13
Command line: `/home/ab/asanhead-1687295/out/f2d4-ASAN_Release/chrome --user-data-dir=/data/tmp/fsa-poc-9hadvm0u --no-first-run --no-default-browser-check --ozone-platform=x11 --flag-switches-begin --flag-switches-end --file-url-path-alias=/gen=/home/ab/asanhead-1687295/out/f2d4-ASAN_Release/gen http://127.0.0.1:43263/hammer.html http://127.0.0.1:43263/driver.html?rounds=14&busy=420`
MiraclePtr Status: NOT PROTECTED
No raw_ptr<T> access to this region was detected prior to this crash.
This crash is still exploitable with MiraclePtr.
Refer to https://chromium.googlesource.com/chromium/src/+/main/base/memory/raw_ptr.md for details.
==1415819==END OF ADDITIONAL INFO
==1415819==ABORTING
Impact analysis
a page served from any origin gets the browser process to free a 152 byte content::Lock and then read and write a pointer field inside it. The trace is browser process, thread T0, with no --type= on the command line. That puts attacker-driven memory corruption on the privileged side of Chrome’s process boundary.
MiraclePtr Status: NOT PROTECTED. BackupRefPtr does not cover this access because the dangling references are the this of the running method and a std::unique_ptr member, neither of which is a raw_ptr.
The cause
What version of Chrome have you found the security issue in?
154.0.8029.0
Is the security issue related to a crash?
Yes, it is related to a crash.
Choose the type of vulnerability
Memory Corruption
How would you like to be publicly acknowledged for your report?
Andrew Boni
- http://127.0.0.1:43263/driver.html?rounds=14&busy=420
- http://127.0.0.1:43263/hammer.html
- https://bughunters.google.com/about/rules/5745167867576320/chrome-vulnerability-reward-program-rules
- https://chromium.googlesource.com/chromium/src/+/main/base/memory/raw_ptr.md
- https://source.chromium.org/chromium/chromium/src/+/main:content/browser/file_system_access/file_system_access_lock_manager.cc;l=172
- https://source.chromium.org/chromium/chromium/src/+/main:content/browser/file_system_access/file_system_access_lock_manager.cc;l=179
- https://source.chromium.org/chromium/chromium/src/+/main:content/browser/file_system_access/file_system_access_lock_manager.cc;l=236
- https://source.chromium.org/chromium/chromium/src/+/main:content/browser/file_system_access/file_system_access_lock_manager.cc;l=248
- https://source.chromium.org/chromium/chromium/src/+/main:content/browser/file_system_access/file_system_access_lock_manager.cc;l=373
- https://source.chromium.org/chromium/chromium/src/+/main:content/browser/file_system_access/file_system_access_lock_manager.cc;l=381
- https://source.chromium.org/chromium/chromium/src/+/main:content/browser/file_system_access/file_system_access_lock_manager.cc;l=389
- https://source.chromium.org/chromium/chromium/src/+/main:content/browser/file_system_access/file_system_access_lock_manager.cc;l=392
- https://source.chromium.org/chromium/chromium/src/+/main:content/browser/file_system_access/file_system_access_lock_manager.cc;l=393