Login  Register

[ANN] SimulationStudio and sandboxed execution for Squeak

Posted by Christoph Thiede on Mar 16, 2021; 11:21am
URL: https://forum.world.st/ANN-SimulationStudio-and-sandboxed-execution-for-Squeak-tp5127804.html

Hi all! :-)


I'm very excited to finally share my new project with you today, which I call SimulationStudio and have made available on GitHub under [1]. Its primary function is to provide an inheritable SimulationContext subclass of Context that makes it easy to simulate entire call trees while customizing the execution or installing custom hooks for various tracing and measurement purposes. Another accomplishment is the Sandbox which allows you to run arbitrary Smalltalk code in an isolated environment, separating any side effects that occur during the simulation from the rest of the image.


Sandbox

The original idea has arisen already one and a half years ago when we were discussing the necessary restrictions of the current MethodFinder design, which uses a huge allow-list of selectors that can be safely performed on unknown objects without the risk of dangerous side effects [2]. It was Eliot who proposed to apply simulation to this problem in order to run the unknown code step-by-step and prevent any side effects. I liked this idea very much and recently was finally able to build a working Sandbox prototype. However, instead of only aborting execution or creating a snapshot when a side effect is reached, I implemented an indirection layer for all side effects so that while the side effects are not actually applied to the entire image, they will be transparently available from the perspective of the code under simulation.


Here is a simple application example:


World color: Color transparent.

World color isTransparent.


If we execute these statements regularly, in the PasteUpMorph, the instance variable color will be set to transparent, then it will be accessed again and sent the message #isTransparent which will eventually be answered true.

However, in this example, your global image state would also be affected, and your wonderful desktop decorations would have been erased irrecoverably. If you only want to find out what the code above returns, you can now run it in an isolated sandbox without actually impairing your image:


Sandbox evaluate: [

    World color: Color transparent.

    World color isTransparent].


The script will answer true, again, but this time the world's actual color won't have changed. And here is how it works: The sandbox uses a SandboxContext under the hood, which is a subclass of Context that is only meant to be used for simulation purposes. This SandboxContext overrides a number of simulation operations, including #object:instVarAt:put: for writing and #object:instVarAt: for reading instance variables. Now, when an attempt is made to produce a side effect by assigning a new value to the color instvar in PasteUpMorph, the SandboxContext notices this attempt and adds the object (World) to the sandbox space, which is just a big WeakIdentityKeyDictionary that maps real-world objects to sandbox-manipulated objects. The actual manipulation then is only applied to the sandbox-manipulated copy of the original object, so the rest of the image does not get affected. Analogously, #object:instVarAt: checks whether the requested object is already part of the sandbox space, and if this is true, it redirects the instvar request to the sandboxed version of the original object. Of course, these indirections are not only implemented for instance variable accesses but also many other bytecode operations such as variable object access (#at:[put:]), identity exchanging/forwarding, instance adoption, and many other VM primitives and modules. By this means, the sandbox space is extended dynamically on demand and all side effects caused by the sandboxed code remain locally in the sandbox environment without affecting the rest of the image.


Sandboxes are thread-safe (i.e. global-state free) and self-transparent, i.e. you can even do the following:

Sandbox evaluate: [Sandbox evaluate: [6 * 7]].


SimulationContext

A central idea while implementing the SandboxContext was to inherit from the regular Context class in order to reuse the existing simulation logic. However, subclassing from Context is not straightforward due to the special role it plays in the connection to the VM Executor (shortly, Context instances are only created on demand for optimization purposes). Eliot wrote a really worthwhile description of this mechanism in [3] recently. Some context primitives and the stack functionality which is used by most bytecode operations won't work in a plain subclass of Context. For this reason, I have created the SimulationContext subclass of Context which overrides these functionalities by providing purely image-based implementations. Another responsibility of SimulationContext is to preserve the class of context instances while simulating a call stack - that is, new stack frames that are created when simulating a message send are automatically converted to instances of the current SimulationContext class, too.


With this base, it is fairly easy to create subclasses of SimulationContext to customize simulation at will, without needing to deal with any further VM magic. So does SandboxContext, but I also created another subclass just for proof of concept, SimulationMsrContext, which integrates with the MessageSendRecorder of the HPI group [4, 5] but provides a complete trace of all message sends. At the same time, SimulationMsrContext eliminates the need for installing and uninstalling global MethodWrappers in the image (which is an exclusive operation and can be slow at some times) and thus makes it even possible to transparently trace the simulation itself. Note that this only a rough experiment with some hick-ups and the concepts do not match each other perfectly. From a long-term perspective, something like a BytecodeBrowser might provide an even more detailed trace of the execution.


While SimulationContexts cannot be executed by the VM any longer, I wrote a small debugging adapter, making it possible to debug all kinds of SimulationContext in a regular debugger (see the debugging extensions on Context class). You can also debug the implementation of the SimulationContext from there by loading the BytecodeDebugger changeset [6].


Applications

I think SimulationStudio opens or at least facilitates a bunch of possibilities around the exciting simulation machinery of Squeak. I already have identified some real use cases for the sandbox:


  • An alternative MethodFinder implementation that is not restricted to an allow-list of selectors as mentioned above. A prototype for this is available in the repository on the class-side of Sandbox.
Example:
Sandbox
    findSelectorsFor: {1. 2. 3. 4}
    arguments: {[:x | x asWords] ascending}
    thatAnswer: {4. 1. 3. 2}.  "==> #(quickSort: sort: sorted:)"
The implementation itself is noticeably simple:

| selectors |
selectors := receiver class allSelectors select: [:sel | sel numArgs = arguments size].
^ Generator on: [:generator |
  selectors do: [:sel |
        | match sandbox |
        sandbox := Sandbox new.
        sandbox stepLimit: 100000.
        match := sandbox
            evaluate: [
                | result |
                result := (receiver perform: sel withArguments: arguments).
                result = expectedResult]
            ifFailed: [false].
        match ifTrue: [generator nextPut: sel] ] ]

As you can see, it would be also a small change only to find methods based on side effects instead on its return value.
  • Type-guessing support for Autocompletion: Autocompletion by Leon Matthes [7] is a code completion implementation for Squeak based on eCompletion [8] that, inter alia, provides type guessing based on previously entered literals that are recognized as the receiver, instance variables, global bindings, or quick return selectors. By combining this idea with the possibility of running arbitrary code in a sandbox, the quality of type guessing and thus completion suggestions can be improved significantly for long expressions in a scripting environment. My long-term vision in this regard is to provide a UI that is equal or superior to the code completion into Chrome Dev Console. :-)
I have available an okayish patch (< 20 lines!) for this in my image and can share you a changeset on demand, but it still requires some fine polish before I will officially release it.
  • I already have many other ideas around dynamic code analysis (for instance, fine-granular execution tracing, logging of side effects, bytecode-specific coverage measurements, revertable debugger stepping, ...) and will definitively have some more fun in this field in the near future. If you are interested, you can watch the repository on GitHub. :-)


Limitations and challenges

Well, first of all ... performance. :-) Some recent measurements that I have run have shown that the simulator reaches about 0.1 percent (sic) of the speed of the VM executor, depending on the domain and the distribution of bytecode operations. The layers added on top of the simulator by SimulationStudio, moreover the fact that I did not yet run any systematic attempts on optimization, slow down the execution even more by further ~50%. While it's still "fast enough for our neurons" in most scenarios I came up with so far, computation-intensive operations such as rendering 4K images can indeed require a certain amount of patience. However, from a conceptual perspective, I comfort myself by saying that this is "only" an optimization problem.


Another challenge is the handling of primitive operations during the sandbox execution. At the moment, I have disabled most primitives that I did not need and tried to fall back on image implementations whenever possible (see SandboxContext >> #doPrimitive:method:receiver:args: and #doNamedPrimitiveIn:for:withArgs:). However, for some areas, mostly external I/O plugins, this is not possible, and some customized solutions will be required to provide these functionalities while running in an isolated sandbox context.


Last but not least, it's hard to preserve a consistent sandbox space while making modifications in the real image. Just take the code example from above and wonder what should happen when the color of the world is changed half-way during the execution. Should the sandboxed copy of the world be affected, too? It is not possible to track changes to the real image space without severe performance drawbacks. This is a synchronization issue for which I don't know a good solution, yet.


I want you! :-)

This is an ongoing project and I'm looking very much forward to your feedback, questions, and ideas on this matter! If you are looking for a challenge, you can try to find a gap in the sandbox isolation. I have spent many hours sealing it in all conscience, I did not check everything systematically so I would not be very surprised (but delighted!) if you manage to find any vulnerability. Ideally, any kind of contribution will be welcome too, of course - just create your issues and pull requests in the repository at [1]! :-)


Carpe Squeak!


Best,

Christoph


PS: Sorry for the long text. I thank everyone who made it this far. :-)


[1] https://github.com/LinqLover/SimulationStudio
[2] http://forum.world.st/MethodFinder-Blocks-tp5105421p5105623.html
[3] http://forum.world.st/Two-new-curious-Context-primitive-questions-tp5125779p5125783.html
[4] https://github.com/hpi-swa/MessageSendRecorder
[5] http://forum.world.st/ANN-MessageSendRecorder-and-MethodWrappers-GitHub-td5120077.html
[7] https://github.com/LeonMatthes/Autocompletion
[8] https://uncomplex.net/ecompletion/



Carpe Squeak!