From: _why Date: 2008-04-25T07:12:39+09:00 Subject: [RCR] sandbox API Hi, everybody. In the #ruby-core design meeting, during the discussion about MVM, there was some mention of the sandbox API. I thought it would be worth while to write up an RCR. I mean: all though there has been some talk about the sandbox extension for Ruby 1.8 on this list, there hasn't been any talk about the API itself. Considering that $SAFE has fallen out of use and there is a renewed interest in managing many namespaces/environments on a single VM, I figured hey. ABSTRACT Ruby of yore has only had one interpreter environment. The sandbox API gives that central environment a means of creating other in-process environments for executing code. Be it restricted sandboxes for running unsafe code or fully-featured sandboxes to offer a clean namespace. PROS & CONS The benefits of this particular API: * Rather simple (yeah?) * Basic (albeit unstable) extensions exist for Ruby 1.8 and JRuby. * Patterned after other successful sandboxes (such as Firefox's XPCNativeWrapper[1] and Io's Sandbox[2]) * Generic enough to work in other Ruby impls. The drawbacks are: * Not fully proven on Ruby 1.8. * My extension does rely on Thread.kill! to stop a Sandbox, which is taboo. (Same problem timeout.rb has.) * Haven't worked out how tainting could play out. * Could be closer coupled with threading to offer concurrent interps in separate threads. THE API All classes and methods are enclosed in the Sandbox module. The primary classes are Sandbox::Full and Sandbox::Safe. Sandbox::Safe is descended from Sandbox::Full. Methods for these two classes are: * self.new(opts = {}) Returns a newly created sandbox. Available options: :init, :ref * eval(str, opts = {}) => obj Evaluates +str+ as Ruby code inside the sandbox and returns the result. Available options: :timeout * load(io, opts = {}) => nil At heart, just an alias for: eval(IO.read(io), opts) * ref(klass) => nil Adds a boxed reference to +klass+ in the sandbox. (Ex.: @box.ref(YAML) would create a YAML class in the sandbox which is derived from Sandbox::BoxedClass, a proxy to the YAML class on the outside.) * require(str) Requires a file into the Sandbox, using the $LOAD_PATH and file permissions of the current sandbox. The Sandbox module itself has a few methods: * Sandbox.safe(opts = {}) An alias for Sandbox::Safe.new(opts) * Sandbox.new(opts = {}) An alias for Sandbox::Full.new(opts) * Sandbox.current Returns an object representing the current Sandbox. * Sandbox.screen(obj) => true or Sandbox::ScreenException Traverses an object and its related symbols to be sure it is entirely composed of objects from the current sandbox. Purely for testing. As for the `opts` hash in the above methods, here's a brief description of those: * init: The portions of Ruby core to initialize. :load - $:, $-I, $LOAD_PATH, $\, $LOADED_FEATURES, load, require, autoload, autoload? :io - IOError, EOFError, IO, FileTest, File, Dir, File::Constants, test, File::Stat, :env - syscall, open, printf, print, putc, puts, gets, readline, getc, select, readlines, p, display, STDIN, STDOUT, STDERR :real - abort, at_exit, caller, exit, trace_var, untrace_var, set_trace_func, warn, ThreadError Thread, Continuation, ThreadGroup, trap, exec, fork, exit!, system, `, sleep, Process, Process::Status, Process::Sys, GC, ObjectSpace, hash, __id__, object_id :all - the whole enchilada (Sandbox::Full assumes :init => :all and Sandbox::Safe assumes :init => nil.) * ref: Classes to create boxed references for. (Ex.: :ref => [RedCloth, BlueCloth]) * timeout: Maximum seconds, a time limit for the sandbox. BOXED CLASSES Inside each Sandbox, a BoxedClass constant is defined. This class has two methods: method_missing and const_missing. So, let's say you're running a web app in the sandbox. And you want it to speak to Mongrel in the main interp. Imagine a MongrelConnector class that acts as medium between the two. -- master.rb -- require 'mongrel' class MongrelConnector def self.each str = yield "" # send str to mongrel end end box = Sandbox.safe box.load 'rails.rb' box.ref MongrelConnector box.eval 'start' -- web.rb -- def start MongrelConnector.each do |cgi| cgi << "hallo!" end end Inside the sandbox (where web.rb is running,) the MongrelConnector class is a BoxedClass. When `each` is called, method_missing switches sandboxes and runs the method on the class outside the box. When method_missing gets an answer back, it switches back inside the sandbox and returns an answer. For primitive data, such as numbers and strings and floats which have no instance variables, the data is marshalled. For other objects, a Sandbox::Ref is received. Both inside and outside the sandbox, a Sandbox::Ref points to data not inside the current sandbox. This ref also has a method_missing, which works just like BoxedClass' method_missing. It is not allowed to pass a Sandbox::Ref for an object whose class is not referred to in the receiving sandbox. So, if, for some reason, a method call tries to return an IO object to a sandbox and no IO class is defined (and properly ref'd,) a Sandbox::TransferException is thrown. THE PRELUDE Beyond the API, it is also required that the Sandbox run versions of common methods which are not exploitable. For example, the freaky freaky sandbox has a lib/sandbox/prelude.rb which includes a pure Ruby version of the `**` method since very high squares can lock the interpreter up in C. AND DONE That's it for now. I'm not an extreme zealot of this API, so I'd be glad to alter it or scrap it. But it has evolved through trial and error, based on xp points awarded during Try Ruby and the sandboxed wiki[3]. Thankyou for your generous attentions. [1] http://developer.mozilla.org/en/docs/XPCNativeWrapper [2] http://iolanguage.com/scm/git/checkout/Io/docs/IoReference.html [3] http://redhanded.hobix.com/inspect/howToLetAnyoneElseFinishYourWiki.html