← WebKit Silent-Fix Report — 2026-W25

44ba2b5c11  Script-triggerable crash inserting an @namespace rule after an @import rule via CSSOM

severity medium class OOB confidence 0.70 WebCore CSSOM exploitable-grade
Chris Dumez Sun Jun 21 22:06:04 2026 -0700 full: 44ba2b5c111ec26ba583c657cb32bfc9f7f01084 bug report ↗ view on GitHub ↗
Primitive: rule-list corruption from index mismatch
Triage note: Inserting a namespace rule at a global index while the engine indexes into a separate namespace vector corrupts the rule list; index-mismatch memory bug.
Contents

The bug at a glance

This is a script-triggerable process abort in WebCore’s CSSOM rule-insertion path: inserting an @namespace rule at a global index that follows one or more @import rules passed an out-of-bounds position into Vector::insert on m_namespaceRules. libc++ hardening (_LIBCPP_HARDENING_MODE_EXTENSIVE) catches the bad subspan and aborts before any write, so on hardened builds this is a reliable denial-of-service rather than memory corruption. On builds without that hardening the same out-of-bounds insert position would be a genuine OOB write into the namespace-rule vector, which is why it is treated as an OOB memory bug.

StyleSheetContents::wrapperInsertRule translates the caller’s global rule index into a vector-local childVectorIndex by subtracting the sizes of m_layerRulesBeforeImportRules and m_importRules, and every insertion branch uses childVectorIndex – except the @namespace branch, which inserted at the original, unadjusted index. With prior @import rules present the global index exceeds the namespace-vector-local index, so index can exceed m_namespaceRules.size(). The fix inserts at childVectorIndex, matching every other branch.

Root cause

CSSStyleSheet::insertRule() validates only that the caller-supplied index is not greater than length(), then forwards it to StyleSheetContents::wrapperInsertRule(rule, index). WebKit stores a stylesheet’s rules across several separate vectors – m_layerRulesBeforeImportRules, m_importRules, m_namespaceRules, and m_childRules – rather than one flat list, because CSS requires @layer-before-import, @import, and @namespace prologue rules to precede ordinary rules in a fixed order. To place a rule, wrapperInsertRule converts the global index into a vector-local index (childVectorIndex) by successively subtracting the sizes of the earlier vectors as it walks past each rule category.

Every branch of the function then inserts into its target vector using childVectorIndex – the @import branch, the layer branch, and the child-rules branch. The @namespace branch, however, called m_namespaceRules.insert(index, *namespaceRule) using the original, unadjusted global index. As long as no @import (or layer-before-import) rules preceded the namespace insertion point, index and childVectorIndex coincided and the bug was masked. But when the sheet already contains @import rules, the global index is strictly greater than the namespace-vector-local index by the number of preceding import/layer rules, so index can exceed m_namespaceRules.size().

The commit gives the minimal case: for a sheet containing @import url(x);, calling insertRule(’@namespace foo url(y);’, 1) reaches the @namespace branch with childVectorIndex == 0 (there are no existing namespace rules) but executes m_namespaceRules.insert(1, …) on an empty vector. Vector::insert(position, …) computes mutableSpan().subspan(position); with position past the vector’s size, that subspan is out of bounds. Under _LIBCPP_HARDENING_MODE_EXTENSIVE libc++ detects the invalid subspan and aborts the process before any element is written – a clean crash. Without extensive hardening, the same out-of-range position would drive an out-of-bounds write shifting/placing the namespace rule beyond the vector’s storage, corrupting the rule list.

The branch is guarded by the check that both m_childRules and m_layerRulesBeforeImportRules are empty (returning false otherwise), so the reachable overshoot comes specifically from preceding @import rules. The fix replaces index with childVectorIndex in the single insert call, so the @namespace branch now matches the adjusted-index convention used by every other branch and by wrapperDeleteRule, placing the rule at the correct namespace-vector-local position (0 in the example) while insertRule still reports the requested global index.

Key code

@namespace insertion uses the adjusted vector-local index (Source/WebCore/css/StyleSheetContents.cpp)

        if (!m_childRules.isEmpty() || !m_layerRulesBeforeImportRules.isEmpty())
            return false;

        m_namespaceRules.insert(childVectorIndex, *namespaceRule);
        
        // For now to be compatible with IE and Firefox if a namespace rule with the same
        // prefix is added, it overwrites previous ones.

Patch walkthrough

  • Source/WebCore/css/StyleSheetContents.cpp — In StyleSheetContents::wrapperInsertRule, the @namespace branch’s m_namespaceRules.insert(index, *namespaceRule) is changed to m_namespaceRules.insert(childVectorIndex, *namespaceRule). childVectorIndex is the global index after subtracting the sizes of m_layerRulesBeforeImportRules and m_importRules, so the namespace rule is inserted at the correct position within its own vector instead of at an over-large global index that can exceed m_namespaceRules.size().
  • LayoutTests/imported/w3c/web-platform-tests/css/cssom/insertRule-namespace-after-import.html — Added WPT test. Case one inserts ‘@namespace foo url(…)’ at index 1 into a sheet with one @import (namespace-local index 0 vs global 1). Case two inserts at index 2 into a sheet with two @import rules (local 0 vs global 2), maximizing the gap. Both assert the rule list grows correctly, imports stay first, and the namespace rule round-trips prefix/URI – previously this crashed the process.
  • LayoutTests/imported/w3c/web-platform-tests/css/cssom/insertRule-namespace-after-import-expected.txt — Added expected output: both subtests PASS, locking in correct, non-crashing insertion after existing @import rules.

Background

StyleSheetContents rule vectors — A stylesheet’s rules are split across m_layerRulesBeforeImportRules, m_importRules, m_namespaceRules, and m_childRules to enforce the CSS-mandated prologue ordering (layers-before-import, then @import, then @namespace, then everything else). A single global rule index must therefore be mapped to the right vector and a local offset within it.

childVectorIndex — The local index computed in wrapperInsertRule by subtracting the sizes of the preceding category vectors from the caller’s global index. Every insertion branch is meant to use it; the @namespace branch’s use of the raw global index instead is the defect.

CSSStyleSheet::insertRule validation — The web-facing entry point only rejects index > length() before forwarding to wrapperInsertRule. It performs no per-vector bounds checking, so an internally mis-adjusted index reaches Vector::insert unguarded, making the bug script-reachable with a valid global index.

Vector::insert and libc++ hardening — Vector::insert(position, value) forms mutableSpan().subspan(position); a position beyond size() is an invalid subspan. Under _LIBCPP_HARDENING_MODE_EXTENSIVE libc++ detects this and aborts before writing, converting a would-be OOB write into a deterministic crash. Non-hardened builds would perform the out-of-bounds operation.

@namespace / @import ordering — CSS requires @namespace rules to follow @import rules in the sheet prologue. This is exactly the configuration that separates the global index from the namespace-local index, so a sheet with any @import is the precondition that exposes the mis-indexing.

Vulnerability window

  1. Latent defect — wrapperInsertRule’s @namespace branch inserted at the unadjusted global index; harmless only while no @import or layer-before-import rules preceded the insertion point, where global and local indices coincided.
  2. Reachability — CSSOM insertRule forwards any index <= length() straight through, so a page with an @import rule can drive the @namespace branch with a global index exceeding m_namespaceRules.size().
  3. Discovery — Filed as bug 317517 as a script-triggerable crash inserting @namespace after @import via CSSOM.
  4. Reproduction — insertRule(’@namespace foo url(y);’, 1) on a sheet containing @import url(x); calls m_namespaceRules.insert(1, …) on an empty vector, aborting under libc++ extensive hardening.
  5. Fix — Committed as 315573@main: insert at childVectorIndex like every other branch, with a WPT test covering one- and two-import sheets.

Proof of concept

The added web-platform test inserts an @namespace rule at a global index following existing @import rules. On an unpatched, libc++-hardened build the @namespace branch calls m_namespaceRules.insert(index, …) with index past the (empty) vector’s size, tripping the subspan check and aborting the process – a script-triggerable crash. On a non-hardened build the same path is an out-of-bounds write into the namespace-rule vector. The full HTML PoC is present verbatim in the diff; this is the essential trigger.

// Derived from the added WPT (insertRule-namespace-after-import.html)
// Sheet contains: @import url("support/a-green.css");
var sheet = document.getElementById("oneImport").sheet;
// global index 1 (after the @import) but namespace-vector-local index 0
sheet.insertRule('@namespace foo url("http://example.com/ns");', 1);
// Two-import variant maximizes the index gap:
var sheet2 = document.getElementById("twoImports").sheet;
sheet2.insertRule('@namespace bar url("http://example.com/ns2");', 2);

Exploitation

  1. Provide a sheet with @import — Author or dynamically create a stylesheet containing at least one @import (or layer-before-import) rule so the global index diverges from the namespace-vector-local index.
  2. Insert @namespace after it — Call sheet.insertRule(’@namespace …’, N) with N equal to the number of preceding import/layer rules, driving the @namespace branch with an over-large index.
  3. Trigger the fault — Vector::insert receives a position beyond m_namespaceRules.size(). On hardened builds libc++ aborts the process (DoS); on non-hardened builds it is an OOB write corrupting the rule list.
  4. Note — The demonstrated impact is a deterministic crash under libc++ extensive hardening (before any write); turning the non-hardened OOB write into controlled corruption is not shown and is inferred.

Detection & hunting

For defenders and SOC / detection engineers:

  • Abort in Vector::insert from wrapperInsertRule
  • CSSOM insertRule of @namespace after @import
  • ASan OOB on m_namespaceRules storage

Audit directions

  • All branches of wrapperInsertRule / wrapperDeleteRule
  • Global-to-local index conversions
  • Unvalidated indices reaching Vector::insert
  • Hardening-dependent crashes hiding OOB writes

Before / after

Loading diff…