Skip to content

Commit

Permalink
Mechanism for guest to call host
Browse files Browse the repository at this point in the history
  • Loading branch information
SteveSandersonMS committed Apr 21, 2023
1 parent b38ab1c commit 805563e
Show file tree
Hide file tree
Showing 16 changed files with 319 additions and 8 deletions.
6 changes: 4 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@ jobs:
- name: .NET test
run: dotnet test test/DotNetIsolator.Test --no-restore --verbosity normal
- name: .NET pack
run: dotnet pack src/DotNetIsolator -c Release --no-restore /p:VersionSuffix=${{ env.PACKAGE_VERSION_SUFFIX }}
run: |
dotnet pack src/DotNetIsolator -c Release --no-restore /p:VersionSuffix=${{ env.PACKAGE_VERSION_SUFFIX }}
dotnet pack src/DotNetIsolator.Guest -c Release --no-restore /p:VersionSuffix=${{ env.PACKAGE_VERSION_SUFFIX }}
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: nuget-packages
path: src/DotNetIsolator/bin/Release/*.nupkg
path: artifacts/*.nupkg
if-no-files-found: error
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ obj/
bin/
*.suo
*.csproj.user
artifacts/
7 changes: 7 additions & 0 deletions DotNetIsolator.sln
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetIsolator.Test", "test
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleSample.GuestCode", "sample\ConsoleSample.GuestCode\ConsoleSample.GuestCode.csproj", "{A17D7A77-9791-4BD5-B6FB-6E300189F8E7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetIsolator.Guest", "src\DotNetIsolator.Guest\DotNetIsolator.Guest.csproj", "{81208A63-AE8F-4CD0-8D63-54D54AFC25A8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -45,6 +47,10 @@ Global
{A17D7A77-9791-4BD5-B6FB-6E300189F8E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A17D7A77-9791-4BD5-B6FB-6E300189F8E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A17D7A77-9791-4BD5-B6FB-6E300189F8E7}.Release|Any CPU.Build.0 = Release|Any CPU
{81208A63-AE8F-4CD0-8D63-54D54AFC25A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{81208A63-AE8F-4CD0-8D63-54D54AFC25A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{81208A63-AE8F-4CD0-8D63-54D54AFC25A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{81208A63-AE8F-4CD0-8D63-54D54AFC25A8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -55,5 +61,6 @@ Global
{AF18FD66-3A86-4689-851E-9947DF27887B} = {037C7D98-EFBB-43B7-AC52-D2C36B20E5F0}
{8B7B978B-AA75-428F-8027-3E6BAF5716D1} = {66E86F53-C28F-4FE3-83B3-0553C0FEDDE1}
{A17D7A77-9791-4BD5-B6FB-6E300189F8E7} = {39286B3D-C643-49DE-A1A0-F51D0F3B945A}
{81208A63-AE8F-4CD0-8D63-54D54AFC25A8} = {037C7D98-EFBB-43B7-AC52-D2C36B20E5F0}
EndGlobalSection
EndGlobal
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,25 @@ 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:

```cs
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.:

```cs
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:
Expand Down
10 changes: 10 additions & 0 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project>
<PropertyGroup>
<PackageOutputPath>$(MSBuildThisFileDirectory)../artifacts</PackageOutputPath>
<VersionPrefix>0.1.0</VersionPrefix>
<VersionSuffix>dev</VersionSuffix>
<Authors>Steve Sanderson</Authors>
<Company>Microsoft</Company>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
</Project>
14 changes: 14 additions & 0 deletions src/DotNetIsolator.Guest/DotNetIsolator.Guest.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MessagePack" Version="2.5.103" />
</ItemGroup>

</Project>
38 changes: 38 additions & 0 deletions src/DotNetIsolator.Guest/DotNetIsolatorHost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using DotNetIsolator.Guest;
using MessagePack.Resolvers;
using MessagePack;
using System.Text;

namespace DotNetIsolator;

public static class DotNetIsolatorHost
{
public static unsafe void Invoke(string callbackName, params object[] args)
=> Invoke<object>(callbackName, args);

public static unsafe T Invoke<T>(string callbackName, params object[] args)
{
var argsSerialized = args.Select(a => a is null
? null
: MessagePackSerializer.Serialize(a.GetType(), a, ContractlessStandardResolverAllowPrivate.Options))
.ToArray();

var callInfo = new GuestToHostCall { CallbackName = callbackName, ArgsSerialized = argsSerialized };
var callInfoBytes = MessagePackSerializer.Serialize(callInfo, ContractlessStandardResolverAllowPrivate.Options);

fixed (void* callInfoPtr = callInfoBytes)
{
var success = Interop.CallHost(callInfoPtr, callInfoBytes.Length, out var resultPtr, out var resultLength);
var result = (int)resultPtr == 0 ? null : new Span<byte>(resultPtr, resultLength);
if (success)
{
return (int)resultPtr == 0 ? default! : MessagePackSerializer.Deserialize<T>(result.ToArray(), ContractlessStandardResolverAllowPrivate.Options);
}
else
{
var errorString = Encoding.UTF8.GetString(result);
throw new InvalidOperationException($"Call to host failed: {errorString}");
}
}
}
}
9 changes: 9 additions & 0 deletions src/DotNetIsolator.Guest/GuestToHostCall.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace DotNetIsolator;

#pragma warning disable CS0649
internal struct GuestToHostCall
{
public string CallbackName;
public byte[]?[] ArgsSerialized;
}
#pragma warning restore CS0649
9 changes: 9 additions & 0 deletions src/DotNetIsolator.Guest/Interop.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Runtime.CompilerServices;

namespace DotNetIsolator.Guest;

internal static class Interop
{
[MethodImpl(MethodImplOptions.InternalCall)]
public static unsafe extern bool CallHost(void* invocationPtr, int invocationLength, out void* result, out int resultLength);
}
1 change: 1 addition & 0 deletions src/DotNetIsolator.WasmApp/DotNetIsolator.WasmApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
<ItemGroup>
<WasiNativeFileReference Include="native\*.c" />
<WasiAfterRuntimeLoaded Include="dotnetisolator_add_assembly_search_hook" />
<WasiAfterRuntimeLoaded Include="dotnetisolator_add_host_callback_internal_calls" />
</ItemGroup>

</Project>
10 changes: 10 additions & 0 deletions src/DotNetIsolator.WasmApp/native/host_callback.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#include <stdio.h>
#include <mono-wasi/driver.h>

__attribute__((import_module("dotnetisolator")))
__attribute__((import_name("call_host")))
int dotnetisolator_call_host(void* invocation, int invocation_length, void** result, int* result_length);

void dotnetisolator_add_host_callback_internal_calls() {
mono_add_internal_call("DotNetIsolator.Guest.Interop::CallHost", dotnetisolator_call_host);
}
9 changes: 4 additions & 5 deletions src/DotNetIsolator/DotNetIsolator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<WasmAppProjectName>DotNetIsolator.WasmApp</WasmAppProjectName>
<VersionPrefix>0.1.0</VersionPrefix>
<VersionSuffix>dev</VersionSuffix>
<Authors>Steve Sanderson</Authors>
<Company>Microsoft</Company>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<BundledFilesDir>IsolatedRuntimeHost\</BundledFilesDir>
<BundledWasmAssembliesDir>$(BundledFilesDir)WasmAssemblies\</BundledWasmAssembliesDir>
<NoWarn>$(NoWarn);NU5100</NoWarn> <!-- We're bundling some .dlls as content, so don't warn about them not being in lib -->
Expand All @@ -24,6 +19,10 @@
<Content Include="build\DotNetIsolator.props" PackagePath="build" />
</ItemGroup>

<ItemGroup>
<Compile Include="..\DotNetIsolator.Guest\GuestToHostCall.cs" />
</ItemGroup>

<!-- Reference the WasmApp project in such a way that we acquire its .wasm and WebAssembly BCL (.dll) files, but not its primary assembly -->
<ItemGroup>
<ProjectReference Include="..\DotNetIsolator.WasmApp\DotNetIsolator.WasmApp.csproj">
Expand Down
67 changes: 67 additions & 0 deletions src/DotNetIsolator/IsolatedRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ public class IsolatedRuntime : IDisposable
private readonly Action<int> _releaseObject;
private readonly ConcurrentDictionary<(string AssemblyName, string? Namespace, string TypeName, string MethodName, int NumArgs), IsolatedMethod> _methodLookupCache = new();
private readonly ShadowStack _shadowStack;
private readonly Dictionary<string, Delegate> _registeredCallbacks = new();
private bool _isDisposed;

public IsolatedRuntime(IsolatedRuntimeHost host)
{
var store = new Store(host.Engine);
store.SetWasiConfiguration(host.WasiConfigurationOrDefault);
store.SetData(this);

_store = store;
_instance = host.Linker.Instantiate(store, host.Module);
Expand All @@ -53,6 +55,17 @@ public IsolatedRuntime(IsolatedRuntimeHost host)
// startExport.Invoke();
}

internal static IsolatedRuntime FromStore(Store store)
{
var runtime = (IsolatedRuntime?)store.GetData();
if (runtime is null)
{
throw new InvalidOperationException("Runtime was not set on the store");
}

return runtime;
}

internal ShadowStack ShadowStack => _shadowStack;

public IsolatedObject CreateObject(string assemblyName, string? @namespace, string className)
Expand Down Expand Up @@ -311,6 +324,9 @@ public TRes Invoke<TRes>(Func<TRes> value)
}
}

public void RegisterCallback(string name, Delegate callback)
=> _registeredCallbacks.Add(name, callback);

private IsolatedMethod LookupDelegateMethod(MulticastDelegate @delegate)
{
var method = @delegate.Method;
Expand All @@ -319,6 +335,57 @@ private IsolatedMethod LookupDelegateMethod(MulticastDelegate @delegate)
return wasmMethod;
}

internal int AcceptCallFromGuest(int invocationPtr, int invocationLength, int resultPtrPtr, int resultLengthPtr)
{
try
{
var invocationInfo = MessagePackSerializer.Deserialize<GuestToHostCall>(
_memory.GetSpan<byte>(invocationPtr, invocationLength).ToArray(), ContractlessStandardResolverAllowPrivate.Options);
if (!_registeredCallbacks.TryGetValue(invocationInfo.CallbackName, out var callback))
{
var errorString = Encoding.UTF8.GetBytes($"There is no registered callback with name '{invocationInfo.CallbackName}'");
var errorStringPtr = CopyValue<byte>(errorString, false);
_memory.WriteInt32(resultPtrPtr, errorStringPtr);
_memory.WriteInt32(resultLengthPtr, errorString.Length);
return 0;
}

var expectedParameterTypes = callback.Method.GetParameters();
var deserializedArgs = new object?[expectedParameterTypes.Length];
for (var i = 0; i < expectedParameterTypes.Length; i++)
{
deserializedArgs[i] = MessagePackSerializer.Deserialize(
expectedParameterTypes[i].ParameterType,
invocationInfo.ArgsSerialized[i],
ContractlessStandardResolverAllowPrivate.Options);
}

var result = callback.DynamicInvoke(deserializedArgs);
var resultBytes = result is null ? null : MessagePackSerializer.Serialize(
callback.Method.ReturnType,
result,
ContractlessStandardResolverAllowPrivate.Options); ; ;

var resultPtr = resultBytes is null ? 0 : CopyValue<byte>(resultBytes, false);
_memory.WriteInt32(resultPtrPtr, resultPtr);
_memory.WriteInt32(resultLengthPtr, resultBytes is null ? 0 : resultBytes.Length);
return 1; // Success
}
catch (Exception ex)
{
// We could supply the raw exception info to the guest, but since we consider the guest untrusted,
// we don't want to expose arbitrary information about the host internals. Ideally this behavior would
// vary based on whether this is a dev or prod scenario, but that's not a concept that exists natively
// in .NET (whereas it does in ASP.NET Core).
Console.Error.WriteLine(ex.ToString());
var resultBytes = Encoding.UTF8.GetBytes("The call failed. See host console logs for details.");
var resultPtr = CopyValue<byte>(resultBytes, false);
_memory.WriteInt32(resultPtrPtr, resultPtr);
_memory.WriteInt32(resultLengthPtr, resultBytes.Length);
return 0; // Failure
}
}

[StructLayout(LayoutKind.Sequential)]
struct Invocation
{
Expand Down
14 changes: 13 additions & 1 deletion src/DotNetIsolator/IsolatedRuntimeHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,14 @@ public IsolatedRuntimeHost WithAssemblyLoader(AssemblyLoadCallback callback)
public IsolatedRuntimeHost WithBinDirectoryAssemblyLoader()
{
var binDir = Path.GetDirectoryName(typeof(IsolatedRuntimeHost).Assembly.Location)!;
return WithDirectoryAssemblyLoader(binDir);
}

public IsolatedRuntimeHost WithDirectoryAssemblyLoader(string directoryPath)
{
return WithAssemblyLoader(assemblyName =>
{
var path = Path.Combine(binDir, $"{assemblyName}.dll");
var path = Path.Combine(directoryPath, $"{assemblyName}.dll");
return File.Exists(path) ? File.ReadAllBytes(path) : null;
});
}
Expand All @@ -89,6 +94,7 @@ public void Dispose()
private void AddIsolatedImports()
{
Linker.DefineFunction("dotnetisolator", "request_assembly", (CallerFunc<int, int, int, int, int>)HandleRequestAssembly);
Linker.DefineFunction("dotnetisolator", "call_host", (CallerFunc<int, int, int, int, int>)HandleCallHost);
}

private int HandleRequestAssembly(Caller caller, int assemblyNamePtr, int assemblyNameLen, int suppliedBytesPtr, int suppliedBytesLen)
Expand Down Expand Up @@ -116,6 +122,12 @@ private int HandleRequestAssembly(Caller caller, int assemblyNamePtr, int assemb
return 0;
}

private int HandleCallHost(Caller caller, int invocationPtr, int invocationLength, int resultPtrPtr, int resultLengthPtr)
{
var runtime = IsolatedRuntime.FromStore(caller.Store);
return runtime.AcceptCallFromGuest(invocationPtr, invocationLength, resultPtrPtr, resultLengthPtr);
}

private static int CopyValue(Func<int, int> malloc, Memory memory, ReadOnlySpan<byte> value)
{
var resultPtr = malloc(value.Length);
Expand Down
1 change: 1 addition & 0 deletions test/DotNetIsolator.Test/DotNetIsolator.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\DotNetIsolator.Guest\DotNetIsolator.Guest.csproj" />
<ProjectReference Include="..\..\src\DotNetIsolator\DotNetIsolator.csproj" />
</ItemGroup>

Expand Down
Loading

0 comments on commit 805563e

Please sign in to comment.