Bug #22335
Updated by eapache_opslevel (Evan Huus) 12 days ago
This is a messy one that originally surfaced as a flaky test in our Rails app, and required a lot of AI-assisted debugging to track down. This bug appears in the latest 4.0.7, though all of the original investigation was done against 3.3.11. If you have: - a method that takes only keyword args - has >31 keyword args - has two a call-time-evaluated default values value (e.g. `{}`) at argument index >=31 - running in a Ruby process that has refined or redefined `Integer#==` at any point Then you get inconsistent stack corruption depending on your process's hash salt. The mechanism is nasty: - In a method with only keyword args, `vm_callee_setup_arg()` and `vm_call_iseq_setup_kwparm_kwarg()` call calls `args_setup_kw_parameters()` with `klocals = argv + kw_param->bits_start - kw_param->num`, i.e. **above `cfp->sp`**, without first extending `cfp->sp` over those locals - `args_setup_kw_parameters()` uses a full hash, keyed by integer, for arguments beyond index 31 - When `args_setup_kw_parameters()` sets a key in that hash, and that key collides with another in the ar_hint that small hashes use for a linear scan (not uncommon for a tiny hash, but still salt-dependent), it triggers an integer comparison - The refinement on `Integer#==` prevents that comparison from taking the fast path and turns it into a real C frame - Which gets pushed onto the stack, when the stack pointer hasn't been extended - Thus overwriting the other args and producing garbage The net effect is that the called method sees garbage values for the overwritten arguments. I've attached a reproducing script, which is unfortunately complex, as it needs to do some work to intentionally hit the correct hash collision depending on the process's salt. A simpler static version doesn't reproduce reliably enough to be useful. Even this version will occasionally get a hash salt with no collisions and fail to reproduce, though that seems pretty rare on my machine.