Repository navigation
Take the review of #508: depth scan and lexer cleanups - #510
Merged
Merged
Conversation
The review of #508 asked for three things in skipIfNestedTooDeeply, none of them a change in behaviour. The mark was set inside the if condition, which reads as a typo for ==; give the flag a name and set it on its own line. The array doubled without bound, which would wrap past a billion brackets; saturate at the largest array size, which no input reaches since the lexer's token tables fill memory first. And say in the javadoc that, after a semicolon inside a function, the scan counts functions the value parse never reads, so it can drop a value the parse would have kept: it errs towards dropping, and only on a value no browser keeps. The churn timing test guarded only a gross regression: the BitSet it was written against passed it in a second. At three million repeats the BitSet took 27 s and the array 0.8 s, so run it at that size with a 10 s budget, which tells them apart with a margin on both sides. 500,000 random inputs and 27 realistic values give the same output as before; the fuzz still never sees more than sixteen functions deep. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The count of open brackets per close character, added in #508 to drop an unmatched close without walking the stack, was an int array indexed by the character, 126 ints zeroed for every lexer. Index it by bracket kind instead, three ints. The orphan check inside the walk that the count made unreachable is gone; the count says a partner is on the stack, so the walk ends at it. Normalized output is unchanged, and the unmatched-close shapes stay linear. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The javadoc added in cf9567f said the scan's over-count, after a semicolon inside a function, drops only values no browser keeps. Not so: CSS if() separates its branches with semicolons, so "margin: 1px if(media(print): 2px; else: calc(...))" is valid CSS. The scan drops it whole once the else branch nests sixteen calc() deep, where the parse before #508 kept "margin:1px" at any depth. Found by Sol-6's review. Say that it can lose valid CSS, that the loss is conservative, and that only a value nested past the limit is affected. No code change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three points from the review of this PR. de57cff removed the bound in CssTokens.Lexer.closeBracket's partner walk, since the per-kind count makes it unreachable. But it cost nothing, and without it any future drift of the count ahead of the stack reads open[-1] and throws ArrayIndexOutOfBoundsException out of sanitize(). Keep it, and say it is a second line of defence. cf9567f capped the depth scan's array growth "never past what an array can hold". It did not: at the cap Arrays.copyOf returns the same length and the next store throws, and the lexer's own tables, several ints per bracket, give out long before depth gets there anyway. Go back to plain doubling with a comment saying why that is enough. The local declared in the case group now has braces, so it is not in scope in the cases after it. And the review asked for a hostile test of the count itself. CssFuzzerTest.testBracketPairingMatchesAStackWalk compares the lexer's pairing with a plain stack walk on random bracket soup, function openers included. Checked against mutations: a count never incremented, or one that falls behind, fails it on output; one that runs ahead with the bound removed throws ArrayIndexOutOfBoundsException in it; with the bound kept, the same drift is harmless. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cf9567f ran the churn timing test at three million repeats with a 10 s budget, so that it would fail on the BitSet it was written against. The review measured what that costs: the input needs about a gigabyte of heap, and runs out of memory at 512 and 768 MB. Surefire sets no heap size, so the default of a quarter of RAM fails it on any machine or container with less than about 4 GB. With the heap tight, the array took two seconds rather than the "under a second" the comment claimed, and a machine three times faster would pass the BitSet inside 10 s anyway. Go back to 500,000 repeats, which fits in 192 MB, under the suite's usual 20 s budget. Say plainly what that catches, bookkeeping whose work per close grows with the depth, and what it does not: the BitSet, whose cost per clear is a sixty-fourth of that. The method javadoc no longer lists the BitSet among the shapes the test tells apart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Member
Author
|
Review follow-up: the code-review pass on this PR and Sol-6's review. Every finding was checked against
Verification after the follow-up: |
This was referenced Sep 25, 2026
|
🎉 This issue has been resolved in |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #508, from the review passes on that PR and on this one. No behaviour change.
CssGrammar
ifcondition that read as a typo for==. The case body has braces, so the local is not in scope in the later cases.if()separates its branches with semicolons (Sol-6's review). The loss is conservative, and only a value nested past the limit is affected.CssTokens
Tests
CssFuzzerTest.testBracketPairingMatchesAStackWalkcompares the lexer's pairing with a plain stack walk on random bracket soup. Checked against mutations: a count that is never incremented, or falls behind, fails it; one that runs ahead with the bound removed throwsArrayIndexOutOfBoundsException; with the bound kept, that drift is harmless.Verification
./mvnw clean verifypasses on JDK 11, 17, 21 and 25 (711 library tests, 7 in the JPMS consumer check).-Xmx192m.main. Lexer normalization of orphaned and mismatched closes is byte-identical. The depth fuzz never sees more than sixteen functions deep, and hostile shapes stay linear.🤖 Generated with Claude Code