Introduction
In this article, we'll take a look at the undocumented and internal (but
useful) class called Microsoft.CLRAdmin.Fusion
that resides in the mscorcfg assembly (a core component in the
.NET framework). Using this class, we'll be able to examine the contents
of the three Fusion assembly caches as well as the managed application
history. Sure - gacutil already provides some of this capability, but
we'll access the functionality programmatically and (hopefully) learn some .NET
spelunking techniques along the way.
Disclaimers: The information presented here is intended for educational purposes
only - using any of the internal Framework classes in production code is
just asking for trouble, especially considering the relatively low version
number (and implied maturity level) of the current framework. In addition, the
code presented with the article has only been tested with version 1.1 of the
.NET Framework.
Fusion
First off, what exactly is Fusion? Fusion is the internal name of the CLR
assembly loader/binder subsystem. Loading an assembly in a managed app is
somewhat more complicated than loading a DLL from a non-managed app. When you
load a DLL using LoadLibrary
/Ex
, you either specify a
path that indicates where the DLL lives or you rely on the LoadLibrary
search path logic. This simple binding mechanism, of course, assists in
creating the 'DLL Hell' that the .NET designers wanted to avoid. The
assembly binding process, on the other hand, involves a number of
configurable inputs. It is Fusion that is using those inputs to make
decisions about where the bits for a requested assembly actually come from. In
order to optimize some of that decision making process, Fusion maintains
several assembly caches, of which at least one should be familiar to you (if
you're reading this article):
-
the GAC, or Global Assembly Cache - all shared assemblies live here
-
the Download Cache - when you execute an assembly from a URL, this is where the
downloaded assemblies end up
-
the 'ZAP' Cache - this cache seems to serve as the home
for pre-compiled native assembly images that are produced by NGEN
Speaking of the GAC, some readers may not be aware that the Explorer shell extension
that presents a custom view of the GAC & Zap cache can be disabled by
adding a binary value named 'DisableCacheViewer' to the registry key
HKLM\Software\Microsoft\Fusion and setting it to a non-zero value. This comes
in handy if you want to see the physical directory structure in Explorer.
Spelunking the Framework
Now that we have an overly simplified view of Fusion and it's caches, let's turn our
attention to the code - what it does and how it works.
Readers who have used the .NET configuration and wizard MMC snap-ins may have
wondered how some of that functionality was implemented. The first time I saw
the 'Fix an Application' applet, for example, I was immediately curious
about the source of the application list since it was presented so quickly
(that is, after I finished chuckling about the name of that
applet). Using
Process Explorer, I quickly discovered that this particular
applet is housed in a managed executable called ConfigWizards.exe.
Then using a hand-rolled dependency viewer for .NET (which may be the topic of
a future article, see Fig 1
for a teaser), I stumbled upon the mscorcfg assembly. Most of the
publicly exposed classes in mscorcfg are related to UI
logic used by the control panel applet and, as such, are not generally useful
outside of that context. However, taking a closer look at mscorcfg with
the absolutely indispensable Reflector
tool reveals a few gems. One such gem is the Microsoft.CLRAdmin.Fusion
class which puts a pretty (and convenient) face over the lower-level and
undocumented (and much less convenient) fusion.dll native exports
"UndocumentedFusion/CreateAssemblyEnum.jpg">CreateAssemblyEnum
& "UndocumentedFusion/CreateHistoryReader.jpg">CreateHistoryReader
.
At this point, some readers may have popped open Reflector,
looked at the CLRAdmin
namespace for themselves and didn't
see the Fusion
class. After going to
Reflector's option settings and checking all the currently
unchecked visibility settings (hint, hint), they now may be wondering: "What
good is an internal or private class? You can't use it outside that
assembly or class, right?" Well, not exactly. You see, there's this marvelous
facility in the framework called, you guessed it,
Reflection. Among other things, reflection allows a
caller to create instances and invoke members on types that would
otherwise be inaccessible. Recall (or learn if you're not aware of it) that
access modifiers like private and internal are only enforced at the language
level, not at the IL level. The EE (execution environment) is only concerned
with whether the caller has the appropriate
ReflectionPermission
grant (see the MemberAccess
&
TypeInformation ReflectionPermissionFlags
for more
information). As long as the caller has the appropriate permissions, the
EE could care less if the member being accessed is marked as
superdupertopsecret
(ok, I made that up) in the source
language, because the caller is trusted.
Update: Greg Fee,
who is a primary developer on the CLR CAS team, started blogging recently
and
this blog entry has more information on the importance of reflection
permissions.
Armed with Reflector (did I
mention that this is an exceedingly useful tool?) and the knowledge that
reflection lets us use any type in any assembly, let's take a closer look
at Microsoft.CLRAdmin.Fusion
and
see what potential goodies are in there:
The Fusion
class has a number of methods
and properties, but we're going to zoom in on only two of those methods.
Specifically: GetKnownFusionApps
and ReadCache
.
GetKnownFusionApps
provides an extremely convenient
means of accessing the information exposed by the lower-level "http://www.codeproject.com/useritems/UndocumentedFusion/CreateHistoryReader.jpg">
CreateHistoryReader
export. It takes no parameters and
returns a StringCollection
, each element of which is a
fully qualified file name of a managed application. It is this mechanism that
provides the list of applications that can be 'fixed' (chuckle). ReadCache
is also a higher-level wrapper, this time for the lower-level "http://www.codeproject.com/useritems/UndocumentedFusion/CreateAssemblyEnum.jpg">
CreateAssemblyEnum
export. ReadCache
takes
two parameters: a destination ArrayList
and an unsigned
int that specifies flags indicating which cache the caller is interested in.
(There are two other methods called ReadFusionCache
and
ReadFusionCacheJustGAC
which wrap ReadCache
and
return the contents of the GAC/ZAP cache or just GAC, respectively.) A
quick search through the
SSCLI (another very useful learning tool - I highly recommend
both it and the excellent book that covers it,
Shared Source CLI Essentials) reveals three bit flags that can
be used here, one for each of the cache types: ASM_CACHE_ZAP
(1), ASM_CACHE_GAC
(2) and ASM_CACHE_DOWNLOAD
(4). After ReadCache
successfully completes, the
ArrayList
that was passed in will contain a collection of
AssemInfo
data structures, each of which represents an assembly in
the requested cache. AssemInfo
is also an internal type
(oh bother) and contains information about an assembly such as display Name,
Version, Locale, PublicKeyToken, etc.
The sample code
So now that we know how to access the managed apps list (aka Fusion Application History) and the contents of the Fusion caches, all that remains is to collect that information and throw it into a simple (read: very quick, very dirty, only-intended-for-demonstration-purposes) GUI:
The GUI consists of the standard tree-splitter-info pane layout. The tree is
organized into the four assembly collections, which are further divided into
product name groups (where possible). The product name information is obtained
by passing a calculated file name of each assembly (where possible) to the
framework "http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfsystemdiagnosticsfileversioninfoclasstopic.asp">
System.Diagnostics.FileVersionInfo
class (which, by the way, makes it trivial to access/interpret the version resource of an image). Implementing additional functionality, such as the ability to add/remove assemblies from the Gac for example, is left as an exercise to the reader (hint: this can be done without resorting to reflection).
The sample code is mostly trivial but the wrapper classes Reflected
,
AssemInfo
& Fusion
may
warrant additional (if not brief) explanation. Reflected
is a general-purpose utility class that simply makes it more convenient to
access the fields and properties of a reflected type. AssemInfo
and Fusion
are simple reflection wrappers
around the real AssemInfo
and
Fusion
types (specifically: wrappers around the members that we are
interested in). Since we can't access the real types directly, these wrappers
make it easy to interact with the real types without having to sprinkle
reflection calls all over the code base.
Let's take a quick look at each of these classes. Here is the utility class
Reflected
:
public class Reflected
{
static object Get(object src, string strName, BindingFlags type)
{
bool bStatic = (src is Type);
Type t = bStatic ? (Type)src : src.GetType();
BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Public |
BindingFlags.DeclaredOnly |
(bStatic ? BindingFlags.Static : BindingFlags.Instance) |
type;
object target = (bStatic ? null : src);
return (t.InvokeMember(strName, bindingFlags, null, target, null));
}
public static object GetProperty(object src, string strName)
{
return (Get(src, strName, BindingFlags.GetProperty));
}
public static object GetField(object src, string strName)
{
return (Get(src, strName, BindingFlags.GetField));
}
}
The real work is done in the private Get
method
which takes a src object to invoke on, a member name to invoke and the type of
invocation. "http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfsystemtypeclassinvokemembertopic.asp">
Type.InvokeMember
allows a caller to invoke any member on a
type through reflection, including the ability to get and set fields. The
"http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfsystemreflectionbindingflagsclasstopic.asp">
bindingFlags
argument tells InvokeMember
how to interpret the name of the member as well as the target member's visibility.
Here is the AssemInfo
wrapper:
public struct AssemInfo
{
public readonly string Name;
public readonly string Locale;
public readonly string Codebase;
public readonly string Modified;
public readonly string OSType;
public readonly string OSVersion;
public readonly string ProcType;
public readonly string PublicKey;
public readonly string PublicKeyToken;
public readonly string Version;
public readonly Fusion.CacheType CacheType;
public readonly string sCustom;
public readonly string sFusionName;
public AssemInfo(object assemInfo)
{
Name = (string)Reflected.GetField(assemInfo, "Name");
Locale = (string)Reflected.GetField(assemInfo, "Locale");
Codebase = (string)Reflected.GetField(assemInfo, "Codebase");
Modified = (string)Reflected.GetField(assemInfo, "Modified");
OSType = (string)Reflected.GetField(assemInfo, "OSType");
OSVersion = (string)Reflected.GetField(assemInfo, "OSVersion");
ProcType = (string)Reflected.GetField(assemInfo, "ProcType");
PublicKey = (string)Reflected.GetField(assemInfo, "PublicKey");
PublicKeyToken = (string)Reflected.GetField(assemInfo, "PublicKeyToken");
Version = (string)Reflected.GetField(assemInfo, "Version");
uint nCacheType = (uint)Reflected.GetField(assemInfo, "nCacheType");
CacheType = (Fusion.CacheType)nCacheType;
sCustom = (string)Reflected.GetField(assemInfo, "sCustom");
sFusionName = (string)Reflected.GetField(assemInfo, "sFusionName");
}
}
Nothing terribly heart pounding here - AssemInfo
is a simple wrapper around the real AssemInfo
type that
we can't access directly. We simply cache the contents of the real instance and
then interact with the wrapper.
And finally, the Fusion
wrapper:
public class Fusion
{
public enum CacheType
{
Zap = 0x1,
GAC = 0x2,
Download = 0x4
}
static Type FusionType;
static Fusion()
{
Assembly a = Assembly.Load("mscorcfg, "
+ "Version=1.0.5000.0, "
+ "Culture=neutral, "
+ "PublicKeyToken=b03f5f7f11d50a3a");
FusionType = a.GetType("Microsoft.CLRAdmin.Fusion");
}
public static String GetCacheTypeString(UInt32 nFlag)
{
object[] args = new object[]{ nFlag };
BindingFlags bindingFlags = (BindingFlags)314;
return ((String)
(FusionType.InvokeMember("GetCacheTypeString",
bindingFlags,
null,
null,
args)));
}
public static void ReadCache(ArrayList alAssems, UInt32 nFlag)
{
object[] args = new object[]{ alAssems, nFlag };
BindingFlags bindingFlags = (BindingFlags)314;
FusionType.InvokeMember("ReadCache",
bindingFlags,
null,
null,
args);
}
public static StringCollection GetKnownFusionApps()
{
object[] args = new object[0];
BindingFlags bindingFlags = (BindingFlags)314;
return ((StringCollection)
(FusionType.InvokeMember("GetKnownFusionApps",
bindingFlags,
null,
null,
args)));
}
}
There are couple of small items to note about this code. First, notice that there is
a hard-coded version reference in the type constructor. This practice should
obviously be avoided in production code (duh). Second, note how we use a
uniform 'cookie-cutter' approach in each of the member implementations. It's as
though each of the method bodies started life as a copy-paste snippet - only
the names, arg lists and return value casts change. (In actuality, the
method bodies didn't start life as snippets; they were generated by a
hand-rolled reflection wrapper generator that uses
CodeDOM - yet another possible topic for a future article).
Points of Interest
Hopefully you've taken away the following points from this article: (and if not, I
blame Jack Mott)
-
Fusion information is easier to extract than you may have previously assumed
-
Because of the CLI's rich type descriptions in the form of metadata, spelunking
has never been easier - go explore!
-
Reflector is The Tool
for browsing types and method decompilation (although
Anakrino
gets an honorable mention)
-
Reflection
is very useful for accessing and invoking types that would otherwise be
inaccessible in your language of choice (unless that happens to be IL)
-
The
SSCLI source distribution can be quite helpful in filling in the
'missing pieces' and just generally learning how things work in the framework
(be aware, however, that the
SSCLI GC and JITter source are pretty different than that
of the shipping .NET product)
History
-
6/12/2003 - Initial article
-
6/20/2003 - Resized screenshot and did some reformatting in order to prevent
horizontal scrolling
-
7/2/2003 - Linked Gree Fee's "Security Ramifications of Reflection" blog entry