Recovering from segfaults in Ruby
2012-11-20
Did you know you can recover from a Segmentation Fault in pure Ruby?
Here's the trick - this code is taken from the rb_vm_bugreport function:
fprintf(stderr, "* Loaded script: %s\n", StringValueCStr(vm->progname));
fprintf(stderr, "\n");
fprintf(stderr, "* Loaded features:\n\n");
for (i=0; i<RARRAY_LEN(vm->loaded_features); i++) {
fprintf(stderr, " %4d %s\n", i, StringValueCStr(RARRAY_PTR(vm->loaded_features)[i]));
}
fprintf(stderr, "\n");
Spotted it yet?
Part of the debugging information Ruby outputs when a segfault occurs is a list of all loaded scripts and C extensions. It does this by iterating over $LOADED_FEATURES (or vm->loaded_features as it's known inside MRI), converting each item to a string and printing it to stderr.
Knowing this, we can arrange for our own code to be executed by the segfault handler by playing around with $LOADED_FEATURES:
def protect_from_segfault
o = Object.new
def o.to_str; throw :recover_from_segfault end
$LOADED_FEATURES << o
catch(:recover_from_segfault) { yield }
$LOADED_FEATURES.delete o
end
We'll use RubyInline to create a C function that will write to a null pointer when it is called:
require "inline"
module Segfault
inline do |builder|
builder.c <<-C
void boom() {
*(int*)NULL = 123;
}
C
end
module_function :boom
end
Let's give it a shot:
puts "About to segfault\n\n"
protect_from_segfault do
Segfault.boom
end
puts "\n\nKeep on truckin' Ruby!"
Running that bit of code gives me this output:
λ ruby recover-segfault.rb
About to segfault
recover-segfault.rb:25: [BUG] Segmentation fault
ruby 2.0.0dev (2012-11-01 trunk 37411) [x86_64-darwin11.4.0]
-- Control frame information -----------------------------------------------
c:0007 p:---- s:0019 e:000018 CFUNC :boom
c:0006 p:0011 s:0016 e:000015 BLOCK recover-segfault.rb:25
c:0005 p:0004 s:0014 e:000013 BLOCK recover-segfault.rb:6 [FINISH]
c:0004 p:---- s:0012 e:000011 CFUNC :catch
c:0003 p:0047 s:0008 e:000007 METHOD recover-segfault.rb:6
c:0002 p:0044 s:0004 e:000598 EVAL recover-segfault.rb:26 [FINISH]
c:0001 p:0000 s:0002 e:001718 TOP [FINISH]
... snip ...
Keep on truckin' Ruby!
I have a ticket open on the Ruby bug tracker to get this fixed up by avoiding calling methods from the segfault handler. Until then, please don't use this 'feature' seriously or rely on the current behaviour (as fun as it may be!)