From 9235aba3074e8c621bc6286e3703ac3a4c0d0113 Mon Sep 17 00:00:00 2001 From: Kevin Ushey Date: Sun, 9 Aug 2026 11:04:29 -0700 Subject: [PATCH 1/2] approx: Grow the delete-handling deque dynamically tre_tnfa_run_approx() kept its delete-handling deque in a fixed 512-pointer array on the C stack, guarded only by assert(). A TNFA with more than 512 states reachable at one position overflows the array: with NDEBUG this corrupts the stack silently, and even with asserts enabled the initial fill loop walks past the array without ever tripping the assertion (it does not wrap), so the overflow is never caught. An alternation of ~200 distinct literals is enough to crash tre_regaexec() (observed on Windows x64, mingw-w64 gcc). Start with a small buffer on the stack and move it to the heap, growing as needed, when it fills during the initial enqueue of reachable states. The circular enqueue in the propagation loop keeps its existing assert() backstop; its capacity references now track the dynamic size. Addresses the '/* XXX - dynamic ringbuffer size */' note. This fix has been carried in GNU R's vendored copy of TRE since 2011 (R commits 155c77fa, 4e5d77d3, d7630204). --- lib/tre-match-approx.c | 42 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/lib/tre-match-approx.c b/lib/tre-match-approx.c index 837e753..7c93945 100644 --- a/lib/tre-match-approx.c +++ b/lib/tre-match-approx.c @@ -474,8 +474,9 @@ tre_tnfa_run_approx(const tre_tnfa_t *tnfa, const void *string, ssize_t len, pretending that all transitions are epsilon transitions, until no more states can be reached with better costs. */ { - /* XXX - dynamic ringbuffer size */ - tre_tnfa_approx_reach_t *ringbuffer[512]; + int rb_size = 256; + tre_tnfa_approx_reach_t *static_ringbuffer[256]; + tre_tnfa_approx_reach_t **ringbuffer = static_ringbuffer; tre_tnfa_approx_reach_t **deque_start, **deque_end; deque_start = deque_end = ringbuffer; @@ -487,7 +488,34 @@ tre_tnfa_run_approx(const tre_tnfa_t *tnfa, const void *string, ssize_t len, continue; *deque_end = &reach_next[id]; deque_end++; - assert(deque_end != deque_start); + /* Grow the buffer (moving to the heap) if full. */ + if (deque_end >= (ringbuffer + rb_size)) + { + tre_tnfa_approx_reach_t **larger_buf; + size_t os = deque_start - ringbuffer; + size_t oe = deque_end - ringbuffer; + rb_size += 512; + if (ringbuffer == static_ringbuffer) + larger_buf = xmalloc(sizeof(*ringbuffer) * rb_size); + else + larger_buf = xrealloc(ringbuffer, sizeof(*ringbuffer) * rb_size); + if (larger_buf == NULL) + { + if (ringbuffer != static_ringbuffer) + xfree(ringbuffer); +#ifndef TRE_USE_ALLOCA + if (buf) + xfree(buf); +#endif /* !TRE_USE_ALLOCA */ + return REG_ESPACE; + } + if (ringbuffer == static_ringbuffer) + /* Moving from stack to heap: copy existing contents. */ + memcpy(larger_buf, ringbuffer, sizeof(static_ringbuffer)); + ringbuffer = larger_buf; + deque_start = ringbuffer + os; + deque_end = ringbuffer + oe; + } } /* Repeat until the deque is empty. */ @@ -519,7 +547,7 @@ tre_tnfa_run_approx(const tre_tnfa_t *tnfa, const void *string, ssize_t len, /* Too many errors or cost too large. */ DPRINT((" delete: from %03d: cost too large\n", id)); deque_start++; - if (deque_start >= (ringbuffer + 512)) + if (deque_start >= (ringbuffer + rb_size)) deque_start = ringbuffer; continue; } @@ -617,15 +645,17 @@ tre_tnfa_run_approx(const tre_tnfa_t *tnfa, const void *string, ssize_t len, /* Add to the end of the deque. */ *deque_end = &reach_next[dest_id]; deque_end++; - if (deque_end >= (ringbuffer + 512)) + if (deque_end >= (ringbuffer + rb_size)) deque_end = ringbuffer; assert(deque_end != deque_start); } deque_start++; - if (deque_start >= (ringbuffer + 512)) + if (deque_start >= (ringbuffer + rb_size)) deque_start = ringbuffer; } + if (ringbuffer != static_ringbuffer) + xfree(ringbuffer); } #ifdef TRE_DEBUG From d216eb2909b363ed47b68052710966c30e94195d Mon Sep 17 00:00:00 2001 From: Kevin Ushey Date: Sun, 9 Aug 2026 11:52:38 -0700 Subject: [PATCH 2/2] tests: Exercise the approximate matcher with a many-branch alternation test_exec() runs every test through the approximate matcher via REG_APPROX_MATCHER, so this exercises the dynamic deque growth in tre_tnfa_run_approx() added in this branch; against the previous fixed-size deque this test crashes. --- tests/retest.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/retest.c b/tests/retest.c index 16ccb40..6d51835 100644 --- a/tests/retest.c +++ b/tests/retest.c @@ -625,6 +625,22 @@ main(int argc, char **argv) test_comp("^!pfast [0-9]{1,15} ([0-9]{1,3}\\.){3}[0-9]{1,3}[0-9]{1,5}$", REG_EXTENDED, 0); + /* An alternation with many branches. The resulting TNFA has well over + 512 states reachable at one position, so the approximate matcher's + delete-handling deque must grow beyond its old fixed size (this + matters here because test_exec() also runs each test with + REG_APPROX_MATCHER). */ + { + static char many_branches[600 * 5]; + char *p = many_branches; + int i; + for (i = 0; i < 600; i++) + p += sprintf(p, "%sx%03d", i > 0 ? "|" : "", i); + test_comp(many_branches, REG_EXTENDED, 0); + test_exec("zzx059zz", 0, REG_OK, 2, 6, END); + test_exec("zzzzzz", 0, REG_NOMATCH); + } + #if KNOWN_BUG /* Should these match or not? */ test_comp("(a)*-\\1b", REG_EXTENDED, 0);