Friday, October 09, 2009

One of the benefits of managed code is sandboxed execution: To avoid that malicious code is executed by plug-ins, an application does not need to use a separate language with restricted features. Plug-ins can be written in any .NET language, because the host can execute them in a sandbox with restricted permissions. Implementing a library that can be called by sandboxed code requires the infamous AllowPartiallyTrustedCallers attribute. With this attribute, you explicitly state that you are confident that your library does not have security vulnerabilities. If your library has to interoperate with native code, you should surely know about a pitfall that can easily allow a user of your library to bypass the sandbox. This potential vulnerability can be caused by a CLR interop feature called thread promotion. In this post, I will explain this pitfall and how to avoid it in your libraries. However, before you can understand the impacts of thread promotion for CAS, I have to explain a little bit more about sandboxing first.

To execute code in a sandbox, the CLR offers an API called the simple sandboxing API. This API creates a new AppDomain in which only assemblies form the GAC and a set of explicitly specified assemblies are executed with full-trust permissions. All other assemblies are executed with permissions specified in a permission-set that is passed to this API.

Obviously, the permission-set that is granted to plug-ins should not contain several permissions. These include the skip-verification permission which allows you to execute code that is not proven to be type-safe and the permission to execute unmanaged code. Both permissions could be used to bypass the sandbox. The more permissions an app grants to a sandbox, the more features can be used by the plug-in. In my opinion, most sandboxes should only have the permission to execute code. All other features it needs should be provided by custom libraries that the host application implements.

Many libraries that are called by plug-ins have to call native code. If the native code called by such a library uses threads that have not executed managed code so far (native-only threads) or if you use technologies that can automatically switch to other threads (like COM), you have to consider thread promotion. Thread promotion is done when a native-only thread invokes a native->mananaged thunk. Normally, native->managed thunks simply stay in the AppDomain that the managed thread is executing. (Due to the .retainappdomain flag of the .vtfixup metadata in the assembly manifest.) However, when a native->managed thunk promotes a native-only thread to a managed thread, it has to pick an AppDomain. Because in this case the CLR does not have any information about which AppDomain is supposed to execute the managed function, the thread starts its managed life in the default AppDomain (the first AppDomain, which is created automatically when the CLR starts). Typically the default AppDomain executes all assemblies with full-trust. Therefore, the managed code executes with full-trust permissions now. If the libraries code calls back into the plug-in e. g. via a delegate, an event or via a virtual function call, the plug-in is now executing with full-trust permissions and not in its sandbox any more.

The following code demonstrates this unintended switch to the default AppDomain. Notice that in main, a new AppDomain called child is created and that the method Program::ExecuteInChildDomain is called in this new AppDomain. ExecuteInChildDomain calls a native function fNative and passes a pointer to a native callback method named fNativeCallback. Now fNative creates a new native thread which invokes a managed function. When created, the new thread is a native-only thread. When the thunk is invoked to call the managed function, the native-only thread is promoted to a managed thread that executes in the default domain, not in the child domain that created the thread.

// DangersOfNativThreadsInSandboxedAppDomains.cpp : main project file.
// cl /clr DangersOfNativThreadsInSandboxedAppDomains.cpp
 
#include <windows.h>
 
typedef void (__stdcall*PFN_NATIVECALLBACK)();
 
void _stdcall fNative(PFN_NATIVECALLBACK pfnCallback);
void _stdcall fNativeCallback();
DWORD WINAPI ThreadMain(LPVOID pfnCallback);
 
using namespace System;
 
ref class Program
{
internal:
  static void DumpAppDomainInfo(String^ method)
  {
    AppDomain^ current = AppDomain::CurrentDomain;
    Console::WriteLine( "Method: {0}, AppDomain: {1}",
      method,
      current->IsDefaultAppDomain() ? "(default AppDomain)"
                                    : current->FriendlyName);
  }
 
  static void Main()
  {
    Program::DumpAppDomainInfo("Program::Main");
 
    AppDomain^ child = AppDomain::CreateDomain("childDomain");
    child->DoCallBack(gcnew CrossAppDomainDelegate(&ExecuteInChildDomain));
  }
 
  static void ExecuteInChildDomain()
  {
    Program::DumpAppDomainInfo("Program::ExecuteInChildDomain");
 
    fNative(&fNativeCallback);
  }
};
 
void main()
{
  Program::Main();
}
 
void __stdcall ManagedFunctionCalledByNativeCallback()
{
  Program::DumpAppDomainInfo("::ManagedFunctionCalledByNativeCallback");
}
 
#pragma unmanaged
 
DWORD WINAPI ThreadMain(LPVOID pfnCallback);
 
void _stdcall fNative(PFN_NATIVECALLBACK pfnCallback)
{
  // this new thread starts as a native-only thread and gets promoted
  // when the new thread calls a managed function for the first time
  HANDLE hThread = ::CreateThread(NULL, 0, &ThreadMain, pfnCallback, 0, 0);
  ::WaitForSingleObject(hThread, 1000);
  ::CloseHandle(hThread);
}
 
DWORD WINAPI ThreadMain(LPVOID pfnCallback)
{
  ((PFN_NATIVECALLBACK)pfnCallback)();
 
  return 0;
}
 
void _stdcall fNativeCallback()
{
  ManagedFunctionCalledByNativeCallback();
}

Now that we know about the problem, it is time to think about a solution: How can a native library that calls managed functions from native code ensure that its managed functions end up in the right application domain? There are two fundamentally different approaches that I would like to mention here: one that is based on an explicit switch of the application domain, and another one that is based on a switch that the CLR can do implicitly.

Explicitly switching application domains can be done either via AppDomain::DoCallback or with helper functions defined in a Visual C++ header file called <msclr/appdomain.h>. This header defines several overloads of a function called call_in_appdomain, each of these functions take the identifier of the application id you want to call and a function pointer. Various overloads exist for the different calling conventions and for different numbers of arguments. To use this, you could modify your implementation of fNative as follows:

DWORD WINAPI ThreadMain(LPVOID pvCallbackInfo);
 
struct CallbackInfo
{
  PFN_NATIVECALLBACK pfnCallback;
  int appDomainId;
};
 
void _stdcall fNative(PFN_NATIVECALLBACK pfnNativeCallback)
{
  DWORD appDomainId;
  HRESULT hr = msclr::_detail::get_clr_runtime_host()
    ->GetCurrentAppDomainId(&appDomainId);
  hr; // error handling ignored for simplicity here
 
  CallbackInfo callbackInfo = { pfnNativeCallback, appDomainId };
  // this newly create thread starts as a native-only thread and gets
  // promoted when the new thread calls a managed function for the first time
  HANDLE hThread = ::CreateThread(NULL, 0, &ThreadMain, &callbackInfo , 0, 0);
  ::WaitForSingleObject(hThread, 1000);
  ::CloseHandle(hThread);
}
 
DWORD WINAPI ThreadMain(LPVOID pvCallbackInfo)
{
  CallbackInfo* pCallbackInfo = (CallbackInfo*)pvCallbackInfo;
  msclr::call_in_appdomain(pCallbackInfo->appDomainId,
    pCallbackInfo->pfnCallback);
 
  return 0;
}

This implementation passes not only the function pointer of the callback function to the thread, but a pointer to a structure that contains the callback function pointer as well as an integer value identifying the callback application domain. The thread then uses this information to invoke the callback function in the right application domain.

As I mentioned, there is a second approach which makes sure that the CLR automatically marshals the call to the right application domain. This approach is based on a method that you have probably used before. It is called Marshal::GetDelegateForFunctionPointer. This method generates a thunk that can be called by native code via a function pointer. Notice that in contrast to other thunks, this thunk is aware of the application domain where it was created. Therefore, it can automatically switch to the right application domain even when this thunk is called by a thread that has just been promoted to a managed thread. The following code shows how to use this method:

[System::Runtime::InteropServices::UnmanagedFunctionPointer(
           System::Runtime::InteropServices::CallingConvention::StdCall)]
delegate void CallbackDelegate();
 
static void ManagedCallback()
{
  Program::DumpAppDomainInfo("Program::ManagedCallback");
}
 
static void ExecuteInChildDomain()
{
  Program::DumpAppDomainInfo("Program::ExecuteInChildDomain");
 
  CallbackDelegate^ cb = gcnew CallbackDelegate(&ManagedCallback);
 
  using System::Runtime::InteropServices::Marshal;
  // this call generates a thunk that can be called by native code
  IntPtr fnPtr = Marshal::GetFunctionPointerForDelegate(cb);
  fNative((PFN_NATIVECALLBACK)fnPtr.ToPointer());
 
  // the delegate’s lifetime determines the lifetime of the generated thunk
  // => keep the delegate alife as long as native code needs the thunk
  GC::KeepAlive(cb);
}

In this post I have explained an issue that you have to understand if you want to write assemblies that use C++/CLI interop and allow partially trusted callers at the same time. In one of my next posts I will describe a safty net that you can implement in your pluggable application that ensures that no undesired bypassing of the sandbox can occur even if your plugin uses assemblies that are not aware of this issue.

10/9/2009 5:53:23 PM (GMT Daylight Time, UTC+01:00)  #    Disclaimer  |   |  Trackback
 Wednesday, September 30, 2009

Recently, a customer came up with a straightforward deployment problem:

I want to deploy an assembly that wraps a native DLL in the GAC. Deploying the wrapped native assembly in System32 is to error prone, deploying the wrapped assembly in WinSxS is too much of an overhead. How can I ensure that the managed wrapper can load the native assembly?

I have thought about a solution to this problem some times, but I have to admit, even though my first idea was right, so far I never found the time to prove that my guess really works. Today I took the time to take a closer look at the solution.

Here is was my initial idea:

From inspecting mscorlib with ILDASM, I knew that an assembly can have links to other files.

.assembly mscorlib
{
… attributes ignored here …
.file nometadata sortkey.nlp
.hash = (6E 30 2E 50 36 FB 60 2C 8E 50 C0 83 54 8B CC EA)
.file nometadata sorttbls.nlp
.hash = (F9 04 A8 31 CD FB 23 72 95 63 5D 14 9A A0 66 8E)
… other .file entries ignored here …
.mresource public sortkey.nlp
{
.file sortkey.nlp at 0x00000000
}
.mresource public sorttbls.nlp
{
.file sorttbls.nlp at 0x00000000
}
}

Furthermore you can see that the files are installed together with mscorlib.dll in the GAC:
dir c:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089
4.550.656 mscorlib.dll
262.148 sortkey.nlp
20.320 sorttbls.nlp
… other files ignored here …

My assumption was that if you can install native DLLs in the same way in the GAC, they can be magically found when needed. Creating assemblies with .file references is easy. Assume the following simple native DLLs:
nativedll2.cpp, compile with CL /LD /EHs nativedll2.cpp
#include
using namespace std;

void __declspec(dllexport) fNative2()
{
cout << "fNative2 called." << endl;
}


nativedll.cpp, compile with CL /LD /EHs nativedll.cpp
#include
#include

using namespace std;

#pragma comment(lib, "nativeDll2.lib")
void __declspec(dllimport) fNative2();

void __declspec(dllexport) fNative()
{
cout << "fNative called." << endl;
fNative2();
}

A wrapper assembly for nativedll.cpp can be created with this source code
testasm.dll:
using namespace System;

#pragma comment(lib, "nativedll.lib")
void __declspec(dllimport) fNative();

public ref class Test
{
public:
void DoSth()
{
Console::WriteLine("Test.DoSth called. Calling native DLL now.");
fNative();
}
};

To compile it with the two native DLLs as linked files, use the following command line:
cl /LD /clr testasm.cpp /link /keyfile:..\keyfile.snk /assemblylinkresource:nativedll.dll /assemblylinkresource:nativedll2.dll

Notice that the linker flag /assemblylinkresource adds the .file entries to the assembly manifest:
.file nometadata nativedll.dll
.hash = (EA 16 B4 08 D1 C4 D2 C0 77 CB 00 C7 93 A0 85 4A 41 BC F2 52 )
.file nometadata nativedll2.dll
.hash = (9D D1 39 D0 4C F9 08 7E 33 2F CD D2 8B C3 3E 55 BD E1 81 27 )
.mresource public nativedll.dll
{
.file nativedll.dll at 0x00000000
}
.mresource public nativedll2.dll
{
.file nativedll2.dll at 0x00000000
}

Given that the DLL is signed, it can be placed into the GAC:
Gacutil –i testasm.dll

Notice the .hash metadata for each .file entry. According to my tests, this metadata is not used for assembly validation (e. g. via SN.EXE –v testasm.dll), but during the installation of the assembly into the GAC. If you modify a linked file before adding the assembly to the GAC, GACUtil will report the following error:
Failure adding assembly to the cache: One or more modules were streamed in which did not match those specified by the manifest. The hash of one or more modules found does not match the hash recorded in the manifest.

Inspecting the assembly’s GAC_32 subdirectory after successfully installing it to the GAC shows the following files:
105.472 nativedll.dll
105.472 nativedll2.dll
24.064 testasm.dll

Obviously, the native DLLs are now installed into the GAC. The interesting question is: Are they really loaded from the GAC? The answer is yes. To prove this, use the following simple client application:

Testapp.cpp (should be in a different directory)
using namespace System;

int main()
{
Console::WriteLine("calling Test.DoSth now");
Test t;
t.DoSth();
Console::WriteLine("Press enter to stop app");
Console::ReadLine();
}

You can compile this file with the following command line:
cl /clr testapp.cpp /FU …relative path to testasm.dll …

Executing testapp.exe should work even when nativedll.dll and nativedll2.dll are not in a location that LoadLibrary normally uses; they are loaded from the GAC. If you want to verify this, compile all sources as shown, run testapp.exe and attach a debugger while the app is waiting at Console::ReadLine. In the debugger, you can see where dependent DLLs are loaded from (via the WinDBG command “lm –v” or the VS modules window).
9/30/2009 5:47:19 PM (GMT Daylight Time, UTC+01:00)  #    Disclaimer  |   |  Trackback