Skip to content

Commit bfdc8fb

Browse files
leifericfclaude
andcommitted
regex: assert state_lock-held at the prim call sites
src/regex/re.c uses file-static globals (re_flags and re_g_state) to hold per-match flag state and capture spans. Every current caller -- prim_re_find, prim_re_matches, prim_split's regex arm, prim_str_replace's regex arm -- runs inside mino_call, which holds state_lock, so the globals are effectively serialized. The contract was not enforced anywhere, so a future refactor that elides state_lock around any of those prims would silently corrupt the next match's capture spans. Add MINO_ASSERT_STATE_SAFE(S) at each prim's regex call site so a missing lock fires a debug-build assert at the offending site rather than leaking into a torn re_g_state. No restructure of the regex module itself (which would otherwise have meant adding a context parameter and reflowing every internal helper). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent bd2cba0 commit bfdc8fb

4 files changed

Lines changed: 28 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Changelog
22

3+
## v0.389.13 — Lock-Invariant Assert on Regex Prim Entries
4+
5+
The regex engine in `src/regex/re.c` uses file-static globals
6+
(`re_flags`, `re_g_state`) for match state. All current callers
7+
go through `re-find`, `re-matches`, `split`, or `str-replace` --
8+
each runs inside `mino_call`, which holds `state_lock`, so the
9+
globals are effectively serialized in practice. The contract was
10+
not enforced anywhere, so a future refactor that elides
11+
`state_lock` around one of those prims would silently corrupt the
12+
next match's capture spans. Add `MINO_ASSERT_STATE_SAFE` at each
13+
prim's regex call site to surface a missing lock at the offending
14+
site under a debug build.
15+
316
## v0.389.12 — Agent Spawn-Rollback Hands Off Concurrent Producer's Queue
417

518
When `pthread_create` (or `_beginthreadex`) refuses the agent

src/mino.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
*/
2929
#define MINO_VERSION_MAJOR 0
3030
#define MINO_VERSION_MINOR 389
31-
#define MINO_VERSION_PATCH 12
31+
#define MINO_VERSION_PATCH 13
3232

3333
/*
3434
* Human-readable version string of the *linked* runtime, e.g. "0.48.0".

src/prim/regex.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,12 @@ mino_val *prim_re_find(mino_state *S, mino_val *args, mino_env *env)
115115
if (mino_type_of(text_val) != MINO_STRING) {
116116
return prim_throw_classified(S, "eval/type", "MTY001", "re-find: second argument must be a string");
117117
}
118+
/* The regex engine uses file-static globals (re_flags +
119+
* re_g_state) for match state, so every caller must serialize
120+
* through state_lock. The debug-build assert surfaces a missing
121+
* lock at the offending call site instead of silently corrupting
122+
* the next match's capture spans. */
123+
MINO_ASSERT_STATE_SAFE(S);
118124
compiled = re_compile(pat_data);
119125
if (compiled == NULL) {
120126
return prim_throw_classified(S, "eval/contract", "MCT001",
@@ -160,6 +166,7 @@ mino_val *prim_re_matches(mino_state *S, mino_val *args, mino_env *env)
160166
if (mino_type_of(text_val) != MINO_STRING) {
161167
return prim_throw_classified(S, "eval/type", "MTY001", "re-matches: second argument must be a string");
162168
}
169+
MINO_ASSERT_STATE_SAFE(S);
163170
compiled = re_compile(pat_data);
164171
if (compiled == NULL) {
165172
return prim_throw_classified(S, "eval/contract", "MCT001",

src/prim/string.c

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,12 @@ mino_val *prim_split(mino_state *S, mino_val *args, mino_env *env)
491491
* conflated 0 and negative for the literal-string path, so the
492492
* regex path matches that). */
493493
const char *pat_src = sep_val->as.regex.source->as.s.data;
494-
re_t compiled = re_compile(pat_src);
494+
re_t compiled;
495+
/* See prim_re_find for the rationale: the regex engine's
496+
* static-global match state requires every caller to be in
497+
* a state-safe window (state_lock held). */
498+
MINO_ASSERT_STATE_SAFE(S);
499+
compiled = re_compile(pat_src);
495500
size_t pos = 0;
496501
if (compiled == NULL) {
497502
return prim_throw_classified(S, "eval/contract", "MCT001",
@@ -908,6 +913,7 @@ mino_val *prim_str_replace(mino_state *S, mino_val *args,
908913
"str-replace: regex has no source pattern");
909914
}
910915
pat_src = match_val->as.regex.source->as.s.data;
916+
MINO_ASSERT_STATE_SAFE(S);
911917
compiled = re_compile(pat_src);
912918
if (compiled == NULL) {
913919
return prim_throw_classified(S, "eval/contract", "MCT001",

0 commit comments

Comments
 (0)