c4d17d27b579dbf5450082dbdc32a9f6c2e68846 Check the input type correctness for CSS abs() function
Triage note: CSS math-expression robustness fix (ClusterFuzz-reported, bug 475197564) reachable from web content. Impact from diff is invalid-state/crash, not proven corruption. watch.
The bug at a glance
This is a type-correctness hardening fix in the CSS math-expression engine. Before the patch, the CSS abs() function did not reject an operand whose resolved type is an ‘intermediate result’ – a type that arises inside multiplicative combinations of incompatible unit categories (e.g. length multiplied by percentage) and that is not a legal standalone value type. Accepting such an operand let a malformed expression like calc(100% + abs(1vmin * 10%)) build a math-expression node it should have refused, meaning downstream length/percentage resolution operates on a value with an ill-defined type. There is no evidence in the diff of a memory-safety consequence: the change gates node construction (returns nullptr, causing normal parse failure) rather than fixing an OOB or UAF, and the accompanying WPT test asserts the value is simply invalid. The realistic impact is spec-noncompliant parsing and potential wrong-type propagation into later math simplification/resolution, which is the sort of latent invariant break that can seed a more serious bug elsewhere but is not itself shown to be exploitable. That places it honestly at watch-tier low. cvss is null: the vendor 5.3 overstates a parser type-validation fix with no demonstrated CIA impact on shipping builds.
CSS math functions (calc/min/max/clamp/abs/sign/round/mod/rem, etc.) form a recursively-composed type system where every node must carry a well-defined CSSMathType, and the engine repeatedly assumes operands are of a resolvable, non-intermediate type. abs()/sign() were relatively recent additions and the type-checking coverage for their operands lagged the arithmetic operators. The angle is invariant coverage drift: sign() and the multiplicative operators already had guards for these degenerate types (the test file already contained 1px * sign(…) invalid cases), but abs() had a narrower check (only IsCalcSize) and let IsIntermediateResult operands through. Any place that assumes ‘if a node was constructed, its type is valid’ is a candidate for confusion when one constructor is more permissive than its siblings.
Root cause
In CSSMathExpressionNode construction for the abs()/sign() family, the code fetched the single operand and rejected it only if operand->IsCalcSize() (a calc-size() value, illegal inside these functions). It did not check whether the operand’s resolved math type is an ‘intermediate result’. An intermediate-result type is produced when incompatible dimension categories are multiplied – for example ‘1vmin * 10%’ multiplies a length by a percentage, yielding a type that has no legal standalone interpretation (it is only meaningful as a transient factor inside a larger product, and even then constrained). Feeding that into abs() – abs(1vmin * 10%) – and then adding it to a length – calc(100% + abs(1vmin * 10%)) – previously succeeded in building a node, so the malformed expression was accepted as a valid . The fix augments the guard: it now also computes CSSMathType(*operand) and, if IsIntermediateResult() is true, returns nullptr. Returning nullptr from the builder aborts node construction, which propagates up as a normal CSS parse failure so the declaration is dropped as invalid – matching the spec and the sibling sign()/multiplication checks.
Key code
abs()/sign() operand check extended to also reject intermediate-result-typed operands.
CSSValueID function_id) {
const CSSMathExpressionNode* operand = operands.front();
- if (operand->IsCalcSize()) {
+ if (operand->IsCalcSize() || CSSMathType(*operand).IsIntermediateResult()) {
return nullptr;
}
Patch walkthrough
third_party/blink/renderer/core/css/css_math_expression_node.cc (abs()/sign() operand validation)— The guard that previously rejected only calc-size() operands is extended with ‘|| CSSMathType(*operand).IsIntermediateResult()’. CSSMathType(*operand) computes the operand’s resolved unit/type category; IsIntermediateResult() is true for the ill-defined types produced by multiplying incompatible dimensions. When either condition holds, the builder returns nullptr, which makes the enclosing math function fail to construct and the whole declaration parse as invalid. This aligns abs()’s strictness with the arithmetic operators and sign(), restoring the invariant that every constructed abs() node has a legal, resolvable operand type.third_party/blink/web_tests/external/wpt/css/css-values/signs-abs-invalid.html— Adds one regression assertion: test_invalid_length(‘calc(100% + abs(1vmin * 10%))’). This sits alongside pre-existing invalid cases for sign() and multiplication (1px * sign(10%), 1px * sign(1em + 10%), etc.), confirming the intent is parity of type-validation across the math functions. The test asserts the value is rejected as an invalid , i.e. the fixed code returns nullptr and the property is not applied.
Background
CSS math functions and calc() type checking — calc() and its relatives (min/max/clamp/abs/sign/round/mod/rem) are parsed into a tree of CSSMathExpressionNode objects. Each node has a computed type derived from CSS Values 4 type-algebra rules: addition/subtraction require matching types, multiplication combines types, and division by a typed value is constrained. A malformed combination must be rejected at parse time.
CSSMathType / IsIntermediateResult() — CSSMathType encapsulates the resolved dimensional type of a math node. An ‘intermediate result’ type is one that only validly exists as a transient factor within a product of unit categories (e.g. the length*percentage product) and is not a legal standalone , , etc. IsIntermediateResult() flags such types so callers can refuse them where a concrete resolvable type is required.
abs() / sign() functions — CSS Values 4 sign functions: abs(A) returns the absolute value of A preserving A’s type; sign(A) returns -1/0/1 as a . Their operand must itself be a valid, type-resolvable numeric expression; an intermediate-result-typed operand has no defined abs() semantics and must be rejected.
IsCalcSize() — Marks a calc-size() value (used for interpolable intrinsic sizing). calc-size() is not permitted as an operand of the math functions, hence the pre-existing reject; the fix extends the same reject list to intermediate-result types.
Vulnerability window
- latent introduction — When abs()/sign() operand validation was written it guarded only IsCalcSize(), while the arithmetic operators and sign() already handled intermediate-result operands – leaving abs() as the permissive outlier.
- discovery — Tracked as crbug 475197564 (Fixed: tag). The precise malformed expression (calc(100% + abs(1vmin * 10%))) is characteristic of a spec-compliance/fuzz finding in the CSS values parser.
- fix — Change-Id I4c9432c…, authored by Daniil Sakhapov, reviewed by Rune Lillesveen, landed 2026-01-16 at r1570330 with a one-line WPT regression assertion.
Proof of concept
The commit shipped a WPT invalidity assertion, not an exploit. The trigger is declarative: parse a length property whose value is calc(100% + abs(1vmin * 10%)) (or any abs( * ) intermediate-result operand added to a length). Pre-fix, Blink constructed a math node and accepted the value; post-fix it returns nullptr and the value is invalid. The reconstructed recipe below is the harness assertion, used to verify rejection – it is a correctness check, not a memory-safety primitive. No memory corruption is demonstrated or implied by the diff, so no exploit code exists or is invented.
// From wpt css-values/signs-abs-invalid.html
test_invalid_length('calc(100% + abs(1vmin * 10%))');
// Manual check: this value must not parse as a valid <length>.
// const el = document.createElement('div');
// el.style.width = 'calc(100% + abs(1vmin * 10%))';
// console.assert(el.style.width === '', 'abs() must reject intermediate-result operand');
Exploitation
- trigger — Include a CSS declaration (inline, stylesheet, or via CSSOM) whose value is abs() applied to an intermediate-result-typed operand added to a concrete dimension, e.g. calc(100% + abs(1vmin * 10%)).
- impact — Pre-fix the value was accepted with an ill-defined type and would propagate into length/percentage resolution and math simplification; the diff shows only a parse-acceptance defect with no proven downstream memory-safety effect. Post-fix the value is cleanly rejected. Not shown to be exploitable for code execution.
Detection & hunting
For defenders and detection engineers:
- Acceptance of spec-invalid math expressions — Differential parsing checks: CSS values using abs()/sign() over multiplicative combinations of incompatible unit categories that resolve without error indicate the pre-fix behavior.
- CSSMathType assertions in math simplification — Watch for DCHECKs or unexpected type outcomes in css_math_expression_node.cc during simplification/resolution of abs()/sign() nodes, which would surface a leaked intermediate-result operand.
Audit directions
- Other math-function builders — Audit the operand validation for the whole math family in css_math_expression_node.cc (min/max/clamp/round/mod/rem/hypot/exp/log/pow/sqrt and sign) to confirm each rejects IsIntermediateResult and IsCalcSize operands consistently; abs() was the outlier, others may be too.
- Intermediate-result type propagation — Trace where IsIntermediateResult()-typed nodes can escape into resolution/interpolation (e.g. animation between calc values, computed-style serialization) and verify each consumer either handles or rejects them; a leaked intermediate type reaching sizing/indexing logic would raise severity.
- Type-algebra unit tests — Extend WPT/unit coverage for multiplicative type combinations feeding every math function, since the gap here was uneven validation coverage rather than a single wrong line.