Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
637303c
feat: merge fix/optimized-nfa-backref-perconfig onto rebased main
jbachorik Jun 27, 2026
172e164
style: spotlessApply
jbachorik Jun 27, 2026
0734fe5
test: add failing routing tests for A1+A2 group-span divergences
jbachorik Jun 28, 2026
2e76df6
feat: route A2 (group absent from alternation branch) to PIKEVM_CAPTURE
jbachorik Jun 28, 2026
4701bc7
feat: route A1 (nullable first element in group body) to PIKEVM_CAPTURE
jbachorik Jun 28, 2026
d038f9f
fix: ratchet fuzz budget 65→13 after A1+A2 PIKEVM_CAPTURE routing
jbachorik Jun 28, 2026
72fc1fa
fix: route \$-in-alternation (B3b) to PIKEVM_CAPTURE
jbachorik Jun 29, 2026
f202930
fix: route anchor-only capturing group body (B3a) to PIKEVM_CAPTURE
jbachorik Jun 29, 2026
0e4d852
fix: decline FIXED_REPETITION_BACKREF when non-empty suffix present (B6)
jbachorik Jun 29, 2026
71b1f25
fix: decline .+ in GREEDY_BACKTRACK and route to PIKEVM_CAPTURE (B4)
jbachorik Jun 29, 2026
479e7a3
fix: route variable-length alternation group (B5) to PIKEVM_CAPTURE
jbachorik Jun 29, 2026
8528c1a
feat: add per-guard routing trace to debugPattern
jbachorik Jun 29, 2026
e1ba3a9
fix: ratchet fuzz budget to 0 after B3a/B3b/B4/B5/B6
jbachorik Jun 29, 2026
48dbdce
fix: address validation divergence 3 in T5-B5
jbachorik Jun 29, 2026
61f7249
fix: address validation divergence 1 in T3-B6
jbachorik Jun 29, 2026
2249b00
fix: address validation divergence 2 in T4-B4
jbachorik Jun 29, 2026
f432793
fix: correct B3b/B4/B5/B6 routing regressions from rebased commits
jbachorik Jun 29, 2026
b7cb821
feat: add patternSkip to FuzzRunner; document extended fuzz findings
jbachorik Jun 29, 2026
e9d0871
chore: move fuzz findings doc to doc/fuzz/2026-06-29.md; enrich repro…
jbachorik Jun 29, 2026
0c81cc2
fix: extract KNOWN_FINDINGS_BUDGET_EXTENDED=43; link to doc/fuzz/2026…
jbachorik Jun 29, 2026
1872342
fix: consolidate to single KNOWN_FINDINGS_BUDGET=43; extend gate to 5…
jbachorik Jun 29, 2026
c579078
fix: gate runs window 25k-50k (skip=25k); budget=34
jbachorik Jun 29, 2026
efb797e
docs: rewrite fuzz/2026-06-29.md with canonical skip=25k findings (34…
jbachorik Jun 29, 2026
4142fd5
fix: allow skip=0 in divergenceGate via intPropNonNeg
jbachorik Jun 29, 2026
e32904c
fix: unwrap transparent groups in B4/B5 detection
jbachorik Jun 29, 2026
716d6ea
fix: preserve fallback for quantified anchor-only groups in PIKEVM route
jbachorik Jun 29, 2026
0bb9205
chore: remove duplicate comment in B6 branch
jbachorik Jun 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 196 additions & 0 deletions doc/fuzz/2026-06-29.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
# Fuzz Sweep — 2026-06-29

**Seed:** `0xC0DEFEED_DEADBEEFL` (`BASE_SEED`)
**Range:** patterns 25 001–50 000 (`skip=25_000`, `count=25_000`)
**Depth:** 3 · **Inputs/pattern:** 16 · **Input max length:** 16
**Raw findings:** 34 · **Unique minimal repros:** 29
**Baseline (0–25 000):** 0 divergences (established after B3a/B3b/B4/B5/B6 fixes)

> **Note on skip semantics:** `skip=N` advances both the pattern RNG and the input RNG by
> `N × inputsPerPattern` steps before the main loop. This is conservative relative to a real
> 50k run (which advances the input RNG by fewer steps for compile-rejected patterns), so the
> inputs for this window differ from a full 0–50k run. A one-shot 50k run produces 43 raw
> findings over this range; with skip=25 000 the canonical count is 34. Findings are fully
> reproducible given the same `(seed, skip, count, inputsPerPattern, depth)` tuple.

---

## How to reproduce

Exact reproduction (hardcoded in `divergenceGate`, `KNOWN_FINDINGS_BUDGET=34`):

```bash
cd <worktree>
./gradlew :reggie-integration-tests:test \
--tests "*.AlgorithmicFuzzTest.divergenceGate" \
--no-daemon
```

Manual equivalent with explicit parameters:

```bash
./gradlew :reggie-integration-tests:test \
--tests "*.AlgorithmicFuzzTest.divergenceGate" \
-Dreggie.fuzz.skip=25000 \
-Dreggie.fuzz.size=25000 \
-Dreggie.fuzz.maxFindings=9999 \
--no-daemon
```

Advance to the next window once this budget reaches 0 (patterns 50 001–75 000):

```bash
./gradlew :reggie-integration-tests:test \
--tests "*.AlgorithmicFuzzTest.divergenceGate" \
-Dreggie.fuzz.skip=50000 \
-Dreggie.fuzz.size=25000 \
-Dreggie.fuzz.maxFindings=9999 \
--no-daemon
```

Then document new findings in `doc/fuzz/YYYY-MM-DD.md`, update `largeSweepConfig` default skip
to 50 000, and set `KNOWN_FINDINGS_BUDGET` to the new count.

---

## Summary

29 unique minimal repros across six root-cause classes (E1–E6). All are pre-existing bugs in
native strategies — none are regressions introduced by the B3a/B3b/B4/B5/B6 routing fixes.

---

## E1 — Find-path group span overextension (2 patterns, 3 findings)

A quantified prefix is followed by a capturing group. On the `findAll()` path the group span
extends beyond the chars it should own. Same root-cause class as A1/A2; those fixes addressed
`matches()` and the first `find()`. These patterns exercise subsequent `findAll()` iterations.

| Pattern | Input | Symptom |
|---------|-------|---------|
| `[^-]{3,}([b].)` | `0acbb1` | `findAll() match 0 group 1 span differs` |
| `[^-]{3,}([b].)` | `c1cb-` | `findAll() match 0 group 1 span differs` |
| `[^-]{3,}([c].)` | `a0bc-` | `findAll() match 0 group 1 span differs` |

---

## E2 — Anchor at unusual position (4 patterns, 9 findings)

An anchor appears inside a group body, after a quantified group, or combined with alternation
and a backreference. The routing logic does not cover all these forms.

### E2a — `\A` + repeated capturing group

`\A` with `{n,}` on a capturing group. The strategy treats `\A` as handled but repeated-group
span bookkeeping conflicts with the start-anchor assertion.

| Pattern | Input | Symptom |
|---------|-------|---------|
| `\A(c){1,}` | `ac` | `find() boolean differs` |
| `\A(c){1,}` | `ac` | `findAll() count differs` |
| `\A(c){1,}` | `0c` | `find() boolean differs` |

### E2b — Quantified charset + `\Z`, no capturing group

The outer match span (group 0) is wrong — a match-boundary error, not a capture-tracking issue.
`hasStringEndAnchorInAlternation` does not apply (no alternation); a separate guard for `\Z`
after a plain quantifier is needed.

| Pattern | Input | Symptom |
|---------|-------|---------|
| `[^c]*\Z` | `` | `first-match span differs` |
| `[^c]*\Z` | `a` | `findAll() count differs` |

### E2c — `^` after a quantified group

`^` appears as a zero-width assertion immediately after a quantified capturing group. Not routed
to a strategy that handles post-quantifier anchors.

| Pattern | Input | Symptom |
|---------|-------|---------|
| `(0)+^` | `0` | `find() boolean differs` |
| `(0)+^` | `0` | `findAll() count differs` |

### E2d — Anchor inside alternation combined with backreference

The routing logic handles `\A`-in-alternation but not when a backreference is also present.

| Pattern | Input | Symptom |
|---------|-------|---------|
| `(c\|a?){3}\A\1?` | `c` | `find() boolean differs` |
| `(c\|a?){3}\A\1?` | `c` | `findAll() count differs` |

---

## E3 — Backreference divergence (5 patterns, 9 findings)

The backreference resolves to a wrong value or the match boolean is wrong. Distinct from E1:
the entire match succeeds or fails where the JDK says otherwise.

| Pattern | Input | Symptom |
|---------|-------|---------|
| `(.b{0})?\1` | `00` | `find() boolean differs` |
| `b(-)\1{1}` | `--` | `find() boolean differs` |
| `b(-)\1{1}` | `--` | `findAll() count differs` |
| `${1}[^a]` | `` | `find() boolean differs` |
| `${1}[^a]` | `` | `findAll() count differs` |
| `(b{1,}){1}\1+\|(])` | `bb` | `findAll() count differs` |
| `^(.{1}\|.{0}){4}\1{3}` | `1cb0` | `find() boolean differs` |
| `^(.{1}\|.{0}){4}\1{3}` | `1cb0` | `findAll() count differs` |
| `^(.{1}\|.{0}){4}\1{3}` | `00_1` | `find() boolean differs` |

---

## E4 — Repeated group last-iteration span (2 patterns, 3 findings)

After a group is matched multiple times the final captured span is wrong on `findAll()`.
The per-iteration span-reset logic does not fire correctly for the last iteration before the
quantifier exits.

| Pattern | Input | Symptom |
|---------|-------|---------|
| `(-+)*` | `1` | `findAll() match 4 group 1 span differs` |
| `(c{2}){1,}` | `c` | `find() boolean differs` |
| `(c{2}){1,}` | `c` | `findAll() count differs` |

---

## E5 — Alternation with mixed-width branches (1 pattern, 2 findings)

Complex alternation where branches have different widths; `find()` boolean and `findAll()` count
are both wrong. Distinct from B5 (variable-length body inside a capturing group): here the outer
match boolean is wrong, not an inner span.

| Pattern | Input | Symptom |
|---------|-------|---------|
| `(-{0}[1]{1})([_][^_]\|\1*).{2}` | `1_0` | `find() boolean differs` |
| `(-{0}[1]{1})([_][^_]\|\1*).{2}` | `1_0` | `findAll() count differs` |

---

## E6 — Simple group routing gap (1 pattern, 3 findings)

A straightforward pattern routes to a strategy that produces wrong `find()` results. Likely a
missing routing guard; use `debugPattern` to confirm the assigned strategy.

| Pattern | Input | Symptom |
|---------|-------|---------|
| `[1]([^b]{2})` | `110` | `find() boolean differs` |
| `[1]([^b]{2})` | `110` | `findAll() count differs` |
| `[1]([^b]{2})` | `10c` | `find() boolean differs` |

---

## Next steps (priority order)

1. **E2b** — add a `\Z`-after-quantifier routing guard (no alternation required); companion to
`hasStringEndAnchorInAlternation`.
2. **E6** — run `./gradlew :reggie-runtime:debugPattern -Ppattern='[1]([^b]{2})'` to identify
the misrouted strategy; likely a one-line guard fix.
3. **E1** — find-path group span: remaining cases after A1/A2; same root-cause, new trigger
shapes (`[^-]{3,}([b].)` — quantified non-capturing prefix followed by a capturing group).
4. **E2a/E2c** — `\A`/`^` + repeated group: new anchor-in-repetition routing class.
5. **E3** — backreference edge cases: optional-group backref, quantified backref in alternation.
6. **E4** — repeated group final-iteration span: per-iteration span-reset in TDFA.
7. **E5** — mixed-width alternation outer match: `find()` boolean wrong on alternation with
backreference branch.
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Spec: preserve-fallback-for-pikevm-anchor-quantifi

## Problem

`ReggieMatcherBytecodeGenerator.resolveRealization()` returns `DELEGATE_PIKEVM` for any
pattern whose `PatternAnalyzer` result is `PIKEVM_CAPTURE` **without first consulting
`FallbackPatternDetector.needsFallback()`**. This means patterns that trigger the B3a route
in `PatternAnalyzer` (anchor-only capturing group, e.g. `($)`) but which also carry a
quantifier on that group (e.g. `($){2}`, `(^)?`) reach the compile-time PIKEVM path even
though `FallbackPatternDetector` marks them unsafe via the B2
(`hasAnchorInQuantifierInCapturingGroup`) and B3 (`hasAnchorInQuantifier`) KEEP-PERMANENT
guards.

The same gap exists in `RuntimeCompiler.compilePikeVm()` (called by generated stubs): it
only applies the B16 nullable-group-content guard, missing B2/B3.

The runtime `Reggie.compile()` path is correct: it calls
`FallbackPatternDetector.needsFallback(ast, PIKEVM_CAPTURE)` at line 494 and falls back to
JDK when non-null.

## Correct behaviour

1. When `@RegexPattern` annotation processing routes a pattern to `PIKEVM_CAPTURE`:
- `resolveRealization()` must call `FallbackPatternDetector.needsFallback(ast, PIKEVM_CAPTURE)`.
- If the result is non-null, the pattern requires JDK fallback:
- If `allowJdkFallback` is set → return `DELEGATE_FALLBACK`.
- Otherwise → throw `UnsupportedOperationException` with the fallback reason, consistent
with the existing `needsJdk` error message format.
- If the result is null → return `DELEGATE_PIKEVM` as before.

2. `RuntimeCompiler.compilePikeVm()` must apply the full `needsFallback()` guard (not just
the ad-hoc `hasNullableGroupContentWithNullableQuantifier` check). If the result is
non-null → throw `UnsupportedPatternException` with the reason string.

3. Patterns like `($)` (no quantifier on the anchor-only group) remain unaffected:
`hasAnchorInQuantifier` returns false for them → `needsFallback()` returns null →
they continue to reach `DELEGATE_PIKEVM`.

## Constraints

- No changes to `FallbackPatternDetector` or `PatternAnalyzer`.
- Error message format in `resolveRealization()` must be consistent with the existing
`needsJdk` path (folding the PIKEVM fallback reason into the `needsJdk` boolean or
reusing the same throw is preferred).
- `FallbackPatternDetector` is already imported in both files — no new dependencies.

## Scope

### Primary fixes

- `reggie-processor/src/main/java/com/datadoghq/reggie/processor/ReggieMatcherBytecodeGenerator.java:103`
— missing fallback guard on PIKEVM_CAPTURE early-exit: add `needsFallback()` check
before returning `DELEGATE_PIKEVM`.

### Auto-expanded sibling fixes

- `reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/RuntimeCompiler.java:299`
— partial guard (B16 only) in `compilePikeVm()`: replace `hasNullableGroupContentWithNullableQuantifier`
check with full `FallbackPatternDetector.needsFallback(ast, MatchingStrategy.PIKEVM_CAPTURE)` call.
FLOW: annotation-processor-generated stub calls `compilePikeVm()` directly, bypassing
`RuntimeCompiler.compile()` which has the correct full guard.
PRECONDITION: pattern has PIKEVM_CAPTURE result from PatternAnalyzer AND triggers B2 or B3.
REACHABLE: yes — `($){2}` at an `@RegexPattern` site reaches `compilePikeVm()` with no B2/B3 check.
CONCLUSION: medium-severity defensive guard that catches patterns slipping through Fix 1 if any
future code path creates a PIKEVM stub without going through `resolveRealization()`.

## Assumptions

- `FallbackPatternDetector.needsFallback(ast, PIKEVM_CAPTURE)` subsumes
`hasNullableGroupContentWithNullableQuantifier` (confirmed: B16 guard at
`FallbackPatternDetector.java:287` includes `strategy == PIKEVM_CAPTURE`).
- Fix 1 folds the PIKEVM fallback check into the existing `needsJdk` boolean rather than
adding a separate throw, to keep uniform error message formatting.
Loading
Loading