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.

Note last edited November 2024.
Incoming links: Ruby.