All Projects → fadden → DynamicScriptSandbox

fadden / DynamicScriptSandbox

Licence: Apache-2.0 license
An experiment with C# .NET, dynamic compilation, and AppDomains

Programming Languages

C#
18002 projects

DynamicScriptSandbox

This is an experiment with C#, dynamic compilation, and AppDomains. The goal was to create an environment in which an application could allow the user to edit C# scripts within the program, then compile and execute them. The programs would be able to call specific functions in the main program, but would be run in a sandbox to reduce the possibility of malicious actions.

This seems mostly correct, but I've had some problems with a similar sandboxing approach in a separate app (https://stackoverflow.com/q/52230527/294248), especially when leases expire while the computer is asleep.

IMPORTANT: .NET AppDomains are deprecated. They are not part of the .NET Core implementation or .NET Standard definition. They're only part of .NET Framework, which is nearing the end of its lifetime. There does not appear to be a way to get equivalent behavior without using OS-specific mechanisms; see e.g. this discussion. While .NET Framework will be supported for many years, newer apps should probably stick to the newer APIs.

Overview

Compiling C# source code into an Assembly is remarkably easy in .NET. The trouble is that the assembly can't be unloaded, so your memory footprint will expand with each recompilation. It's also harder to keep the compiled code from deliberately or inadvertently doing bad things.

By placing the compiled assembly in a separate AppDomain, we create a way to discard the compiled code -- just unload the AppDomain -- and expand the set of security tools. However, we introduce a new problem relating to object lifetimes.

The AppDomain is (effectively or actually) a separate virtual machine, with an independent garbage collection domain. The objects in the main app's AppDomain don't automatically keep objects in the plugin's AppDomain from being discarded by the garbage collector. The approach Microsoft used to handle this was to set an expiration time on objects created across an AppDomain boundary. The timer resets when the object is used, but eventually it will be collected. To prevent this, a "sponsor" object in the main app must renew the "lease".

This project demonstrates dynamic compilation into a dedicated AppDomain with proper handling of lease expiration. For example, the "script test" does the following:

  • Creates a new AppDomain (the "plugin" domain). Some security restrictions are set in place.
  • Loads a "script compiler" assembly into the new domain, from a .DLL file. The main app, running in the "host" domain, refers to this through the IPlugin interface.
  • Passes the source code across the domain boundary, where it is compiled into an assembly. A reference to an IScript interface is returned, along with all compiler diagnostics.
  • The main app calls an IScript method, passing a reference back to itself through an IHost interface. The script calls back across the AppDomain boundary into the main app, modifies the return value, and returns it.
  • Unloads the AppDomain.

It's possible to use the interfaces directly, calling objects across the AppDomain boundary, so long as the objects are subclasses of MarshalByRefObject. When serialized, the remote side gets a proxy object that forwards the calls, rather than a copy of the object itself.

This is similar to any number of "C# plugin" examples, but they were all lacking in one area or another.

About the Code

This was developed with Microsoft Visual Studio Community 2015 on Windows 10. I don't know if it will work with Mono on Linux.

To test it yourself, download the project, open it with Visual Studio, and hit Ctrl-Shift-B to build the main app and the plugin assembly. Hit F5 to run the tests in the debugger. Uncaught exceptions in either AppDomain will be caught by the debugger, and exceptions will be serialized across the AppDomain boundary.

There are three projects:

  1. The main project (which includes the DomainManager, the Sponsor wrapper, and three increasingly complex test programs).
  2. PluginCommon, which has the common interfaces as well as the implementation of PluginLoader.
  3. ScriptPlugin, an implementation of IPlugin that knows how to compile C# code.

Most build settings are at their defaults, but the ScriptPlugin project's output directory is set to a subdirectory of the main project. The idea was to put the plugin assembly DLL in a place where the test project could find it. In a "real" app this would live elsewhere.

I tried to make the sandboxing as restrictive as possible, but it appears that the use of the compiler requires full trust. If you just want to execute previously-built code, you can severely limit the plugin's capabilities, e.g. disallow all disk access except for read-only access to the plugin directory. The right answer is probably to assemble the code to a temp file in a trusted AppDomain, and execute it in another.

License

This code is distributed under the Apache 2 open-source license.

Acknowledgments

When figuring out how to make this work, the following pages were invaluable:

["What is the best scripting language to embed in a C# desktop application?"] (http://stackoverflow.com/a/596097/294248)

["A Plug-In System Using Reflection, AppDomain and ISponsor"] (http://www.brad-smith.info/blog/archives/500)

["How to: Run Partially Trusted Code in a Sandbox"] (https://msdn.microsoft.com/en-us/library/bb763046.aspx)

["Remoting Example: Lifetimes"] (https://msdn.microsoft.com/en-us/library/6tkeax11(v=vs.85).aspx)

Related technologies:

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].