Delegate.Sandbox
PM> Install-Package Delegate.Sandbox
What is it?
Delegate.Sandbox is library that provides a Computation Expression named
SandboxBuilder
, sandbox{ return 42 }
, which ensures that values returned
from the computation are I/O side-effects safe (IOSafe
) and if not, they are
marked as unsafe (Unsafe
) and returning an exception.
The library allows to bind >>=
several sandbox computations together in order
to create side-effect free code and based on the final result, then proceed to
perform the desired side-effects.
Examples
The following example shows that even though there is a call to printfn
, the
output is not passed to the console and hereby, no side-effect is generated:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: |
|
Evaluates to the following output:
Sum of x and y and then power2: IOSafe 1764
Remark: No output is written to the console
In the next example, we add System.Console.ReadLine()
in order to block the
function until somebody presses enter. Additionally, if side-effects were allowed,
the entered input would affect return value of the function:
1: 2: 3: |
|
Evaluates to the following output:
Prints only 'IOSafe FooBar': IOSafe FooBar
Remark: No blocking readline or input from console.
The next example show how we try to get access to the current file directory,
count the amount of files and add it to the final result. This action will try
to perform an File IO
which is not allowed in the sandbox
. Due to this, the
whole function is evaluated to an Unsafe
value, which contain the Exception
thrown at runtime:
1: 2: 3: 4: |
|
Evaluates to the following output:
Sum of x and y (with error msg): Unsafe System.Security.SecurityException: Request for the permission of type 'System.Security.Permissions.FileIOPermission, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' failed. at System.Security.CodeAccessSecurityEngine.Check(Object demand, StackCrawlMark& stackMark, Boolean isPermSet) at System.Security.CodeAccessPermission.Demand() at System.IO.FileSystemEnumerableIterator1..ctor(String path, String originalUserPath, String searchPattern, SearchOption searchOption, SearchResultHandler
1 resultHandler, Boolean checkHost) at System.IO.Directory.EnumerateFiles(String path) at Program.addition'@12-1.Invoke(Unit unitVar) at Delegate.Sandbox.GlobalValues.SandboxBuilder.Delaya The action that failed was: Demand The type of the first permission that failed was: System.Security.Permissions.FileIOPermission The first permission that failed was:<IPermission class="System.Security.Permissions.FileIOPermission, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" PathDiscovery="D:\...\."/>
The demand was for:<IPermission class="System.Security.Permissions.FileIOPermission, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" PathDiscovery="D:\...\."/>
The granted set of the failing assembly was:<PermissionSet class="System.Security.PermissionSet" version="1"> <IPermission class="System.Security.Permissions.SecurityPermission, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Flags="UnmanagedCode, Execution"/> </PermissionSet>
The assembly or AppDomain that failed was: Sandbox, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null The method that caused the failure was: IOEffect`1 Invoke(Microsoft.FSharp.Core.Unit) The Zone of the assembly that failed was: MyComputer The Url of the assembly that failed was: file:///D:/.../bin/Release/Sandbox.EXE
Remark: The computation and bindings works like the
Either
Monad where you either have a value of the typeIOSafe
or you have anException
of the typeUnsafe
. The main point here is that the I/O side-effect is NOT performed and the computation catches the attempt by tainting the whole expression and providing the thrownException
which can be re-thrown or logged in order to revise and fix the code.
How it works and limitations
- A few words on the
SandboxBuilder
works:- The library is built on top of the AppDomain Class which
allows to Run Partially Trusted Code in a Sandbox. The
SandboxBuilder
is only allowed to execute code(SecurityPermissionFlag.Execution)
, which is the minimum permission that can be granted (Principle of least privilege). sandbox
is implemented as a computation expression that only implements
return (Return : v:'b -> 'b IOEffect
), which ensures that values returning from the computation are of the desired value type, and delay (Delay : f:(unit -> 'a IOEffect) -> 'a IOEffect
), which tries to evaluate the function at the newly created domain (AppDomain
) with the minimum granted permission instead of the executingAppDomain.CurrentDomain
. If the function evaluation is successful then anIOSafe 'a
value is returned, otherwise anUnsafe
Exception
is returned.- In order to ensure that
IOEffect
types are only instantiated from inside the computation expression, a few examples:IOSafe "42"
orIOSafe (fun _ -> Directory.EnumerateFiles(".") |> Seq.length)
, we use type encapsulation and we afterwards expose them with the help of active patterns. For more info on this matter, please see this Gist from Scott Wlaschin. - To remove
System.Console
I/O side-effects, we need to execute someSecurityPermissionFlag.UnmanagedCode
before we instantiate theSandboxBuilder
. This is handled byRemoveConsoleInOutEffects
. When the type is instantiated, theSystem.Console.SetIn
,System.Console.SetOut
andSystem.Console.SetError
are set toStream.Null
. Once this task is performed, theSecurityPermissionFlag.UnmanagedCode
flag is removed in order for the newAppDomain
runs with the minimal permission possible. For more information, please look into the code (about +80 lines) at GitHub
- The library is built on top of the AppDomain Class which
allows to Run Partially Trusted Code in a Sandbox. The
- We describe a few limitations we found while we were making the library:
- No code optimization: When a project that refers to the library is built in
Release
mode, default is set toOptimize code
, then it will not work as some of the code is transformed to useReflection
which is not supported in theAppDomain
. - Unit tests: As stated before,
Reflection
is not supported and because NUnit uses this approach to execute the test, then it will not work either. This makes it really difficult to test code, mostly becauseUnsafe
types are runtime and not compile time. - F# Interactive (fsiAnyCpu.exe): As the computation expression is built on
top of the
AppDomain
, it will not be possible to use this library in interactive mode (scripts, ...).
- No code optimization: When a project that refers to the library is built in
Contributing and copyleft
The project is hosted on GitHub where you can report issues, fork the project and submit pull requests.
The library is available under an Open Source MIT license, which allows modification and redistribution for both commercial and non-commercial purposes. For more information see the License file in the GitHub repository.
member Clone : unit -> obj
member DynamicInvoke : params args:obj[] -> obj
member Equals : obj:obj -> bool
member GetHashCode : unit -> int
member GetInvocationList : unit -> Delegate[]
member GetObjectData : info:SerializationInfo * context:StreamingContext -> unit
member Method : MethodInfo
member Target : obj
static member Combine : params delegates:Delegate[] -> Delegate + 1 overload
static member CreateDelegate : type:Type * method:MethodInfo -> Delegate + 9 overloads
...
Full name: System.Delegate
module IOEffect
from Delegate.Sandbox.GlobalValues
--------------------
type 'a IOEffect =
private | IOSafe of 'a
| Unsafe of exn
override ToString : unit -> string
Full name: Delegate.Sandbox.GlobalValues.IOEffect<_>
Full name: Delegate.Sandbox.GlobalValues.IOEffect.bind
Full name: Index.addition
Full name: Delegate.Sandbox.GlobalValues.sandbox
Full name: Index.power2
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printfn
Full name: Index.result
Full name: Index.fooBar
static member BackgroundColor : ConsoleColor with get, set
static member Beep : unit -> unit + 1 overload
static member BufferHeight : int with get, set
static member BufferWidth : int with get, set
static member CapsLock : bool
static member Clear : unit -> unit
static member CursorLeft : int with get, set
static member CursorSize : int with get, set
static member CursorTop : int with get, set
static member CursorVisible : bool with get, set
...
Full name: System.Console
Full name: Index.addition'
static member CreateDirectory : path:string -> DirectoryInfo + 1 overload
static member Delete : path:string -> unit + 1 overload
static member EnumerateDirectories : path:string -> IEnumerable<string> + 2 overloads
static member EnumerateFileSystemEntries : path:string -> IEnumerable<string> + 2 overloads
static member EnumerateFiles : path:string -> IEnumerable<string> + 2 overloads
static member Exists : path:string -> bool
static member GetAccessControl : path:string -> DirectorySecurity + 1 overload
static member GetCreationTime : path:string -> DateTime
static member GetCreationTimeUtc : path:string -> DateTime
static member GetCurrentDirectory : unit -> string
...
Full name: System.IO.Directory
Directory.EnumerateFiles(path: string, searchPattern: string) : Collections.Generic.IEnumerable<string>
Directory.EnumerateFiles(path: string, searchPattern: string, searchOption: SearchOption) : Collections.Generic.IEnumerable<string>
from Microsoft.FSharp.Collections
Full name: Microsoft.FSharp.Collections.Seq.length