mirror of
https://github.com/mruby/mruby
synced 2026-06-08 16:11:16 +00:00
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:
@@ -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 */
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user