• Stars
    star
    598
  • Rank 74,853 (Top 2 %)
  • Language
    C#
  • License
    MIT License
  • Created over 1 year ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A library for running isolated .NET runtimes inside .NET

DotNetIsolator [EXPERIMENTAL]

Lets your .NET code run other .NET code in an isolated environment easily.

Basic concept:

  1. Create as many IsolatedRuntime instances as you like.
    • Each one is actually a WebAssembly sandbox built with dotnet-wasi-sdk and running on Wasmtime.
    • Each one has a completely separate memory space and no direct access to the host machine's disk/network/OS/etc.
  2. Make .NET calls into IsolatedRuntime instances.
    • Either create IsolatedObject instances within those runtimes then invoke their methods
    • ... or just call a lambda method directly
    • You can pass/capture/return arbitrary values across the boundary, and they will be serialized automatically via Messagepack

This is experimental and unsupported. It may or may not be developed any further. There will definitely be functional gaps. There are no guarantees about security.

Getting started

First, install the package:

dotnet add package DotNetIsolator --prerelease

Now try this code:

// Set up an isolated runtime
using var host = new IsolatedRuntimeHost().WithBinDirectoryAssemblyLoader();
using var runtime = new IsolatedRuntime(host);

// Output: I'm running on X64
Console.WriteLine($"I'm running on {RuntimeInformation.OSArchitecture}");

runtime.Invoke(() =>
{
    // Output: I'm running on Wasm
    Console.WriteLine($"I'm running on {RuntimeInformation.OSArchitecture}");
});

Or, for a more involved example:

// Set up the runtime
using var host = new IsolatedRuntimeHost().WithBinDirectoryAssemblyLoader();
using var isolatedRuntime = new IsolatedRuntime(host);

// Evaluate the environment info in both the host runtime and the isolated one
var realInfo = GetEnvironmentInfo();
var isolatedInfo = isolatedRuntime.Invoke(GetEnvironmentInfo);
Console.WriteLine($"Real env: {realInfo}");
Console.WriteLine($"Isolated env: {isolatedInfo}");

static EnvInfo GetEnvironmentInfo()
{
    var sysRoot = Environment.GetEnvironmentVariable("SystemRoot") ?? "(Not set)";
    return new EnvInfo(
        Environment.GetEnvironmentVariables().Count,
        $"SystemRoot={sysRoot}");
}

// Demonstrates that you can return arbitrarily-typed objects
record EnvInfo(int NumEnvVars, string ExampleEnvVar)
{
    public override string ToString() => $"{NumEnvVars} entries, including {ExampleEnvVar}";
}

Output, which will differ slighly on macOS/Linux:

Real env: 64 entries, including SystemRoot=C:\WINDOWS
Isolated env: 0 entries, including SystemRoot=(Not set)

API guides

Creating an IsolatedRuntimeHost

First you must create an IsolatedRuntimeHost. These host objects can be shared across users, since they don't hold any per-runtime state. If you're using DI, you could register it as a singleton.

The purpose of this is to:

  • Start up Wasmtime. This takes ~400ms so you only want to do it once and not every time you instantiate an IsolatedRuntime
  • Configure assembly loading

Configuring assembly loading

The isolated .NET runtime instances need to load .NET assemblies in order to do anything useful. This package includes a WebAssembly-specific .NET base class library (BCL) for low-level .NET types such as int, string, Dictionary<T, U>, etc. Isolated runtimes always have permission to load these prebundled BCL assemblies.

You will almost always also want to load application-specific assemblies into your isolated runtimes, so that you can run your own code. The easiest way to configure assembly loading is to use WithBinDirectoryAssemblyLoader:

using var host = new IsolatedRuntimeHost()
    .WithBinDirectoryAssemblyLoader();

This grants permission to load .NET assemblies from your host application's bin directory. This makes it possible to:

  • Invoke lambda methods (since the code for those methods is inside the DLLs in your bin directory)
  • Instantiate objects of arbitrary types inside isolated runtimes
  • Pass/return types declared in your own application or other packages that you reference

Note that WithBinDirectoryAssemblyLoader does not allow the guest code to escape from its sandbox. Even though it can load assemblies from the host application, it can only use them within its sandbox.

If you want to impose stricter controls over assembly loading, then instead of WithBinDirectoryAssemblyLoader, you can use WithAssemblyLoader:

using var host = new IsolatedRuntimeHost()
    .WithAssemblyLoader(assemblyName =>
    {
        switch (assemblyName)
        {
            case "MyAssembly":
                return File.ReadAllBytes("some/path/to/MyAssembly.dll");
        }

        return null; // Unknown assembly. Maybe another loader will find it.
    });

You can register as many assembly loaders as you wish.

Creating an IsolatedRuntime

Once you have an IsolatedRuntimeHost, it's trivial to create as many runtimes as you like:

using var runtime1 = new IsolatedRuntime(host);
using var runtime2 = new IsolatedRuntime(host);

Currently, each runtime takes ~8ms to instantiate.

Calling lambdas

Once you have an IsolatedRuntime, you can dispatch calls into them by using Invoke and lambda methods:

var person1 = new Person(3);
var person2 = new Person(9);

var sumOfAges = runtime.Invoke(() =>
{
    // This runs inside the isolated runtime.
    // Notice that we can use closure-captured values/objects too.
    // They will be serialized in using MessagePack.
    return person1.Age + person2.Age;
});

// Output: The isolated runtime calculated the result: 12
Console.WriteLine($"The isolated runtime calculated the result: {sumOfAges}");

record Person(int Age);

Note that if the lambda mutates the value of captured objects or static fields, those changes will only take effect inside the isolated runtime. It cannot affect objects in the host runtime, since there is no direct sharing of memory:

public static int StaticCounter = 0;

private static void Main(string[] args)
{
    using var host = new IsolatedRuntimeHost().WithBinDirectoryAssemblyLoader();
    using var runtime = new IsolatedRuntime(host);

    int localValue = 0;

    runtime.Invoke(() =>
    {
        StaticCounter++;
        localValue++;
        Console.WriteLine($"(isolated) StaticCounter={StaticCounter}, localValue={localValue}");
    });

    Console.WriteLine($"(host)     StaticCounter={StaticCounter}, localValue={localValue}");

    // The output is:
    // (isolated) StaticCounter=1, localValue=1
    // (host)     StaticCounter=0, localValue=0
}

Instantiating isolated objects

Using lambdas is convenient, but only works if the isolated runtime is allowed to load the assemblies from your bin directory (because that's where the code is).

As an alternative, you can manually instantiate isolated objects inside the isolated runtime, then call methods on them. For example:

// Generic API
IsolatedObject obj1 = runtime.CreateObject<Person>();

// String-based API (useful if the host app doesn't reference the assembly containing the type)
IsolatedObject obj2 = runtime.CreateObject("MyAssembly", "MyNamespace", "Person");

CreateObject requires the object type to have a parameterless constructor. Support for constructor parameters isn't yet implemented (but would be simple to do).

Calling methods on isolated objects

You can use Invoke or InvokeVoid to find a method and invoke it in a single step. For example, if the object has a method void DoSomething(int value):

isolatedObject.InvokeVoid("DoSomething", 123);

If it has a return value, you must specify the type as a generic parameter. For example, if the object has a method TimeSpan GetAge(bool includeGestation):

TimeSpan result = isolatedObject.Invoke<bool, TimeSpan>("GetAge", /* includeGestation */ true);

Alternatively you can capture a reference to an IsolateMethod so you can invoke it later. This is similar to a MethodInfo so it isn't bound to a specific target object.

var getAgeMethod = isolatedObject.FindMethod("GetAge");

// ... then later:
var age = getAgeMethod.Invoke<bool, TimeSpan>(isolatedObject, /* includeGestation */ true);

You can also find methods without having to instantiate any objects first:

var getAgeMethod = isolatedRuntime.GetMethod(typeof(Person), "GetAge");

Calling the host from the guest

The host may register named callbacks that can be invoked from guest code. For example:

using var runtime = new IsolatedRuntime(host);
runtime.RegisterCallback("addTwoNumbers", (int a, int b) => a + b);
runtime.RegisterCallback("getHostTime", () => DateTime.Now);

To call these from guest code, have the guest code's project reference the DotNetIsolator.Guest package, and then use DotNetIsolatorHost.Invoke, e.g.:

var sum = DotNetIsolatorHost.Invoke<int>("addTwoNumbers", 123, 456);
var hostTime = DotNetIsolatorHost.Invoke<DateTime>("getHostTime");

Note that if you're calling via a lambda, then the guest code is in the same assembly as the host code, so in that case you need the host project to reference the DotNetIsolator.Guest package.

Security notes

If you want to rely on this isolation as a critical security boundary in your application, you should bear in mind that:

  • This is an experimental prerelease package. No security review has taken place. There could be defects that allow guest code to cause unintentional effects on the host.
  • WebAssembly itself defines an extremely well-proven sandbox (browsers run untrusted WebAssembly modules from any website, and have done so for years with a solid track record), but:
    • Wasmtime is a different implementation than what runs inside your browser. Learn more at Security and Correctness in Wasmtime.
    • The security model for WebAssembly doesn't directly address side-channel attacks (e.g., spectre). There are robust solutions for this but it's outside the scope of this repo.

In summary:

  • If you used this as one layer in a multi-layered security model, it would be a pretty good layer! But nobody's promising it's bulletproof on its own.
  • If you're not running potentially hostile code, and are merely using this to manage the isolation of your own code, most of the above considerations don't apply.

Support and feedback

This is completely unsupported. There are no promises that this will be developed any further. It is published only to help people explore what they could do with this sort of capability.

You are free to report issues but please don't assume you'll get any response, much less a fix.

More Repositories

1

WebWindow

.NET Core library to open native OS windows containing web UI on Windows, Mac, and Linux. Experimental.
TypeScript
1,983
star
2

CarChecker

A sample Blazor WebAssembly application that includes authentication, in-browser data storage, offline support, localization, responsive layouts, and more. For a video walkthrough, see this link:
C#
811
star
3

BlazeOrbital

Sample application for Blazor WebAssembly on .NET 6
C#
524
star
4

dotnet-wasi-sdk

Packages for building .NET projects as standalone WASI-compliant modules
C#
519
star
5

BlazorDesktop

TypeScript
286
star
6

BlazorInputFile

A file input component for Blazor applications
C#
251
star
7

presentation-2019-06-NDCOslo

NDC Oslo 2019
160
star
8

BlazorElectronExperiment.Sample

HTML
158
star
9

BlazorOnGitHubPages

A simple example of hosting a Blazor WebAssembly app on GitHub pages
HTML
105
star
10

BlazorUnitTestingPrototype

C#
93
star
11

presentation-2020-01-DotNetConf

80
star
12

wasm-component-sdk

Tooling for creating WebAssembly components from C#
C#
77
star
13

presentation-2019-10-NDCSydney

73
star
14

AudioBrowser

A media browser app using Blazor WebAssembly with .NET 7 and 8 features
C#
70
star
15

BlazorGrpcSamples

C#
61
star
16

StatefulReconnection

JavaScript
48
star
17

ProductsManager

Simple Blazor list/editor sample using .NET 7 features
JavaScript
46
star
18

MonacoRazor

A Blazor component that provides the Monaco code editor
HTML
46
star
19

presentation-2020-01-NdcBlazorComponentLibraries

Shell
46
star
20

PictureFixer

C#
44
star
21

presentation-2020-01-NDCLondon

43
star
22

BlazorComponentDemos

JavaScript
27
star
23

MyDotNet8PWA

Sample of a .NET 8 Blazor Web app configured to serve an offline-enabled Blazor PWA
HTML
24
star
24

aspnetcore-in-browser

HTML
23
star
25

presentation-2021-01-NDCLondon

22
star
26

GreenhouseMonitor

Sample application for ASP.NET Core on WASI
C#
21
star
27

RazorComponents.MaterialDesign

JavaScript
21
star
28

MinimalDotNetWasmNativeAOT

Small examples of .NET compiling to wasi-wasm via NativeAOT-LLVM
C#
15
star
29

il2wasm

Do not use
C
15
star
30

ghaction-rewrite-base-href

GitHub action to rewrite the base href in an HTML file
JavaScript
12
star
31

spiderlightning-dotnet

C
12
star
32

CircuitPersisterExample

Sample
HTML
9
star
33

RazorComponentsSentimentAnalysis

Example of combining Razor Components with ML.NET for realtime sentiment analysis while typing
C#
9
star
34

TemporaryBlazorWebNet8TemplateExperiments

Just to try out some different conventions and file structures
CSS
8
star
35

BlazorExtraLinking

Experiment with more linking on a Blazor app
HTML
6
star
36

blazor-benchmarks

JavaScript
5
star
37

AngularBasicTemplate

C#
5
star
38

CurrencyConverter

HTML
5
star
39

BlazorPerfTest

Scenarios to help define perf tuning goals
HTML
4
star
40

BlazorElectronExperiment.Packages

TypeScript
3
star
41

sample-with-caching

CSS
2
star
42

angular2-universal-patch

Updates angular2-universal to support Angular 2.3+. Will no longer be needed when Angular 4 is released.
JavaScript
2
star
43

SignalRFirefoxRepro

HTML
1
star
44

chunktest

HTML
1
star