Bypassing ensure in Ruby
Ruby’s ensure
block ensures, as the name says, that no matter what exception occurs, some code will still be executed. For example:
begin
raise 'boom'
ensure
puts 'still executed'
end
… will print still executed
. This approach is common in Ruby, and is often used in functions that acquire a resource, yield it, and then close it when the given block is executing. File.open
does this, for example:
File.open('muaha.txt', 'w') do |io|
io << 'good'
end
The file muaha.txt
will be closed after the block has finished executing, which we can verify:
$evil_io
File.open('muaha.txt', 'w') do |io|
io << 'good'
$evil_io = io
end
$evil_io << 'bad'
This gives an error:
lol.rb:8:in `write': closed stream (IOError)
from lol.rb:8:in `<<'
from lol.rb:8:in `<main>'
The code in ensure
always executes, so the above example would still raise an IOError
even if an exception occurs within the block.
The code in ensure
always executes… or does it? If we wrap the code in a Fiber, and instead of raising an exception, we yield, then we can bypass the ensure
:
$evil_io
Fiber.new do
File.open('muaha.txt', 'w') do |io|
io << 'good'
$evil_io = io
Fiber.yield # <---- this avoids ensure!
end
end.resume
$evil_io << 'bad'
This raises no error, and muaha.txt
will be written to:
% cat muaha.txt
goodbad
So, if you ever want (or need) to bypass an ensure
block, now you know how to.
But please don’t.