|
| 1 | +{ |
| 2 | + "meta": { |
| 3 | + "task": "TASK-54 standing differential regexp fuzzer", |
| 4 | + "measured": "2026-07-27, DuckDB 1.5.5 vs the engine (rust regex 1.13 behind retrans.rs), tests/test_duckdb_regexp_fuzz.py grammar", |
| 5 | + "method": "random grammar-biased patterns run through regexp_matches/full_match/extract/replace on both engines over the divergence-prone subject rows; every class below reproduced from a concrete generated case, then re-swept to zero divergences (40k cases, 8 seeds) after the fix", |
| 6 | + "contract": "identical rows OR engine build-time reject OR both error; duck-errors-engine-serves and value mismatches are failures" |
| 7 | + }, |
| 8 | + "findings": [ |
| 9 | + { |
| 10 | + "construct": "\\1-\\9 outside a character class", |
| 11 | + "witness": "regexp_full_match(s, '\\1((?m)\\W\u06639|...)a?')", |
| 12 | + "duckdb": "Invalid Input Error: invalid escape sequence: \\1 (RE2 has no backrefs)", |
| 13 | + "rust_regex": "octal(true) — pinned for in-class octal parity — reads \\1 as an octal escape and SERVES", |
| 14 | + "decision": "reject at bind ('backreference-style \\N escape'); in-class \\1 stays served (octal in both)" |
| 15 | + }, |
| 16 | + { |
| 17 | + "construct": "stacked quantifiers, full grammar (not just */+ pairs)", |
| 18 | + "witness": "'z{3,}+' 'a.{2}*' 'h??*' 'X?*' 'a{1}+' '(?:..)*{2}' '\\pL{0,}+'", |
| 19 | + "duckdb": "Invalid Input Error: bad repetition operator (one lazy '?' is the only legal follower of a quantifier)", |
| 20 | + "rust_regex": "silently reinterprets and serves", |
| 21 | + "decision": "reject: after a quantifier and optional single lazy '?', any further * + ? or strict {bounds} is an error" |
| 22 | + }, |
| 23 | + { |
| 24 | + "construct": "nested counted repetition, bound product over 1000", |
| 25 | + "witness": "regexp_matches(s, '(?:^(c{3,}\\dz0){1,999}\\p{L}+?\\D)|..')", |
| 26 | + "duckdb": "Invalid Input Error: invalid repetition size: {1,999}", |
| 27 | + "rust_regex": "no product cap at these sizes — serves", |
| 28 | + "decision": "reject when nested {m,n} bound products exceed 1000; unbounded inner {n,} counts as over-cap (conservative: '(a{2,}){3}' rejects)" |
| 29 | + }, |
| 30 | + { |
| 31 | + "construct": "whitespace inside repetition bounds", |
| 32 | + "witness": "regexp_replace(s, '.*a{1, 3}\\D*', '') on 'abc123def'", |
| 33 | + "duckdb": "'{1, 3}' is LITERAL (no match, input unchanged)", |
| 34 | + "rust_regex": "parses it as the 1..3 repetition and replaces ('123def') — silent wrong answer", |
| 35 | + "decision": "reject '{...}' bodies that are strict bounds after whitespace stripping but contain whitespace" |
| 36 | + }, |
| 37 | + { |
| 38 | + "construct": "'--' / '&&' / '~~' inside a character class", |
| 39 | + "witness": "regexp_replace(s, '[a-fz\\x41][--\u0660-\u0669]{0,}', '\\\\') ; regexp_matches(s, '[c\\s&&][^\u0660-\u0669~~\\052]')", |
| 40 | + "duckdb": "literals / ranges ('[--\u0660]' = range '-'..'\u0660'; backwards ones error 'invalid character class range: h--')", |
| 41 | + "rust_regex": "class set operations (difference/intersection/symmetric difference) — silent wrong answers measured both directions", |
| 42 | + "decision": "reject any doubled '-' '&' '~' inside a class" |
| 43 | + }, |
| 44 | + { |
| 45 | + "construct": "non-POSIX '[' inside a character class", |
| 46 | + "witness": "regexp_replace(s, '..|[]a-f[b]]\\S\\A{0,}', '$1')", |
| 47 | + "duckdb": "'[' is a literal class member", |
| 48 | + "rust_regex": "opens a NESTED class — silent wrong answers", |
| 49 | + "decision": "reject '[' inside a class unless it starts a '[:name:]' POSIX element" |
| 50 | + }, |
| 51 | + { |
| 52 | + "construct": "POSIX element desyncing the class tracker", |
| 53 | + "witness": "translate_pattern('[[:alpha:]\\d]') pre-fix emitted '[[:alpha:](?-u:\\d)]'", |
| 54 | + "duckdb": "'[[:alpha:]\\d]' = alpha \u222a digits", |
| 55 | + "rust_regex": "same semantics once the element is consumed atomically", |
| 56 | + "decision": "FIX (not reject): consume '[:...:]' atomically; '[[:alpha:]\\d]' now translates to '[[:alpha:]0-9]'" |
| 57 | + }, |
| 58 | + { |
| 59 | + "construct": "Perl class as a range endpoint inside a class", |
| 60 | + "witness": "regexp_extract(s, '..[--\\s[:^digit:]]..', 3, 'i') ; shapes '[a-\\d]' '[\\d-a]' '[\\x08-\\s]'", |
| 61 | + "duckdb": "Invalid Input Error: invalid escape sequence / invalid character class range", |
| 62 | + "rust_regex": "the in-class expansion can COMPILE (e.g. '[\\x08-\\t\\n..]') and serve", |
| 63 | + "decision": "reject a Perl class escape adjacent to an unescaped range '-' on either side; trailing literal '-' ('[\\d-]') stays fine" |
| 64 | + }, |
| 65 | + { |
| 66 | + "construct": "range starting at the class-leading literal ']'", |
| 67 | + "witness": "regexp_full_match(s, '[]-Z]\\w^h{999}') ; '[^]-0-7]'", |
| 68 | + "duckdb": "']-Z' parses as a RANGE (backwards ones error: 'invalid character class range: ]-Z')", |
| 69 | + "rust_regex": "treats the '-' as literal and serves; forward ranges ('[]-a]') would silently disagree on membership", |
| 70 | + "decision": "reject '-' directly after the class-leading ']' unless immediately before ']' ('[]-]' stays fine)" |
| 71 | + }, |
| 72 | + { |
| 73 | + "construct": "anchor-only pattern with 2+ text anchors", |
| 74 | + "witness": "regexp_matches(s, '$\\z(?i)') over __THIS__ vs as a constant", |
| 75 | + "duckdb": "SELF-INCONSISTENT (measured): row path FALSE for 'hello world' / TRUE only for '' (literal-comparison optimization); constant fold TRUE everywhere. 'd$\\z', '($\\z)', 'x|$\\z', '^()^' are all rescued and consistent", |
| 76 | + "rust_regex": "matches the empty string at the anchors (agrees with duckdb's constant fold only)", |
| 77 | + "decision": "reject patterns consisting only of ^ $ \\A \\z, \\b, flag groups, and empty '(?:)' when they contain 2+ text anchors — unservable either way" |
| 78 | + }, |
| 79 | + { |
| 80 | + "construct": "capture group under an exactly-zero repetition", |
| 81 | + "witness": "regexp_replace(s, '\\P{L}(\u00e9{1,999}\\x41+|\u212a*9?\u0663){0}', '$1-\\1')", |
| 82 | + "duckdb": "keeps the group in the count: \\1 in range, non-participating -> '' ; replaces (' ' -> '$1-')", |
| 83 | + "rust_regex": "erases the group from the compiled regex — the rewrite's MaxSubmatch pre-check sees 0 groups and no-ops; later group numbers would shift too", |
| 84 | + "decision": "reject a capturing group followed by {0}/{0,0}; '(?:a){0}' and 'a{0}' stay fine" |
| 85 | + }, |
| 86 | + { |
| 87 | + "construct": "rewrite template: out-of-range backref AFTER a bad escape", |
| 88 | + "witness": "regexp_replace(s, '..|.', '\\x\\x\\2', 'ccg') with 0 groups", |
| 89 | + "duckdb": "input UNCHANGED — RE2's MaxSubmatch pre-check scans the whole template first, out-of-range wins over the global bad-escape consume quirk", |
| 90 | + "rust_regex": "n/a (bind-time template translation ordered the checks left-to-right)", |
| 91 | + "decision": "FIX: translate_rewrite pre-scans the whole template for out-of-range \\N before the bad-escape scan" |
| 92 | + }, |
| 93 | + { |
| 94 | + "construct": "\\C (any byte)", |
| 95 | + "witness": "regexp_replace over 'h\u00e9llo \u0663\u0664' produced invalid UTF-8 (pyarrow decode failure on the ORACLE side)", |
| 96 | + "duckdb": "serves raw bytes that can split multibyte chars", |
| 97 | + "rust_regex": "does not support \\C — engine build-rejects via the compile error", |
| 98 | + "decision": "no change (already an identical-reject); fuzzer harness treats undecodable duckdb output as a duckdb error" |
| 99 | + } |
| 100 | + ] |
| 101 | +} |
0 commit comments