Delegate A/S

Delegate.Sandbox

The Delegate.Sandbox library can be installed from NuGet:
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: 
open System
open System.IO
open Delegate.Sandbox

let inline (>>=) m f = IOEffect.bind f m

let addition x y = sandbox{ return x + y }
let power2 x = sandbox{ printfn "Injected side-effect"; return x * x }
let result = addition 21 21 >>= power2

printfn "Sum of x and y, then power2: %A" result

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: 
let fooBar = sandbox{ return Console.ReadLine() + "FooBar" }

printfn "Prints only 'IOSafe FooBar': %A" fooBar

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: 
let addition' x y = sandbox{ 
  return (Directory.EnumerateFiles(".") |> Seq.length) + x + y }

printfn "Sum of x and y, then power2 (with error msg): %A" (addition' 21 21 >>= power2)

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, SearchResultHandler1 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 type IOSafe or you have an Exception of the type Unsafe. 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 thrown Exception 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 executing AppDomain.CurrentDomain. If the function evaluation is successful then an IOSafe 'a value is returned, otherwise an Unsafe Exception is returned.
    • In order to ensure that IOEffect types are only instantiated from inside the computation expression, a few examples: IOSafe "42" or IOSafe (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 some SecurityPermissionFlag.UnmanagedCode before we instantiate the SandboxBuilder. This is handled by RemoveConsoleInOutEffects. When the type is instantiated, the System.Console.SetIn, System.Console.SetOut and System.Console.SetError are set to Stream.Null. Once this task is performed, the SecurityPermissionFlag.UnmanagedCode flag is removed in order for the new AppDomain runs with the minimal permission possible.
    • For more information, please look into the code (about +80 lines) at GitHub

  • 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 to Optimize code, then it will not work as some of the code is transformed to use Reflection which is not supported in the AppDomain.
    • 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 because Unsafe 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, ...).

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.

namespace System
namespace System.IO
type Delegate =
  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
namespace Delegate.Sandbox
val m : 'a IOEffect
val f : ('a -> 'b IOEffect)
Multiple items
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<_>
val bind : f:('a -> 'b IOEffect) -> _arg1:'a IOEffect -> 'b IOEffect

Full name: Delegate.Sandbox.GlobalValues.IOEffect.bind
val addition : x:int -> y:int -> int IOEffect

Full name: Index.addition
val x : int
val y : int
val sandbox : SandboxBuilder

Full name: Delegate.Sandbox.GlobalValues.sandbox
val power2 : x:int -> int IOEffect

Full name: Index.power2
val printfn : format:Printf.TextWriterFormat<'T> -> 'T

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printfn
val result : int IOEffect

Full name: Index.result
val fooBar : string IOEffect

Full name: Index.fooBar
type Console =
  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
Console.ReadLine() : string
val addition' : x:int -> y:int -> int IOEffect

Full name: Index.addition'
type Directory =
  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) : Collections.Generic.IEnumerable<string>
Directory.EnumerateFiles(path: string, searchPattern: string) : Collections.Generic.IEnumerable<string>
Directory.EnumerateFiles(path: string, searchPattern: string, searchOption: SearchOption) : Collections.Generic.IEnumerable<string>
module Seq

from Microsoft.FSharp.Collections
val length : source:seq<'T> -> int

Full name: Microsoft.FSharp.Collections.Seq.length
Fork me on GitHub