DESIGN AND IMPLEMENTATION OF THE J-SEAL2 MOBILE AGENT KERNEL WALTER BINDER CoCo Software Engineering GmbH, Margaretenstr. 22/9, A-1040 Vienna, Austria E-mail:
[email protected] J-SEAL2 is a secure, portable, and efficient execution environment for mobile agents. The core of the system is a micro-kernel fulfilling the same functions as a traditional operating system kernel: protection, communication, domain termination, and resource accounting. This paper describes the key concepts of the J-SEAL2 micro-kernel and how they are implemented in Java.
1
Introduction
Recently a large number of Java 1 [10] based mobile agent systems has emerged2. In fact, Java is a good choice for the implementation of execution environments for mobile agents, as it offers many features that ease the development of mobile agent platforms, such as portable code, language safety and classloader namespaces for isolation, serialization for state capture, and multithreading. In addition to this, high performance Java runtime systems are available for many hardware platforms and operating systems. Despite these advantages, current Java environments do not provide sufficiently strong security mechanisms for the protection and isolation of mobile agents. Since the vast majority of current mobile agent platforms simply relies on the insufficient security features of Java, these systems are not suited for commercial mobile agent applications. There are no guarantees that the system and mobile agents are protected from each other. Because of pervasive aliasing in the Java Development Kit (JDK), there is no real protection boundary between different components. Furthermore, malicious agents can easily mount denial of service attacks against the platform, possibly even crashing the Java Virtual Machine (JVM) [16]. Moreover, if a misbehaving agent is detected, the platform does not guarantee that the agent can be safely removed from the system. Some researchers have shown that an abstraction similar to the process concept in operating systems is necessary in order to create secure execution environments for mobile agents [1]. However, proposed solutions were either incomplete or required modifications of the Java runtime system. In contrast, the J-SEAL2 mobile agent microkernel, first described in [4], ensures important security guarantees without requiring any modifications to the underlying Java implementation. J-SEAL2 is implemented in pure Java and runs every Java 2 platform (JDK 1.2 or higher) [17]. In traditional operating systems the kernel is responsible for protecting processes from each other. A process cannot access a foreign memory region unless that region has been explicitly declared to be shared. Furthermore, the operating system offers some Inter-Process Communication (IPC) facilities, allowing for a controlled co-operation between different processes. When a process is terminated, the kernel ensures that it is removed from the system freeing all resources the terminated process possesses. In 1
Java is a trademark of Sun Microsystems, Inc. For an (incomplete) list of different mobile agent platforms see The Mobile Agent List available at http://www.informatik.uni-stuttgart.de/ipvr/vs/projekte/mole/mal/mal.html. Most of the systems presented there are based on Java. 2
1
addition to this, the kernel must ensure that the termination of a process does not corrupt any shared system state. For instance, if a process is terminated while it is performing a kernel operation, the operating system must ensure that the terminated process leaves the kernel in a consistent state. Finally, the operating system guarantees that a process can only use the resources (CPU time, memory) it has been given. The J-SEAL2 micro-kernel fulfills the same role as an operating system kernel: It ensures protection of different agents and system components, provides secure communication facilities, enforces safe domain termination with immediate resource reclamation, and controls resource allocation. This paper is structured as follows: In section 2 we give a brief overview of the JSEAL2 mobile agent system. In section 3 we state requirements for kernel code written in Java and show how kernel entry and exit can be implemented efficiently. The following 4 sections deal with protection, communication, protection domain termination, and resource accounting. In each section we state our requirements for the J-SEAL2 kernel and discuss various implementation issues and techniques that help to meet the requirements efficiently. Moreover, we discuss limitations and suggest some extensions to the JDK in order to improve functionality and performance. In section 8 we compare the J-SEAL2 kernel to related work. In section 9 we present our plans for future improvements of the J-SEAL2 kernel. The last section summarizes the current state of implementation of our mobile agent kernel. 2
System overview
J-SEAL2 [4] is a complete redesign of JavaSeal [21, 22], a secure mobile agent system developed at the University of Geneva3. JavaSeal extends the Java programming environment with a model of mobile agents and strong hierarchical protection domains. These extensions are based on a formal model of distributed computation, Jan Vitek’s SEAL Calculus [23]. JavaSeal enables the expression and effective enforcement of security policies, but it incurs rather high overhead and does not scale well. Due to performance problems (e.g., inefficient communication between different protection domains, enormous agent startup overhead, etc.), JavaSeal is not suited for large-scale applications. J-SEAL2 is compatible with JavaSeal, but yields much better performance. J-SEAL2 provides an efficient communication model, a high level communication framework built on top of the micro-kernel, a new component model for services, as well as a flexible and convenient configuration mechanism based on XML [5]. Like JavaSeal, the J-SEAL2 kernel maintains a tree hierarchy of agents and services. Each agent and service executes in a protection domain of its own, called a sealed object or seal for short. A parent seal can create an arbitrary number of children seals. The root of the tree hierarchy is the RootSeal, which is responsible for starting system services and applications. The J-SEAL2 kernel acts as a reference monitor, it ensures that there is no sharing of object references with different seals. Communication between distinct seals requires kernel primitives, objects are passed always by deep copy within so-called capsules. Furthermore, J-SEAL2 prevents the sharing of loaded classes, if they contain non-final static variables that may be used as covert communication channels.
3
The University of Geneva is not engaged in J-SEAL2.
2
Seals are multithreaded domains. Every seal can run an arbitrary number of strands (secure threads) concurrently. The J-SEAL2 kernel ensures that a parent seal may terminate its children at any time. As a consequence, all strands in the child domain are stopped and all memory resources used by the child seal become eligible for garbage collection immediately. In the JavaSeal kernel synchronous message passing through named channels is the only inter-agent communication mechanism. This model only supports direct communication between seals that are neighbors in the seal hierarchy. Channel communication ensures that a parent seal is able to isolate a child completely from other seals. J-SEAL2 supports the same channel operations as JavaSeal, but offers some improvements, such as optional timeouts for all synchronous channel primitives, as well as an asynchronous send operation. In addition to improved channel communication, the J-SEAL2 kernel introduces external references [4], which allow indirect sharing4 of objects by different seals. External references enable efficient communication shortcuts in deep seal hierarchies. For security reasons, external references are tracked by the JSEAL2 kernel and may be invalidated at any time. External references are the basis for efficient high level communication frameworks. 3
Kernel code
The core of the J-SEAL2 mobile agent system is a compact micro-kernel, which offers a minimal set of abstractions necessary to program secure agent environments: protection domains for agents and services (seals), concurrent activities (strands), as well as communication facilities (channels and external references). 3.1
Requirements
Since seals are multithreaded and certain kernel structures must be accessible from different seals, a kernel synchronization protocol must ensure proper synchronization and prevent deadlocks. Kernel code must not synchronize on objects that are accessible by agents. Otherwise, agents could cause kernel code to block infinitely (i.e., a strand of an agent could initiate a kernel operation after a different strand has obtained a lock on an object, which the kernel also needs). Instead, kernel code shall lock only internal structures, such as private members of kernel abstractions. Kernel operations are to be performed atomically: they must either succeed or leave the kernel state unchanged. Kernel operations must take care not to cause any uncaught exceptions. In particular, special attention has to be paid to exceptions that can occur asynchronously, such as ThreadDeath and OutOfMemoryError. ThreadDeath is thrown if a thread stops another one with the aid of Thread.stop(). The stopped thread immediately exits all monitors it holds and throws ThreadDeath. Since this may leave objects in an inconsistent state, Thread.stop() has been deprecated in the Java 2 platform. However, because the Java 2 platform does not offer any alternative mechanisms for thread stopping, protection domain termination must be based on Thread.stop(). The kernel has to ensure that stop requests are deferred while a thread is accessing kernel structures. 4
The properties of different sharing models (copying, direct sharing, and indirect sharing) are explained in [2].
3
OutOfMemoryError is thrown whenever the virtual machine runs out of memory and the garbage collector fails to reclaim enough memory. Kernel operations must be designed to avoid OutOfMemoryError when the operation has already modified some kernel state. A simple solution is to allocate all objects that might be required (worst case estimation) in advance before any changes are made. Following this approach, kernel structures should not employ any JDK classes, such as the collections framework, since these classes allocate objects on demand. 3.2
Implementation issues
Operating systems employ a privileged processing mode for kernel operations. Only a process executing in kernel mode has access to all processor instructions and kernel internals (special memory regions). The J-SEAL2 kernel uses a similar distinction: When a strand initiates a kernel operation, it enters a privileged kernel mode. When the kernel operation completes (succeeds or fails), the strand leaves the kernel mode and continues execution in user mode. The main purpose of the kernel mode in J-SEAL2 is to prevent a strand from being stopped while accessing kernel internals. Stop requests are deferred until the strand to be stopped leaves the kernel. In addition to deferring stop requests, kernel mode is used to synchronize primitives affecting the J-SEAL2 kernel abstractions, such as protection domain creation and termination, strand creation and stopping, as well as communication requests. For this purpose, we distinguish between exclusive kernel mode and shared kernel mode. A strand entering exclusive kernel mode is blocked until no other strand is executing in kernel mode. While a strand is executing in exclusive kernel mode, no other strand can enter the kernel. Protection domain termination and strand stopping are always performed in exclusive kernel mode. Therefore, strands executing in kernel mode are guaranteed not to be stopped asynchronously. The only exception to this rule is a strand trying to stop itself. A simple check is sufficient to detect this situation. The J-SEAL2 implementation ensures that operations modifying kernel structures (such as protection domain creation and termination) operate in exclusive kernel mode, while operations requiring only read access to kernel internals execute in shared kernel mode. In this case, access to kernel structures need not be synchronized, since exclusive kernel mode ensures that there are no concurrent reads or writes while a writer is accessing the kernel. This observation suggests a simple single writer / multiple reader lock for controlling access to the kernel: entering exclusive kernel mode corresponds to acquiring the write lock, whereas shared kernel mode requires a read lock. The lock implementation ensures that a strand waiting for the write lock will enter the kernel before strands waiting for a read lock. This property makes sure that domain termination cannot be delayed infinitely. The lock object must be implemented very carefully: When a strand is trying to obtain a read lock or the write lock, it is still executing in user mode. Entering kernel mode means that the lock has been obtained successfully. Thus, the state of the lock object must not be changed before the required lock is really available, otherwise this would be a forbidden modification of a kernel object (the lock object) by a strand executing in user mode. As a result, sophisticated queuing techniques to control the order
4
of kernel entries cannot be implemented easily, since enqueuing a request is a modification of kernel structures. Since classloading affects the internal state of the Java runtime system, we must ensure that classloading always occurs in kernel mode. A strand must not be stopped while it is loading a class, since this might corrupt some internal structures of the JVM. However, in general classloading occurs asynchronously, dependent on the virtual machine implementation. Therefore, we do not know whether a classloading strand already executes in kernel mode or not. For this reason, the J-SEAL2 kernel offers a conditional kernel enter operation: a shared lock is only obtained if the requesting strand is not yet executing in kernel mode. Implementing this operation requires keeping track of all strands that are executing in kernel mode at any time. Because entering kernel mode is a very frequent operation (for instance, each communication involves at least one switch into kernel mode), adding strands to and removing strands from the set of strands executing in kernel mode must be highly optimized. Although kernel entry and exit are extremely frequent operations, measurements indicate that less than 3% of the overall CPU time is spent for obtaining and releasing kernel locks. The benchmark measures communication latency and throughput in deep seal hierarchies, thus it shows the highest possible kernel entry/exit frequency (worst case). Because operations requiring exclusive kernel mode are not executed frequently, the kernel lock does not become a significant performance bottleneck. Resource control (see section 7) ensures that an agent cannot enter the kernel arbitrarily. Therefore, the kernel lock cannot be abused for denial of service attacks. 3.3
Future improvements
The Real Time Specification for Java [19] introduces the concept of Asynchronous Transfer of Control (ATC), which enables the implementation of safe asynchronous thread termination. ATC-deferred sections prevent the corruption of kernel structures when a strand is terminated asynchronously. Moreover, classloading can employ ATCdeferred sections to ensure the consistency of the runtime system. This avoids the overhead of maintaining a set of strands executing in kernel mode. However, ATC cannot replace our concept of kernel mode operation: In order to ensure proper synchronization and to avoid deadlocks, a dedicated synchronization protocol is necessary when a strand enters kernel mode. Efficient implementations of ATC will help to further minimize the overhead of kernel mode entry and exit. 4
Protection
The kernel of a traditional operating system ensures that a process can only access its own memory pages. The operating system kernel relies on the memory management unit of the processor in order to detect illegal memory accesses. A mobile agent kernel has to enforce similar protection. The kernel must protect itself as well as each agent from any other agent in the system.
5
4.1
Requirements
Language safety in Java already guarantees some basic protection, as it is not possible to forge object references. In Java safety depends on strong typing, memory protection (overflow checks), automatic memory management, and byte-code verification [24]. However, language safety itself does not guarantee protection in a mobile agent execution environment. Pervasive aliasing in object-oriented languages leads to a situation where it is impossible to determine which objects belong to a certain agent and therefore to check whether an access to a particular object is valid or not. It is crucial to introduce the concept of strong protection domains, similar to the process abstraction in operating systems. A protection domain draws a boundary around a component (i.e., a mobile agent or a service). It encapsulates the set of classes required by the component, some concurrent activities (strands), as well as all objects allocated by these strands. In general, strands shall only execute in their own protection domain. Special precaution is necessary to allow strands to cross domain boundaries. Furthermore, the kernel must prevent object references from being passed over protection domain boundaries. The kernel ensures that an object reference exists in only a single protection domain. This property is very important for memory accounting, too. Each protection domain has associated its own set of classes. In general, different components must not share the same classes, since this would mean to share also the static variables in these classes (i.e., shared static variables would introduce aliasing of object references between different domains, or even worse, if static variables were not final, they could be used as covert communication channels the kernel could not control). However, some classes from the JDK must be shared by all components in order to ensure correct function of the JVM. Nevertheless, mobile agents must not employ JDK classes comprising functionality that undermines protection. For this reason, extended byte-code verification of agent classes is necessary. Another issue to be addressed by a mobile agent system is protection of resources, such as files or network ports. While in monolithic operating systems the kernel usually deals with resource protection, micro-kernel architectures simply ensure that security policies can be implemented at a higher level. Similarly, a mobile agent micro-kernel does not have to deal with security policies. Rather, it must make sure that only privileged domains can access system resources. For Java this means that agent domains must not have access to certain core packages, such as java.io or java.net. Such restrictions can be enforced by extended byte-code verification. 4.2 4.2.1
Implementation issues Classloading
The J-SEAL2 kernel employs a separate classloader namespace for each protection domain. A global configuration defines the set of classes to be shared by all domains. These classes are loaded by the JVM system classloader. All other classes are loaded by the protection domain classloader (replicated classes). In order to ensure proper function of the Java runtime system, all JDK classes are shared. This does not introduce security problems, as the extended J-SEAL2 verifier assures that agents cannot use dangerous JDK functionality. Since replicating classes limits performance and increases agent startup overhead as well as memory consumption (above all, the same methods are compiled multiple times),
6
the J-SEAL2 kernel is designed to minimize replicated kernel classes. In the current implementation only three small classes from the communication subsystem have to be replicated. This is necessary to ensure that serialized object graphs received by a protection domain are resurrected using the classloader of the receiving domain. Agent classes as well as classes from the J-SEAL2 framework and library are replicated. To minimize the overhead of replicating framework and library classes, the JSEAL2 classloader can be configured to cache the class files residing in certain library packages. Since loading a class file from disk is the most significant performance bottleneck, a proper caching configuration yields a speedup in agent creation by more than factor 2. 4.2.2
Extended verification
Agent class files are verified by a special J-SEAL2 verifier in order to ensure that the agent does not use certain JDK and kernel classes. By this means the kernel protects itself and the underlying JVM from being corrupted by malicious or badly programmed agents. Each protection domain can have its own directives declaring which classes may be referenced. Directives include the following types of restrictions: 1. 2. 3. 4. 5.
access can be restricted to certain packages, eventually including subpackages access to individual classes can be allowed or forbidden access to individual class members can be allowed or forbidden extension of non-final classes can be prohibited agent classes must reside in a particular package or in a subpackage thereof
The possibility to prevent the extension of certain classes and to control access at the level of individual class members helps to structure the J-SEAL2 kernel in a clean way. For example, we used multiple packages to separate different parts of the kernel. Public access modifiers were necessary to allow the interoperation of distinct kernel components. The directives ensure that agents cannot access certain kernel internals which had to be declared public for software engineering reasons. More generally, we think it is important to distinguish between software engineering practices and security engineering techniques: Java access modifiers and subtyping are very useful for software engineering purposes, whereas security engineering takes place in the specification of directives. To ensure that a given class file does not violate a particular set of directives it is sufficient to verify that the constant pool of the class file does not refer to forbidden classes or members and that extension of the superclass is not prohibited. Each member (field, method, constructor) that is accessed by a method/constructor of the verified class has an entry in the class file constant pool. Thus, it is not necessary to verify method/constructor code. The verification algorithm consists of two passes: In the first pass, the constant pool is parsed and a constant pool representation is created. In the second pass, the constant pool representation is scanned for class references, member references, and type signatures. Class references as well as class names in type signatures must denote allowed classes. Member references have to specify allowed members. An efficient array representation of the constant pool as well as an optimized internal representation of the verification directives help to minimize verification costs. Benchmarks measuring agent startup overhead indicate that verification overhead is less than 9% of the CPU time spent for classloading. These measurements also include the
7
costs for additional verification to ensure proper protection domain termination (see section 6). 4.3
Future improvements
Even though the J-SEAL2 kernel employs caching techniques to minimize the overhead of replicating classes, reloading kernel and framework classes is still one of the most time consuming performance bottlenecks. Since some of the replicated framework classes could be shared safely, we will work on mechanisms to detect automatically whether a certain library class can be shared safely. For the moment, a J-SEAL2 administrator can modify the global sharing configuration to avoid the replication of certain library classes. While trying to maximize sharing of classes whenever it does not introduce security risks, optimizations shall also minimize the time necessary to load and link replicated classes. As mentioned before, with the aid of class file caches the IO costs for loading replicated kernel and framework classes from the disk can be reduced significantly. Therefore, the most time consuming operation in reloading replicated classes is ClassLoader.defineClass(). This operation also involves byte-code verification by the JVM [16]. However, we can assume that the class files of the J-SEAL2 kernel and of libraries conform to the Java language specification [10], because we have full control over them. Unfortunately, the JDK does not offer any options to disable byte-code verification for certain classes. An enhanced classloader API giving the programmer control over byte-code verification would help to minimize classloading overhead and to further improve agent startup performance. The extended verification of agent classes by the J-SEAL2 classloader becomes performance critical for agents with large class archives. The current implementation requires two passes to verify the class file constant pool. This is because most Java compilers produce class files, where most of the constant pool entries for classes, type signatures, and members are forward-references to other constant pool entries. A Java compiler avoiding forward-references is a precondition to replace the two-pass algorithm with a single-pass verifier. 5
Communication
In operating systems co-operating processes can exchange messages through InterProcess Communication (IPC) facilities provided by the kernel. Process protection ensures that IPC is the only means to exchange information over process boundaries. Also in the case of shared memory, processes must first obtain a shared memory segment from the kernel before they can use it for communication. As a mobile agent kernel isolates agents from each other, it must also provide some means for inter-agent communication in order to allow agents to collaborate. 5.1
Requirements
As we have stressed in the previous section, a secure mobile agent kernel has to provide strong protection domains. An agent executes within a protection domain, it is isolated from the rest of the system. If agents need to exchange some messages, all communication partners have to ask the kernel to establish a communication channel. The kernel ensures that communication partners can only access certain channels if they have the necessary permissions.
8
The J-SEAL2 kernel offers two different communication facilities, channels and external references. In both cases, messages are passed by value. The kernel creates a deep copy of the message before it passes it to the receiver. In that way, the kernel prevents direct sharing of object references between different protection domains. This property is crucial for protection domain isolation, as aliasing between different domains would undermine protection. Furthermore, resource accounting is largely simplified by the fact that every object reference exists in only a single protection domain. Channels support communication only between neighbors in the seal hierarchy. If two neighbor seals issue matching send and receive communication offers, the kernel passes the message from the sender to the receiver. The matching algorithm ensures that the sender really wants to send the message to the receiver, and also that the receiver is willing to accept a message from the sender. Details about the channel matching algorithm can be found in [21, 22, and 23]. With the aid of channel communication, it is possible to achieve complete mediation. This means that it is possible to intercept all messages going in and out of an agent. However, channel communication becomes a significant performance bottleneck, if messages must be routed through a series of protection domains, since communication involves strand switches proportional to the communication partners’ distance in the seal hierarchy. External references are an optimization, they support indirect sharing of objects between distinct seals that are not necessarily neighbors in the seal hierarchy. An external reference acts as a capability to invoke methods on a shared object. When an external reference is passed to another seal, the receiver gets a copy of the communicated external reference. The sender may invalidate that copy (as well as, in a synchronized way, recursively all further copies of that copy) at any time with immediate effect, i.e., strands calling methods through the copy immediately leave the callee’s protection domain and throw an appropriate exception in the caller’s domain. This property clearly distinguishes external references from capabilities in the J-Kernel [11, 12]. Details on the external reference communication model can be found in [4]. 5.2
Implementation issues
Since in J-SEAL2 there is no direct sharing of object references between different protection domains, all communication involves the copying of messages. J-SEAL2 employs Java serialization to create a deep copy of an object graph of serializable objects. Therefore, only serializable objects can be passed between different protection domains. The kernel ensures that a communicated serialized object graph is deserialized within the target domain. Thus, the deserialized object graph only refers to classes from the receiving domain. As serialization and deserialization are expensive operations, J-SEAL2 offers optimizations for certain object types that are frequently used in communication messages, such as Java primitive types, arrays of primitive types, as well as strings. Primitive types do not include object references, they can be passed within simple wrappers. The classes for arrays of primitive types and for strings are always loaded by the system classloader, thus we need not take care whether they are copied within the target domain. For arrays of primitive types, we can use array cloning or System.arraycopy(). For strings, there is a special constructor taking another string as argument. All these optimizations are performed by the J-SEAL2 kernel, they are
9
transparent to the programmer. Performance measurements5 show that capsule optimizations improve the performance of capsule creation and opening by a factor of 2050. External references complicate the serialization and deserialization of object graphs. External references are not serializable, but they have to be treated in a very special way. Since the kernel keeps track of external references and their copies, they must be separated from the rest of the serialized object graph. The kernel employs a dedicated copying algorithm for external references, details can be found in [4]. The J-SEAL2 implementation of communication channels ensures that a message is copied to the receiving protection domain only after a communication match. Send and receive requests are treated as equivalent communication offers. They are inserted in kernel queues residing in the same protection domain as the issuing strand. The kernel checks neighbor domains for matching offers. Only if the search is successful, the kernel copies the message to the receiving domain. This implementation is very different from traditional message passing, where a sender directly inserts a message into the receiver queue. By separating sender and receiver queues the J-SEAL2 kernel ensures that an agent cannot mount a denial of service attack against a neighbor domain by filling its receiver queues with messages, eventually causing the receiver to exceed its memory limits. 6
Termination
Operating systems provide means to terminate running processes. All memory resources the terminated process had allocated before become available to other processes. The operating system kernel must ensure that neither its own resources nor any other shared resources are corrupted when a process is killed. 6.1
Requirements
When a protection domain is terminated, all strands belonging to that domain have to be stopped. In Java the only means to stop a running thread asynchronously is Thread.stop(). However, this operation has been deprecated in the Java 2 platform, as it is inherently unsafe. When a thread is stopped, it exits all monitors immediately and throws ThreadDeath. As a consequence, objects may be left in an inconsistent state. In section 3 we have already stated requirements for kernel code to ensure that shared resources, such as internal structures of the kernel and of the Java runtime system, cannot be corrupted when a strand is stopped asynchronously. The idea is to defer stop requests if the strand to be stopped is accessing the kernel. A simple solution is to ensure that no other strand can access the kernel while a strand is stopping another one. When a thread is stopped with the aid of Thread.stop(), there is no guarantee that the stopped thread will really terminate. Depending on the executed code, ThreadDeath might be caught or a finally{} clause might execute some infinite loop. Since the kernel must ensure immediate resource reclamation when a protection domain is terminated, special verification is necessary to ensure that agent code cannot prevent or delay domain termination. Thus, exception handlers of agents must 5
In this benchmark we created capsules consisting of a string, a byte array, and a long value. The sizes of the string and the array were chosen independently between 0 and 1000.
10
immediately rethrow caught ThreadDeath exceptions. J-SEAL2 offers a class file rewriting tool to modify exception handlers accordingly. At runtime the extended bytecode verifier simply checks whether all agent classes have been rewritten correctly. In addition to these restrictions, agents must not define finalizers or class finalizers. These special methods are invoked by the garbage collector before an object or a class is reclaimed. If these methods contained some infinite loops, they would hang up the whole system. 6.2
Implementation issues
Safe strand stopping is achieved through special kernel entry and exit sequences. A strand terminating a protection domain enters exclusive kernel mode. It is blocked until all other strands have left the kernel. Terminating a protection domain is an atomic kernel operation. All strands belonging to the domain are stopped within the same kernel operation. Since the strands to be stopped cannot enter the kernel, this approach enforces safe domain termination. In order to prevent the corruption of internal structures inside the JVM, classloading always occurs in kernel mode. Details about kernel mode can be found in section 3. In order to prevent agents from delaying their termination, the J-SEAL2 verifier ensures that agent methods do not use forbidden Java constructs. The method signatures of all defined methods must be different from finalize() and classFinalize(). Furthermore, the exception tables of all methods are examined in order to determine the types of caught exceptions. Agents are not allowed to catch ThreadDeath. If an exception handler catches a supertype of ThreadDeath (i.e., Throwable or Error), the handler code has to determine whether the actual type of a caught exception is ThreadDeath. In this case, the handler must rethrow ThreadDeath immediately. The JVM supports a special catch type in the exception table to indicate that all exceptions are caught by a particular exception handler. Java compilers use this feature to compile finally{} clauses and synchronized{} statements [16]. The J-SEAL2 verifier ensures that these special exception handler include code to rethrow a caught ThreadDeath exception immediately, too. Because exception handlers catching all exceptions are not present in the original Java code, we have implemented a byte-code rewriting tool to process agent classes generated by standard Java compilers. This tool has to be used by J-SEAL2 programmers before they package their agents. It examines all exception handlers that could potentially catch ThreadDeath exceptions and inserts a short byte-code sequence (4 JVM instructions) to rethrow a caught ThreadDeath exception immediately. Our implementation is based on the JavaClass API by Markus Dahm [7]. Note that rewritten exception handlers rethrow ThreadDeath exceptions before any locks are released. This is important because the monitorexit instruction of the JVM might throw NullPointerException or IllegalMonitorStateException. However, since we are rethrowing ThreadDeath before releasing locks, an eventually locked object will never be unlocked and could cause a deadlock, if another strand tried to lock it later. Since there is no direct sharing of objects between different protection domains and because kernel code must not lock objects that are accessible by agents (see section 3), only objects belonging to the terminated agent may be left in a locked state. This is not a problem, since the agent is removed from the system anyway. The J-SEAL2 verifier simply checks whether all agent classes have been rewritten accordingly. Benchmarks show that our verification mechanism does not introduce much
11
overhead at runtime. Even for complex agents the total verification costs, including constant pool verification as described in section 4, does not exceed 10% of the total time for classloading. 7
Resource Accounting
Operating system kernels provide mechanisms to enforce resource limits for processes. The scheduler assigns processes to CPUs reflecting process priorities. Furthermore, only the kernel has access to all memory resources. Processes have to allocate memory regions from the kernel, which verifies that memory limits for the processes are not exceeded. A mobile agent kernel must prevent denial of service attacks, such as agents allocating all available memory. For this purpose, accounting of physical resources (i.e., memory, CPU time, network usage, etc.) and logical resources (i.e., number of strands, number of nested protection domains, etc.) is crucial. 7.1
Requirements
The J-SEAL2 kernel manages a tree hierarchy of nested protection domains. The resource control facilities shall reflect the hierarchical system structure. Hierarchical process models have been used successfully by operating system kernels, such as the Fluke microkernel [8]. The Fluke kernel employs a hierarchical scheduling protocol, CPU Inheritance Scheduling [9], in order to enforce scheduling policies. In this model, a parent domain donates a certain percentage of its own CPU resources to a child process. Initially, the root of the hierarchy possesses all CPU resources. A generalization of CPU Inheritance Scheduling fits very well to the J-SEAL2 hierarchical domain model. At system startup the root domain, RootSeal, owns all physical and logical resources, for example 100% CPU time, some amount of virtual memory, unlimited network usage, the maximum number of strands the underlying JVM is able to cope with, an unlimited number of subdomains, etc. When a nested protection domain is created, the creator donates some part of its own resources to the new domain. The J-SEAL2 kernel shall account for the following resources. The first column names the resource, the second column indicates the unit of measurement, and the third column indicates how much of the resource the root domain initially holds: relative CPU time max. active memory max. cumulative memory
Physical resources % 100% byte JVM heap max. byte unlimited
max. active strands max. cumulative strands max. active subdomains max. cumulative subdomains
Logical resources (integer) (integer) (integer) (integer)
JVM thread max. unlimited unlimited unlimited
Note that the J-SEAL2 kernel is not responsible for network accounting. This is because the micro-kernel does not provide access to the network. Instead, network access can be provided by multiple services. These network services or some mediation layers in the hierarchy are responsible for network accounting according to application-specific security policies.
12
Since J-SEAL2 is designed for large-scale applications, where a large number of services and agents are executing concurrently, design and implementation must minimize the overhead of resource accounting. Some domains, such as core services, are fully trusted. Their resource consumption need not be controlled by the kernel. If a domain has unlimited access to a resource, no accounting for that resource may be necessary. Furthermore, in certain situations protection domains that are neighbors in the hierarchy may choose to share certain resources. In this case, resource limits are enforced together for a set of protection domains. As a result, resource fragmentation is minimized. For example, consider an agent creating a subagent for a certain task. Frequently the creating agent does not want to donate some resources to the subagent, but it rather prefers to share its own resources with the subagent. Resource accounting also affects the kernel communication facilities. A domain must be able to limit the size of incoming messages. Otherwise, malicious domains holding more memory resources could easily mount denial of service attacks by sending large messages. For channels, this means that the receiver can specify a maximum size for incoming messages. If a communication partner attempts to send a larger message, it will cause an exception. Because external references support two-way message exchange (argument and result messages), the callee as well as the caller have to specify separate limits for incoming messages. When an external reference is created initially, the creator (callee) specifies a maximum size for the argument message. The caller may associate a limit for the result message with the copy of the external reference it holds. Details on external reference communication can be found in [4]. 7.2
Implementation issues
The current implementation of the J-SEAL2 kernel is prepared for resource accounting, but it actually does not enforce resource limits. Because the Java 2 platform does not offer any means for resource accounting, implementing resource control for physical resources is very difficult in pure Java. Accounting for logical resources does not make sense unless there are also facilities to control the usage of physical resources. A combination of resource control for physical resources as well as for logical resources is necessary in order to prevent denial of service attacks. We plan to integrate resource control in the next release of the J-SEAL2 kernel. The J-SEAL2 kernel is designed to ease a future integration of resource accounting. It guarantees accountability, i.e., all objects belong to exactly one protection domain. References to an object exist only within a single domain. This property also holds for internal kernel structures. Therefore, it is possible to account each allocated object to exactly one protection domain. This feature not only simplifies resource accounting, but it is also crucial for immediate resource reclamation during domain termination. In the following subsections we discuss different approaches to implement accounting for physical resources in a Java environment. 7.2.1
Memory accounting via byte-code rewriting
Memory accounting can be achieved with the aid of byte-code rewriting. This approach was used for JRes [6], a resource accounting interface for Java developed at Cornell University. JRes enforces memory limits for individual threads. For this purpose, it
13
maintains the total amount of memory allocated for each thread. Before an object is allocated, JRes checks whether the allocating thread exceeds its memory limit. The code of every method is rewritten such that appropriate byte-code is inserted whenever an object allocation occurs in the original method code. For array allocation instructions, code computing the array size is created. JRes creates or modifies finalizers in order to detect when the garbage collector frees an object. Moreover, JRes maintains weak references to arrays in order to detect deallocation of arrays. Memory accounting by byte-code rewriting has the big advantage that it can be implemented in pure Java. While JRes uses native code to obtain weak references to arrays, Java 2 reference objects can be used to achieve the same functionality in a portable way. Moreover, byte-code rewriting is necessary only for classes of domains with memory limits. Thus, fully trusted domains do not cause any overhead. However, byte-code rewriting does not deal with objects allocated by native code. For instance, it is not possible to account for the usage of JVM internals, such as compiled methods. Thus, we cannot guarantee that byte-code rewriting prevents all kinds of denial of service attacks by memory allocation. In addition to this, byte-code rewriting has a significant impact on performance: The authors of JRes report that memory accounting yields up to 18% overhead. However, this does not include the costs for byte-code rewriting, which can be even much higher. We suggest to rewrite agent classes offline, when the agent is packaged by the developer. Then the kernel simply has to verify that all methods have been rewritten correctly, which is significantly faster than rewriting all byte-code during classloading. The J-SEAL2 kernel itself and some JDK classes shall be rewritten offline, too. This can be done when the J-SEAL2 system is installed on a computer. 7.2.2
CPU time accounting via native code
CPU time accounting for individual threads can be accomplished by associating an operating system handle with each thread. A polling thread periodically queries the operating system for CPU time consumption. The JRes [6] resource accounting interface has successfully implemented this approach. However, this technique for CPU time accounting relies on native code, which depends on the specific operating system the JVM executes on. Furthermore, the Java runtime system must use native threads that are mapped to operating system threads. Although CPU time accounting by a polling thread has a certain configurable delay in reaction time, it is an appropriate mechanism for building a simple control algorithm to approximate an intended schedule. We can use a high priority polling thread to periodically obtain information on the total CPU time consumption for each protection domain. Comparing the actual CPU consumption with the desired value (the product of the relative share in CPU time of the domain and the total time the domain exists) we can determine which domains have received too much or too little CPU time. If the difference exceeds some threshold, the priorities of all threads in the domain are adjusted. Over a longer observation period this simple control algorithm will approximate the desired schedule. The control algorithm becomes somewhat more complex, if we also deal with multiple protection domains sharing a certain CPU quota. In this case, the total sum of CPU consumption of all threads executing in the corresponding domains must be calculated. Furthermore, if we allow a parent domain to change the CPU shares of its children domains, the desired CPU time consumption has to be computed incrementally. Several other refinements are possible, for instance, the CPU consumption of long ago
14
periods shall not have the same importance for scheduling decisions as recent CPU consumption. In addition to this, the kernel must prevent priority inversion. For example, consider the case when the priority of a thread is lowered while it is holding a kernel lock. Thus, another high priority thread waiting for an exclusive kernel lock may be delayed for rather long time. Even though the kernel is not designed to support real time applications, it must ensure that kernel operations are short. A simple solution is a priority ceiling protocol, where a thread executes in kernel mode always at the highest possible priority. When the kernel scheduler decides that the priority of a thread shall be lowered, this action is deferred until that thread leaves the kernel. 7.2.3
CPU time accounting via byte-code rewriting
If we measure CPU time consumption in an abstract unit of measurement, such as the number of byte-code instructions executed, we can employ byte-code rewriting techniques for CPU time accounting. For each domain the kernel maintains a counter indicating how many byte-code instructions may be executed in a certain period of time. If the counter becomes negative, the priorities of all strands in the domain are lowered. The kernel periodically executes a high priority strand incrementing the instruction counters of all domains by their relative share in CPU time. If a counter becomes positive again, the priorities of the strands in the domain may be increased. In order to prevent denial of service attacks, it is crucial that a thread decrements the counter of its domain at least once in each basic block. Above all, counter updates are necessary for each method invocation and inside loops. Techniques developed for profiling and tracing of programs can help to place counter updates in a way to minimize accounting overhead [3]. However, we must ensure that the algorithm determining the positions where to place accounting code does not consume much resources, as the J-SEAL2 verifier has to guarantee at runtime that the agents’ classes have been rewritten correctly. Therefore, a complex analysis is not feasible. Like memory accounting via byte-code rewriting, this approach can be implemented in pure Java. It has the drawback that we cannot control execution of native code in the Java runtime system. Furthermore, we expect a significant overhead in the verifier as well as during execution of rewritten code. 7.2.4
Accounting via the Java Virtual Machine Profiling Interface
The Java Virtual Machine Profiling Interface (JVMPI) [18] allows profiling of Java applications executing in a Java 2 runtime system. JUM, a Java Usage Monitor [15], is a profiling agent that communicates with the JVM through JVMPI. It collects information about memory allocation and CPU time consumption for each thread and offers a Java interface to access the accounting information. In contrast to JRes [6], JUM is able to account also for objects allocated by native code. However, JUM is not able to enforce memory limits. While JRes allows for exact pre-accounting of memory resources, where an overuse callback is generated before a thread can exceed its memory limit, a resource control mechanism based on JUM can only react after a memory overuse is detected. Since JUM has to communicate with the Java runtime system, it is implemented in native code and has to be ported for different computer architectures and operating
15
systems. Currently, JUM supports only some UNIX platforms. It employs the proc filesystem to obtain operating system information on the CPU time consumption of native threads. In addition to these limitations, the JVMPI is an experimental interface, it is not yet a standard profiling interface. 7.2.5
Accounting via Java extensions
Although resource control is important for many different kinds of Java based systems, such as application servers, servlet engines, Java enabled web browsers, etc., currently there is no Java standard extension for resource accounting. However, the Real Time Specification for Java [19] contains some concepts that can be used for resource control. Among other things, real time threads have associated processing parameters as well as memory parameters. Processing parameters allow to define the CPU time a thread shall receive in a certain period of time and also its latest completion time. Memory parameters allow to set the heap maximum for a thread as well as to limit the memory allocation rate, which helps to prevent denial of service attacks through the garbage collector. Moreover, parameter objects can be shared by multiple threads. In contrast to the Java Language Specification [10], which does not specify any scheduling algorithm for concurrent threads, the Real Time Specification for Java [19] specifies that every implementation has to support fixed-priority preemptive scheduling with at least 28 unique priority levels. Thus, the kernel scheduling algorithm can take advantage of the specific scheduling policy of the underlying JVM in a portable manner. 8
Related Work
Our work on the J-SEAL2 mobile agent micro-kernel is related to work on protection in single-language mobile code environments. Especially the Utah Flux Research Group has worked on the design and implementation of secure single address space operating systems implemented in Java [1, 2, 20]. Like J-SEAL2, the Alta [2, 20] operating system is a micro-kernel design. It provides a hierarchical process model supporting CPU accounting through CPU Inheritance Scheduling [9], where a process may donate some percentage of its CPU resources to nested child processes. However, the Alta design cannot be implemented in pure Java. Alta relies on modifications to the JVM, whereas J-SEAL2 runs on every Java 2 implementation. We are convinced that the portability of a mobile agent platform is crucial for its successful deployment in large-scale projects. Furthermore, considering the enormous pace of new JVM implementations offering rapidly increasing performance, it is almost impossible to maintain a modified JVM offering sufficient performance. J-Kernel [11, 12] is a Java micro-kernel supporting multiple protection domains. In JKernel communication is based on capabilities. Java objects can be shared indirectly by passing references to capability objects. However, J-Kernel is lacking the hierarchical model of J-SEAL2. Moreover, in J-Kernel cross-domain calls may block infinitely and may delay protection domain termination. J-Kernel supports per thread memory accounting via byte-code rewriting [6]. Like J-SEAL2, J-Kernel is implemented completely in Java, only CPU accounting requires native code. Luna [13, 14] is a Java extension that provides a task model for Java based on a type system, which distinguishes between task-local pointers and remote pointers shared between multiple tasks. Access to remote pointers is controlled by permits that can be
16
revoked at any time. When a task is terminated, Luna revokes all remote pointers into that task. While J-SEAL2 supports only coarse-grained indirect sharing between different domains with the aid of external references, remote pointers in Luna enable fine-grained direct sharing of individual objects between distinct tasks. However, Luna is not portable, as it is based on an extension to a Java runtime system. 9
Future Work
Most importantly, the next release of the J-SEAL2 kernel shall provide resource accounting for physical and logical resources. As we have seen in section 7, currently there is no standard Java extension for resource accounting. Because portability is crucial for the success of J-SEAL2, we will implement accounting for physical resources with the aid of byte-code rewriting (see section 7). While byte-code rewriting already has been used successfully for memory accounting [6], currently there are no experiences with CPU time accounting via byte-code rewriting. We will have to experiment with different algorithms in order to find the best trade-off between accounting accuracy and little overhead. Other important features to be offered in a future J-SEAL2 release include remote configuration and administration without disruption of the agent platform. Furthermore, we are integrating a flexible architecture for debugging and monitoring mobile agent applications. A graphical user interface will allow to monitor and configure multiple JSEAL2 platforms from a single place. A load-balancing service will help to maintain server clusters for large-scale applications. 10 Conclusion We have presented design and implementation issues that must be addressed by Java based mobile agent platforms. For security reasons, it is crucial that a mobile agent system is structured in a similar way as an operating system, where the kernel is separated clearly from all other parts of the system. The kernel is responsible for protection, communication, protection domain termination, and resource control. The J-SEAL2 mobile agent system is based on a micro-kernel architecture providing the necessary security features for commercial mobile agent applications. The J-SEAL2 kernel is implemented in pure Java, it is portable over different operating systems and hardware platforms. The J-SEAL2 micro-kernel offers strong protection domains, efficient communication facilities, and safe protection domain termination with immediate resource reclamation. J-SEAL2 is prepared for resource accounting, which will be implemented in the next release. Readers interested in getting an evaluation version of the J-SEAL2 platform, including network and GUI services, developer documentation, as well as some small demonstration applications, may contact the author by email. Acknowledgements The author thanks Ciarán Bryce, Rudolf Freund, Andreas Krall, Klaus Rapf, and Jan Vitek for many suggestions and good ideas.
17
11 References 1. G. Back and W. Hsieh. Drawing the Red Line in Java. In Proceedings of the Seventh IEEE Workshop on Hot Topics in Operating Systems, Rio Rico, AZ, March 1999. Available at http://www.cs.utah.edu/flux/papers/redline-hotos7.ps.gz 2. G. Back, P. Tullmann, L. Stoller, W. C. Hsieh, and J. Lepreau. Java Operating Systems: Design and Implementation. Technical Report UUCS-98-015, University of Utah, Department of Computer Science, August 1998. Available at http://www.cs.utah.edu/flux/papers/javaos-tr98015.ps.gz 3. T. Ball and J. R. Larus. Optimally Profiling and Tracing Programs. In ACM Transactions on Programming Languages and Systems, 16(3):1319-1360, July 1994. Available at http://www.research.microsoft.com/~tball/ 4. W. Binder. J-SEAL2 – A secure high-performance mobile agent system. In Proceedings of the IAT’99 Workshop on Agents in Electronic Commerce, Hong Kong, December 1999. 5. T. Bray, J. Paoli, and C. M. Sperberg-McQueen. Extensible Markup Language (XML) 1.0. W3C Recommendation, February 1998. Available at http://www.w3.org/TR/1998/REC-xml-19980210 6. G. Czajkowski and T. von Eicken. JRes: A Resource Accounting Interface for Java. In Proceedings of the 1998 ACM OOPSLA Conference, Vancouver, BC, October 1998. Available at http://www.cs.cornell.edu/slk/papers/oopsla98.pdf 7. M. Dahm. Byte Code Engineering. Java-Information-Tage 1999 (JIT’99), September 1999. Available at http://www.inf.fu-berlin.de/~dahm/JavaClass/ 8. B. Ford, M. Hibler, J. Lepreau, P. Tullmann, G. Back, and S. Clawson. Microkernels Meet Recursive Virtual Machines. In Proceedings of the Second Symposium on Operating Systems Design and Implementation (OSDI’96), October 1996. Available at http://www.cs.utah.edu/flux/papers/fluke-rvm.ps.gz 9. B. Ford and S. Susarla. CPU Inheritance Scheduling. In Proceedings of the Second Symposium on Operating Systems Design and Implementation, October 1996. Available at http://www.cs.utah.edu/flux/papers/inherit-sched.ps.gz 10. J. Gosling, B. Joy, and G. L. Steele. The Java Language Specification. The Java Series. Addison-Wesley, Reading, MA, USA, 1996. Available at http://java.sun.com/docs/books/jls/html/index.html 11. C. Hawblitzel, C.-C. Chang, G. Czajkowski, D. Hu, and T. von Eicken. Implementing Multiple Protection Domains in Java. In Proceedings of the 1998 USENIX Annual Technical Conference, New Orleans, LA, June 1998. Available at http://www.cs.cornell.edu/slk/papers/usenix98-final.pdf 12. C. Hawblitzel and T. von Eicken. A Case for Language-Based Protection. Technical Report TR98-1670, Cornell University, Department of Computer Science, March 1998. Available at http://www.cs.cornell.edu/slk/papers/TR98-1670.pdf 13. C. Hawblitzel and T. von Eicken. Type System Support for Dynamic Revocation. In ACM SIGPLAN Workshop on Compiler Support for System Software, May 1999. Available at http://www.cs.cornell.edu/Info/People/hawblitz/hawblitz.html 14. C. Hawblitzel and T. von Eicken. Tasks and Revocation for Java (or, Hey! You got your Operating System in my Language!). Draft, November 1999. Available at http://www.cs.cornell.edu/Info/People/hawblitz/hawblitz.html 15. F.-X. Le Louarn. JUM – a Java Usage Monitor. Available at http://www.iro.umontreal.ca/~lelouarn/jum.html
18
16. T. Lindholm, F. Yellin. The Java Virtual Machine Specification, Second Edition. Addison-Wesley, 1999. Available at http://java.sun.com/docs/books/vmspec/ 2nd-edition/html/VMSpecTOC.doc.html 17. Sun Microsystems. JAVA 2 SDK, Standard Edition. Available at http://java.sun.com/products/jdk/1.2/ 18. Sun Microsystems. Java Virtual Machine Profiler Interface (JVMPI). Available at http://java.sun.com/products/jdk/1.2/docs/guide/jvmpi/index.html 19. The Real Time for Java Experts Group. Real Time Specification for Java. Available at http://www.rtj.org 20. P. Tullmann and J. Lepreau. Nested Java Processes: OS Structure for Mobile Code. In Proceedings of the Eighth ACM SIGOPS European Workshop, September 1998. Available at http://www.cs.utah.edu/flux/papers/npmjava-esigops98web.ps.gz 21. J. Vitek and C. Bryce. The JavaSeal Mobile Agent Kernel. In Proceedings of the Joint Symposium ASA/MA’99, First International Symposium on Agent Systems and Applications (ASA’99) and Third International Symposium on Mobile Agents (MA’99), Palm Springs, California, USA, October 1999. Available at http://cuiwww.unige.ch/OSG/publications/OO-articles/TechnicalReports/99/ kernel.pdf 22. J. Vitek, C. Bryce, and W. Binder. Designing JavaSeal or How to Make Java Safe for Agents. In D. Tsichritzis, editor, Electronic Commerce Objects, pages 105-126, University of Geneva, July 1998. Available at http://cuiwww.unige.ch/ OSG/publications/OO-articles/TechnicalReports/98/javaSeal.pdf 23. J. Vitek and G. Castagna. Towards a Calculus of Secure Mobile Computations. In Workshop on Internet Programming Languages, Chicago, Ill., May 1998. Available at http://cuiwww.unige.ch/OSG/publications/OO-articles/TechnicalReports/98/ calcSecure.pdf 24. F. Yellin. Low level security in Java. In Fourth International Conference on the World-Wide Web, MIT, Boston, December 1995.
19