Work around PCRE2 10.46/10.47 JIT start-optimization losing matches - #111
Merged
Conversation
The JIT fast-forward (start-of-match optimization) in PCRE2 10.46 and
10.47 skips valid start positions for patterns that begin with an
optional atom followed by a literal and a bounded class:
pcre2_jit_match finds nothing for 0?a[b ]{0,2} on " ab " while
pcre2_match returns (1, 4). Verified against raw libpcre2 builds of the
10.46 and 10.47 releases (both affected) and current upstream main
(10.48-dev, fixed). Since JIT is enabled by default, affected patterns
silently returned wrong results on every currently shipping PCRE2.
Following the jit_anchor_fixup_needed() precedent, the runtime is probed
once at module init (jit_start_optimize_broken): compile the probe
pattern, run pcre2_jit_match vs pcre2_match(PCRE2_NO_JIT) on the same
code, and compare. On a broken runtime, Pattern_create injects
PCRE2_NO_START_OPTIMIZE into the compile options of JIT-bound patterns
— an option bit available on every PCRE2 version, so no new symbol
dependency — and masks it back out of the pattern's public flags (the
bit shares a value with Flag.SUBSTITUTE_MATCHED on the Python side; a
caller-requested NO_START_OPTIMIZE is preserved). Fixed runtimes probe
clean and compile exactly as before.
Validation on the affected 10.46 system runtime: the verifying clobber
suite (PR #110) ran 3.55M differential cases with
PYPCRE_CLOBBER_STRICT_JIT=1 (JIT-vs-interpreter divergence = failure)
and found ZERO divergences, versus roughly ten per minute before the
workaround. New tests/test_jit_start_optimize_workaround.py pins the
known repro family (must pass on both broken and fixed runtimes) and
asserts the injected bit does not leak into Pattern.flags. Full pytest
suite green on CPython 3.14.7 free-threaded (GIL=0) and GIL builds.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Summary
Works around the PCRE2 10.46/10.47 JIT start-optimization bug found by the verifying clobber suite (#110): the JIT fast-forward scan skips valid start positions for patterns beginning with an optional atom + literal + bounded class —
pcre2_jit_matchfinds nothing for0?a[b ]{0,2}on" ab "whilepcre2_matchreturns(1, 4). Since pypcre enables JIT by default, affected patterns silently returned wrong results.Affected-version matrix (verified with raw
libpcre2-8builds via ctypes, pypcre not involved):main(10.48-dev, built with its pinned sljit)So every currently shipping release is affected; only unreleased main is fixed.
The fix
Follows the existing
jit_anchor_fixup_needed()precedent exactly:jit_start_optimize_broken(), run eagerly at module init): compile0?a[b ]{0,2}, runpcre2_jit_matchvspcre2_match(PCRE2_NO_JIT)on the same compiled code against" ab ", compare results._jit_start_optimize_broken()is exposed for introspection/tests.Pattern_createinjectsPCRE2_NO_START_OPTIMIZEinto the compile options of JIT-bound patterns. The option bit exists on every PCRE2 version (unlikepcre2_set_optimize, added in 10.43), so there is no new symbol dependency and old runtimes still import.Pattern.flags(it shares the value0x10000withFlag.SUBSTITUTE_MATCHEDon the Python side); an explicitly caller-requestedNO_START_OPTIMIZEis preserved as before.Perf note: on broken runtimes, JIT-bound patterns lose PCRE2's first-code-unit fast-forward (pypcre's own literal prescan is also disabled for them since
PCRE2_INFO_FIRSTCODEUNITis no longer populated). Correctness over speed, and only where the runtime is broken.Validation
PYPCRE_CLOBBER_STRICT_JIT=1(any JIT-vs-interpreter divergence fails the test): 3,549,787 differential cases, zero divergences — previously ~10 divergences per minute. The workaround covers the whole observed bug family, not just the probe pattern.tests/test_jit_start_optimize_workaround.py: pins six known-diverging pattern/subject pairs againstreunder default (JIT) compilation — these assertions hold on both broken runtimes (via the workaround) and fixed runtimes (natively) — and asserts the flags contract.Worth reporting upstream to PCRE2Project alongside the two still-unfixed engine behaviors documented in #110 (auto-possessification changing results around explicit possessives;
{0,0}zero-repetition alternation groups matching nothing) — bug 1 needs a backport request since the fix only exists on main.🤖 Generated with Claude Code