The Java language is designed to enforce type safety. This means that programs are prevented from accessing memory in inappropriate ways. More specifically, every piece of memory is part of some Java object. Each object has some class. For example, a calendar-management applet might use classes like Date, Appointment, Alarm, and GroupCalendar. Each class defines both a set of objects and operations to be performed on the objects of that class. In our calendar management example, the Alarm class might define a turnOn operation, but the Date class would neither need nor allow turnOn. Type safety means that a program cannot perform an operation on an object unless that operation is valid for that object.
Type safety is the most essential element of Java's security. To understand why, consider the following slightly contrived example. A calendar-management applet defines a class called Alarm. This class is represented in memory as shown in Figure 2.10. Alarm defines an operation turnOn, which sets the first field to true. The Java runtime library defines another class called Applet, whose memory layout is also shown in Figure 2.10. Note that the first field of Applet is fileAccessAllowed, which determines whether the applet is allowed access to files on the hard disk.
Suppose a program tried to apply the turnOn operation to an Applet object. If the operation were allowed to go ahead, it would do what turnOn was supposed to do, and set the first field of the object to true. Since the object was really in the Applet class, setting the first field to true allows the applet to access the hard disk. The applet would then be allowed (incorrectly) to delete files.
This example shows what can go wrong if type safety is violated. In our experience, every type safety violation has created an opportunity for an untrusted applet to break out of Java's security restrictions. Given the importance of type safety, the next section explains Java's strategy for ensuring type safety.
Every Java object is stored in some region of the computer's memory. Java labels every object by putting a class tag next to the object. One simple way to enforce type safety is to check the class tag of the object before every operation on the object. This will help make sure the object's class allows the operation. This approach is called dynamic type checking.
Though dynamic type checking works, it is inefficient. The more time a system spends checking class tags, the more slowly programs run. To improve performance, Java uses static type checking whenever it can. Java looks at the program before it is run and carefully tries to determine which way the tag checking operations will come out. This is more complicated, but more efficient than dynamic type checking. If Java can figure out that a particular tag checking operation will always succeed, then there is no reason to do it more than once. The check can safely be removed, speeding up the program. Similarly, if Java can figure out that a particular tag checking operation will always fail, then it can generate an error before the program is even loaded.
The designers of Java carefully crafted the Java language and byte code formats to facilitate static type checking. The byte code Verifier is a very effective static type checker, eliminating almost all of the tag checking operations from Java programs. The result is a type safe program that runs quite efficiently.
Static type checking has other advantages, too. For example, static type checking can be done at compile time, thus informing the software developer of any type errors before the code is shipped.
There is only one problem with Java's static type checking strategy: It's complicated. Though Java's designers obviously got the overall strategy right, there are a great many details that have to be perfect for type safety to be enforced. An error in any of these details would be a tiny but crucial hole in Java's type safety enforcement dike.
A clever cracker who finds such a hole can launch a type confusion attack. Such an attacker could write a Java applet designed to trigger a tiny type enforcement error. The attacker could then create a situation like our Alarm/Applet example in which the program has one kind of object but Java thinks that object has some other kind. As in the example, this seemingly harmless confusion can be exploited to breach Java's security. If you recall that two of the three parts of the base Java security sandbox are themselves Java classes, it becomes immediately apparent what sorts of havoc type confusion can cause. Several real-life type confusion attacks are discussed in Chapter 5.
Type safety is the cornerstone of Java security. There is much more to the rest of the edifice, of course, but without type safety the entire building would be unsound.
Type safety guarantees that programs will not do terrible and dangerous things such as treating pointers as integers (or vice versa) or falling off the end of an array. These are the sorts of things that make it very easy to write insecure code in C and C++.
The typing constraints in Java exist to prevent arbitrary access to memory. This in turn makes it possible for a software module to encapsulate its state. This encapsulation takes the form of allowing a software module to declare that some of its methods and variables may not be accessed by anything outside the code itself. The more control is placed on access points (and the fewer access points there are), the better a module can control access to its state.
It is this idea that permeates the design of the Security Manager. The VM controls access to potentially dangerous operating system calls by wrapping the calls in an API that invokes a security check before making the call. Only the VM can make a direct system call. All other code must call into the VM through explicit entry points that implement security checks.
As we will see in the next chapter, encapsulation turns out to be essential to the design of the Java 2 access control system as well.
Copyright ©1999 Gary McGraw and Edward Felten.