Understanding how the various components of the Java Virtual Machine (JVM) cooperate to provide a secure execution environment will enable you to understand how to administer your own security policy using the new features of Java 2 and to know when you should consider implementing your own extensions to provide a more tailored security policy.
The Java Virtual Machine, Close Up
In this article we identify and introduce those components. The following figure shows a simplified representation of the JVM:
Figure. Components of the JVM
The JVM components that play a role in the security framework are the class loader, class file verifier and security manager.
The Class Loader
Before the JVM can run a Java program, it needs to locate and load the classes which comprise that program into memory. In a traditional execution environment, this service is provided by the operating system which loads code from the file system in a platform-specific way.
The operating system has access to all of the low level I/O functions and has a set of locations on the file system which it searches for programs or shared code libraries. Depending on the operating system, this can be a list of directories to look in using environment variables, such as Path and CLASSPATH, or a LINKLIST, which is included in each executable that specifies where to find components.
In the Java run-time environment things are more complicated by the fact that not all class files are loaded from the same type of location and may not be under the local operating system’s control to ensure integrity. However, in general, classes can be divided into two categories, trusted and untrusted.
• Trusted Classes
Trusted classes are class files that the JVM can assume are well behaved and safe. By making this assumption, the JVM can execute these classes more quickly because the verification and authorization steps can be skipped.
On the Java 2 platform, where increased security is one of the main goals, the classes that are considered trusted have been restricted even further than in previous releases. By default, Java 2 considers only the Java Runtime Environment (JRE) classes to be fully trusted. These are the classes found in the boot class path. All others are subject to verification and permission checking. These are the classes that form the JVM’s base functionality. They are shipped with the JVM implementation and are defined in the Java specification.
In reality, Java 2 uses an internal list of directories (boot class path) to look in for these classes.
this list was the CLASSPATH environment variable and all classes found in this path setting were considered trusted and treated the same as the JRE core classes, unless, of course, an application explicitly changed this policy with its own SecurityManager implementation.
• Untrusted Classes
With Java 2, all local files outside the boot class path are not automatically treated as trusted, neither are files loaded from a network source such as a remote Web server. This simply means these class files will be verified by the class file verifier upon loading and the code will be subjected to the security policy. The permission structure is quite granular in Java 2. There are, in effect, levels, or more precisely groups, of trust (or untrust).
For instance, Java 2 supports a new extension class framework. This framework allows the group of classes in the extensions directory to be treated as extensions to the JVM core classes. These classes are subjected to verification and the security policy, but the default policy is AllPermission, as shown below in the lines extracted from the java.policy file that comes with the installation of the Java 2 SDK, Standard Edition, V1.2:
With many possible sources for class files and the different checks required, different mechanisms are required to locate and load classes.
The ClassLoader class, in the package java.lang, is an abstract class and until Java 2 there was not a concrete implementation of a ClassLoader shipped with the JDK.
Prior to Java 2 , application writers, such as Web browser manufacturers, were required to implement any class loading requirements beyond those the JVM’s internal class loader would provide. This internal loader would have loaded classes from the local file system from locations specified by the CLASSPATH system environment variable.
Beginning with Java 2, the internal loader is restricted to handling only the JVM’s core and extension classes. A new class, SecureClassLoader, in the package java.security, extends ClassLoader to provide function to build the protection domains for a class. Another new class, java.net.URLClassLoader, extends SecureClassLoader to provide a general purpose class loader to load class files from a list of local file directories or HTTP-based URLs.
Application developers using Java 2 still have a great deal of flexibility in implementing their class loading and security requirements, but can now also take advantage of a lot of function and a robust and flexible security model built into the JDK.
The Class File Verifier
Some of the class files loaded by the JVM will come from untrusted sources. These files need to be checked prior to execution to ensure that they do not threaten the integrity of the JVM. The class file verifier is invoked by the class loader to perform a series of tests on class files which are regarded as potentially unsafe.
These tests check all aspects of a class file from its size and structure down to its run-time characteristics. Only when these tests have been passed is the file made available for use.
The heap is an area of memory used by the JVM to store Java objects during the execution of a program. Precisely how objects are stored on the heap is implementation specific and this adds another level of security since it means that a hacker can have no idea of how the JVM represents objects in memory. This in turn makes it far more difficult to mount an attack that depends on accessing memory directly.
One of the interesting features of the JVM design is that as objects are no longer needed, they are automatically marked for garbage collection and at some point the memory they occupied is freed up and made available for reuse.
The Class Area
The class area is where the JVM stores class-specific information such as static methods and static fields. When a class is loaded and has been verified, the JVM creates an entry in the class area for that class.
Often the class area is simply a part of the heap. In this case classes may also be garbage collected once they are no longer used. Alternatively, if the JVM implementation places the class area in a separate part of memory, it will require additional logic on the part of the JVM implementer to clean up classes which are not being used.
When a just-in-time (JIT) compiler is present, the native code generated for class methods is also stored in the class area.
The Native Method Loader
Many of the core Java classes, such as those classes representing GUI elements or networking features, require native-code implementations to access the underlying operating system functions. These native methods are composed of a Java wrapper – which specifies the method signature – and a native-code implementation – often a DLL or shared library.
The native method loader is responsible for locating and loading these shared libraries into the JVM. Note that it is not possible for the JVM to perform any validation or verification of native code.
Once native code has been loaded, it is stored in the native method area for speedy access when required.
The Security Manager
Even when untrusted code has been verified, it is still subject to run-time restrictions. The security manager is responsible for enforcing these restrictions. It is the security manager component, of a Web browser’s JVM for instance, that prevents applets from reading or writing to the file system, accessing the network in an unsafe way, making inquiries about the run-time environment, printing and so on.
Prior to Java 2, in an application such as a Web browser, the security manager was provided by the application manufacturer as part of the application.
In Java 2, the manufacturer now has an alternative. He can choose to use the policy based SecurityManager implementation provided with the JDK and supply policy information to be added to the policy database. The manufacturer can still provide his own security manager, if he so chooses, adding to or replacing function supplied by the JDK’s SecurityManager.
The Execution Engine
The execution engine is the heart of the JVM. It is the virtual processor which executes bytecode. Memory management, thread management and calls to native methods are also performed by the execution engine.
Since Java bytecodes are interpreted at run time in the execution engine, Java programs generally execute more slowly than the equivalent native platform code. This performance overhead occurs because each bytecode instruction must be translated into one or more native instructions each time it is encountered.
The performance of Java is still significantly better than that of other interpreted languages because the bytecode instructions were designed to be very low level – the simplest instructions have a one-to-one correlation with native machine code instructions.
Nevertheless, Sun saw that there would be a need to improve the execution performance of Java and to do so in a way which did not compromise the Write Once, Run Anywhere goal and did not undermine the security of the JVM.
Since all bytecode instructions are ultimately translated to native machine code by the JVM interpreter, the principal ways of speeding performance involve making this translation as quick as possible and performing it as few times as possible.
The security and portability of Java is dependent on the bytecode and class file format. This is what enables code to be run on any JVM and to be rigorously tested to ensure that it is safe prior to execution. Translating bytecode into native machine code and producing an executable file as happens with other programming languages would compromise the security and portability of Java. Thus, any translation must occur after a class file has been loaded and verified.
Two options present themselves:
1. Translate the whole class file into native code as soon as it is loaded and verified.
2. Translate the class file on a method-by-method basis as needed. The first option seems quite attractive but it is possible that many of the methods in a class file will never be executed. Time to translate these methods is therefore wasted. The second option was the one selected by Sun. In this case, the first time a method is called, it is translated into native code, which is then stored in the class area. The class specification is updated so that future calls to the method run the native code rather than the original bytecode.
This meets our requirement that bytecode should be translated as few times as is necessary – once when the code is executed and not at all in the case of code which is not executed.
The process of translating the bytecode to native code on the fly is known as JIT compilation and performed by the JIT compiler. Sun provided a specification for how and when JIT compilers should execute and vendors were left to implement their own JIT compilers as they chose.
JIT-compiled code executes much more quickly than regular bytecode – between 10 to 50 times faster – without impacting portability or security.