Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in InterestGroups
DescriptionInsufficient validation of untrusted input in InterestGroups
ComponentInterestGroups
Bug ClassLogic Error
Tracker512772489
Fix commit0da3fa38b0da (chromium/src) +19/-10
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
content/browser/interest_group/interest_group_auction.cc
modified
for
content/browser/interest_group/interest_group_auction.cc
modified

Files Changed

  • content/browser/interest_group/interest_group_auction.cc
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,
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • content/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

  1. Implement validation in InterestGroupAuction::BuyerHelper::OnBiddingSignalsReceived (or ideally at the Mojo trait level if typemapping is introduced) to discard priority_vector maps containing non-finite values (!std::isfinite).
  2. Alternatively, explicitly reject NaN priorities in OnBiddingSignalsReceivedInternal alongside 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.

View on issue tracker