CVE-2026-10970
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/interest_group/interest_group_auction.cc |
modified | |
forcontent/browser/interest_group/interest_group_auction.cc |
modified |
Files Changed
content/browser/interest_group/interest_group_auction.cc
Patch
From 0da3fa38b0dac70eaa194098f1ef0bbab88563ed Mon Sep 17 00:00:00 2001 From: Paul Jensen <[email protected]> Date: Thu, 14 May 2026 15:16:26 -0700 Subject: [PATCH] Protected Audience: Ignore non-finite entries in priority vector Non-finite values are not permitted in priority vectors submitted via JSON or WebIDL, so this should not cause any loss of functionality. Fixed: 512772489 Change-Id: I66074bfccdb0a962f4f90ce01c318307e8c06f2a Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7846153 Commit-Queue: Paul Jensen <[email protected]> Auto-Submit: Paul Jensen <[email protected]> Reviewed-by: Maks Orlovich <[email protected]> Cr-Commit-Position: refs/heads/main@{#1630798} --- diff --git a/content/browser/interest_group/interest_group_auction.cc b/content/browser/interest_group/interest_group_auction.cc index 04f467f..82c9df3a 100644 --- a/content/browser/interest_group/interest_group_auction.cc +++ b/content/browser/interest_group/interest_group_auction.cc @@ -1638,16 +1638,25 @@ update_if_older_than); std::optional<double> new_priority; if (!priority_vector.empty()) { - new_priority = CalculateInterestGroupPriority( - *auction_->config_, *(state->bidder), auction_->auction_start_time_, - priority_vector, - (interest_group.priority_vector && - !interest_group.priority_vector->empty()) - ? state->calculated_priority - : std::optional<double>()); - if (*new_priority < 0) { - auction_->auction_metrics_recorder_ - ->RecordBidFilteredDuringReprioritization(); + bool valid_priority_vector = true; + for (const auto& [unused_signal_name, value] : priority_vector) { + if (!std::isfinite(value)) { + valid_priority_vector = false; + break; + } + } + if (valid_priority_vector) { + new_priority = CalculateInterestGroupPriority( + *auction_->config_, *(state->bidder), auction_->auction_start_time_, + priority_vector, + (interest_group.priority_vector && + !interest_group.priority_vector->empty()) + ? state->calculated_priority + : std::optional<double>()); + if (*new_priority < 0) { + auction_->auction_metrics_recorder_ + ->RecordBidFilteredDuringReprioritization(); + } } } OnBiddingSignalsReceivedInternal(state, new_priority,
Original Bug Report
Sandbox Escape in FLEDGE via NaN priority causing SWO violation and out-of-bounds std::sort
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: The FLEDGE Fledge bidding signals Mojo interface lacks NaN validation for priority_vector. A compromised worklet can inject NaN, which bypasses filters and violates Strict Weak Ordering in std::sort. This causes std::lower_bound to return an out-of-bounds iterator, eventually leading to crossed iterators in a second std::sort and arbitrary heap corruption via libc++’s insertion sort.
Affected files:
content/browser/interest_group/interest_group_auction.cccontent/browser/interest_group/interest_group_priority_util.cc
Estimated timestamp from git blame: 2025-04-28
Summary
A vulnerability in the FLEDGE (Protected Audience API) implementation in the browser process allows a compromised auction worklet utility process to trigger memory corruption. By providing a NaN (Not-a-Number) value in the priority_vector through the OnBiddingSignalsReceived Mojo interface, an attacker can violate the Strict Weak Ordering (SWO) requirement of std::sort.
This violation leaves the array unpartitioned, causing a subsequent binary search (std::lower_bound) to return an iterator pointing past the intended size limit. When the vector is resized and a final std::sort is called on the remaining elements, the iterators passed to std::sort are crossed (first > last). Due to a quirk in libc++, this negative length bypasses boundary checks and triggers an infinite out-of-bounds memory sweep, treating uninitialized heap memory as std::unique_ptr<BidState> and providing a powerful primitive for a Sandbox Escape.
Technical Details
(Note: The following steps trace the vulnerability theoretically based on code analysis; our tooling agent does not have the ability to run a working exploit POC.)
1. NaN Injection and Propagation
The auction_worklet::mojom::GenerateBidClient::OnBiddingSignalsReceived Mojo interface definition accepts a map<string, double> for priority_vector. Unlike the blink::mojom::InterestGroup struct (which uses C++ traits to enforce std::isfinite), this specific Mojo method lacks validation attributes.
A compromised worklet can send a value such as {"browserSignals.one": NaN}. The browser process receives this in InterestGroupAuction::BuyerHelper::OnBiddingSignalsReceived and passes it to CalculateInterestGroupPriority.
Inside CalculateInterestGroupPriority, the dot product is calculated:
caclulated_priority += signals_pair->second * priority_pair.second;
Because 1.0 * NaN = NaN, the function returns NaN.
2. Filtering Bypass
Execution proceeds to OnBiddingSignalsReceivedInternal, which attempts to filter out negative priorities:
bool bid_filtered = new_priority.has_value() && *new_priority < 0;
According to the IEEE-754 standard, NaN < 0 evaluates to false. Therefore, the bid bypasses the discard logic, and state->calculated_priority is set to NaN.
3. Strict Weak Ordering Violation
During the auction’s ApplySizeLimitAndSort() phase, std::sort is called with the BidStatesDescByPriorityAndGroupByJoinOrigin comparator. This comparator uses std::tie():
return std::tie(a->calculated_priority, a->group_by_origin_id, ...) >
std::tie(b->calculated_priority, b->group_by_origin_id, ...);
Because comparisons involving NaN (like NaN > x and x > NaN) both return false, std::tie concludes the calculated_priority fields are equivalent and falls through to comparing group_by_origin_id. This creates cycles (e.g., A > B, B > C, C > A), violating the Strict Weak Ordering (SWO) transitivity requirement of std::sort.
Because SWO is violated, std::sort silently fails to partition the array, leaving a scrambled vector where larger values can appear to the right of smaller values.
4. Erroneous Binary Search and Crossed Iterators
The code then extracts the target priority at the cut-off boundary (min_priority) and uses std::lower_bound to find the start of the lowest priority band for shuffling:
double min_priority = bid_states_[size_limit_ - 1]->calculated_priority;
auto rand_begin = std::lower_bound(bid_states_.begin(), bid_states_.end(),
min_priority, BidStatesDescByPriority());
std::lower_bound uses a binary search algorithm. Because the underlying array is not partitioned due to the previous SWO violation, the binary search takes incorrect branches. It can overshoot the target and return an iterator (rand_begin) that points strictly greater than bid_states_.begin() + size_limit_.
The vector is then resized, destroying excess elements:
bid_states_.resize(size_limit_);
A final sort is executed to restore subgroupings:
std::sort(rand_begin, bid_states_.end(),
BidStatesDescByPriorityAndGroupByJoinOrigin());
Because rand_begin was driven past the limit by the faulty binary search, rand_begin is numerically greater than bid_states_.end(). The iterators are crossed (first > last).
5. libc++ Exploitation Primitive
Inside Chromium’s libc++ implementation of std::sort, the length is calculated as __len = __last - __first. Because first > last, this yields a negative integer.
__introsort switches on the length for small array optimizations (switch (__len) { case 0: case 1: return; ... }). Negative numbers bypass this switch and fall through to a threshold check: if (__len < 24). Since a negative number is smaller than 24, libc++ mistakenly invokes __insertion_sort.
__insertion_sort utilizes a runaway loop:
for (_RandomAccessIterator __i = __first + 1; __i != __last; ++__i)
Because __first is already past __last, incrementing __i moves it infinitely away from the termination condition. The loop sweeps linearly across the browser heap.
Inside the loop, the out-of-bounds heap memory is treated as a valid std::unique_ptr<BidState>. As the algorithm attempts to shift elements (*__j = std::move(*__k)), it invokes the std::unique_ptr assignment operator, which calls reset() on the out-of-bounds memory.
If an attacker uses FLEDGE worklets to spray the browser heap adjacent to the vector prior to the auction, the reset() call extracts the attacker-controlled pointer and executes delete on it. This invokes the ~BidState() destructor on attacker-specified memory, causing cascading arbitrary frees of internal objects (like std::map, flat_set, and Mojo handles). This provides a highly controllable Use-After-Free primitive in the unsandboxed Browser process, bypassing MiraclePtr (since the unique pointers themselves are read out-of-bounds).
Suggested Fix
- Implement validation in
InterestGroupAuction::BuyerHelper::OnBiddingSignalsReceived(or ideally at the Mojo trait level if typemapping is introduced) to discardpriority_vectormaps containing non-finite values (!std::isfinite). - Alternatively, explicitly reject
NaNpriorities inOnBiddingSignalsReceivedInternalalongside the negative priority filter.
Evaluated with Chrome root at commit: eca8648a4e1cdfdda68c495a6003059fed641955
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.