Hey Nick,

Looking forward to the RFC!

On Tue, Aug 6, 2024, at 19:28, Nick Lockheart wrote:
> > 
> > This looks quite valuable, and I assume auto loading would work just
> > like normal? Register an autoloader that will eventually require the
> > file and call this function?
> > 
> > It would be nice to provide a simplified api as well, maybe
> > “CopyCurrentEnvironment()” or something?  In most cases, it is
> > easier/faster to find things to remove vs. adding everything on every
> > plugin/request every time. 
> > 
> > In saying that, it would be great if there was an api for “sharing” a
> > base-sandbox pool via shm (or similar to a pool) so that the base vm
> > doesn’t need to be recreated potentially hundreds of times per
> > request. 
> > 
> 
> I didn't want to be too overwhelming on the first post, but since it
> seems the feedback is positive, here's a more complete list of what I
> think should be included:
> 
> 
> // Passthroughs:
> 
> // Make all user and built-in global functions
> // available inside the sandbox:
> SPLSandBox::PassGlobalFunctions();

Bike shed: maybe have PassGlobals() instead? Or rather, PassNamespace and have 
\ be a valid namespace. 

> 
> // Make all built-in (but not user) functions
> // available inside the sandbox:
> SPLSandBox::PassBuiltInFunctions();
> 
> 
> // Make all built-in (but not user) functions
> // available inside the sandbox, EXCEPT blacklisted functions:
> SPLSandBox::PassBuiltInFunctionsExcept(['eval','exit']);
> 
> (assuming exit becomes a function).

I feel like exit (function or not) should just return from the sandbox and 
shouldn’t be disable-able. For example, a plugin might detect a valid etag and 
set headers to 302 and exit. 

> 
> 
> // Allow only specific functions to be called (whitelist method):
> $aWhiteList = ['array_key_exists','in_array'];
> SPLSandBox::PassFunctions($aWhiteList);

Might be a good idea to combine these two? Allow passing a whitelist AND a 
blacklist? Are these supposed to be static or on an instance of a sandbox?

> 
> // Allow specific classes to be used by sandbox code:
> $aClassList = ['\MyAPP\PluginAPI'];
> SPLSandBox::PassClasses($aClassList);
> 
> 
> // Allow specific constants to be seen by sandbox code:
> SPLSandBox::PassConstants(['\DB_USERNAME','\DB_PASSWORD']);
> 
> 
> 
> // Language Construct Callbacks:
> 
> The callbacks allow the outer code to control and monitor certain
> language features of the sandboxed code during execution.
> 
> // Called when the sandbox code tries to include or require something:
> SPLSandBox::RegisterIncludeHandler();

Does including a file from outside the sandbox (next call) call this handler?

> 
> // Includes a file into the sandbox:
> SPLSandBox::Include('path/to/file.php');
> 
> // Your sandbox autoloader logic could be incorporated here:
> SPLSandBox::RegisterAutoLoadHandler();
> 
> 
> // But, for unit testing with mocks and stubs,
> // it might be better to use:
> SPLSandBox::RegisterNewHandler();
> 
> The NewHandler callback is called every time sandboxed code tries to
> instantiate an object with `new`.

Why not use the current autoload logic?

> // Example: Override what `new` returns to code running in the sandbox:
> function MyNewHandler(string $ClassName, array $aConstructorArgs){
> 
>    if($ClassName === '\DateTime'){
>       return new FakeDate();
>    }
>    return new $ClassName($aConstructorArgs);
> }
> 
> 
> // Every time a sandboxed class calls a method, call this first:
> SPLSandBox::RegisterMethodCallHandler();
> 
> Useful for unit testing to monitor if the tested class is calling the
> methods it should be calling. Ignores visibility rules.
> 
> Could also allow for infinite recursion detection from the outside.

I think this is handled automatically now. 

> 
> 
> // The companion for static method calls, gets called
> // every time a method is called on a class statically:
> SPLSandBox::RegisterStaticMethodCallHandler();
> 
> 
> 
> // Each time a sandboxed loop iterates, call this first:
> // Allows the outer code to put limit breaks on the sandboxed code.
> SPLSandBox::RegisterLoopHandler();
> 
> The callback takes the type of loop, and the variables that make up the
> loop ($i for for(), $Key => $value for foreach(), etc)
> 
> 
> // If the sandboxed code calls echo, print, or
> // causes any output to occur (ie outside of <?php tags:
> SPLSandBox::RegisterEchoHandler();
> 
> Could be used to make sure templates behave as desired, but perhaps
> even more useful, it lets you *fail* unit tests if any output occurs
> from a test that shouldn't produce output.
> 
> ie. Catch echo statements used in testing, or whitespace after a
> closing ?> tag.
> 
> 
> // If the sandbox code tries to use `exit` or `die`,
> // call this function instead:
> SPLSandBox::RegisterExitHandler();
> 
> You'll probably want to destroy the sandbox from the outside (see
> below), rather than letting sandboxed code halt the test framework or
> main application.
> 
> 
> // If sandboxed code throws, it should *not*
> // be a throw in the outer application space.
> // Every exception throw triggers this callback,
> // even if there is a catch block:
> SPLSandBox::RegisterExceptionHandler();
> 
> // When a catch block runs, invoke this callback first:
> SPLSandBox::RegisterCatchHandler();
> 
> Allows unit tests to make sure that exceptions are handled correctly.
> 
> 
> // For non-Exceptions (warning, notice, deprecated, fatal,
> // and yes, maybe even parse because we're in a sandbox):
> SPLSandBox::RegisterErrorHandler();
> 
> 
> 
> // Inside your Handlers, you may want to know the file and line
> // that triggered the callback. 
> SPLSandBox::GetCurrentLine();
> SPLSandBox::GetFileName();
> SPLSandBox::GetClassName();
> SPLSandBox::GetFunctionName();
> 
> For example, if you want to catch any echo left behind from debugging,
> you might also want to find the line and file where the statement is
> located to remove it. The above methods would be usable inside any of
> the callbacks.
> 
> 
> // Inside your Handlers, abort execution:
> SPLSandBox::Stop();

> 
> 
> // Resource Limits, hopefully self explanatory:
> SPLSandBox::SetMemoryLimit();
> SPLSandBox::SetExecutionTimeLimit();
> 
> 
> // Mocks, Stubs:
> 
> // Put a function from the outer application
> // into the sandbox as the specified name:
> SPLSandBox::MockFunction('\mocks\fopen','\fopen');
> 
> 
> // Put a class from the outer application
> // into the sandbox as the specified name:
> SPLSandBox::MockClass('\Mocks\FakeTime','\DateTime');
> 
> 
> // Set global variables (and super globals) inside the sandbox:
> SPLSandBox::MockGlobal('$_GET',$aGetVars); 
> 
> 
> // Set global constants inside the sandbox:
> SPLSandBox::MockConstant('MY_CONSTANT',$Value); 
> 
> 
> 
> // Invocation
> 
> // You can run procedural code to setup your test environment.
> // Runs the array of code lines in the sandbox context:
> $aProceduralCode = [
>    "$a = 1;",
>    "$b = 2;",
>    "$c = DoSomething($a, $b);"
> ];
> 
> SPLSandBox::Procedure($aProceduralCode);
> 
> 
> // You can get a pointer to an object instantiated in the sandbox:
> $oClass = SPLSandBox::GetInstance('ClassName');
> 
> // And use it like you normally would:
> $oClass->DoSomething();
> 
> This class runs entirely in the sandbox.
> 
> 
> // You cleanup the resource with:
> SPLSandBox::Destroy();
> 

Looks pretty exciting and useful! Since you’re imagining it being a part of 
SPL, why not implement this in its own extension? It looks like the pecl 
process is pretty convoluted to get an extension listed there, but many popular 
extensions live outside of pecl too.

— Rob

Reply via email to