One of the major issues in supporting mobile code is to provide a suitable level of System Safety, which provides appropriate controls on accesses to resources by the executing code so the security of the execution system is not compromised [7,10]. Ideally it would be desirable to be able to formally verify that the imported code will not violate the systems security policy, however in practice this is difficult (cf. for example the work on proof carrying code requiring a hand-crafted proof for each [8]). The other key issue is Run-Time Safety, which is concerned with ensuring that the code will behave in accordance with the source specification. This is usually provided by a combination of language features in conjunction with the run-time system.
A number of approaches to providing a safe execution environment have been investigated (see survey by [10]), with much of the focus being on securing procedural languages such as Java [3,9] or Tcl/Tk [4,11]. However here we are interested in how best to extend our existing work on Safe Erlang, which provides the necessary run-time safety, in order to best support a range of Safety Policies, and be able to impose differing levels of system safety on different mobile code instances.
Safe Erlang is an extension of the functional language Erlang [6,5]. It includes extensions to Erlang to enhance its ability to support safe execution of remotely sourced code. These extensions are in three areas:
We have been working with Erlang, rather than the more traditional procedural languages, such as Java or SafeTCL, because we believe that functional languages have a very high degree of intrinsic run-time safety. This means the changes required to provide a safe execution environment are much smaller than those needed to secure a procedural language, as discussed in [6]. Erlang is a functional language, developed by Ericsson, and is currently being used for a number of production telecommunications applications [1,2].
The approach outlined in this paper for specifying a safety policy is based on executing the untrusted mobile code in a constrained environment, which is trusted to prevent direct access to any external resources by the executing untrusted code, and then applying filters to messages between the untrusted code and server processes which mediate the access to resources. Since Erlang generally uses message passing Remote Procedure Calls (RPC's) for servicing requests (including local requests), and since we can enforce this use through our Safe Erlang extensions, this provides a very convenient choke point at which to impose a safety policy. By restricting the problem domain to just verifying that the messages conform to the desired policy, the problem should be considerably more tractable, whilst still being highly extensible. Using Safe Erlang's restricted subnodes, these checks need only be applied to untrusted code running in a restricted subnode. Further, they are done only when the untrusted code is requesting a possibly insecure service provided by a server running in a more trusted subnode. This significantly reduces the overhead of performing such checks. By implementing this approach in a system based on a functional language, it is possible to have the security policy specification written in this language, and then use it directly as the actual check function.
The Java language and run-time system provide a sandbox for code to be executed in, with suitable controls on its operations. The Java Class Loader, Security Manager, and Access Manager interact to determine which particular, potentially unsafe, operations are permitted.
The Java Class Loader is responsible for loading classes as required. It alone knows the origin of the class, whether it has been signed, and hence which security or access manager should be used with it. It is also responsible for managing and enforcing namespace separation rules, which ensures that class/package names sourced from different locations (CODEBASEs) are partitioned into separate namespaces to prevent confusion/subversion due to name collisions. Different Class Loaders are used for code loaded from different sources, and trusted code is permitted to specify the use of a new Class Loader.
The Java Security Manager is called by all Applications Programmer Interface (API) code to see if a requested operation, with the arguments as supplied, is permitted for the requesting class. Only one Security Manager is defined for any instance of a Java run-time system. It is usually selected by trusted code at startup, and cannot subsequently be changed. It includes methods to resolve requests relating to file access, network access, execution of other programs on the local system, and access to system resources. These methods need to be carefully written to handle the varying decisions. They interact with the Class Loader to determine what the requesting class is permitted to do. If different policies need to be applied to different classes, then the Security Manager must have code provided to distinguish between the various cases. In practice, this has proved to be difficult.
Java 1.2 provides a new Access Manager class. It supplements and generalises the features previously provided by the Security Manager (which is maintained to support the current security API). The Access Manager may be consulted by the Security Manager in order to determine whether some action is permitted. In contrast to the Security Manager, customisation of the Access Manager is provided by specifying rules in a policy file, rather than having to modify the actual code. This significantly improves its generality.
The Java approach involves a single execution sandbox. Hence all code running must be vetted for safety - some of the code being permitted to perform ünsafe" operations (because it was sourced locally, or comes from a signed class), whilst other code must be restricted (because it is remotely sourced, untrusted, code). This necessarily complicates the policy specification - in actual code in the case of the Security Manager, or in the policy file for the Access Manager. Also, the realm of what operations are vetted is constrained by which of the API calls include a check with the Security/Access manager as to whether the operation is permitted, although the Access Manager includes an extensible Permissions class, which user applications and classes can choose to use.
Policies are specified in a file, which supplies rules (written as Tcl statements) which govern when each of the categories of potentially ünsafe" operations are permitted, and with what arguments.
The use of multiple interpreters (sandboxes) provides significant flexibility and isolation between the various mobile code modules and the main system. Further the use of a policy specification which contains Tcl commands allows a good degree of flexibility in policy specification, whilst still keeping the policy relatively straightforward. However, Tcl/Tk policies still are bound by the domain of potentially ünsafe" operations which may be vetted.
This approach shares a number of similarities with the approach supported by SafeTCL. In both cases multiple execution environments with differing trust levels are supported, along with a flexible policy written in the language used to validate potentially unsafe access requests. Where it differs is in the application of the check to RPC messages, rather than by modifying the code in the API libraries; and in the use of a functional language, which provides a clean specification of the policy.
In Erlang, a general server mechanism is supplied which acts as a wrapper around a number of general services (such as file access). It uses the efficient, lightweight, message passing mechanism supplied by Erlang and used for all inter-process communications. It is quite straightforward to adapt the general server to support the use of a message check function, and then advertising this "protected" service in the restricted environments where unsafe code is executing. Usually no further changes to the actual server code is required, other than ensuring that the server is started using the modified server mechanism, rather than the default. Thus, unlike other systems, extending the scope of which services are to be protected merely requires that the existing server be started using the new general server mechanism with an appropriate check function, rather than requiring any changes to the actual server code (provided only that the server uses the general server handler).
Writing a suitable check function, in general, defaults to looking at the list of requests supported by the server, and itemising them as always permitted, always denied, or permitted for some parameter values. This then translates fairly directly into the code for the check function. For example, file access may be restricted to files in some specified directory. This requires a policy that includes checking the supplied filename to ensure that it is appropriate.
Whether to impose a policy or not on any particular service is a decision made by the owner of the node. An execution environment can be tailored, comprising one or more restricted subnodes each with a policy imposed by limiting them to using a predefined set of servers, which execute in trusted subnodes, and which have the required check function imposed.
A key feature in implementing this check function is the use of Erlang's pattern matching ability to resolve which of the many possible messages are valid. The check monitor function has the form check(Mod,Type,Msg) where Mod is the name of the server module the policy is imposed upon, Type is the particular RPC message type (call/cast/info) being checked, and Msg is the message received by safe_gen_server to be relayed to the call-back routines in the actual server module Mod. The Msg is usually a tuple naming the requested function and providing the necessary parameters.
For example, the monitor function could look as follows:
check(file,call,get_cwd) -> ok; check(file,call,{delete,F}) -> valid_name(F), ok; check(file,call,{read_file,F}) -> valid_name(F), ok; check(file,call,{write_file,F,Bin}) -> valid_name(F), ok; ... % other clauses removed check(file,info,_) -> ok; check(Mod,Type,Msg) -> exit(policy_violation).This indicates that for the file server, get_cwd is ok; as are requests to delete, read or write a file, provided a valid name (say current dir only) is supplied. Info messages (exits etc) are allowed. All other calls will result in a policy_violation exception.
safe_gen_server is a modified version of the existing gen_server module, which implements the security monitor function on messages received for the server it manages. This function is installed by specifying the new {check, Fun} option in the call to safe_gen_server:start(M,A,Opts) (and related variants). Then, when it receives a message, it will invoke the nominated check function first before invoking the appropriate call-back routine in the server module. If the check function exits, an appropriate error message can be returned to the calling client.
It is left open whether a single check function, covering all modules relevant to a particular safety policy, or whether separate functions for each module, is most appropriate. The current implementation handles both.
To use the check facility, the servers must be started with the check option specified. This would typically require calling safe_gen_server explicitly, rather than using the usual start function in the server module. Alternatively, the server module could be modified to provide a variant of start for this. The server modules may also need minor changes and recompilation to work in the safe environment anyway (this is certainly true of the prototypes). In either case, the effect is to impose the check function on RPC messages before they are passed to the existing server code.
safe_gen_server has also been modified so that an explicit list of node capabilities or global names must be supplied for the multi_call/cast interfaces. This is because, with the capability concept, processes must explicitly have capabilities for any resources they wish to use (or know registered names for those capabilities).
For ease of use, policy for an SSErl node may be specified by a policy module in a prescribed form. This module must contain and export a number of functions which are used to create the SSErl node with the desired security policy. These functions are:
An example of a policy module is given below. It is intended for use by the SSErl test module as a demonstration. This policy limits access to files to just those in the current directory. More specifically, the functions perform the following:
-module(safety_pol). % required policy functions we export -export([proc_rights/0, aliases/0, init_servers/0, check/3]). %% proc_rights is the list of side-effects permitted for process in node %% from possible list of: [db,extern,open_port] proc_rights() -> []. %% aliases is the initial list of module name aliases to use %% here alias: the gen_server modules, safe servers, stdlib modules %% all of which are compiled for the SSErl env aliases() -> [ % safe servers {file,safe_file}, % extended gen_server modules with support for check function {gen,safe_gen},{gen_server,safe_gen_server},{proc_lib,safe_proc_lib}, ]. %% init_servers calls any server init routines servicing new node, %% these servers are run in current node context %% returns these in a list of registered names init_servers() -> % start the safe daemons with check function option {ok, Filer} = safe_file:start([{check, fun check/3}]), [{safe_file_server,Filer}]. %% check(Mod,Type,Msg) - server message safety check function %% called by safe_gen_server to check server msgs %% when configured in option list (by init_servers()) %% returns ok | {EXIT,{policy_violation,{Mod,Type,Msg}} % handle checks for safe_file server check(safe_file,call,get_cwd) -> ok; check(safe_file,call,stop) -> ok; check(safe_file,call,{delete,Nam}) -> valid_name(Nam), ok; check(safe_file,call,{file_info,Nam}) -> valid_name(Nam), ok; check(safe_file,call,{read_file,Nam}) -> valid_name(Nam), ok; check(safe_file,call,{rename,Fr,To}) -> valid_name(Fr), valid_name(To), ok; check(safe_file,call,{write_file,Nam,_}) -> valid_name(Nam), ok; check(safe_file,info,_) -> ok; % everything else is rejected check(Mod,Type,Msg) -> exit(policy_violation). ... code for valid_name/1 omitted, which checks that ... plain filenames only (no directories) are specified
%% policynode/3 - creates subnode which implements policy module in parent policynode(Parent,Name,Policy) -> % establish various parameters from policy module PRi = apply(Policy,proc_rights,[]), % limit process rights Mods = apply(Policy,aliases,[]), % initial module alias table Names = apply(Policy,init_servers,[]), % start servers, get names % create new node with specified policy CN = newnode(Parent,Name,[{proc_rights,PRi},{names,Names},{modules,Mods}]).The safety:policynode(Parent,NodeName,PolicyModule) function may be called as follows to create a custom subnode:
PolNode = safety:policynode(node(),testnode,safety_pol).
Once it has been created, processes in it may be spawned as usual, eg. a simple test function in the test module could be run as:
spawn(PolNode,test,test,[]).
So with this approach, a SSErl node with a custom policy may be created by writing an appropriate policy module, and then simply using policynode to instantiate it.
RpcN = safety:policynode(node(),rpc,safe_rpc_policy), RpcNode = safety:restrictx(RpcN,[module,newnode]), Rpc = spawn(RpcNode, safe_rpc, loop, []), register(safe_rpc, Rpc), {Rpc,RpcNode};In turn, the safe_rpc server itself may be subject to a policy check function, limiting its use.
The prime limitation is that not all ßervices" that one might wish to impose a policy upon, use the gen_server general server mechanism. Whilst it works for file and for rpc, other services such as: pxw,sockets,unix do not. Work is progressing on the best approach to handling these.
Also, the Safe Erlang system itself is only a prototype, proof of concept. It has shown that the proposed language extension does indeed seem capable of supporting a safe environment, but as it is implemented using a modified compiler as an additional layer above standard Erlang code, it cannot actually provide a completely safe system at present. This will require a new run-time system with internal support for the safety extensions.
The general approach outlined does however, seem to function efficiently.