Introduction
This is a follow-up article to CLR-Hosting-Customizing-the-CLR. In this article we will create some sample customizations where we modify assembly-loading, strengthen security, log exceptions, and replace the default memory handler.
Background
When I started experimenting with these interfaces the main goal was to find ways to help me in testing and debugging managed applications.
Sometimes I insert instrumenting code directly in the source code, aka the old printf
-method, but nowadays I usually use Trace calls. The disadvantage is that I have to remove them when I am done. What can be more troublesome is that some assemblies come without source code. Even in-house assemblies can be a problem. In my project we include precompiled assemblies made by another department, the downside is that I lack read permissions for their source tree.
Another approach I use is looking at the program in a debugger, preferably Windbg with some managed extension, such as Sos or Sosex. The disadvantage of this is that it is time-consuming, and sometimes hard to know when to break execution and where to look.
What attracted me with the customization of the CLR approach was that, the Application Under Test (AUT) would be unaware of that it was under supervision. It doesn't need code modifications, and it would work for all applications out of the box.
Customizations
In this article we will see how to build a Sandbox, log exceptions, change assembly loading, and replace the memory manager. There are many customization interfaces left to explore, but those are beyond the scope of this article.
AppSandboxer.exe
A Sandbox is about two related concepts.
Isolation
Isolation can be interesting in order to limit side-effects from applications. Everything that happens inside a Sandbox is local, it doesn't affect the rest of the system. It can for instance be used to retry trial software over and over again " /> , or rerun tests in a fresh environment, just by deleting the old and creating a new one. This is similar to undo disks, when working with virtual machines.
Limiting runtime permissions
Instead of creating a complete copy of the system inside a sandbox to achieve isolation, it is possible to remove permissions, so side-effects are limited. If you have downloaded a program, and the origin is unknown, it might not be totally safe to run it. If you have a firewall, you should be able to remove the internet access for the application. If we downloaded an app that only is supposed to show the time, we might be interested to remove IO permissions, but let it have access to the internet for time synchronization. The more permissions we remove, the less side-effects are propagated to the rest of the system.
Security Manager and PermissionSet
In an early version of the CLR host interface there was IHostSecurityManager
, which contains method such as ResolvePolicy
, ProvideAssemblyEvidence
, ProvideAppDomainEvidence
, and DetermineApplicationTrust
. I made a small customization, but this interface was of limited use. The Policy related to AppDomains had been deprecated, adding Zone information to the Evidence to strengthen security had no effect. It has been superseded by the use of PemissionSet
.
The easiest way to strengthen security is to do it in the AppDomainManager
itself. When the method CreateAppDomain
is called, you simply create a PermissionSet
object, to which you add or remove permissions. Then you pass this permission set as a parameter when you create the AppDomain
.
Implementation
Our Sandbox will allow Execution, IO read permission (current dir), and interaction with the console. The code builds on the previous article, where we showed the boiler plate code to get started with AppDomainManagers. Below I just list the essential code. Full source can be found in the source attachment.
public sealed class AppDomainManagerSandboxer : AppDomainManager, IAppDomainManager
{
public void Run(string assemblyFilename, string friendlyName)
{
var appDomainInfo = new AppDomainSetup();
appDomainInfo.ApplicationBase = new System.IO.FileInfo(assemblyFilename).DirectoryName;
PermissionSet permSet = new PermissionSet(PermissionState.None);
permSet.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));
permSet.AddPermission(new UIPermission(PermissionState.Unrestricted));
permSet.AddPermission(new FileIOPermission(FileIOPermissionAccess.PathDiscovery,
AccessControlActions.View, appDomainInfo.ApplicationBase));
permSet.AddPermission(new FileIOPermission(FileIOPermissionAccess.Read,
AccessControlActions.View, appDomainInfo.ApplicationBase));
var strongNames = new StrongName[0];
ad = AppDomain.CreateDomain(friendlyName, AppDomain.CurrentDomain.Evidence,
appDomainInfo, permSet, strongNames);
int exitCode = ad.ExecuteAssembly(assemblyFilename);
AppDomain.Unload(ad);
}
}
Running the Sample
First SampleApp6.exe is executed normally, then within our Sandbox, with IO permissions removed. The whole exception is traced in full detail to the OutputDebug. I recommend using a program such as DebugView.exe to see those types of logs.
AppRedirector.exe
Ever wondered why web applications load assemblies from the bin directory and not from the root? We too can emulate this behavior by manipulating PrivateBinPathProbe
and PrivateBinPath
.
Implementation
public sealed class AppDomainManagerRedirector : AppDomainManager, IAppDomainManager
{
public void Run(string assemblyFilename, string friendlyName)
{
var appDomainInfo = new AppDomainSetup();
appDomainInfo.PrivateBinPathProbe = "*";
var baseDir = System.IO.Path.GetDirectoryName(assemblyFilename);
appDomainInfo.ApplicationBase = baseDir;
var appDir = System.IO.Path.Combine(baseDir, "bin");
var pluginDir = System.IO.Path.Combine(baseDir, "plugins");
appDomainInfo.PrivateBinPath = pluginDir + ";" + appDir;
AppDomain ad = AppDomain.CreateDomain(friendlyName, null, appDomainInfo);
AppDomainManager appDomainManager = ad.DomainManager;
try
{
int exitCode = ad.ExecuteAssembly(assemblyFilename);
System.Diagnostics.Trace.WriteLine(string.Format("ExitCode={0}", exitCode));
System.Diagnostics.Trace.WriteLine("Executed Run");
}
catch (System.Exception)
{
string message = string.Format("Unhandled Exception in {0}",
System.IO.Path.GetFileNameWithoutExtension(assemblyFilename));
System.Diagnostics.Trace.WriteLine(message);
}
finally
{
AppDomain.Unload(ad);
}
}
}
Running the Sample
In this particular implementation, we tell the App to use the subfolders bin and plugins, and completely ignore the root folder. We use SampleApp5.exe for demo purposes, which just prints "--- abc ---". I have made a modified version of TestLib.dll in the bin folder, which instead prints "*** abc ***" to get a visual difference of the output.
AppSupervisor.exe
For testing, logging, and diagnostics, it can be useful to log runtime data. Far too often, I have seen misusage of try/catch
. Sometimes exceptions are silently ignored, leaving the app in a corrupt state, then the app crashes at a later time when some other code runs. We will make a small Exception logger that will log all managed exceptions that occur, regardless whether they are handled or not. This way we can go back and see if indeed these exceptions occurred for the right reason. We will achieve this by registering a handler for FirstChanceException
s in all AppDomains.
Implementation
public sealed class AppDomainManagerSupervisor : AppDomainManager, IAppDomainManager
{
public override AppDomain CreateDomain(string friendlyName,
Evidence securityInfo, AppDomainSetup appDomainInfo)
{
Trace("CreateDomain");
System.Diagnostics.Trace.WriteLine(string.Format("AppDomain::CreateDomain({0})", friendlyName));
var appDomain = base.CreateDomain(friendlyName, securityInfo, appDomainInfo);
appDomain.FirstChanceException += AppDomainFirstChanceException;
return appDomain;
}
public static void AppDomainFirstChanceException(object sender,
System.Runtime.ExceptionServices.FirstChanceExceptionEventArgs e)
{
System.Console.Error.WriteLine("AppDomainFirstChanceException - First Chance Exception");
var sb = new StringBuilder();
sb.AppendLine("AppDomainFirstChanceException - First Chance Exception");
sb.AppendLine(e.Exception.StackTrace);
System.Diagnostics.Trace.WriteLine(sb.ToString());
}
}
Running the Sample
SampleApp1.exe throws a NotImplementedException
, the exception however is caught inside the program so we will never know about it. Running it through AppSupervisor.exe, it will be detected as a first chance exception and logged to DebugOutput
.
AppExperimental.exe
A natural add on to a supervisor would also be to add an UnhandledExceptionHandler
. Normally, if an exception is not caught, it will eventually lead to a crash of the application. First it will unwind the stack and continue looking for exception handlers, until it runs out of stacks to unwind. What is possible to do is, adding a global Exception handler on the Application level or on the AppDomain level.
Implementation
public sealed class AppDomainManagerExperimental : AppDomainManager, IAppDomainManager
{
public void Run(string assemblyFilename, string friendlyName)
{
Application.ThreadException += ApplicationThreadException;
AppDomain.CurrentDomain.UnhandledException += AppDomainUnhandledException;
AppDomain ad = AppDomain.CreateDomain(friendlyName);
ad.UnhandledException += AppDomainUnhandledException;
AppDomainManager appDomainManager = ad.DomainManager;
try
{
int exitCode = ad.ExecuteAssembly(assemblyFilename);
System.Diagnostics.Trace.WriteLine(string.Format("ExitCode={0}", exitCode));
}
catch (System.Exception)
{
string message = string.Format("Unhandled Exception in {0}",
System.IO.Path.GetFileNameWithoutExtension(assemblyFilename));
Trace(message);
System.Console.Error.WriteLine(message);
}
finally
{
AppDomain.Unload(ad);
}
}
}
Running the Sample
For the purpose of demonstration, I will use SampleApp2.exe. It throws a NotImplementedException
, which is not caught by any handler.
Funny... it didn't end up in either of our exception handlers.
SampleApp3.exe works exactly like SampleApp2.exe, but it actually installs the Unhandled Exception Handler itself.
Look closely. The handler stops working.
The unhandled exception handler installed by SampleApp3.exe stops working. It is a known bug in the CLR. It was reported for the 2.0 runtime. You can read more about it here: CLR hosting exception handling in a non-CLR-created thread. The explanation is: "The behavior is indeed a bug caused by the CLR execution engine and the CRT competing for the UnhandledExceptionFilter
". We are executing via COM, which automatically inserts a try/catch
. The unhandled exception filter can only be set once. COM sets it first. Then the CLR tries to do it. According to this page, it was supposed to be fixed for the release of v4.0. But I didn't get it to work. If anyone does, please tell.
Replacing the Memory Manager
IHostControl::GetHostManager
is an interface where you can plug in your own implementation of a memory handler.
We shall see how it can be done.
Implementation
First we will need a modified IHostControl
implementation, and supply a custom memory manager called MyHostMemoryManager
.
HRESULT STDMETHODCALLTYPE MyHostControlMemoryManager::GetHostManager(REFIID riid,void **ppv)
{
if (riid == IID_IHostMemoryManager)
{
IHostMemoryManager *pMemoryManager = new MyHostMemoryManager();
*ppv = pMemoryManager;
return S_OK;
}
else
{
*ppv = NULL;
return E_NOINTERFACE;
}
}
Now we are finished with our IHostControl
implementation. The next step is to implement the IHostMemoryManager
.
class MyHostMemoryManager : public IHostMemoryManager
{
public:
virtual HRESULT STDMETHODCALLTYPE CreateMalloc(
DWORD dwMallocType,
IHostMalloc **ppMalloc)
{
*ppMalloc = new MyHostMalloc();
return S_OK;
}
virtual HRESULT STDMETHODCALLTYPE VirtualAlloc(
void *pAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect,
EMemoryCriticalLevel eCriticalLevel,
void **ppMem)
{
*ppMem = ::VirtualAlloc(pAddress, dwSize, flAllocationType, flProtect);
return S_OK;
}
virtual HRESULT STDMETHODCALLTYPE VirtualFree(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD dwFreeType)
{
::VirtualFree(lpAddress, dwSize, dwFreeType);
return S_OK;
}
virtual HRESULT STDMETHODCALLTYPE VirtualQuery(
void *lpAddress,
void *lpBuffer,
SIZE_T dwLength,
SIZE_T *pResult)
{
*pResult = ::VirtualQuery(lpAddress, (PMEMORY_BASIC_INFORMATION) lpBuffer, dwLength);
return S_OK;
}
virtual HRESULT STDMETHODCALLTYPE VirtualProtect(
void *lpAddress,
SIZE_T dwSize,
DWORD flNewProtect,
DWORD *pflOldProtect)
{
::VirtualProtect(lpAddress, dwSize, flNewProtect, pflOldProtect);
return S_OK;
}
virtual HRESULT STDMETHODCALLTYPE GetMemoryLoad(
DWORD *pMemoryLoad,
SIZE_T *pAvailableBytes)
{
*pMemoryLoad = 30; *pAvailableBytes = 100 * 1024*1024;
return S_OK;
}
virtual HRESULT STDMETHODCALLTYPE RegisterMemoryNotificationCallback(
ICLRMemoryNotificationCallback *pCallback)
{
return S_OK;
}
virtual HRESULT STDMETHODCALLTYPE NeedsVirtualAddressSpace(
LPVOID startAddress,
SIZE_T size)
{
return S_OK;
}
virtual HRESULT STDMETHODCALLTYPE AcquiredVirtualAddressSpace(
LPVOID startAddress,
SIZE_T size)
{
return S_OK;
}
virtual HRESULT STDMETHODCALLTYPE ReleasedVirtualAddressSpace(
LPVOID startAddress)
{
return S_OK;
}
};
Oh my god! That is a dirty implementation. No error handling. Even skipping the implementation in a few methods. Don't ever use this code in a real project. The important method here is really the MyHostMemoryManager::CreateMalloc
where we implement the IHostMalloc
interface.
class MyHostMalloc : public IHostMalloc
{
public:
HRESULT STDMETHODCALLTYPE Alloc(SIZE_T cbSize,
EMemoryCriticalLevel eCriticalLevel,
void** ppMem)
{
void* memory = new char[cbSize];
*ppMem = memory;
g_noAllocs++;
return S_OK;
}
HRESULT STDMETHODCALLTYPE DebugAlloc(SIZE_T cbSize,
EMemoryCriticalLevel eCriticalLevel,
char* pszFileName,
int iLineNo,
void** ppMem)
{
void* memory = new char[cbSize];
ZeroMemory(memory, cbSize);
*ppMem = memory;
return S_OK;
}
HRESULT STDMETHODCALLTYPE Free(void* pMem)
{
g_noFrees++;
delete [] pMem;
return S_OK;
}
};
The implementation of IHostMalloc
is straightforward. It is just a dummy implementation, forwarding calls to new
and delete
. What is possible to do is adding magic numbers before and after a memory region, or add your own counters. I added two counters, one for the number of Allocs and one for the number of Frees.
Just for the record, new
and delete
are very slow. Adding more CPUs won't help, calls to new
and delete
never run in parallel. The hardware must guarantee that a memory region is only allocated once. A good memory manager would therefore allocate a big block of memory once and give out portions of it, so the stall can be avoided. Hey, just like the default one in the CLR runtime does. " />
Running the Sample
The memory manager is present in all AppLaunchers, just add "-mem" as the second parameter.
The last two lines contain counters that are implemented by our custom implementation.
Troubleshooting
The .net app may fail to start for several reasons. One reason is that the assemblies are not found, or the .config is not found. FUSLOGVW.exe is an excellent tool for troubleshooting assembly load errors.
WPF apps may not start. I had to mark the main thread of the AppLauncher as STA, Single Thread Apartment. In wpf, only an STA thread may allocate objects.
The .net app runs in the process space of AppLauncher. It will not read the correct config file. Please copy the MyNetApp.exe.config to AppLauncher.exe.config.
The probing path and searchpath of DLLs may be wrong. Assemblies are only loaded from current directory or subfolders. It doesnt seem to work starting the AppLauncher from one location and loading DLLs from another location. There is a permission error.
All these errors are revealed by FUSLOGVW.exe
Conclusion
It was unfortunate that the UnhandledException
handler didn't work. Personally, it would have been nice to get a customization that gives me extra logging when I need to test or diagnose bugs in an application. I see some potential in using the CLR hosting interfaces. If you write something cool and useful with these interfaces, please tell.
Points of Interest
This article is a follow up article to CLR-Hosting-Customizing-the-CLR, it is mandatory reading. The main source of documentation is of course MSDN itself, .NET Framework 2.0 Hosting Interfaces.
A great book regarding the CLR Hosting API, is Customizing the Microsoft® .NET Framework Common Language Runtime. It is a bit old, but the best (and perhaps the only one in existence).
History
- 11th July, 2012, Initial post
- 24th May, 2019, updated source code to support WPF apps, and added Troubleshooting section.