Skip to content

JIT: fix start positions with an offset limit and in the prefix scan (2/3, stacked on #1004) - #1005

Open
shivneelmistry wants to merge 2 commits into
PCRE2Project:mainfrom
shivneelmistry:fix/jit-start-position
Open

shivneelmistry wants to merge 2 commits into
PCRE2Project:mainfrom
shivneelmistry:fix/jit-start-position

Conversation

@shivneelmistry

@shivneelmistry shivneelmistry commented Sep 24, 2026 •

Copy link
Copy Markdown

Stacked on #1004. Only the last commit is new; review ed406ce. I'll rebase once #1004 is merged.

This is the second of three stacked PRs from JIT vs. interpreter differential fuzzing:

  1. JIT: fix variable-length lookbehinds and (*THEN) in assertions (1/3) #1004: variable-length lookbehinds and (*THEN) in assertions
  2. This PR: JIT start positions, offset limit and prefix scan
  3. Make partial matching consistent between JIT and interpreter (3/3, stacked on #1005) #1006: partial matching consistency

Both bugs let the JIT try matches at the wrong start positions. Both give wrong results in normal (non-partial) matching.

Fixes

Offset limit bumpalong. mainloop_entry() checked STR_PTR < limit before advancing one character. In UTF mode, or when a CRLF is skipped, that advance is more than one code unit, so the next attempt could start past the limit. The limit is now checked after the advance, in the same place for all paths. On failure it jumps to the normal "no more start positions" exit, which keeps a soft partial match found earlier.

Pattern Subject Interpreter JIT (before)
/\b/utf,use_offset_limit \x{300}\x{4e2d}\x{1f600}1\=offset_limit=6 no match match at 9
/(?=X)/newline=crlf,use_offset_limit,no_start_optimize \r\nX\=offset_limit=1 no match match at 2

Prefix scan with a stale repeat count. In scan_prefix(), OP_TYPEEXACT sets repeat before the type is examined. If the type is not supported (for example \R, \X, \h), the branch is abandoned, but repeat was not reset before the next alternative was taken off the stack. The next alternative's first character was then repeated into later positions of the prefix, and fast-forwarding skipped real matches. For a class this also tripped SLJIT_ASSERT(last == TRUE && repeat == 1) in debug builds.

Pattern Subject Interpreter JIT (before)
/(?:xyzw\R{4}|abc)/ abc abc no match
/(?:wxyz\h{2}|abcd)/ abcd abcd no match
/(Z|[ab])\R{4}/ any debug build asserts

Tests

  • 5 new testinput2 cases. The unpatched JIT gets 3 of them wrong; the other two guard the debug assertion and a \X variant.
  • testinput10 (8-bit) and testinput12 (16/32-bit) get the UTF offset-limit cases. Code-unit offsets differ by width, so the expected results differ too.
  • 3 new rows in pcre2_jit_test.c.
  • Every expected-output file only gains lines.

The whole stack was validated as described in #1004.

🤖 Generated with Claude Code

shivneelmistry and others added 2 commits September 23, 2026 23:45
Four fixes found by differential testing of the JIT against the
interpreter:

- JIT: a variable-length lookbehind no longer moves STR_END to the
  lookbehind point. Assertions inside the lookbehind (\b, \B, $,
  lookaheads) now see the real end of the subject, and each branch is
  checked to end exactly at the lookbehind point instead. This makes the
  STR_END restore added for OP_ASSERTBACK_NA backtracking in PCRE2Project#912
  unnecessary; its test still passes.
  Example: /y(?<!.{1,2}\b)b/ on "eyb" matched with the interpreter only.

- JIT: (*THEN) in a branch of a variable-length lookbehind retried the
  next start position instead of moving to the next alternative.
  Example: /(?<=a?(*THEN)(?<=e)|x)B/ on "eBx" matched with JIT only.

- JIT: (*THEN) in a standalone positive assertion nested inside a
  negative assertion escaped to the outer assertion, contrary to
  pcre2pattern ("The effect of (*THEN) is not allowed to escape beyond
  an assertion").
  Example: /(?!(?=(*THEN)(*F))?)/ on "x" matched with JIT only.

- Compile: a conditional group without a "no" branch inside a
  lookbehind was given the length of its "yes" branch only, so the
  lookbehind was treated as fixed length. Both matchers could miss
  matches, and the JIT could return a match that ends before it starts.
  Example: /(*naplb:(?(?=x)a))/ at offset 2 of "ae" returned (2,1)
  from the JIT.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Two JIT bugs where a match attempt started at the wrong place:

- With PCRE2_USE_OFFSET_LIMIT, the bumpalong loop checked the limit
  before advancing, so a multi-unit UTF character or a skipped CRLF
  could move the next attempt past the limit.
  Example: /\b/utf,use_offset_limit on "\x{300}\x{4e2d}\x{1f600}1" with
  offset_limit=6 matched at offset 9 with JIT only.

- scan_prefix() kept the repeat count from an OP_TYPEEXACT whose type it
  could not handle and applied it to the next alternative. The computed
  prefix was then wrong and fast-forwarding skipped real matches (and it
  tripped an SLJIT_ASSERT in debug builds).
  Example: /(?:xyzw\R{4}|abc)/ did not match "abc" with JIT.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@NWilson

NWilson commented Sep 24, 2026

Copy link
Copy Markdown
Member

I can't really contribute anything to the review of this one.

I do however have infinite free Astra tokens, so I can throw some compute at it.

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.

2 participants