The Role of Application Domains


To meet the reliability and security required by modern computing environments, operating systems and other software platforms must provide a means to isolate unrelated applications from one another at run time. This isolation is necessary to ensure that code running in one application cannot adversely affect another, thereby exploiting a security vulnerability or causing that application to crash.

The Microsoft Windows operating system uses processes to enforce this isolation. If you've used Microsoft Internet Information Services (IIS) and ASP in the past, you probably recall two configurations you could select that controlled where your application code ran relative to the code for IIS itself. In one configuration you chose to run your application in the same process as IIS. Your other option was to run your application in a different process than IIS. The tradeoff between these two options was one of reliability versus performance. Running in-process with IIS meant no process boundaries were crossed between your application and IIS. However, if some application running in the IIS process had a bug that caused it to access an invalid memory location, not only would that application crash, but the entire IIS process would go down, bringing your application with it. Choosing to run your application in a different process isolates you from this unpredictability, but that isolation comes at a performance cost. In this configuration all calls between your application and IIS must cross a process boundary, which introduces a performance penalty. This tradeoff between reliability and performance doesn't scale in many situations, including the Web server scenario just described.

One of the reasons process boundaries prove expensive as a way to enforce isolation is related to the way memory accesses are validated. Memory access in the Windows operating system is process-relative. That is, the same value for a memory address maps to two different locations in two different processeseach location is scoped to the process in which it is used. This isolation is essential in preventing one process from accessing another's memory. This isolation is enforced in hardware. Memory addresses in the Windows operating system are not references into the processor's physical memory. Instead, every address is virtual, giving the application a flat, contiguous view of memory. So each time memory is accessed by the application, its address must be translated by the processor from a virtual address to a physical address. This translation involves the use of a CPU register and several lookup tables examined by the processor. This CPU register holds a value that points to a list of physical pages available to the process. Each time a process switch occurs in the Windows operating system, the value of this register must be adjusted. In addition, the processor must take a virtual address and break it down into pieces that identify the page and the offset within the page of the physical memory being accessed. These checks must be done at the hardware level to ensure that all memory accesses are to valid memory addresses within the process. The reason that memory access must be validated on the fly in this fashion is because the Windows operating system cannot determine ahead of time which memory in the process the application will access.

Another factor in the cost of using processes as an isolation boundary is the expense of switching the context associated with execution from one thread to another. All threads in the Windows operating system are process-relativethey can run only in the process in which they are created. When processes are used to isolate IIS from your application, the transfer of control between the two processes involves a thread context switch. This thread switch, along with the cost of translating memory address and some overhead associated with communication between processes, accounts for most of the expense associated with using processes to achieve application isolation.

Type Safety and Verification

In contrast, the CLR can statically determine how a given process will access memory before it is actually run. This is possible primarily because the Microsoft Intermediate Language (MSIL) is at a higher level and much more descriptive than native processor instructions such as x86 or Intel Architecture 64 (IA-64). The CLR relies on this more descriptive instruction set to tell whether a given assembly is type safe, that is, whether all accesses to memory made from within the assembly go through public entry points to valid objects. In this way, the CLR can ensure that a given assembly cannot access invalid regions of memory within the process. That is, they cannot produce an access violation that can bring down the entire process. The practice of determining program correctness is referred to as verification.

Given code that is verified to be type safe, the CLR can provide the isolation typically associated with a process without having to rely on the process boundary. This is where application domains enter the picture. They are the CLR's construct for enforcing isolation at a cost lower than that of a process boundary, which means you can run multiple managed applications in the same operating system process and still get the level of isolation that previously was possible only by using a process boundary. In this way, application domains are the reason that applications such as ASP.NET scale much better than their unmanaged predecessors. Because of the isolation they provide within a single process, application domains are often described as subprocesses. Figure 5-1 shows a single Windows process running three separate, unrelated applications. Each application runs in its own application domain.

Figure 5-1. One process can contain multiple application domains.


Not all managed assemblies are verifiably type safe, however. If an assembly cannot be verified to be type safe, you cannot rely on application domains for isolation. Such code can make an invalid memory access just as unmanaged code can, causing the entire process to come down. Whether code is verifiable depends in part on the programming language used to write the assembly. The MSIL instruction set is built to support a wide variety of languages, including those that include support for direct pointer manipulation. As a result, your ability to generate verifiable code depends on whether the language you are using exposes some of the unsafe aspects of MSIL. For example, all code generated by the Microsoft Visual Basic.NET compiler is type safe. Visual Basic.NET exposes no language features that let you write unverifiable code. In contrast, C++ is typically not verifiable because C++ code often makes extensive use of pointers. C# is somewhere in the middle. By default, C# produces verifiable code. However, you can use pointers in C# by using the unsafe language keyword.

To leverage the full benefits of application domain isolation, you must ensure that only code that can be verified to be type safe is allowed to run in your process. You do this using the CLR's Code Access Security (CAS) system. I've talked about verifiability mostly from the perspective of reliability, but the ability to verify that code won't make random memory accesses is critical for security reasons as well. Verification can be relied on to make sure that code won't overrun buffers or access data it shouldn't, for example. In fact, CAS depends completely on verifiabilitywithout type-safe code, CAS can offer you little protection. A complete description of CAS is beyond the scope of this book, but for now suffice it to say that the system works by granting a set of permissions to code based on some characteristic of that code that is meaningful to administrators or to the host application. Examples of permissions include the ability to write to the file system or the registry and the ability to read and write data from the network. Because the notion of verifiability is so central to security, one of the permissions defined by the Microsoft .NET Framework is the SkipVerification[1] permissionor the ability to run code that can't be verified. As a host, you can control which permissions are granted to code running in the application domains you create. Ensuring that nonverifiable code can never run in your process is simply a matter of customizing the CAS system to never grant SkipVerification permission. The details of how to accomplish this are explored in detail in Chapter 10.

[1] Technically, SkipVerification isn't permission itself in the true sense. It's a state of another permission called SecurityPermission. See the Microsoft .NET Framework SDK for details.

Application Isolation

In the preceding section, I talked about isolation primarily from the perspective of preventing unwanted memory access. The isolation provided by application domains has many more dimensions, however. Application domains provide isolation for most everything you rely on process isolation to do, including the following:

  • Type visibility

  • Configuration data

  • Security settings

  • Access to static data and members

Type Visibility

Application domains form a boundary around the types that can be called from code running in that domain. Assemblies are always loaded into a process within the context of a domain. If the same assembly is loaded by code in two domains, the assembly is loaded twice.[2] Assemblies loaded into the same domain can freely use each other's public types and methods. However, a type in one application domain can't automatically see and use public types in other application domains. Communicating across domains in this way requires a formal mechanism for discovering types and calling them. In the CLR, this formal mechanism is provided by the remoting infrastructure. Just as a process or a machine forms a boundary across which calls must be removed, so does an application domain. In this way, the remote domain must explicitly expose a type so it can be accessed from another domain, thereby enforcing that cross-domain calls come through known entry points.

[2] This is true in general; however, an optimization known as domain-neutral loading changes this loading behavior. Domain-neutral loading does not change any semantic behavior with respect to isolation though. I talk about domain-neutral code much more in Chapter 9.

Configuration Data

Each application domain has a configuration file that can be used to customize everything from the local search path for assemblies to versioning and remoting information. In addition, code running within the application domain can store its own settings in the configuration file. Examples of these settings include connection strings to a database or a most recently used files list.

Security Settings

Applications domains can be used to further scope the CAS permissions granted to code running within the domain. This further scoping must be set up explicitly by the extensible applicationit is not enabled by default. Permission grants can be customized by the extensible application in two ways. The first is to define custom CAS policy for the domain. This policy maps a notion of code identity (called evidence) to a set of permissions that the code is granted. Examples of evidence include the location from which the code was loaded, its strong-name signature, an Authenticode signature, or any custom evidence provided by the host. Real-world examples of how application domain policy is used include SQL Server 2005 and ASP.NET. In SQL Server 2005, domain policy is used to restrict the permissions that code loaded out of the database is granted. ASP.NET uses domain policy to implement the Minimum, Low, Medium, High, Full permission scheme.

The second way to configure security for an application domain is to set security evidence on the domain itself. This evidence is evaluated by policy just as the evidence from an assembly is. The union of these two evaluations forms the final set of permissions that are granted to the assembly. If the grants from the domain are less than the grants from the assembly, the domain essentially wins. For example, the Microsoft Internet Explorer host uses this feature to make sure that no code running in the domain is granted more permissions than the Web site would get. This is implemented by setting domain-level evidence to the URL of the Web site from which the code was loaded.

The CAS system is so extensible that an entire chapter is dedicated to it later in the book. In Chapter 10, I describe how to implement both application domain CAS policy and domainlevel security evidence.

Access to Static Data and Members

I've discussed how application domains are used to restrict access to types and their members. However, static data and members don't require instantiation of a type, but still must be isolated per domain to prevent either accidental or malicious leaking of data across domain boundaries. This behavior usually falls out naturally because assemblies are not shared across domains by default. However, when code is loaded domain neutral, the jit-compiled code is shared among all domains in the process to minimize the working set. In this case, the CLR provides this isolation by maintaining a separate copy of each static data member or method for each application domain into which the assembly is loaded.

Runtime Concepts Not Isolated by Application Domains

Not all resources that are isolated to a process in Win32 are isolated to an application domain in the CLR. In some cases, isolating these resources to an application domain isn't necessary because of type safety or other checks implemented by the runtime. In other cases, resources aren't isolated to an application domain because they are built on the underlying Win32 primitives, which, of course, have no notion of application domains.

Examples of resources not isolated to an application domain include the garbage collection heap, managed threads, and the managed thread pool. In the case of the garbage collection heap, the notion of type safety can be relied upon so that different heaps need not be maintained for each application domain. In the case of threads and the thread pool, the CLR has knowledge of which application domain a thread is currently running in and takes great care to ensure that no data or behavior is inadvertently leaked when a thread crosses into another domain. I talk more about this later in the "Application Domains and Threads" section.

The .NET Framework libraries provide a number of classes that enable add-ins to create and use synchronization primitives such as events and mutexes. These primitives are examples of managed concepts that are built directly on the unmanaged equivalents. Because the unmanaged notion of an event or mutex clearly has no knowledge of application domains, the managed equivalents are technically not constrained by the application domain boundary. However, application domain isolation of the managed synchronization primitives can be achieved in practice by controlling access to the name you give the object when it is created. For example, if you create an unnamed managed synchronization object, there would be no way that object could be manipulated directly from another domain unless you explicitly pass the underlying operating system handle to the other domain. Perhaps a "truer" notion of application domainscoped synchronization primitives might show up in the future if the underlying Windows operating system were to add the notion of an application domain to its current process structure.

The other important aspect of process management that hasn't yet been scoped to the application domain level is debugging. In Win32 when a thread hits a breakpoint in one process, all threads in that process are suspended, but the threads in other processes continue to run. Application domains don't mirror this same behavior yet, unfortunately. When you set a breakpoint in managed code, all threads in that process stop (regardless of which application domain they are running in at the time) instead of just the one you're debugging. Clearly, this behavior isn't ideal and is a significant restriction in many scenarios. There's no underlying technical reason why this can't be implemented given the current architecture. It just hasn't been done because of time constraints and priorities. It's likely this debugging behavior will be fixed in upcoming releases of the CLR.



    Customizing the Microsoft  .NET Framework Common Language Runtime
    Customizing the Microsoft .NET Framework Common Language Runtime
    ISBN: 735619883
    EAN: N/A
    Year: 2005
    Pages: 119

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net