shugo (Shugo Maeda) wrote in #note-2:
I've created a pull request for a comprehensive fix: https://github.com/ruby/ruby/pull/17777
When rb_iterate0() returns, and the ifunc's data points into the machine stack of the current execution context, ifunc->func is replaced with a stub that raises a RuntimeError:
As John commented on the pull request, the above heuristic check of ifunc's data doesn't work because it's not ASAN-safe and data can be a non-VALUE immediate value which looks like a stack pointer.
So I gave up the heuristic approach, and added the following internal functions:
rb_block_call_noescape()
rb_check_block_call_noescape()
And I've converted the vulnerable call sites to them.
Always passing a GC managed object would be more natural as an API, but I chose to just forbid calling the block after return, for two reasons:
- There seem to be few use cases for it: the converted call sites have crashed on such calls ever since they were introduced, and nobody has complained.
- Even if we made them memory-safe by passing a GC managed object, the behavior would still be semantically questionable, e.g., calling the block captured from
Set#& afterwards would mutate the already-returned Set.
By the way, while working on this pull request I hit a code generation bug of clang 17 with -O1 -fPIC: the local variable cfp of rb_iterate0(), which is never changed after setjmp(), gets clobbered across longjmp(), so breaks escaped rb_iterate0() and btest failed with "unexpected break". The pull request works around it by making the locals read after longjmp() volatile. Since the bug is latent in rb_iterate0() on master (any innocent change to the function can change the stack slot allocation and trigger it, as mine did), it may be worth applying the volatile for cfp even if this pull request is not merged.
@jhawthorn (John Hawthorn)
I hadn't noticed that this issue is assigned to you. If you were planning to fix it yourself, please feel free to ignore my pull request.