Skip to content

symbol.c: skip tombstones when migrating to the hash table - #7018

Merged
matz merged 1 commit into
mruby:masterfrom
takumin:symbol-migrate-tombstone
Aug 9, 2026
Merged

symbol.c: skip tombstones when migrating to the hash table#7018
matz merged 1 commit into
mruby:masterfrom
takumin:symbol-migrate-tombstone

Conversation

@takumin

@takumin takumin commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

migrate_to_hash_table() dereferences the tombstones symbol GC leaves behind.

Symbol GC sweeps a dynamic symbol by freeing its name buffer and storing NULL in
mrb->symtbl[i] as a tombstone (phase 3 of mrb_symbol_gc()). Every other reader of
symtbl checks for that NULL:

function guard
sym_check() if (tagged_ptr == NULL) return FALSE;
find_symbol_linear() if (mrb->symtbl[i] == NULL) continue;
hash table rebuild in mrb_symbol_gc() if (mrb->symtbl[i] == NULL) continue;

migrate_to_hash_table() does not. It walks the whole table to build the buckets and
untags each entry unconditionally, so the first migration from linear mode to hash mode
after a sweep reads through a null pointer.

Reachability

Migration fires at mrb->symidx >= MRB_SYMBOL_LINEAR_THRESHOLD (256 by default), symbol
GC at mrb->dynamic_sym_count >= MRB_SYMBOL_MAX (4096 by default). symidx never
shrinks and symidx >= dynamic_sym_count always holds, so on a default build the table
has been in hash mode for a long time before the first sweep and migrate_to_hash_table()
is never reached again.

The bug is therefore reachable only when MRB_SYMBOL_MAX < MRB_SYMBOL_LINEAR_THRESHOLD,
which is a configuration a memory constrained target would plausibly pick: a small symbol
budget is exactly the reason to set MRB_SYMBOL_MAX at all. With MRB_SYMBOL_MAX=16:

$ ./build/host/bin/mruby -e '2000.times { |i| "filler-symbol-name-#{i}".to_sym }'
src/debug.c:91:22: runtime error: load of null pointer of type 'const uint8_t'
AddressSanitizer: SEGV on unknown address 0x000000000000
    #0 mrb_packed_int_decode      src/debug.c:91
    #1 migrate_to_hash_table      src/symbol.c:309
    #2 sym_intern                 src/symbol.c:439
    #3 mrb_intern                 src/symbol.c:464
    #4 mrb_intern_str             src/symbol.c:511
    #5 mrb_str_intern             src/string.c:2241

The failure is deterministic, not allocation dependent. Building the same tree with a
larger budget makes it go away:

MRB_SYMBOL_MAX result of the snippet above
16 UBSan load of null pointer, then SEGV in mrb_packed_int_decode
512 ok
4096 (default) ok

Fix

Skip tombstones the way the rebuild in mrb_symbol_gc() already does. ht->symlink and
ht->buckets come from mrb_calloc(), so a skipped index keeps the zero that means
"no chain", which is the same invariant that rebuild relies on.

No test is added: the crash needs a non default MRB_SYMBOL_MAX, so it cannot be
expressed in test/t/.

Verification

Reproduced on master with a clang address,undefined build:

MRuby::Build.new('symmax16') do |conf|
  conf.toolchain :clang
  conf.gembox 'default'
  conf.cc.defines << 'MRB_SYMBOL_MAX=16'
  conf.enable_sanitizer "address,undefined"
  conf.enable_debug
end

With the patch applied the same build runs the snippet to completion, exit 0, with no
UBSan report. rake test on a plain host build passes (1942 and 105 assertions, 0 KO,
0 crash).

Summary by CodeRabbit

  • Bug Fixes
    • Improved symbol-table rebuilding by safely skipping removed entries, preventing invalid or stale symbols from being restored.

Symbol GC sweeps a dynamic symbol by freeing its name buffer and storing
`NULL` in `mrb->symtbl[i]` as a tombstone. `sym_check()`,
`find_symbol_linear()` and the hash table rebuild at the end of
`mrb_symbol_gc()` all check for that `NULL`, but `migrate_to_hash_table()`
untags every entry unconditionally and dereferences the null pointer.

The migration only runs while the table is still in linear mode, so this is
reachable when `MRB_SYMBOL_MAX` is smaller than
`MRB_SYMBOL_LINEAR_THRESHOLD`, a plausible choice for a memory constrained
target. Symbol GC then leaves tombstones long before `mrb->symidx` reaches
the migration threshold, and the first migration afterwards crashes in
`mrb_packed_int_decode()`.

`ht->symlink` and `ht->buckets` come from `mrb_calloc()`, so a skipped index
keeps the zero that means "no chain", which is the same invariant the
rebuild in `mrb_symbol_gc()` already relies on.
@takumin
takumin requested a review from matz as a code owner August 9, 2026 03:24
@github-actions github-actions Bot added the core label Aug 9, 2026
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 52076459-8ca8-4abc-afd2-100ef2d6bef7

📥 Commits

Reviewing files that changed from the base of the PR and between fd6a182 and e32ab1c.

📒 Files selected for processing (1)
  • src/symbol.c

📝 Walkthrough

Walkthrough

The symbol-table migration to hash mode now skips tombstone entries with NULL symbol pointers during bucket and collision-link rebuilding.

Changes

Symbol table migration

Layer / File(s) Summary
Skip reclaimed entries during hash migration
src/symbol.c
Hash-table rebuilding skips reclaimed symbol-table entries before dereferencing symbol pointers or updating collision links.

Estimated code review effort: 2 (Simple) | ~5 minutes

Suggested reviewers: matz

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: skipping tombstones during migration to the hash table.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@matz
matz merged commit 291e786 into mruby:master Aug 9, 2026
21 checks passed
@takumin
takumin deleted the symbol-migrate-tombstone branch August 9, 2026 10:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants