The Artima Developer Community
Sponsored Link

Chapter 3 of Inside the Java Virtual Machine
Security
by Bill Venners

<<  Page 2 of 17  >>

Advertisement

The Class Loader Architecture

In Java's sandbox, the class loader architecture is the first line of defense. It is the class loader, after all, that brings code into the Java virtual machine--code that could be hostile or buggy. The class loader architecture contributes to Java's sandbox in three ways:

  1. it prevents malicious code from interfering with benevolent code,
  2. it guards the borders of the trusted class libraries, and
  3. it places code into categories (called protection domains) that will determine which actions the code will be allowed to take.

The class loader architecture prevents malicious code from interfering with benevolent code by providing separate name-spaces for classes loaded by different class loaders. A name-space is a set of unique names -- one name for each loaded class -- that the Java virtual machine maintains for each class loader. Once a Java virtual machine has loaded a class named Volcano into a particular name-space, for example, it is impossible to load a different class named Volcano into that same name-space. You can load multiple Volcano classes into a Java virtual machine, however, because you can create multiple name-spaces inside a Java application by creating multiple class loaders. If you create three separate name-spaces (one for each of three class loaders) in a running Java application, then, by loading one Volcano class into each name-space, your program could load three different Volcano classes into your application.

Name-spaces contribute to security because you can in effect place a shield between classes loaded into different name-spaces. Inside the Java virtual machine, classes in the same name-space can interact with one another directly. Classes in different name-spaces, however, can't even detect each other's presence unless you explicitly provide a mechanism that allows them to interact. If a malicious class, once loaded, had guaranteed access to every other class currently loaded by the virtual machine, that class could potentially learn things it shouldn't know or interfere with the proper execution of your program.

Figure 3-1 shows the name-spaces associated with two class loaders, both of which have loaded a type named Volcano. Each name in a name space is associated with the type data in the method area that defines the type with that name. Figure 3-1 shows arrows from the names in the name- spaces to the types in the method area that define the type. The class loader on the left, which is shown dark gray, has loaded the two dark gray types named Climber and Volcano. They class loader on the right, which is shown light gray, has loaded the two light gray types named BakingSoda and Volcano. Because of the nature of name-spaces, when the Climber class mentions the Volcano class, it refers to the dark gray Volcano, the Volcano loaded in the same name space. It has no way to know that the other Volcano, which is sitting in the same virtual machine, even exists. For the details on how the class loader architecture achieves its separation of namespaces, see Chapter 8, "The Linking Model."



Figure 3-1. Class loaders and name-spaces.

The class loader architecture guards the borders of the trusted class libraries by making it possible for trusted packages to be loaded with different class loaders than untrusted packages. Although you can grant special access privileges between types belonging to the same package by giving members protected or package access, this special access is granted to members of the same package at runtime only if they were loaded by the same class loader.

Often, a user-defined class loader relies on other class loaders--at the very least, upon the class loaders created at virtual machine startup--to help it fulfill some of the class load requests that come its way. Prior to 1.2, class loaders had to explicitly ask for the help of other class loaders. A class loader could ask another user-defined class loader to load a class by invoking loadClass() on a reference to that user-defined class loader. Or, a class loader could ask the bootstrap class loader to attempt to load a class by invoking findSystemClass(), a static method defined in class ClassLoader. In version 1.2, the process by which one class loader asks another class loader to try and load a type was formalized into a parent-delegation model. Starting with 1.2, each class loader except the bootstrap class loader has a "parent" class loader. Before a particular class loader attempts to load a type in its custom way, it by default "delegates" the job to its parent -- it asks its parent to try and load the type. The parent, in turn, asks its parent to try and load the type. The delegation process continues all the way up to the bootstrap class loader, which is in general the last class loader in the delegation chain. If a class loader's parent class loader is able to load a type, the class loader returns that type. Else, the class loader attempts to load the type itself.

In most Java virtual machine implementations prior to 1.2, the built-in class loader (which was then called the primordial class loader) was responsible for loading locally available class files. Such class files usually included the class files that made up the Java application being executed plus any libraries needed by the application, including the class files of the Java API. Although the manner in which the class files for requested types were located was implementation specific, many implementations searched directories and JAR files in an order specified by a class path.

In 1.2, the job of loading locally available class files was parceled out to multiple class loaders. The built-in class loader, previously called the primordial class loader, was renamed the "bootstrap" class loader to indicate that it was now responsible for loading only the class files of the core Java API. The name bootstrap class loader comes from the idea that the class files of the core Java API are the class files required to "bootstrap" the Java virtual machine.

Responsibility for loading other class files, such as the class files for the application being executed, class files for installed or downloaded standard extensions, class files for libraries discovered in the class path, and so on, was given in 1.2 to user-defined class loaders. When a 1.2 Java virtual machine starts execution, therefore, it creates at least one and probably more user-defined class loaders before the application even starts. All of these class loaders are connected in one chain of parent-child relationships. At the top of the chain is the bootstrap class loader. At the bottom of the chain is what came in 1.2 to be called the "system class loader." Prior to 1.2, the name "system class loader" was sometimes used to refer to the built-in class loader, which was also called the primordial class loader. In 1.2, the name system class loader was more formally defined to mean the default delegation parent for new user-defined class loaders created by a Java application. This default delegation parent is usually going to be the user-defined class loader that loaded the initial class of the application, but may be any user-defined class loader decided upon by the designers of the Java Platform implementation.

For example, imagine you write a Java application that installs a class loader whose particular manner of loading class files is by downloading them across a network. Imagine you run this application on a virtual machine that instantiates two user-defined class loaders on startup: an "installed extensions" class loader and a "class path" class loader. These class loaders are connected in a parent-child relationship chain along with the bootstrap class loader as shown in Figure 3-2. The class path's class loader's parent is the installed extensions class loader, whose parent is the bootstrap class loader. As shown in Figure 3-2, the class path class loader is designated as the system class loader, the default delegation parent for new user-defined class loaders instantiated by the application. Assume that when you application instantiates its network class loader, it specifies the system class loader as its parent.



Figure 3-2. A parent-child class loader delegation chain.

Imagine that during the course of running the Java application, a request is made of your class loader to load a class named Volcano. Your class loader would first ask its parent, the class path class loader, to find and load the class. The class path class loader, in turn, would make the same request of its parent, the installed extensions class loader. This class loader, would also first delegate the request to its parent, the bootstrap class loader. Assuming that class Volcano is not a part of the Java API, an installed extension, or on the class path, all of these class loaders would return without supplying a loaded class named Volcano. When the class path class loader responds that neither it nor any of its parents can load the class, your class loader could then attempt to load the Volcano class in its custom manner, by downloading it across the network. Assuming your class loader was able to download class Volcano, that Volcano class could then play a role in the application's future course of execution.

To continue with the same example, assume that at some time later a method of class Volcano is invoked for the first time, and that method references class java.util.HashMap from the Java API. Because it is the first time the reference was used by the running program, the virtual machine asks your class loader (the one that loaded Volcano) to load java.util.HashMap. As before, your class loader first passes the request to its parent class loader, and the request gets delegated all the way up to the bootstrap class loader. But in this case, the bootstrap class loader is able to return a java.util.Hashmap class back to your class loader. Since the bootstrap class loader was able to find the class, the installed extensions class loader doesn't attempt to look for the type in the installed extensions. The class path class loader doesn't attempt to look for the type on the class path. And your class loader doesn't attempt to download it across the network. All of these class loaders merely return the java.util.HashMap class returned by the bootstrap class loader. From that point forward, the virtual machine uses that java.util.HashMap class whenever class Volcano references a class named java.util.HashMap.

Given this background into how class loaders work, you are ready to look at how class loaders can be used to protect trusted libraries. The class loader architecture guards the borders of the trusted class libraries by preventing untrusted classes from pretending to be trusted. If a malicious class could successfully trick the Java virtual machine into believing it was a trusted class from the Java API, that malicious class could potentially break through the sandbox barrier. By preventing untrusted classes from impersonating trusted classes, the class loader architecture blocks one potential approach to compromising the security of the Java runtime.

Given the parent-delegation model, the bootstrap class loader is able to attempt to load types before the standard extensions class loader, which is able to attempt to load types before the class path class loader, which is able to attempt to load types before your network class loader. Thus, given the manner in which the parent-child delegation chain is built, the most trusted library, the core Java API, is checked first for each type. After that, the standard extensions are checked. After that, local class files that are sitting on the class path are checked. So if some mobile code loaded by your network class loader wants to download a type across the network with the same name as something in the Java API, such as java.lang.Integer, it won't be able to do it. If a class file for java.lang.Integer exists in the Java API, it will be loaded by the bootstrap class loader. The network class loader will not attempt to download and define a class named java.lang.Integer. It will just use the type returned by its parent, the one loaded by the bootstrap class loader. In this way, the class loader architecture prevents untrusted code from replacing trusted classes with their own versions.

But what if the mobile code, rather than trying to replace a trusted type, wants to insert a brand new type into a trusted package? Imagine what would happen if your network class loader from the previous example was requested to load a class named java.lang.Virus. As before, this request would first be delegated all the way up the parent-child chain to the bootstrap class loader. Although the bootstrap class loader is responsible for loading the class files of the core Java API, which includes a package named, java.lang, it is unable to find a member of the java.lang package with the name, Virus. Assuming this class was also not found among the installed extensions or on the local class path, your class loader would proceed to attempt to download the type across the network.

Assume your class loader is successful in the download attempt and defines the type named java.lang.Virus. Java allows classes in the same package to grant each other special access privileges that aren't granted to classes outside the package. So, since your class loader has loaded a class that by its name (java.lang.Virus) brazenly declares itself to be part of the Java API, you might expect it could gain special access to the trusted classes of java.lang and could possibly use that special access for devious purposes. The class loader mechanism thwarts this code from gaining special access to the trusted types in the java.lang package, because the Java virtual machine only grants that special package access between types loaded into the same package by the same class loader. Since the trusted class files of the Java API's java.lang package were loaded by the bootstrap class loader, and the malicious java.lang.Virus class was loaded by your network class loader, these types do not belong to the same runtime package. The term runtime package, which first appeared in the second edition of the Java Virtual Machine Specification, refers to a set of types that belong to the same package and that were all loaded by the same class loader. Before allowing access to package- visible members (members declared with protected or package access) between two types, the virtual machine makes sure not only that the two types belong to the same package, but that they belong to the same runtime package -- that they were loaded by the same class loader. Thus, because java.lang.Virus and the members of java.lang from the core Java API don't belong to the same runtime package, java.lang.Virus can't access the package-visible members and types of the Java API's java.lang package.

This concept of a runtime package is one motivation for using different class loaders to load different kinds of classes. The bootstrap class loader loads the class files of the core Java API. These class files are the most trusted. An installed extensions class loader loads class files from any installed extensions. Installed extensions are quite trusted, but need not be trusted to the extent that they are allowed to gain access to package-visible members of the Java API by simply inserting new types into those packages. Because installed extensions are loaded with a different class loader than the core API, they can't. Likewise, code found on the class path by the class path class loader can't gain access to package-visible members of the installed extensions or the Java API.

Another way class loaders can be used to protect the borders of trusted class libraries is by simply disallowing the loading of certain forbidden types. For example, you may have installed some packages that contain classes you want your application to be able to load through your network class loader's parent, the class path class loader, but not through your own network class loader. Assume you have created a package named absolutepower and installed it somewhere on the local class path, where it is accessible by the class path class loader. Assume also that you don't want classes loaded by your class loader to be able to load any class from the absolutepower package. In this case, you would write your class loader such that the very first thing it does is make sure the requested class doesn't declare itself as a member of the absolutepower package. If such a class is requested, your class loader, rather than passing the class name to its parent class loader, would throw a security exception.

The only way a class loader can know whether or not a class is from a forbidden package, such as absolutepower, is by the class's name. Thus a class loader must have a list of the names of forbidden packages. Because the name of class absolutepower.FancyClassLoader indicates it is part of the absolutepower package, and the absolutepower package is on the list of forbidden packages, your class loader should throw a security exception absolutely.

Besides shielding classes in different namespaces and protecting the borders of trusted class libraries, class loaders play one other security role. Class loaders must place each loaded class into a protection domain, which defines what permissions the code is going to be given as it runs. More information about this vitally important security job of class loaders will be given later in this chapter.

<<  Page 2 of 17  >>


Sponsored Links



Google
  Web Artima.com   
Copyright © 1996-2019 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use