mruby-regexp: copy named-capture names into owned arena

re_compile stored raw pointers into the pattern source in
pat->named_captures[i].name. Two ways this could dangle:

  - With /x, the source was c.stripped, freed at end of compile.
    Later reads (regexp construction, MatchData[:name] lookup) hit
    freed memory.

  - Without /x, the pointer aliased the input string's RSTRING_PTR.
    Mutating that string after Regexp.new could re-buffer it, leaving
    name dangling.

Allocate one arena buffer per regexp (only when num_named > 0) and
copy all names in. Common-case regexps without named captures pay
zero bytes.

Reported by OSS-Fuzz (testcase 5695283416858624).

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Yukihiro "Matz" Matsumoto
2026-05-13 12:34:50 +09:00
parent 403b75fbeb
commit 9e93337479
3 changed files with 40 additions and 0 deletions
@@ -67,6 +67,7 @@ typedef struct mrb_regexp_pattern {
uint16_t num_captures; /* number of capture groups (including group 0) */
uint32_t flags;
re_named_capture *named_captures;
char *named_arena; /* owned storage for named_captures[i].name; NULL if num_named == 0 */
uint16_t num_named;
mrb_bool has_backref; /* true if pattern uses \1-\9 */
mrb_bool needs_backtrack; /* true if pattern needs backtracking engine */
+20
View File
@@ -884,7 +884,26 @@ re_compile(mrb_state *mrb, const char *pattern, mrb_int len, uint32_t flags)
pat->num_captures = c.num_captures;
pat->flags = flags;
pat->named_captures = c.named_captures;
pat->named_arena = NULL;
pat->num_named = c.num_named;
/* Copy capture names into an owned arena. Until this point the names
point into the pattern source (or into c.stripped, which gets freed
below in /x mode). After this loop the regexp owns its names. */
if (c.num_named > 0) {
size_t total = 0;
for (uint16_t i = 0; i < c.num_named; i++) total += c.named_captures[i].name_len;
if (total > 0) {
pat->named_arena = (char*)mrb_malloc(mrb, total);
size_t off = 0;
for (uint16_t i = 0; i < c.num_named; i++) {
uint16_t n = c.named_captures[i].name_len;
memcpy(pat->named_arena + off, c.named_captures[i].name, n);
pat->named_captures[i].name = pat->named_arena + off;
off += n;
}
}
}
pat->has_backref = c.has_backref;
pat->needs_backtrack = c.needs_backtrack;
@@ -959,6 +978,7 @@ re_free(mrb_state *mrb, mrb_regexp_pattern *pat)
mrb_free(mrb, pat->code);
mrb_free(mrb, pat->classes);
mrb_free(mrb, pat->named_captures);
mrb_free(mrb, pat->named_arena);
mrb_free(mrb, pat->prefix);
mrb_free(mrb, pat->cached_visited);
mrb_free(mrb, pat->cached_threads[0]);
+19
View File
@@ -361,6 +361,25 @@ assert("MatchData#named_captures") do
assert_equal "host", nc["b"]
end
assert("Regexp - named captures survive /x preprocessing") do
# Regression: with /x, re_compile freed the stripped buffer that
# named_captures[i].name pointed into.
re = /(?<n>\d+) # comment
\s* (?<u>\w+) /x
m = re.match("42 px")
assert_equal "42", m[:n]
assert_equal "px", m[:u]
end
assert("Regexp - named captures survive source string mutation") do
# Regression: name pointer used to alias RSTRING_PTR of the source.
s = String.new("(?<key>\\d+)")
re = Regexp.new(s)
s.replace("X" * 10000) # force buffer reallocation
m = re.match("abc 123 def")
assert_equal "123", m[:key]
end
assert("Regexp - positive lookahead (?=...)") do
md = /\w+(?=@)/.match("user@host")
assert_equal "user", md[0]