INTRODUCTION
I'm using the next C# code example shared by David Heffernan' for loading a .NET assembly from the application's resources and run It from memory:
Assembly a = Assembly.Load(bytes);
MethodInfo method = a.EntryPoint;
if (method != null)
method.Invoke(a.CreateInstance(method.Name), null);
Here I just share an adaptation in VB.NET that I am using too:
Public Shared Sub Execute(ByVal resource As Byte(), ByVal parameters As Object())
Dim ass As Assembly = Assembly.Load(resource)
Dim method As MethodInfo = ass.EntryPoint
If (method IsNot Nothing) Then
Dim instance As Object = ass.CreateInstance(method.Name)
method.Invoke(instance, parameters)
If (instance IsNot Nothing) AndAlso (instance.GetType().GetInterfaces.Contains(GetType(IDisposable))) Then
DirectCast(instance, IDisposable).Dispose()
End If
instance = Nothing
method = Nothing
ass = Nothing
Else
Throw New EntryPointNotFoundException("Entrypoint not found in the specified resource. Are you sure it is a .NET assembly?")
End If
End Sub
PROBLEM
The problem is that if the executed assembly has an application exit instruction, then it also terminates my main/host application too. For example:
ConsoleApplication1.exe compiled from this source-code:
Module Module1
Sub Main()
Environment.Exit(0)
End Sub
End Module
When I add ConsoleApplication1.exe to the application resources, and then I load it and run it with the Assembly.Load
methodology, it also terminates my application because the call to Environment.Exit
.
QUESTION
How can I prevent this, without modifying the source code of the executed assembly?.
Maybe I could do somehting like associate a kind of exit event handler to the executed assembly to handle/ignore it properly?. What are my options at this point?.
PS: For me no matter if the given solution is written in C# or VB.NET.
Please note two things, the first is that my intention is to resolve this issue in an automated/abstracted way, I mean the final result should just need to call the "Execute" method passing the resource and the arguments and don't worry about the rest; and secondly, I want the executed assembly to be ran synchronuslly, not async... in case of that could matter for a possible solution.
Update: My first solution doesn't work for assemblies contained in the resources of a program like OP asked; instead it loads it from the disk. The solution for loading from a byte array will follow (in progress). Note that the following points apply to both solutions:
As the Environment.Exit()
method throws an exception because of lack of permissions, execution of the method will not continue after it is encountered.
You're going to need all the permissions that your Main method needs, but you can find those quickly by just typing in "Permission" in intellisense, or by checking the SecurityException
's TargetSite
property (which is an instance of MethodBase
and will tell you which method failed).
If another method in your Main needs the UnmanagedCode
permission, you're out of luck, at least using this solution.
Note that I found that the UnmanagedCode
permission was the one that Environment.Exit()
needs purely by trial and error.
Solution 1: When the assembly is on the disk
Okay, here's what I've found so far, bear with me. We're going to create a sandboxed AppDomain:
AppDomainSetup adSetup = new AppDomainSetup();
adSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
// This is where the main executable resides. For more info on this, see "Remarks" in
// https://msdn.microsoft.com/en-us/library/system.appdomainsetup.applicationbase(v=vs.110).aspx#Anchor_1
PermissionSet permission = new PermissionSet(PermissionState.None);
// Permissions of the AppDomain/what it can do
permission.AddPermission(new SecurityPermission(SecurityPermissionFlag.AllFlags & ~SecurityPermissionFlag.UnmanagedCode));
// All SecurityPermission flags EXCEPT UnmanagedCode, which is required by Environment.Exit()
// BUT the assembly needs SecurityPermissionFlag.Execution to be run;
// otherwise you'll get an exception.
permission.AddPermission(new FileIOPermission(PermissionState.Unrestricted));
permission.AddPermission(new UIPermission(PermissionState.Unrestricted));
// the above two are for Console.WriteLine() to run, which is what I had in the Main method
var assembly = Assembly.LoadFile(exePath); // path to ConsoleApplication1.exe
var domain = AppDomain.CreateDomain("SomeGenericName", null, adSetup, permission, null); // sandboxed AppDomain
try
{
domain.ExecuteAssemblyByName(assembly.GetName(), new string[] { });
}
// The SecurityException is thrown by Environment.Exit() not being able to run
catch (SecurityException e) when (e.TargetSite == typeof(Environment).GetMethod("Exit"))
{
Console.WriteLine("Tried to run Exit");
}
catch (SecurityException e)
{
// Some other action in your method needs SecurityPermissionFlag.UnmanagedCode to run,
// or the PermissionSet is missing some other permission
}
catch
{
Console.WriteLine("Something else failed in ConsoleApplication1.exe's main...");
}
Solution 2: When the assembly is a byte array
Warning: cancerous solution follows.
When changing my solution to load a byte array, OP and I discovered a weird exception file not found exception: even if you pass in a byte array to Assembly.Load()
, domain.ExecuteAssemblyByName()
still searches the disk for the assembly, for some weird reason. Apparently we weren't the only ones with the issue: Loading Byte Array Assembly.
First, we have a Helper
class:
public class Helper : MarshalByRefObject
{
public void LoadAssembly(Byte[] data)
{
var a = Assembly.Load(data);
a.EntryPoint.Invoke(null, null);
}
}
which as you can see, loads the assembly using Assembly.Load()
and calls it's entry point. This is the code we'll be loading into the AppDomain
:
AppDomainSetup adSetup = new AppDomainSetup();
adSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
// This is where the main executable resides. For more info on this, see "Remarks" in
// https://msdn.microsoft.com/en-us/library/system.appdomainsetup.applicationbase(v=vs.110).aspx#Anchor_1
PermissionSet permission = new PermissionSet(PermissionState.None);
// Permissions of the AppDomain/what it can do
permission.AddPermission(new SecurityPermission(SecurityPermissionFlag.AllFlags & ~SecurityPermissionFlag.UnmanagedCode));
// All SecurityPermission flags EXCEPT UnmanagedCode, which is required by Environment.Exit()
// BUT the assembly needs SecurityPermissionFlag.Execution to be run;
// otherwise you'll get an exception.
permission.AddPermission(new FileIOPermission(PermissionState.Unrestricted));
permission.AddPermission(new UIPermission(PermissionState.Unrestricted));
// the above two are for Console.WriteLine() to run, which is what I had in the Main method
var domain = AppDomain.CreateDomain("SomeGenericName", null, adSetup, permission, null); // sandboxed AppDomain
try
{
Helper helper = (Helper)domain.CreateInstanceAndUnwrap(typeof(Helper).Assembly.FullName, typeof(Helper).FullName);
// create an instance of Helper in the new AppDomain
helper.LoadAssembly(bytes); // bytes is the in-memory assembly
}
catch (TargetInvocationException e) when (e.InnerException.GetType() == typeof(SecurityException))
{
Console.WriteLine("some kind of permissions issue here");
}
catch (Exception e)
{
Console.WriteLine("Something else failed in ConsoleApplication1.exe's main... " + e.Message);
}
Note that in the second solution, the SecurityException
becomes a TargetInvocationException
with it's InnerException
property being a SecurityException
. Unfortunately, this means that you cannot use e.TargetSite
to see which method threw the exception.
Conclusion/Things to keep in mind
This solution isn't perfect. It would be much better to somehow go through the IL of the method and artificially remove the call to Environment.Exit()
.
All credits go to Kirill Osenkov - MSFT
I can successfully load the assembly into another AppDomain and call its entry point.
Environment.Exit always shuts down the hosting process.
Workaround for this would be, to return an int from Main of the loaded console application. Zero for success and other numbers for errors.
Instead of this:
Module Module1
Sub Main()
// your code
Environment.Exit(0)
End Sub
End Module
write: (I hope this is valid VB.NET :-))
Module Module1
Function Main() As Integer
// your code
Return 0 // 0 == no error
End Function
End Module
Demo - C#
class Program
{
static void Main(string[] args)
{
Launcher.Start(@"C:\Users\path\to\your\console\app.exe");
}
}
public class Launcher : MarshalByRefObject
{
public static void Start(string pathToAssembly)
{
TextWriter originalConsoleOutput = Console.Out;
StringWriter writer = new StringWriter();
Console.SetOut(writer);
AppDomain appDomain = AppDomain.CreateDomain("Loading Domain");
Launcher program = (Launcher)appDomain.CreateInstanceAndUnwrap(
typeof(Launcher).Assembly.FullName,
typeof(Launcher).FullName);
program.Execute(pathToAssembly);
AppDomain.Unload(appDomain);
Console.SetOut(originalConsoleOutput);
string result = writer.ToString();
Console.WriteLine(result);
}
/// <summary>
/// This gets executed in the temporary appdomain.
/// No error handling to simplify demo.
/// </summary>
public void Execute(string pathToAssembly)
{
// load the bytes and run Main() using reflection
// working with bytes is useful if the assembly doesn't come from disk
byte[] bytes = File.ReadAllBytes(pathToAssembly); //"Program.exe"
Assembly assembly = Assembly.Load(bytes);
MethodInfo main = assembly.EntryPoint;
main.Invoke(null, new object[] { null });
}
}
Also to note:
Also, note that if you use LoadFrom you'll likely get a FileNotFound
exception because the Assembly resolver will attempt to find the
assembly you're loading in the GAC or the current application's bin
folder. Use LoadFile to load an arbitrary assembly file instead--but
note that if you do this you'll need to load any dependencies
yourself.
There is only one way to do this. You have to dynamically instrument all the code that the assembly is going to get executed. This boils down to intercepting system calls. There is no easy way to do this. Note that this does not require modifying the source code.
Why can't the .NET security system do this? While the system could have provided you with a security permission that you can use to control calls to Environment.Exit
, that would not really solve the problem. The assembly could still call into unmanaged code. Other answers have pointed out that this can be done by creating an AppDomain and revoking SecurityPermissionFlag.UnmanagedCode
. Indeed, this works, but you indicated in the comments that you to enable the assembly to call unmanaged code.
That's it if you want to run the code in the same process.You could also run the code in another process, but then you have to do interprocess communication.
More AppDomain
code could help in finding a solution.
Code can be found at LoadUnload
The small applications included in the project LoadUnload contain AppDomain
code that you maybe able to adapt in your solution.