Skip to content

fix(jit): conditionally apply match/fullmatch anchoring workaround#97

Merged
Qubitium merged 3 commits into
mainfrom
devin/jit-anchor-runtime-detection
Jul 27, 2026
Merged

fix(jit): conditionally apply match/fullmatch anchoring workaround#97
Qubitium merged 3 commits into
mainfrom
devin/jit-anchor-runtime-detection

Conversation

@Qubitium

Copy link
Copy Markdown
Contributor

Summary

Some PCRE2 builds (e.g. the system libpcre2-8 on this Ubuntu host, 10.39, and the upstream-reported 10.46) ignore PCRE2_ANCHORED/PCRE2_ENDANCHORED when passed as match-time options to pcre2_jit_match(). That made Pattern.match() and Pattern.fullmatch() report wrong-position matches or matches that did not reach the required end.

Instead of always paying for the post-JIT ovector check and possible pcre2_match() re-run, we now probe the linked library once during PyInit_pcre_ext_c() and only enable the workaround when the probe detects a non-compliant JIT:

  • jit_anchor_fixup_needed() compiles two tiny probe patterns, enables JIT, and calls pcre2_jit_match() with PCRE2_ANCHORED/PCRE2_ENDANCHORED to verify the returned ovector respects the flags.
  • The result is cached in an atomic int (jit_anchor_fixup_needed_state).
  • Pattern_execute() applies the ovector verification + interpreter re-run only when jit_anchor_fixup_needed() returns 1.
  • A private pcre_ext_c._jit_anchor_fixup_needed() is exposed so tests/debugging can see whether the workaround is active.

The regression tests from the fork (tests/test_jit_anchoring.py) are included; they pass with and without the workaround active.

Verification

Local build against libpcre2-8 10.39:

  • python setup.py build_ext --inplace succeeds.
  • pcre_ext_c._jit_anchor_fixup_needed() returns 1 for this library, confirming the probe flags the non-compliant JIT.
  • python -m pytest tests/test_jit_anchoring.py -v passes (16 tests).
  • Targeted core suite also passes: test_basic, test_bytes, test_core, test_jit, test_utf8, test_pattern, test_module, test_re_compat, test_errors, test_flags, test_api_parity, test_version, test_memory (199 passed, 10 skipped).

Link to Devin session: https://app.devin.ai/sessions/3cf75f5f1108416eb288c8dcc282c0e4
Requested by: @Qubitium

Some PCRE2 builds (e.g. libpcre2-8 10.46, and also 10.39 here) ignore
PCRE2_ANCHORED/PCRE2_ENDANCHORED when passed as match-time options to
pcre2_jit_match(). Rather than always paying for the post-JIT ovector
check and possible pcre2_match() re-run, probe the linked library once at
module load and only enable the workaround when the probe detects a
non-compliant JIT.

- Add jit_anchor_fixup_needed() which compiles two tiny probe patterns,
  enables JIT, and calls pcre2_jit_match() with ANCHORED/ENDANCHORED to
  see whether the returned ovector respects the flags.
- Cache the result in an atomic int set during PyInit_pcre_ext_c().
- Apply the ovector verification + interpreter re-run in Pattern_execute()
  only when the probe returns non-compliant.
- Expose _jit_anchor_fixup_needed() for tests/debugging.
- Add tests/test_jit_anchoring.py from the upstream fork to ensure
  match()/fullmatch() remain correct with and without the fix active.
@Qubitium Qubitium self-assigned this Jul 27, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

Open in Devin Review

Comment thread pcre_ext/pcre2.c Outdated
Comment on lines 1789 to 1798
} else if (mode == EXEC_MODE_FULLMATCH && jit_ovector[1] != offset_limit) {
jit_endanchor_uncertain = 1;
}
}
}

if (!pattern_jit_get(self)) {
if (!pattern_jit_get(self) || jit_endanchor_uncertain) {
PCRE2_CALL_MAYBE_RELEASE_GIL(pcre2_match(self->code,
(PCRE2_SPTR)buffer,
exec_length,

@devin-ai-integration devin-ai-integration Bot Jul 27, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Fullmatch end-anchor check relies on offset_limit equaling the subject end used by pcre2_match

The new FULLMATCH workaround compares jit_ovector[1] != offset_limit and, when unequal, re-runs pcre2_match() with PCRE2_ANCHORED | PCRE2_ENDANCHORED (pcre_ext/pcre2.c:1789-1803). This is correct in the truncation path (offset-limit-option disabled) because exec_length is set to offset_limit (pcre_ext/pcre2.c:1725-1730), so PCRE2_ENDANCHORED anchors exactly at offset_limit. However, when offset_limit_option_enabled() is true (PCRE2_USE_OFFSET_LIMIT path, pcre_ext/pcre2.c:1706-1723), exec_length remains the full subject length while offset_limit is byte_end (endpos). PCRE2's offset limit constrains the match start, and PCRE2_ENDANCHORED anchors to the end of the whole subject (exec_length), not to offset_limit. In that path the endpos-fullmatch semantics can be subtly wrong (both for the compliant-JIT ovector[1] comparison and for the fallback pcre2_match re-run). This is a pre-existing concern shared with the non-JIT path (!pattern_jit_get) rather than something introduced by this PR — behavior stays consistent with the interpreter path — but it is worth confirming that endpos-fullmatch with the offset-limit option active is actually exercised by the tests (they appear to use the truncation path).

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review. I checked this and it appears to work as intended because the match context has PCRE2_USE_OFFSET_LIMIT set with offset_limit whenever use_offset_limit_option is active. On the 10.39 system lib I verified:

p.fullmatch(b"abX", endpos=2)  # -> Match span=(0, 2)
p.fullmatch(b"2025-10-08X", endpos=10)  # -> Match span=(0, 10)
p.fullmatch(b"2025-10-08XX", endpos=11)  # -> None

I also added regression tests in tests/test_jit_anchoring.py covering fullmatch with endpos and trailing content, with and without JIT, and they pass locally and in CI. If there is a specific PCRE2 build where PCRE2_USE_OFFSET_LIMIT does not make PCRE2_ENDANCHORED respect offset_limit, the new tests will catch it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The concern about the PCRE2_USE_OFFSET_LIMIT re-run path is addressed in commit 21f563f (pushed after the review snapshot af76399):

  • The pcre2_match() re-run now uses interpreter_length = offset_limit whenever jit_endanchor_uncertain is set, regardless of whether PCRE2_USE_OFFSET_LIMIT is active. This makes PCRE2_ENDANCHORED anchor at the requested endpos boundary even on builds where PCRE2_USE_OFFSET_LIMIT is interpreted as only a start constraint.
  • The new TestEndposWithTrailingContent tests exercise fullmatch(..., endpos=N) with trailing content under both JIT and no-JIT, and CI is green. On the tested 10.39 runtime offset_limit_option_enabled() is true, so these tests do cover the PCRE2_USE_OFFSET_LIMIT path as well as the truncation path.

If there is a specific PCRE2 build where the PCRE2_USE_OFFSET_LIMIT re-run still misbehaves, the added tests will catch it.

Qubitium added 2 commits July 27, 2026 04:06
…ndpos fallback

When the JIT anchoring workaround needs to re-run fullmatch through
pcre2_match(), pass only the requested endpos prefix as the subject
length. This makes PCRE2_ENDANCHORED anchor at the intended boundary
regardless of whether the active PCRE2 build treats PCRE2_USE_OFFSET_LIMIT
as influencing end-anchored matches.

Adds endpos regression tests for match/fullmatch/search/finditer with
trailing content, pos+endpos, and multibyte text.
@devin-ai-integration

Copy link
Copy Markdown
Contributor

Pushed a fix and expanded coverage for this:

  • Pattern_execute() now truncates the pcre2_match() re-run to offset_limit (endpos) when jit_endanchor_uncertain is set, so PCRE2_ENDANCHORED is guaranteed to anchor at the requested boundary even if PCRE2_USE_OFFSET_LIMIT does not influence end-anchored matches on a given PCRE2 build.
  • Added TestEndposWithTrailingContent covering match/fullmatch/search/finditer with endpos and trailing content, pos+endpos, .*/alternation patterns, and multibyte UTF-8 text — all parametrized over JIT and no-JIT.

tests/test_jit_anchoring.py now has 40 tests and passes locally. The unrelated test_threads.py::test_ensure_thread_pool_resizes_existing_pool failure is pre-existing on this environment.

@Qubitium
Qubitium merged commit d28143d into main Jul 27, 2026
19 checks passed
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