Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions src/symbol.c
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,29 @@ mrb_sym_name_len(mrb_state *mrb, mrb_sym sym, mrb_int *lenp)
#endif
}

/*
* Tells whether symbol GC may free the buffer holding the symbol's name.
*
* Only dynamic symbols own an individual allocation. Inline symbols carry
* their name in the value, presym names are static data and literal names
* come from the symbol pool, so none of those is ever freed and a string
* may share them.
*/
static mrb_bool
sym_name_freeable_p(mrb_state *mrb, mrb_sym sym)
{
#if MRB_SYMBOL_MAX > 0
if (SYMBOL_INLINE_P(sym)) return FALSE;
if (sym <= MRB_PRESYM_MAX) return FALSE;
sym -= MRB_PRESYM_MAX;
if (sym > mrb->symidx) return FALSE;
return (mrb->sym_flags[sym] & SYM_FL_DYNAMIC) != 0;
#else
(void)mrb; (void)sym;
return FALSE;
#endif
}

/*
* Symbol GC: mark and sweep unreferenced dynamic symbols.
* Called lazily when dynamic symbol count reaches MRB_SYMBOL_MAX.
Expand Down Expand Up @@ -978,7 +1001,7 @@ sym_name(mrb_state *mrb, mrb_value vsym)
const char *name = mrb_sym_name_len(mrb, sym, &len);

mrb_assert(name != NULL);
if (SYMBOL_INLINE_P(sym)) {
if (SYMBOL_INLINE_P(sym) || sym_name_freeable_p(mrb, sym)) {
return mrb_str_new_frozen(mrb, name, len);
}
return mrb_str_new_static_frozen(mrb, name, len);
Expand Down Expand Up @@ -1154,7 +1177,8 @@ sym_inspect(mrb_state *mrb, mrb_value sym)
* sym: The symbol to convert.
*
* Returns the mruby string value corresponding to the symbol.
* If the symbol is an inline symbol, a new string is created.
* The name is copied for an inline symbol and for a dynamic symbol, whose
* name buffer symbol GC may free while the string is still alive.
* Otherwise, a static string (sharing the symbol's name buffer) is returned.
* Returns an undefined value if the symbol is invalid (though this should not happen).
*/
Expand All @@ -1170,6 +1194,9 @@ mrb_sym_str(mrb_state *mrb, mrb_sym sym)
RSTR_SET_ASCII_FLAG(mrb_str_ptr(str));
return str;
}
if (sym_name_freeable_p(mrb, sym)) {
return mrb_str_new(mrb, name, len);
}
return mrb_str_new_static(mrb, name, len);
}

Expand Down
19 changes: 19 additions & 0 deletions test/t/symbol.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,22 @@ def initialize; @source = 42; end
assert_equal 0, :"".to_s.length
assert_true :"".is_a?(Symbol)
end

assert('Symbol#to_s and Symbol#name outlive symbol GC') do
# Symbol GC frees the name buffer of a dynamic symbol. A returned string
# longer than the embedded limit used to share that buffer instead of
# copying it, so every later read of the string was a use after free.
name = "gc-target-symbol-" + "a" * 24
str = name.to_sym.to_s
frozen = name.to_sym.name

# Reach the dynamic symbol limit so a sweep runs, then hand the freed
# blocks to something else.
6000.times { |i| "gc-filler-symbol-name-#{i}".to_sym }
GC.start
reuse = []
3000.times { |i| reuse << "z" * 60 + i.to_s }

assert_equal name, str
assert_equal name, frozen
end
Loading