While researching Ruby's new-in-1.9 Object methods untrusted?, untrust, and trust, I discovered something I did not know about the $SAFE variable: in addition to being Thread-local, it is also Proc-local. Proc objects (both procs and lambdas) have their own copy of $SAFE, and they run at whatever $SAFE level was in effect when the Proc was created not the safe level that is in effect when they are invoked. This was discussed three years ago over at _why's old blog.
There is a corollary, however that was demonstrated but not discussed in a recent ruby-core post by Shugo Maeda: if you set $SAFE inside a proc or a lambda, that setting is local, and does not change the global safe level. Googling for "safe_eval" implementations shows that everyone uses Thread.new to create a sandbox with a locally elevated safe level. It turns out, however, that an ordinary lambda will also work. (One can argue, however, that a thread gives extra safety because it can be monitored and killed to guard against runaway code that doesn't terminate.)
In any case, here is the method I defined to try this out. Pass it a safe level (it defaults to 4) and a block of code, and it will execute the block at that level without altering the global safe level and without creating a new thread:
# Execute a block at the specified safe level. Tested with Ruby 1.9 and 1.8.7
def safely(level = 4)
sandbox = lambda do # Set up a sandbox
$SAFE = level # Go to the specified safe level for this lambda only
yield # Invoke the block at that level
end
sandbox.call # Invoke the sandbox without changing $SAFE globally
end
# Use this method like this.
x = safely { eval('"untrusted code"') }
[x, x.tainted?, x.untrusted?] # => ["untrusted code", true, true]
I'm not convinced that this method is actually useful in practice, but I thought it was interesting, and it reminds me again how cool it is to be able to define methods that accept blocks and act like control statements.



