Skip to content

Fix reading past the end of unterminated block strings - #1989

Draft
spawnia wants to merge 2 commits into
masterfrom
fix-unterminated-block-string-read
Draft

spawnia wants to merge 2 commits into
masterfrom
fix-unterminated-block-string-read

Conversation

@spawnia

@spawnia spawnia commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

Unterminated block strings ending in a quote and a multibyte character make the Lexer read past the document.
"""a"é raises Uninitialized string offset 7 instead of a SyntaxError.
The triple-quote lookahead in readBlockString() now compares the next three bytes instead of decoding byte by byte.

The old lookahead could land inside a multibyte character

After a quote, it moved the cursor one byte, decoded a character, and moved one byte again.
When the next character was multibyte, the second step started on a continuation byte.
readChar() read that byte as the lead byte of a 2-byte character.
Backslashes followed by a multibyte character hit the same path.

Results are unchanged apart from the crash, and block strings lex faster

50,000 random block strings of quotes, backslashes, \""", newlines and multibyte characters lex the same on master and on this branch.
The only differences are the 1,218 inputs where master read out of bounds.

The lookahead decodes three fewer characters per character.
A document with 20,000 block string arguments parses in 0.98s instead of 1.76s.

Lexer tests now fail on PHP warnings

phpunit.xml.dist does not fail on warnings, so the new cases passed even before the fix.
LexerTest::lexOne() turns warnings into exceptions while lexing.

🤖 Generated with Claude Code

The triple-quote lookahead in readBlockString() moved one byte at a time
and decoded at each step, so after a quote it could land inside a
multibyte character and read past the end of the body.
Comparing the next three bytes with substr() avoids decoding there.

LexerTest::lexOne() now turns PHP warnings into exceptions,
so out-of-bounds reads fail the tests instead of passing with a warning.

🤖 Generated with Claude Code
🤖 Generated with Claude Code

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant