.NET Security Workshop
Home About Workshops Articles Writing Talks Books Contact

4. Validation and Verification

When an assembly is loaded it is checked to make sure that it does not contain code that is corrupted or intentionally dangerous. These checks are performed by the runtime and the JIT compiler, and they are called validation and verification.

All assemblies are PE (portable executable) files and so the first action when the assembly is loaded is to make sure that it contains valid PE file data. For example, the PE file has a 'data directory table' and one of the entries points to the Common Language Runtime Header which indicates where metadata is stored, so the validation step will ensure that the values in this header are valid.

Next, the runtime checks that the data in the metadata tables are valid. The metadata tables hold various important information, for example, there is a table containing information about methods which includes a pointer to where the IL is stored for each method. In addition, there are tables that describe the assemblies that are referenced, external files used by the assembly (and hashes of those files), a table with the identity of the assembly (its name, version, culture and strong name signature) and a table for the assembly's declarative security. If any of these tables is corrupt it could cause a runtime error, or worse, it could cause the code to perform an unexpected action. Thus, the runtime must validate all the date in these tables. The runtime performs two validation checks on metadata: structural and semantic metadata validation. Structural checks ensure that the pointers in the metadata tables point to valid locations. Semantic checks ensure that runtime rules are obeyed, for example, that inheritance rules are obeyed.

Once the metadata tables have been validated the runtime can trust the pointers to the methods' IL and so it then walks through all of the IL validating that the bytes in the IL stream are legal combinations for IL. The IL validation also checks to make sure that jumps in the IL are to locations within the method.

Once the assembly has been validated, the IL can be loaded and run. The computer's processor does not run IL, instead, a separate stage is performed called just-in-time (JIT) compilation. This is performed on a method by method basis; it is just-in-time because the first time a method is called it is JIT compiled and the machine code is cached in memory so that the next time it is called the JIT compiled code will be used. When a method is JIT compiled the code is verified.

Verification is not an exact process. The verifier iterates through every code path and verifies that the IL is safe. Legal IL can be unsafe, that is, IL can access memory directly and perform pointer arithmetic which can potentially allow code to perform an action that could corrupt memory. The verification algorithm checks each IL instruction and simulates the state of the stack when the instruction will be called. The JIT compiler does not use data, instead, it looks at the number of items on the stack and their types and compares these with the expected number and type of parameters for that instruction.

If an assembly contains any unsafe code at all then it means that the assembly is not verifiable. An assembly that is not verifiable is not necessarily a bad assembly, it just means that you must apply more personal trust to it when it is loaded. Such an assembly must come from a publisher that you trust have not done anything nasty in the unsafe code. Non-verifiability is added as part of the evidence passed to code access security to determine the permissions of the assembly (something that will be covered on the next page).

4.1 Verifying An Assembly

The examples on the previous page illustrated occasions when metadata validation failed because the actual metadata hash is different to the hash in the assembly metadata tables or that the strong name signature disagrees with the actual hash of the assembly.

In general, an assembly that is non-verifiable can only be executed if it has full trust. Full trust is assigned to code that is installed on the local machine. You can test to see if an assembly is verifiable with the PEVerify tool. For example, create the following library code (verifyAssem.cs):

using System;

public class Verify
{
   public void Test()
   {}
}

Compile this as a library (csc /t:library verifyAssem.cs) and pass the library to PEVerify:

C:\TestFolder>peverify verifyAssem.dll

Microsoft (R) .NET Framework PE Verifier Version 1.1.4322.573
Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.

All Classes and Methods in verifyAssem.dll Verified

You cannot conclusively state that an assembly is verified not to have unsafe code, however, the .NET code verification routine is very conservative. This means that if an assembly passes verification there is little chance that the code contains unsafe code, however, this conservative algorithm may not verify code that does not contain unsafe code.

C# allows you to generate unsafe code to get direct access to memory. It does this by allowing you to use pointer syntax and pointer arithmetic in a block of code marked with fixed. If a method contains unsafe code it must be marked with the unsafe keyword, and if an assembly contains unsafe code it should be compiled with /unsafe.

Delete the Test method and add the following:

unsafe public void strrev(string str)
{
   fixed (char* p = str)
   {
      char *start = p;
      char *left = p;
      start += str.Length - 1;
      while (left <= start)
      {
         char ch = *left;
         *left++ = *start;
         *start-- = ch;
      }
   }
}

The object identified in the fixed block is pinned, this prevents the garbage collector from moving the object. If the object was not pinned then the pointer (p) would become invalid if the object was moved during a garbage collection. The pointer created by the fixed keyword is called a pinning pointer. The object will be pinned for the time that the pinning pointer points to the object. Variables in IL are in scope for the entire duration of the method, however, the pinning pointer is only assigned at the location of the fixed declaration, C# will assign the pinning pointer to null at the location indicated by the closing brace of the fixed block. Thus the object will only be pinned during the fixed block.

In this case the pinning pointer is an interior pointer to the array of characters maintained by the string object. You cannot change this pointer, but you can use it to initialise other pointers in the fixed bock. You can perform pointer arithmetic and dereference the pointer. This code reverses the characters held in the immutable string object through direct access to its internal data. That is, the string is no longer immutable. As you can see unsafe code can do things that, strictly speaking, should not be allowed in managed code.

Next create the source code for a process that uses this code (app.cs). The process has a Main method that looks like this:

static void Main(string[] args)
{
   Console.WriteLine("before: '{0}'", args[0]);
   Verify v = new Verify();
   v.strrev(args[0]);
   Console.WriteLine("after: '{0}'", args[0]);
}

Compile this process and run it to ensure that it works the way expected.

Finally pass the library to PEVerify and you'll see that the tool will identify that the code is not verifiable and identifies the issues.

.NET Version 3.0
Curiously, peverify in .NET 3.0/2.0 fails, but with the single error:

[IL]: Error: [C:\TestFolder\verifyAssem.dll : Verify::strrev][offset 0x00000006][found ref 'System.String'] Expected numeric type on thestack.

This is not what we would expect.

Before moving on it is interesting to use ILDASM to view the manifest of the library. The two relevant items are show here:

.assembly verifyAssem
{
   .permissionset reqmin = () // omitted for clarity
   .hash algorithm 0x00008004
   .ver 0:0:0:0
}
.custom instance void [mscorlib]System.Security.UnverifiableCodeAttribute::.ctor() = ( 01 00 00 00 )


.NET Version 3.0
Version 8.0 of the framework adds two custom attributes to the assembly: [CompilationRelaxation(CompilationRelaxation.NoStringInterning)] and [RuntimeCompatibility(WrapNonExceptionThrows=true)]. These attributes are added to all library assemblies and can be ignored in the rest of this discussion.

These items are used by code access security. In particular, the .permissionset entry defines the minimum permissions that the assembly is required to have before it is loaded. This permission set is defined by the following XML (for v1.1):

<PermissionSet class="System.Security.PermissionSet" version="1">
   <IPermission class="System.Security.Permission.SecurityPermission, mscorlib,
      Version=1.0.5000.0, Culture=neutral, PublicKeyToken=.b77a5c561934e089"
      version="1" Flags="SkipVerification" />
</PermissionSet>

This indicates that the assembly must have the SecurityPermission permission initialised with SecurityPermissionFlag.SkipVerification. As the flag and the attribute indicate, the assembly announces that it has unverifiable code and therefore the JIT compiler should not bother to verify the assembly.

.NET Version 3.0
The reqmin item is also different in version 3.0/2.0:

.permissionset reqmin
   = {[mscorlib]System.Security.Permissions.SecurityPermissionAttribute
      = {property bool 'SkipVerification' = bool(true)}}

This is essentially the same as the XML inserted by the compiler for v1.1 code, but is more compact. Bear this in mind when you read the sections about permissions on a later page.

4.2 Validating An Assembly

Note that PEVerify performs both verification and validation on an assembly. You can also use ILDASM /adv to perform validation. To do this, type the following at the command line

ildasm /adv verifyAssem.dll

This starts ILDASM in GUI mode, from the View menu you can open the MetaInfo menu and click on the Validate item. This sets a validate flag and you can perform the action by pressing Ctrl-M or by selecting the Show! item on the MetaInfo menu.

.NET Version 3.0
Version 3.0/2.0 of ILDASM will list all the metadata tables and the various heaps in the metadata section before it will perform the validation. Thus, you should scroll to the bottom of the popup window to see the result of the validation.

Although this assembly is not verifiable it does contain valid IL and so you should get the message: No Warnings or errors found.

I hope that you enjoy this tutorial and value the knowledge that you will gain from it. I am always pleased to hear from people who use this tutorial (contact me). If you find this tutorial useful then please also email your comments to mvpga@microsoft.com.

Errata

If you see an error on this page, please contact me and I will fix the problem.

Page Five

This page is (c) 2007 Richard Grimes, all rights reserved