Chapter 23. System Programming

 

GLENDOWER: I can call spirits from the vasty deep. HOTSPUR: Why, so can I, or so can any man; But will they come when you do call for them?

 
 --William Shakespeare, King Henry IV, Part 1

Sometimes your application must interact with the runtime system of the Java virtual machine or the underlying operating system. Such interactions include executing other programs, shutting down the runtime system, and reading and writing the system properties that allow communication between the operating system and the runtime system. Three main classes in java.lang provide this access:

  • The System class provides static methods to manipulate system state. It provides for the reading and writing of system properties, provides the standard input and output streams, and provides a number of miscellaneous utility functions. For convenience, several methods in System operate on the current Runtime object.

  • The Runtime class provides an interface to the runtime system of the executing virtual machine. The current Runtime object provides access to functionality that is per-runtime, such as interacting with the garbage collector, executing other programs and shutting down the runtime system.

  • The Process class represents a running process that was created by calling Runtime.exec to execute another program, or through direct use of a ProcessBuilder object.

These interactions and others can require security to protect your computer's integrity. So we also look at security and how different security policies are enforced in the runtime system. We start, though, by looking at the System class.

The System Class

The System class provides static methods to manipulate system state and acts as a repository for system-wide resources. System provides functionality in four general areas:

  • The standard I/O streams

  • Manipulating system properties

  • Utilities and convenience methods for accessing the current Runtime

  • Security

You'll learn about security in more detail in Section 23.5 on page 677.

Standard I/O Streams

The standard input, output, and error streams are available as static fields of the System class.

  • public static final InputStream in

    • Standard input stream for reading data.

  • public static final PrintStream out

    • Standard output stream for printing messages.

  • public static final PrintStream err

    • Standard error stream for printing error messages. The user can often redirect standard output to a file. But applications also need to print error messages that the user will see even if standard output is redirected. The err stream is specifically devoted to error messages that are not rerouted with the regular output.

For historical reasons, both out and err are PrintStream objects, not PrintWriter objects. See “Print Streams” on page 525 for a discussion about PrintStream and PrintWriter types.

Although each of the standard stream references is declared final, the static methods setIn, setOut, and setErr allow you to redefine the actual streams to which these references are bound (by using native code to bypass the language level restriction of not assigning final variables). These methods are security checked and throw SecurityException if you do not have permission to change the standard streams.

System Properties

System properties define the system environment. They are stored by the System class in a Properties object (see page 620). Property names consist of multiple parts separated by periods. For example, here is a dump of the standard properties on one system:

# Standard System Properties
java.version=1.5.0_02
java.vendor=Sun Microsystems Inc.
java.vendor.url=http://java.sun.com/
java.vm.specification.version=1.0
java.vm.specification.vendor=Sun Microsystems Inc.
java.vm.specification.name=Java Virtual Machine Specification
java.vm.version=1.5.0_02-b09
java.vm.vendor=Sun Microsystems Inc.
java.vm.name=Java HotSpot(TM) Client VM
java.specification.version=1.5
java.specification.vendor=Sun Microsystems Inc.
java.specification.name=Java Platform API Specification
java.home=/opt/jdk1.5.0_02/jre
java.class.version=49.0
java.class.path=classes
java.library.path=/opt/jdk1.5.0_02/jre/lib/i386/client:/opt/jdk1.5.0_02/jre/lib/i386:/opt
System Properties/jdk1.5.0_02/jre/../lib/i386
java.io.tmpdir=/tmp
java.compiler=
java.ext.dirs=/opt/jdk1.5.0_02/jre/lib/ext
os.name=Linux
os.arch=i386
os.version=2.4.20-31.9smp
file.separator=/
path.separator=:
line.separator=

user.name=dholmes
user.home=/home/dholmes
user.dir=/home/dholmes/JPL-4e/src

These properties are defined on all systems, although the values will certainly vary. On any given system many more properties may be defined. Some of the standard properties are used by classes in the standard packages. The File class, for example, uses the file.separator property to build up and break down pathnames. You are also free to use properties. The following method looks for a personal configuration file in the user's home directory:

public static File personalConfig(String fileName) {
    String home = System.getProperty("user.home");
    if (home == null)
        return null;
    else
        return new File(home, fileName);
}

The methods of the System class that deal with the system properties are

  • public static Properties getProperties()

    • Gets the Properties object that defines all system properties.

  • public static String getProperty(String key)

    • Returns the value of the system property named in key.

  • public static String getProperty(String key, String defaultValue)

    • Returns the value of the system property named in key. If it has no definition, it returns defaultValue.

  • public static String setProperty(String key, String value)

    • Sets the value of the system property named in key to the given value, returning the previous value or null if the property was not previously set.

  • public static String clearProperty(String key)

    • Removes the system property named in key, returning the previous value, or null if the property was not previously set.

  • public static void setProperties(Properties props)

    • Sets the Properties object that defines all system properties to be props.

All these methods are security checked and may throw SecurityException. However, being denied access to the entire set of properties does not necessarily mean you will be denied access to individual properties.

Property values are stored as strings, but the strings can represent other types, such as integers or booleans. Methods are available to read properties and decode them into some of the primitive types. These decoding methods are static methods of the primitive type's class. Each method has a String parameter that names the property to retrieve. Some forms have a second parameter (shown as def later) that is the default value to return if no property is found with that name. Methods that lack a default value parameter return an object that contains the default value for the type. All these methods decode values in the standard formats for constants of the primitive type:

public static boolean Boolean.getBoolean(String name)
public static Integer Integer.getInteger(String name)
public static Integer
   Integer.getInteger(String name, Integer def)
public static Integer Integer.getInteger(String name, int def)
public static Long Long.getLong(String name)
public static Long Long.getLong(String name, Long def)
public static Long Long.getLong(String name, long def)

The getBoolean method is different from the others—it returns a boolean value instead of an object of class Boolean. If the property isn't present, getBoolean returns false; the other methods return null.

The classes Character, Byte, Short, Float, and Double do not have property fetching methods. You can get the value as a string and use the mechanisms described in “String Conversions” on page 316 to convert to the appropriate primitive type.

Utility Methods

The System class also contains a number of utility methods:

  • public static long currentTimeMillis()

    • Returns the current time in milliseconds since the epoch (00:00:00 GMT, January 1, 1970). The time is returned in a long. Sophisticated applications may require more functionality (see “Time, Dates, and Calendars” on page 695).

  • public static long nanoTime()

    • Returns the current value, in nanoseconds, of the most precise available system timer. This method can only be used to measure elapsed time and is not related to any notion of calendar time or wall-clock time, and is not relative to the epoch. Although the precision is in nanoseconds, there are no guarantees as to the accuracy.

  • public static void arraycopy(Object src, int srcPos, Object dst, int dstPos, int count)

    • Copies the contents of the source array, starting at src[srcPos], to the destination array, starting at dst[dstPos]. Exactly count elements will be copied. If an attempt is made to access outside either array an IndexOutOfBoundsException is thrown. If the values in the source array are not compatible with the destination array an ArrayStoreException is thrown. “Compatible” means that each object in the source array must be assignable to the component type of the destination array. For arrays of primitive types, the types must be the same, not just assignable; arraycopy cannot be used to copy an array of short to an array of int. The arraycopy method works correctly on overlapping arrays, so it can be used to copy one part of an array over another part. You can, for example, shift everything in an array one slot toward the beginning, as shown in the method squeezeOut on page 318.

  • public static int identityHashCode(Object obj)

    • Returns a hashcode for obj using the algorithm that Object.hashCode defines. This allows algorithms that rely on identity hashing to work even if the class of obj overrides hashCode.

A number of other methods in System are convenience methods that operate on the current Runtime object—which can be obtained via the static method Runtime.getRuntime. For each method an invocation System.method is equivalent to the invocation Runtime.getRuntime().method; consequently, these methods are described as we discuss the Runtime class. The methods are

public static void exit(int status)
public static void gc()
public static void runFinalization()
public static void loadLibrary(String libname)
public static void load(String filename)

Creating Processes

As you have learned, a running system can have many threads of execution. Most systems that host a Java virtual machine can also run multiple programs. You can execute new programs by using one of the Runtime.exec convenience methods. Each successful invocation of exec creates a new Process object that represents the program running in its own process. You can use a Process object to query the process's state and invoke methods to control its progress. Process is an abstract class whose subclasses are defined on each system to work with that system's processes. The two basic forms of exec are

  • public Process exec(String[] cmdArray) throws IOException

    • Runs the command in cmdArray on the current system. Returns a Process object (described below) to represent it. The string in cmdArray[0] is the name of the command, and any subsequent strings in the array are passed to the command as arguments.

  • public Process exec(String command) throws IOException

    • Equivalent to the array form of exec with the string command split into an array wherever whitespace occurs, using a default StringTokenizer (see page 651).

The newly created process is called a child process. By analogy, the creating process is a parent process.

Creating processes is a privileged operation and a SecurityException is thrown if you are not permitted to do it. If anything goes wrong when the system tries to create the process, an IOException is thrown.

Process

The exec methods return a Process object for each child process created. This object represents the child process in two ways. First, it provides methods to get input, output, and error streams for the child process:[1]

  • public abstract OutputStream getOutputStream()

    • Returns an OutputStream connected to the standard input of the child process. Data written on this stream is read by the child process as its input.

  • public abstract InputStream getInputStream()

    • Returns an InputStream connected to the standard output of the child process. When the child writes data on its output, it can be read from this stream.

  • public abstract InputStream getErrorStream()

    • Returns an InputStream connected to the error output stream of the child process. When the child writes data on its error output, it can be read from this stream.

Here, for example, is a method that connects the standard streams of the parent process to the standard streams of the child process so that whatever the user types will go to the specified program and whatever the program produces will be seen by the user:

public static Process userProg(String cmd)
    throws IOException
{
    Process proc = Runtime.getRuntime().exec(cmd);
    plugTogether(System.in,  proc.getOutputStream());
    plugTogether(System.out, proc.getInputStream());
    plugTogether(System.err, proc.getErrorStream());
    return proc;
}
// ... definition of plugTogether ...

This code assumes that a method plugTogether exists to connect two streams by reading the bytes from one stream and writing them onto the other.

The second way a Process object represents the child process is by providing methods to control the process and discover its termination status:

  • public abstract int waitFor() throws InterruptedException

    • Waits indefinitely for the process to complete, returning the value it passed to either System.exit or its equivalent (zero means success, nonzero means failure). If the process has already completed, the value is simply returned.

  • public abstract int exitValue()

    • Returns the exit value for the process. If the process has not completed, exitValue throws IllegalThreadStateException.

  • public abstract void destroy()

    • Kills the process. Does nothing if the process has already completed. Garbage collection of a Process object does not mean that the process is destroyed; it will merely be unavailable for manipulation.

For example, the following method returns a String array that contains the output of the ls command with the specified command-line arguments. It throws an LSFailedException if the command completed unsuccessfully:

// We have imported java.io.* and java.util.*
public String[] ls(String dir, String opts)
    throws LSFailedException
{
    try {
        // start up the command
        String[] cmdArray = { "/bin/ls", opts, dir };
        Process child = Runtime.getRuntime().exec(cmdArray);
        InputStream lsOut = child.getInputStream();
        InputStreamReader r = new InputStreamReader(lsOut);
        BufferedReader in = new BufferedReader(r);

        // read the command's output
        List<String> lines = new ArrayList<String>();
        String line;
        while ((line = in.readLine()) != null)
            lines.add(line);
        if (child.waitFor() != 0)   // if the ls failed
            throw new LSFailedException(child.exitValue());
        return lines.toArray(new String[lines.size()]);
    } catch (LSFailedException e) {
        throw e;
    } catch (Exception e) {
        throw new LSFailedException(e.toString());
    }
}

In the ls method we want to treat the output as character data, so we wrap the input stream that lets us read the child's output through an InputStreamReader. If we wanted to treat the child's output as a stream of bytes, we could easily do that instead. If the example were written to use the second form of exec, the code would look like this:

String cmd = "/bin/ls " + opts + " " + dir;
Process child = Runtime.getRuntime().exec(cmd);

Process is an abstract class. Each implementation of a Java virtual machine may provide one or more appropriate extended classes of Process that can interact with processes on the underlying system. Such classes might have extended functionality that would be useful for programming on the underlying system. The local documentation should contain information about this extended functionality.

Note that there is no requirement that the child process execute asynchronously or concurrently with respect to the parent process—in a particular system exec could appear to block until the child process terminates.

Exercise 23.1Write the plugTogether method. You will need threads.

Process Environments

Two other forms of Runtime.exec enable you to specify a set of environment variables, which are system-dependent values that can be queried as desired by the new process. Environment variables are passed to exec as a String array; each element of the array specifies the name and value of an environment variable in the form name=value. The name cannot contain any spaces, although the value can be any string. The environment variables are passed as the second parameter:

public Process exec(String[] cmdArray, String[] env)
   throws IOException
public Process exec(String command, String[] env)
   throws IOException

The single argument forms of exec are equivalent to passing null for env, which means that the created process inherits the environment variables of its parent.

Environment variables are interpreted in a system-dependent way by the child process's program. They can hold information such as the current user name, the current working directory, search paths, or other useful information that may be needed by a running program. The environment variables mechanism is supported because existing programs on many different kinds of platforms understand them. You can get the environment variables of the current runtime process from the System.getenv method, which returns an unmodifiable map of all the name/value pairs. Individual values can be retrieved using the System.getenv method which takes a string argument representing the name of the environment variable. The preferred way to communicate between different virtual machine runtimes is with system properties (see page 663). However, this remains the only means of querying and subsequently modifying the environment when executing a non-Java program.

There remain two further forms of exec that allow the initial working directory of the child process to be specified:

public Process exec(String[] cmdArray, String[] env, File dir)
   throws IOException
public Process exec(String command, String[] env, File dir)
   throws IOException

The child process is given an initial working directory as specified by the path of dir. If dir is null, the child process inherits the current working directory of the parent—as specified by the system property user.dir. The one- and two-argument forms of exec are equivalent to passing null for dir.

ProcessBuilder

The exec methods of Runtime are convenience methods that use the more general ProcessBuilder class. ProcessBuilder encapsulates the three key attributes of an external process: the command, the environment, and the current working directory. You can then start an external process with those attributes by using the ProcessBuilder object's start method—which returns a Process object, just as exec does.

ProcessBuilder has two constructors: One takes a sequence of String objects that represent the command; the other a List<String> that represents the command.

A fourth attribute you can control with ProcessBuilder, but not with exec, is whether the standard error stream of the process is redirected to match the standard output stream of the process. Redirecting the error stream merges all output into one stream, which makes it much easier to correlate when errors occur relative to normal processing.

ProcessBuilder provides methods to query and set each of the four attributes. Each query method simply names the attribute and returns its value—for example, command returns the current command as a List<String>. The other query methods are

  • public File directory()

    • Returns the working directory.

  • public Map<String, String> environment()

    • Returns a map containing all the environment variable settings

  • public boolean redirectErrorStream()

    • Returns true if the standard error stream should be redirected to the standard output stream.

The setting methods have the same names as the query methods, but they take suitable arguments for the attribute and return the ProcessBuilder object. However, there is no method to set the environment. Instead, the map returned by environment can be modified directly and that will affect the environment of any process subsequently started. Once a process has been started, any subsequent changes to the attributes of the ProcessBuilder that started it have no effect on that process.

By returning the ProcessBuilder object different calls to set attributes can be chained together and ultimately start invoked. For example, here is how we could rewrite the ls example, but with the error stream redirected:

public String[] ls(String dir, String opts)
    throws LSFailedException
{
    try {
        // start up the command
        Process child =
            new ProcessBuilder("/bin/ls", opts, dir).
                redirectErrorStream(true).start();

        // ... as before ...

}

Portability

Any program that uses exec or ProcessBuilder is not portable across all systems. Not all environments have processes, and those that have them can have widely varying commands and syntax for invoking those commands. Also, because running arbitrary commands raises serious security issues, the system may not allow all programs to start processes. Be very cautious about choosing to do this because it has a strong negative impact on your ability to run your program everywhere.

Exercise 23.2Write a program that runs exec on its command-line arguments and prints the output from the command, preceding each line of output by its line number.

Exercise 23.3Write a program that runs exec on command-line arguments and prints the output from the command, killing the command when a particular string appears in the output.

Shutdown

Normally, an execution of a virtual machine terminates when the last user thread terminates. A Runtime can also be shut down explicitly with its exit method, passing an integer status code that can be communicated to the environment executing the virtual machine—zero to indicate successful completion of a task and non-zero to indicate failure. This method abruptly terminates all threads in the runtime system no matter what their state. They are not interrupted or even stopped. They simply cease to exist as the virtual machine itself stops running—no finally clauses are executed.

In either case, when exit is invoked or the last user thread terminates, the shutdown sequence is initiated. The virtual machine can also be shut down externally, such as by a user interrupting the virtual machine from a keyboard (on many systems by typing control-C) or when the user logs out or the computer is shut down.

All the methods related to shutting down the runtime system are security checked and throw SecurityException if you don't have required permissions.

Shutdown Hooks

An application can register a shutdown hook with the runtime system. Shutdown hooks are threads that represent actions that should be taken before the virtual machine exits. Hooks typically clean up external resources such as files and network connections.

  • public void addShutdownHook(Thread hook)

    • Registers a new virtual-machine shutdown hook. If hook has already been registered or has already been started an IllegalArgumentException is thrown.

  • public boolean removeShutdownHook(Thread hook)

    • Unregisters a previously registered virtual machine shutdown hook. Returns true if hook was registered and has been unregistered. Returns false if hook was not previously registered.

You cannot add or remove shutdown hooks after shutdown has commenced; you will get IllegalStateException if you try.

When shutdown is initiated, the virtual machine will invoke the start method on all shutdown hook Thread objects. You cannot rely on any ordering—shutdown hook threads may be executed before, after, or at the same time as any other shutdown hook thread depending on thread scheduling.

You must be careful about this lack of ordering. Suppose, for example, you were writing a class that stored state in a database. You might register a shutdown hook that closed the database. However, a program using your class might want to register its own shutdown hook that writes some final state information through your class to the database. If your shutdown hook is run first (closing the database), the program's shutdown hook would fail in writing its final state. You can improve this situation by writing your class to reopen the database when needed, although this might add complexity to your class and possibly even some bad race conditions. Or you might design your class so that it doesn't need any shutdown hooks.

It is also important to realize that the shutdown hook threads will execute concurrently with other threads in the system. If shutdown was initiated because the last user thread terminated, then the shutdown hook threads will execute concurrently with any daemon threads in the system. If shutdown was initiated by a call to exit, then the shutdown hook threads will execute concurrently with both daemon and user threads. Your shutdown hook threads must be carefully written to ensure correct synchronization while avoiding potential deadlocks.

Your shutdown hooks should execute quickly. When users interrupt a program, for example, they expect the program to terminate quickly. And when a virtual machine is terminated because a user logs out or the computer is shut down, the virtual machine may be allowed only a small amount of time before it is killed forcibly. Interacting with a user should be done before shutdown, not during it.

The Shutdown Sequence

The shutdown sequence is initiated when the last user thread terminates, the Runtime.exit method is invoked, or the external environment signals the virtual machine to shutdown. When shutdown is initiated all the shutdown hook threads are started and allowed to run to completion. If any of these threads fails to terminate the shutdown sequence will not complete. If shutdown was initiated internally the virtual machine will not terminate. If shutdown was signaled from the external environment then failure to shutdown may result in a forced termination of the virtual machine.

If a shutdown hook thread incurs an uncaught exception, no special action is taken; it is handled like any other uncaught exception in a thread. In particular, it does not cause the shutdown process to abort.

When the last shutdown hook thread has terminated, the halt method will be invoked. It is halt that actually causes the virtual machine to cease running. The halt method also takes an integer status as an argument whose meaning is the same as that of exit: Zero indicates successful execution of the entire virtual machine. A shutdown hook can invoke halt to end the shutdown phase. Invoking halt directly—either before or during the shutdown phase—is dangerous since it prevents uncompleted hooks from doing their cleanup. You will probably never be in a situation where invoking halt is correct.

If exit is called while shutdown is in progress the call to exit will block indefinitely. The effect of this on the shutdown sequence is not specified, so don't do this.

In rare circumstances, the virtual machine will abort rather than perform an orderly shutdown. This can happen, for example, if an internal error is detected in the virtual machine that prevents an orderly shutdown—such as errant native code overwriting system data structures. However, the environment hosting the virtual machine can also force the virtual machine to abort—for example, under UNIX systems a SIGKILL signal will force the virtual machine to abort. When the virtual machine is forced to terminate in this way, no guarantees can be made about whether or not shutdown hooks will run.

Shutdown Strategies

Generally, you should let a program finish normally rather than forcibly shut it down with exit. This is particularly so with multithreaded programs, where the thread that decides it is time for the program to exit may have no idea what the other threads are doing.

Designing a multithreaded application in a way that makes it safe to exit at arbitrary points in the program is either trivial or extremely complex. It is trivial when none of the threads are performing actions that must be completed—for example, having a thousand threads all trying to solve a numerical problem. It is complex whenever any of the threads perform actions that must be completed.

You can communicate the fact that shutdown is required by writing your threads to respond to the interrupt method—as you learned on page 365. You can broadcast the shutdown request to all your threads by invoking the interrupt method of the parent ThreadGroup of your application. This is no guarantee that a program will terminate, however, because libraries that you have used may have created user threads that do not respond to interrupt requests—the AWT graphics library is one well-known example.

There are two circumstances when you must invoke exit: when it is the only way to terminate some of the threads and hence your application, and when your application must return a status code. In both cases you need to delay the call to exit until all your application threads have had a chance to terminate cleanly. One way is for the thread initiating the termination to join the other threads and so know when those threads have terminated. However, an application may have to maintain its own list of the threads it creates because simply inspecting the ThreadGroup may return library threads that do not terminate and for which join will not return.

The decision to shutdown a program should be made at a high-level within the application—often within main or the run method of a thread in the top-level application ThreadGroup or in the code that responds to events in a graphical user interface. Methods that encounter errors should simply report those errors via exceptions that allow the high-level code to take appropriate action. Utility code should never terminate an application because it encounters an error—this code lacks knowledge of the application that allows an informed decision to be made; hence, exceptions are used to communicate with higher-level code.

The Rest of Runtime

The Runtime class provides functionality in six different areas:

  • Interacting with the garbage collector and querying memory usage

  • Asking for the number of processors available

  • Executing external programs

  • Terminating the current Runtime

  • Loading native code libraries

  • Debugging

You have already learned about the first four of these areas (you learned about interacting with the garbage collector via the gc and runFinalization methods in Chapter 17, on page 452, and about availableProcessors in Chapter 14 on page 359). Now is the time to cover the final two.

Loading Native Code

In Chapter 2, you learned about the native modifier (see page 74) that can be applied to methods and which signifies that the method's implementation is being provided by native code. At runtime the native code for such methods must be loaded into the virtual machine so that the methods can be executed. The details of this process are system-dependent, but two methods in Runtime allow this activity to occur:

  • public void loadLibrary(String libname)

    • Loads the dynamic library with the specified library name. The library corresponds to a file in the local file system that is located in some place where the system knows to look for library files. The actual mapping from the library name to a file name is system-dependent.

  • public void load(String filename)

    • Loads the file specified by filename as a dynamic library. In contrast with loadLibrary, load allows the library file to be located anywhere in the file system.

Typically, classes that have native methods load the corresponding library as part of the initialization of the class by placing the load invocation in a static initialization block. However, the library could be lazily loaded when an actual invocation of the method occurs.

Loading native code libraries is, naturally, a privileged operation, and you will get a SecurityException if you do not have the required permissions. If the library cannot be found or if an error occurs while the system tries to load the library, an UnsatisfiedLinkError is thrown.

A related method in class SystemmapLibraryName—maps a library name into a system-dependent library name. For example, the library name "awt" might map to "awt.dll" under Windows, while under UNIX it might map to "libawt.so".

Debugging

Two methods in Runtime support the debugging of applications:

  • public void traceInstructions(boolean on)

    • Enables or disables the tracing of instructions depending on the value of on. If on is true, this method suggests that the virtual machine emit debugging information for each instruction as it is executed.

  • public void traceMethodCalls(boolean on)

    • Enables or disables the tracing of method calls depending on the value of on. If on is true, this method suggests that the virtual machine emit debugging information for each method when it is called.

The format of this debugging information and the file or other output stream to which it is emitted depend on the host environment. Each virtual machine is free to do what it wants with these calls, including ignoring them if the local runtime system has nowhere to put the trace output, although they are likely to work in a development environment.

These methods are fairly low-level debugging tools. Typically, a virtual machine will also support high-level debuggers and profilers through the JVM Tool Interface (JVMTI); interested readers should consult that specification for more information.

Security

Security is a very complex issue and a full discussion of it is well beyond the scope of this book—you can read Inside Java 2 Platform Security, Second Edition, a companion book in this series, for all the details. What we can do, however, is provide an overview of the security architecture and some of its key components. Information on other aspects of security is given in “java.security and Related Packages — Security Tools” on page 732.

To perform a security-checked operation you must have permission to perform that operation. Together, all permissions in a system and the way in which they are assigned define the security policy for that system. A protection domain encloses a set of classes whose instances are granted the same set of permissions and that all come from the same code source. Protection domains are established via the class loading mechanism. To enable the security policy of a system and activate the protection domains, you need to install a security manager.[2]

The classes and interfaces used for security are spread across a number of packages so we use the fully qualified name the first time we introduce a specific class or interface.

The SecurityManager Class

The java.lang.SecurityManager class allows applications to implement a security policy by determining, before performing a possibly unsafe or sensitive operation, whether it is being attempted in a security context that allows the operation to be performed. The application can then allow or disallow the operation.

The SecurityManager class contains many methods with names that begin with the word “check.” These methods are called by various methods in the standard libraries before those methods perform certain potentially sensitive operations, such as accessing files, creating and controlling threads, creating class loaders, performing some forms of reflection, and controlling security itself. The invocation of such a check method typically looks like this:

SecurityManager security = System.getSecurityManager();
if (security != null) {
    security.checkXXX(...);
}

The security manager is given an opportunity to prevent completion of the operation by throwing an exception. A security manager routine simply returns if the operation is permitted but throws a SecurityException if the operation is not permitted.

You can get and set the security manager with methods of the System class:

  • public static void setSecurityManager(SecurityManager s)

    • Sets the system security manager object. If a security manager already exists this new manager will replace it, provided the existing manager supports replacement and you have permission to replace it; otherwise, a SecurityException is thrown.

  • public static SecurityManager getSecurityManager()

    • Gets the system security manager. If none has been set null is returned and you are assumed to have all permissions.

The security manager delegates the actual security check to an access control object. Each check method just invokes the security manager's checkPermission method, passing the appropriate java.security.Permission object for the requested action. The default implementation of checkPermission then calls

java.security.AccessController.checkPermission(perm);

If a requested access is allowed, checkPermission returns quietly. If denied, a java.security.AccessControlException (a type of SecurityException) is thrown.

This form of checkPermission always performs security checks within the context of the current thread—which is basically the set of protection domains of which it is a member. Given that a protection domain is a set of classes with the same permissions, a thread can be a member of multiple protection domains when the call stack contains active methods on objects of different classes.

The security manager's getSecurityContext method returns the security context for a thread as a java.security.AccessControlContext object. This class also defines a checkPermission method, but it evaluates that method in the context that it encapsulates, not in the context of the calling thread. This feature is used when one context (such as a thread) must perform a security check for another context. For example, consider a worker thread that executes work requests from different sources. The worker thread has permission to perform a range of actions, but the submitters of work requests may not have those same permissions. When a work request is submitted, the security context for the submitter is stored along with the work request. When the worker thread performs the work, it uses the stored context to ensure that any security checks are performed with respect to the submitter of the request, not the worker thread itself. In simple cases, this might involve invoking the security manager's two-argument checkPermission method, which takes both a Permission object and an AccessControlContext object as arguments. In more general situations, the worker thread may use the doPrivileged methods of AccessController, which you will learn about shortly.

Permissions

Permissions fall into a number of categories, each managed by a particular class, for example:

  • File—. java.io.FilePermission

  • Network—. java.net.NetPermission

  • Properties—. java.util.PropertyPermission

  • Reflection—. java.lang.reflect.ReflectPermission

  • Runtime—. java.lang.RuntimePermission

  • Security—. java.security.SecurityPermission

  • Serialization—. java.io.SerializablePermission

  • Sockets—. java.net.SocketPermission

All but FilePermission and SocketPermission are subclasses of java.security.BasicPermission, which itself is an abstract subclass of the top-level class for permissions, which is java.security.Permission. BasicPermission defines a simple permission based on the name. For example, the RuntimePermission with name "exitVM" represents the permission to invoke Runtime.exit to shutdown the virtual machine. Here are some other basic RuntimePermission names and what they represent:

  • "createClassLoader"—. Invoke the ClassLoader constructors.

  • "setSecurityManager"—. Invoke System.setSecurityManager.

  • "modifyThread"—. Invoke Thread methods interrupt, setPriority, setDaemon, or setName.

Basic permissions are something you either have or you don't. The names for basic permissions follow the hierarchical naming scheme used for system properties (see page 663). An asterisk may appear at the end of the name, following a "." or by itself, to signify a wildcard match. For example: "java.*" or "*" are valid; "*java" or "x*y" are not valid.

FilePermission and SocketPermission are subclasses of Permission. These classes can have a more complicated name syntax than that used for basic permissions. For example, for a FilePermission object, the permission name is the pathname of a file (or directory) and can cover multiple files by using "*" to mean all files in the specified directory and using "-" to mean all files in the specified directory as well as all files in all subdirectories.

All permissions can also have an action list associated with them that defines the different actions permitted by that object. For example, the action list for a FilePermission object can contain any combination of "read", "write", "execute", or "delete", specifying actions that can be performed on the named file (or directory). Many basic permissions do not use the action list, but some, such as PropertyPermission do. The name of a PropertyPermission is the name of the property it represents and the actions can be "read" or "write", which let you invoke System.getProperty and System.setProperty, respectively, with that property name. For example, a PropertyPermission with the name "java.*" and action "read" allows you to retrieve the values of all system properties that start with "java.".

Security Policies

The security policy for a given execution of the runtime system is represented by a java.security.Policy object or, more specifically, by a concrete subclass of the abstract Policy class. The Policy object maintains the sets of permissions that have been assigned to the different protection domains, according to their code source. How the security policy is communicated to the Policy object is a function of the actual implementation of that policy. The default implementation is to use policy files to list the different permissions that are granted to each code source.

For example, a sample policy file entry granting code from the /home/sysadmin directory read access to the file /tmp/abc is

grant codeBase "file:/home/sysadmin/" {
    permission java.io.FilePermission "/tmp/abc", "read";
};

To find out how security policies are defined in your local system, consult your local documentation.

Classes loaded by the bootstrap loader are considered to be trusted and do not need explicit permissions set in the security policy. Some virtual machine implementations also support the standard extensions mechanism, which allows classes to be identified as trusted by placing them in special locations accessed by the extensions class loader. These classes do not need explicit permissions set either.

Access Controllers and Privileged Execution

The AccessController class is used for three purposes:

  • It provides the basic checkPermission method used by security managers to perform a security check.

  • It provides a way to create a “snapshot” of the current calling context by using getContext, which returns an AccessControlContext.

  • It provides a means to run code as privileged, thus changing the set of permissions that might otherwise be associated with the code.

We have discussed (to the extent we intend to) the first two of these. In this section we look at what it means to have privileged execution.

A protection domain (represented by java.security.ProtectionDomain) encompasses a code source (represented by java.security.CodeSource) and the permissions granted to code from that code source, as determined by the security policy currently in effect. A code source extends the notion of a code base (the location classes were loaded from) to include information about the digital certificates associated with those classes. Digital certificates can be used to verify the authenticity of a file and ensure that the file has not been tampered with in any way. Classes with the same certificates and from the same location are placed in the same domain, and a class belongs to one and only one protection domain. Classes that have the same permissions but are from different code sources belong to different domains.

Each applet or application runs in its appropriate domain, determined by its code source. For an applet (or an application running under a security manager) to be allowed to perform a secured action, the applet or application must be granted permission for that particular action. More specifically, whenever a secure action is attempted, all code traversed by the execution thread up to that point must have permission for that action unless some code on the thread has been marked as privileged. For example, suppose that access control checking occurs in a thread of execution that has a chain of multiple callers—think of this as multiple method calls that potentially cross the protection domain boundaries. When the AccessControllercheckPermission method is invoked by the most recent caller, the basic algorithm for deciding whether to allow or deny the requested action is as follows:

  • If the code for any caller in the call chain does not have the requested permission, an AccessControlException is thrown, unless a caller whose code is granted the said permission has been marked as privileged and all parties subsequently called by this caller (directly or indirectly) have the said permission.

Marking code as privileged enables a piece of trusted code to temporarily enable access to more actions than are directly available to the code that called it. This is necessary in some situations. For example, an application may not be allowed direct access to files that contain fonts, but the system utility to display a document must obtain those fonts on behalf of the user. This requires that the system utility code becomes privileged while obtaining the fonts.

The doPrivileged method of AccessController take as an argument a java.security.PrivilegedAction<T> object whose run method defines the code to be marked as privileged. For example, your call to doPrivileged can look like the following:

void someMethod() {
    // ...normal code here...
    AccessController.doPrivileged(
        new PrivilegedAction<Object>() {
            public Object run() {
                // privileged code goes here, for example:
                System.loadLibrary("awt");
                return null; // nothing to return
            }
        }
    );
    // ...normal code here...
}

The doPrivileged method executes the run method in privileged mode. Privileged execution is a way for a class with a given permission to temporarily grant that permission to the thread that executes the privileged code. It will not let you gain permissions that you do not already have. The class defining someMethod must have the permission RuntimePermission("loadLibrary.awt"); otherwise, any thread that invokes someMethod will get a SecurityException. It is guaranteed that privileges will be revoked after the PrivilegedAction object's run method returns.

The PrivilegedAction<T> interface's single method run returns a T object. Another form of doPrivileged takes a PrivilegedExceptionAction<T> object, whose run method also returns T, but which can throw any checked exception. For both of these methods there is a second overloaded form of doPrivileged that takes an AccessControlContext as an argument and uses that context to establish the permissions that the privileged code should run with.

You should use doPrivileged with extreme care and ensure that your privileged sections of code are no longer than necessary and that they perform only actions you fully control. For example, it would be an extreme security risk for a method with, say, all I/O permissions, to accept a Runnable argument and invoke its run method within a privileged section of code, unless you wouldn't mind if that method removed all the files on your disk.

 

Power corrupts. Absolute power is kind of neat.

 
 --John Lehman, U.S. Secretary of the Navy, 1981–1987


[1] For historical reasons these are byte stream objects instead of character streams. You can use InputStreamReader and OutputStreamWriter to convert the bytes to characters under the system default encoding or another of your choosing.

[2] Some virtual machines allow a startup argument that causes a default security manager to be created and installed. For example, using the JDK 5.0 you define the system property java.security.manager by passing the argument -Djava.security.manager to the java command.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.232.239