This month's article continues the discussion of Java's security model begun in last month's "Under the Hood," which provided a general overview of the security mechanisms built into the Java virtual machine (JVM). I also looked closely at one aspect of those security mechanisms: the JVM's built-in safety features. This month's article takes a look at yet another aspect of the JVM's built-in security mechanisms: the class loader architecture.
A sandbox refresher
Java's security model is focused on protecting end-users from hostile programs downloaded from untrusted sources across a network. To accomplish this goal, Java provides a customizable "sandbox" in which Java programs run. A Java program can do anything within the boundaries of its sandbox, but it can't take any action outside those boundaries. The sandbox for untrusted Java applets, for example, prohibits many activities, including:
By making it impossible for downloaded code to perform certain actions, Java's security model protects users from the threat of hostile code. For more information on the sandbox concept, see last month's "Under the Hood."
The class loader architecture
One aspect of the JVM that plays an important role in the security sandbox is the class loader architecture. In the JVM, class loaders are responsible for importing binary data that defines the running program's classes and interfaces. In the block diagram shown in Figure 1, a single mysterious cube identifies itself as "the class loader," but in reality, there may be more than one class loader inside a JVM. Thus, the class loader cube of the block diagram actually represents a subsystem that may involve many class loaders. The JVM has a flexible class loader architecture that allows a Java application to load classes in custom ways.
Figure 1. Java's class loader architecture |
A Java application can use two types of class loaders: a "primordial" class loader and class loader objects. The primordial class loader (there is only one of them) is a part of the JVM implementation. For example, if a JVM is implemented as a C program on top of an existing operating system, then the primordial class loader will be part of that C program. The primordial class loader loads trusted classes, including the classes of the Java API, usually from the local disk.
At run time, a Java application can install class loader objects that load classes in custom ways, such as by downloading class files across a network. The JVM considers any class it loads through the primordial class loader to be trusted, regardless of whether or not the class is part of the Java API. It views with suspicion, however, those classes it loads through class loader objects. By default, it considers them to be untrusted. While the primordial class loader is an intrinsic part of the virtual machine implementation, class loader objects are not. Instead, class loader objects are written in Java, compiled into class files, loaded into the virtual machine, and instantiated just like any other object. They really are just another part of the executable code of a running Java application. You can see a graphical depiction of this architecture in Figure 2.
Figure 2. Java's class loader architecture |
Because of class loader objects, you don't have to know at compile-time all the classes that may ultimately take part in a running Java application. They enable you to dynamically extend a Java application at run time. As it runs, your application can determine what extra classes it needs and load them through one or more class loader objects. Because you write the class loader in Java, you can load classes in any manner: You can download them across a network, get them out of some kind of database, or even calculate them on the fly.
Class loaders and name-spaces
For each class it loads, the JVM keeps track of which class loader -- whether primordial or object -- loaded the class. When a loaded class first refers to another class, the virtual machine requests the referenced class from the same class loader that originally loaded the referencing class. For example, if the virtual machine loads class Volcano
through a particular class loader, it will attempt to load any classes Volcano
refers to through the same class loader. If Volcano
refers to a class named Lava
, perhaps by invoking a method in class Lava
, the virtual machine will request Lava
from the class loader that loaded Volcano
. The Lava
class returned by the class loader is dynamically linked with class Volcano
.
Because the JVM takes this approach to loading classes, classes can by default only see other classes that were loaded by the same class loader. In this way, Java's architecture enables you to create multiple name-spaces inside a single Java application. A name-space is a set of unique names of the classes loaded by a particular class loader. For each class loader, the JVM maintains a name-space, which is populated by the names of all the classes that have been loaded through that class loader.
Once a JVM 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 JVM, however, because you can create multiple name-spaces inside a Java application. You can do so simply by creating multiple class loaders. If you create three separate name-spaces (one for each of the 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.
A Java application can instantiate multiple class loader objects either from the same class or from multiple classes. It can, therefore, create as many (and as many different kinds of) class loader objects as it needs. Classes loaded by different class loaders are in different name-spaces and cannot gain access to each other unless the application explicitly allows it. When you write a Java application, you can segregate classes loaded from different sources into different name spaces. In this way, you can use Java's class loader architecture to control any interaction between code loaded from different sources. You can prevent hostile code from gaining access to and subverting friendly code.
Class loaders for applets
One example of dynamic extension with class loaders is the Web browser, which uses class loader objects to download the class files for an applet across a network. A Web browser fires off a Java application that installs a class loader object -- usually called an applet class loader -- that knows how to request class files from an HTTP server. Applets are an example of dynamic extension, because when the Java application starts, it doesn't know which class files the browser will ask it to download across the network. The class files to download are determined at run time, as the browser encounters pages that contain Java applets.
The Java application started by the Web browser usually creates a different applet class loader object for each location on the network from which it retrieves class files. As a result, class files from different sources are loaded by different class loader objects. This places them into different name-spaces inside the host Java application. Because the class files for applets from different sources are placed in separate name-spaces, the code of a malicious applet is restricted from interfering directly with class files downloaded from any other source.
Cooperation between class loaders
Often, a class loader object relies on other class loaders -- at the very least, upon the primordial class loader -- to help it fulfill some of the class load requests that come its way. For example, imagine you write a Java application that installs a class loader whose particular manner of loading class files is achieved by downloading them across a network. Assume that during the course of running the Java application, a request is made of your class loader to load a class named Volcano
.
One way you could write the class loader is to have it first ask the primordial class loader to find and load the class from its trusted repository. In this case, since Volcano
is not a part of the Java API, assume the primordial class loader can't find a class named Volcano
. When the primordial class loader responds that it can't 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 some time later a method of class Volcano
is invoked for the first time, and that the method references class String
from the Java API. Because it is the first time the reference is used by the running program, the virtual machine asks your class loader (the one that loaded Volcano
) to load String
. As before, your class loader first passes the request to the primordial class loader, but in this case, the primordial class loader is able to return a String
class back to your class loader.
The primordial class loader most likely didn't have to actually load String
at this point because, given that String
is such a fundamental class in Java programs, it was almost certainly used before and therefore already loaded. Most likely, the primordial class loader just returned the String
class that it had previously loaded from the trusted repository.
Since the primordial class loader was able to find the class, your class loader doesn't attempt to download it across the network; it merely passes to the virtual machine the String
class returned by the primordial class loader. From that point forward, the virtual machine uses that String
class whenever class Volcano
references a class named String
.
Class loaders in the sandbox
In Java's sandbox, the class loader architecture is the first line of defense against malicious code. It is the class loader, after all, that brings code into the JVM -- code that could be hostile.
The class loader architecture contributes to Java's sandbox in two ways:
The class loader architecture guards the borders of the trusted class libraries by making sure untrusted classes can't pretend to be trusted. If a malicious class could successfully trick the JVM into believing it was a trusted class from the Java API, that malicious class potentially could 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.
Name-spaces and shields
The class loader architecture prevents malicious code from interfering with benevolent code by providing protected name-spaces for classes loaded by different class loaders. As mentioned above, name-space is a set of unique names for loaded classes that is maintained by the JVM.
Name-spaces contribute to security because you can, in effect, place a shield between classes loaded into different name-spaces. Inside the JVM, 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 the classes to interact. If a malicious class, once loaded, had guaranteed access to every other class currently loaded by the virtual machine, that class potentially could learn things it shouldn't know, or it could interfere with the proper execution of your program.
Creating a secure environment
When you write an application that uses class loaders, you create an environment in which the dynamically loaded code runs. If you want the environment to be free of security holes, you must follow certain rules when you write your application and class loaders. In general, you will want to write your application so that malicious code will be shielded from benevolent code. Also, you will want to write class loaders such that they protect the borders of trusted class libraries, such as those of the Java API.
Name-spaces and code sources
To get the security benefits offered by name-spaces, you need to make sure you load classes from different sources through different class loaders. This is the scheme, described above, used by Java-enabled Web browsers. The Java application fired off by a Web browser usually creates a different applet class loader object for each source of classes it downloads across the network. For example, a browser would use one class loader object to download classes from http://www.niceapplets.com, and another class loader object to download classes from http://www.meanapplets.com.
Guarding restricted packages
Java allows classes in the same package to grant each other special access privileges that aren't granted to classes outside the package. So, if your class loader receives a request to load a class that by its name brazenly declares itself to be part of the Java API (for example, a class named java.lang.Virus
), your class loader should proceed cautiously. If loaded, such a class could gain special access to the trusted classes of java.lang
and could possibly use that special access for devious purposes.
Consequently, you would normally write a class loader so that it simply refuses to load any class that claims to be part of the Java API (or any other trusted runtime library) but that doesn't exist in the local trusted repository. In other words, after your class loader passes a request to the primordial class loader, and the primordial class loader indicates it can't load the class, your class loader should check to make sure the class doesn't declare itself to be a member of a trusted package. If it does, your class loader, instead of trying to download the class across the network, should throw a security exception.
Guarding forbidden packages
In addition, you may have installed some packages in the trusted repository that contain classes you want your application to be able to load through the primordial class loader, but that you don't want to be accessible to classes loaded through your class loader. For example, assume you have created a package named absolutepower
and installed it on the local repository accessible by the primordial 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 to 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 the primordial class loader, should throw a security exception.
The only way a class loader can know whether or not a class is from a restricted package, such as java.lang
, or a forbidden package, such as absolutepower
, is by the name of the class. Thus, a class loader must be given a list of the names of restricted and forbidden packages. Because the name of class java.lang.Virus
indicates it is from the java.lang
package, and java.lang
is on the list of restricted packages, your class loader should throw a security exception if the primordial class loader can't load it. Likewise, 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.
A security-minded class loader
A common way to write a security-minded class loader is to use the following four steps:
By performing steps one and three as outlined above, the class loader guards the borders of the trusted packages. With step one, it prevents a class from a forbidden package to be loaded at all. With step three, it doesn't allow an untrusted class to insert itself into a trusted package.
Conclusion
The class loader architecture contributes to the JVM's security model in two ways:
Both of these capabilities of Java's class loader architecture must be used properly by programmers so as to reap the security benefit they offer. To take advantage of the name-space shield, code from different sources should be loaded through different class loader objects. To take advantage of trusted package border guarding, class loaders must be written so they check the names of requested classes against a list of restricted and forbidden packages.
For a walk through of the process of writing a class loader, including sample code, see Chuck McManis's JavaWorld article, "The basics of Java class loaders."
Next month
In next month's article, I'll continue the discussion of the JVM's security model by describing the class verifier.
This article was first published under the name Security and the Class Loader Architecture in JavaWorld, a division of Web Publishing, Inc., August 1997.
Have an opinion? Be the first to post a comment about this article.
Bill Venners has been writing software professionally for 12 years. Based in Silicon Valley, he provides software consulting and training services under the name Artima Software Company. Over the years he has developed software for the consumer electronics, education, semiconductor, and life insurance industries. He has programmed in many languages on many platforms: assembly language on various microprocessors, C on Unix, C++ on Windows, Java on the Web. He is author of the book: Inside the Java Virtual Machine, published by McGraw-Hill.
Artima provides consulting and training services to help you make the most of Scala, reactive
and functional programming, enterprise systems, big data, and testing.