Example: an in-memory file system

To end the tutorial, we'll present a larger example that demonstrates how to put the J-Kernel's features together.

Wallach et al. described how a technique called type hiding could be used as a security mechanism.  The basic idea is to prevent tasks from having direct access to dangerous classes.  Safer classes with identical interfaces are substituted in place of the dangerous classes.  These safer classes can perform whatever security checks are necessary.

The J-Kernel uses type hiding and type substitution extensively.  You've already seen that each task gets its own java.lang.System class, while the real java.lang.System class is hidden away.  This section of the tutorial demonstrates how to write your own such replacement classes.  In this case, we'll develop alternate versions of the file system classes that act on a simple in-memory "file system".  To keep the examples small, we'll implement only two file system classes:  java.io.FileInputStream and java.io.FileOutputStream.

The file system itself will be extremely simple.  A file system object will just be a hash table that maps file names to byte arrays that hold the files' data.   A FileOutputStream will write data into a byte array.  When the FileOutputStream is closed, the byte array is immediately copied into the in-memory file system.  When a FileInputStream is opened, it copies the contents of the in-memory file into an intermediate buffer, and then reads from this buffer.

We'll make the file system a capability, so that it can be used by many tasks.   Here is a remote interface for the file system, along with an implementation of the remote interface:

// memfile/MemFileSystem.java
package memfile;

import cornell.slk.jkernel.core.Remote;
import cornell.slk.jkernel.core.RemoteException;
import java.io.FileNotFoundException;

public interface MemFileSystem extends Remote
{
    void writeFile(String name, byte[] contents) throws RemoteException;
    byte[] readFile(String name) throws RemoteException, FileNotFoundException;
}

// memfile/MemFileSystemImpl.java
package memfile;

import cornell.slk.jkernel.core.Remote;
import cornell.slk.jkernel.core.RemoteException;
import java.io.FileNotFoundException;
import java.util.Hashtable;

public class MemFileSystemImpl implements MemFileSystem
{
    Hashtable files = new Hashtable(); // maps Strings to byte arrays

    public void writeFile(String name, byte[] contents)
    {
        files.put(name, contents);
    }

    public byte[] readFile(String name)
        throws FileNotFoundException
    {
        byte[] contents = (byte[]) files.get(name);
        if(contents == null) throw new FileNotFoundException(name);
        else return contents;
    }
}

Sun's FileInputStream and FileOutputStream API's were designed with a single file system in mind.  But we could have many objects that implement the MemFileSystem interface above.  When a user asks to open a new FileInputStream or FileOutputStream, which MemFileSystem object should be used?  To answer that, we'll create a static field in each task that holds that task's file system object.  This static field will be put in a class MemFileSystemAdaptor, and each task gets a separate copy of MemFileSystemAdaptor:

// memfile/MemFileSystemAdaptor.java
package memfile;

public class MemFileSystemAdaptor
{
    public static MemFileSystem fileSystem;
}

Now we can implement classes conforming to the FileInputStream and FileOutputStream API's:

// memfile/MemFileInputStream.java
package memfile;

import cornell.slk.jkernel.core.RemoteException;
import java.io.InputStream;
import java.io.FileNotFoundException;
import java.io.ByteArrayInputStream;

public class MemFileInputStream extends InputStream
{
    private ByteArrayInputStream bStream;
    
    public MemFileInputStream(String name)
        throws FileNotFoundException
    {
        byte[] b;

        try
        {
            b = MemFileSystemAdaptor.fileSystem.readFile(name);
        }
        catch(RemoteException e) {throw new FileNotFoundException(e.toString());}

        bStream = new ByteArrayInputStream(b);
    }

    public int available() {return bStream.available();}
    public void close() {}
    public int read() {return bStream.read();}
    public int read(byte[] b) {return bStream.read(b, 0, b.length);}
    public int read(byte[] b, int off, int len) {return bStream.read(b, off, len);}
    public long skip(long n) {return bStream.skip(n);}
}

// memfile/MemFileOutputStream.java
package memfile;

import cornell.slk.jkernel.core.RemoteException;
import java.io.OutputStream;
import java.io.IOException;
import java.io.ByteArrayOutputStream;

public class MemFileOutputStream extends OutputStream
{
    private ByteArrayOutputStream bStream = new ByteArrayOutputStream();
    private String name;

    public MemFileOutputStream(String name)
    {
        this.name = name;
    }

    public void close()
        throws IOException
    {
        try
        {
            MemFileSystemAdaptor.fileSystem.writeFile(name, bStream.toByteArray());
        }
        catch(RemoteException e) {throw new IOException(e.toString());}
    }

    public void write(byte[] b) {bStream.write(b, 0, b.length);}
    public void write(byte[] b, int start, int len) {bStream.write(b, start, len);}
    public void write(int i) {bStream.write(i);}
}

The classes are named memfile.MemFileInputStream and memfile.MemFileOutputStream rather than java.io.FileInputStream and java.io.FileOutputStream.   This is acceptable, because we can create a resolver which returns the bytecode for the memfile classes when asked for the java.io classes:

   TableResolver renameResolver = new TableResolver()
        .setMapping("java.io.FileOutputStream", (byte[])
            Start.data.mainResolver.resolveClassName("memfile.MemFileOutputStream"))
        .setMapping("java.io.FileInputStream", (byte[])
            Start.data.mainResolver.resolveClassName("memfile.MemFileInputStream"))
        ;

The J-Kernel will change references to the java.io classes to point to the memfile classes instead, so that the virtual machine's verifier isn't aware that a substitution took place.  Nevertheless, in the current version of the J-Kernel we recommend giving replacement classes the same names as the classes that they replace, rather than relying on the renaming tricks just shown.  The reason is that reflective Java routines like Class.getName and Class.forName are not aware of that class references have been renamed, and can give unintuitive results (or fail to work entirely), because the internal name of a class doesn't match what the user expected.

When you do make classes whose names are the same as common system classes, you have to be careful to keep the bytecode for these out of the system class path.  Otherwise, the virtual machine might try to use your classes as if they were the system classes.  For example, the J-Kernel's default replacements for classes in the java.lang package are located in the directory cornell/slk/jkernel/stdlib/java/lang, which is not part of the system class path.

Now let's write an application that uses the file classes.  I'll define an interface called App which the application should implement, so that there is some way of launching the application:

// App.java
public interface App
{
    void run(String[] args) throws Exception;
}

Here's a test application that implements this:

// TestApp.java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.PrintWriter;

public class TestApp implements App
{
    public void run(String[] args)
        throws Exception
    {
        if(args[0].equals("write"))
        {
            System.out.println("writing a message");

            FileOutputStream fileOut = new FileOutputStream("temp.txt");

            PrintWriter writer = new PrintWriter(fileOut);
            writer.println("Buzz buzz.");
            writer.close();

            fileOut.close();
        }
        else if(args[0].equals("read"))
        {
            FileInputStream fileIn = new FileInputStream("temp.txt");
            System.out.println("reading a message:");

            byte[] b = new byte[fileIn.available()];
            fileIn.read(b);
            System.out.println(new String(b));

            fileIn.close();
        }
    }
}

To start an application, we create a task for the application, set up some static fields in the new task, and then instantiate and run the appropriate App object in the new task.  The classes Launch and LaunchImpl will be used to make a seed object for the new task, and will take care of setting up the task:

// Launch.java
import cornell.slk.jkernel.core.Remote;
import cornell.slk.jkernel.core.RemoteException;

public interface Launch extends Remote
{
    void launch(LaunchData data, String appName, String[] args) throws Exception;
}

// LaunchData.java
import cornell.slk.jkernel.core.FastCopyTree;
import cornell.slk.jkernel.core.RemoteOutputStream;
import cornell.slk.jkernel.core.RemoteInputStream;
import memfile.MemFileSystem;

public class LaunchData implements FastCopyTree
{
    public RemoteOutputStream remoteOut;
    public RemoteOutputStream remoteErr;
    public RemoteInputStream remoteIn;

    public MemFileSystem memFileSystem;
}

// LaunchImpl.java
import memfile.MemFileSystemAdaptor;
import java.io.PrintStream;
import cornell.slk.jkernel.util.RemoteOutputStreamAdaptor;
import cornell.slk.jkernel.util.RemoteInputStreamAdaptor;

public class LaunchImpl implements Launch
{
    public void launch(LaunchData data, String appName, String[] args)
        throws Exception
    {
        System.setOut(
            new PrintStream(new RemoteOutputStreamAdaptor(data.remoteOut)));
        System.setErr(
            new PrintStream(new RemoteOutputStreamAdaptor(data.remoteErr)));
        System.setIn(
            new RemoteInputStreamAdaptor(data.remoteIn));

        MemFileSystemAdaptor.fileSystem = data.memFileSystem;

        Class appClass = Class.forName(appName);
        App app = (App) appClass.newInstance();

        app.run(args);
    }
}

Finally, we'll write a parent task that creates a MemFileSystem, and launches two child tasks that run TestApp, which uses the in-memory file system created by the parent task.  The parent task creates a separate file system capability for each child task.  Although this is not necessary (the parent could give the same capability to both child tasks), it allows the two capabilities to be revoked separately.

// Hello.java
import cornell.slk.jkernel.core.Task;
import cornell.slk.jkernel.core.SharedClass;
import cornell.slk.jkernel.core.Resolver;
import cornell.slk.jkernel.core.Capability;
import cornell.slk.jkernel.std.Start;
import cornell.slk.jkernel.util.TableResolver;
import cornell.slk.jkernel.util.CompoundResolver;
import memfile.MemFileSystem;
import memfile.MemFileSystemImpl;

public class Hello
{
    public static void main(String[] args)
        throws Exception
    {
        System.out.println("Hello.");

        // Create a resolver for the shared classes:
        TableResolver sharedResolver = new TableResolver()
            .setMapping("Launch", Task.shareClass("Launch"))
            .setMapping("LaunchData", Task.shareClass("LaunchData"))
            .setMapping("memfile.MemFileSystem", Task.shareClass("memfile.MemFileSystem"))
            ;

        // Set up a resolver that maps
        //   FileOutputStream -> MemFileOutputStream
        //   FileInputStream -> MemFileInputStream
        TableResolver renameResolver = new TableResolver()
            .setMapping("java.io.FileOutputStream", (byte[])
                Start.data.mainResolver.resolveClassName("memfile.MemFileOutputStream"))
            .setMapping("java.io.FileInputStream", (byte[])
                Start.data.mainResolver.resolveClassName("memfile.MemFileInputStream"))
            ;

        // Create a resolver for the new tasks:
        Resolver childResolver = new CompoundResolver(
            new Resolver[] {
                Start.data.standardResolver,
                sharedResolver,
                renameResolver,
                Start.data.mainResolver
            });

        // Create two child tasks:
        Task child1 = new Task(childResolver);
        Task child2 = new Task(childResolver);

        // Seed the tasks with LaunchImpl objects:
        Launch launch1 = (Launch) child1.seed("LaunchImpl");
        Launch launch2 = (Launch) child2.seed("LaunchImpl");

        // Create an in-memory file system, along with
        // a file system capability for each task:
        MemFileSystem fileSystem = new MemFileSystemImpl();
        Capability fileSystem1 = Capability.create(fileSystem);
        Capability fileSystem2 = Capability.create(fileSystem);

        // Set up the LaunchData:
        LaunchData data = new LaunchData();
        data.remoteOut = Start.data.remoteOut;
        data.remoteErr = Start.data.remoteErr;
        data.remoteIn = Start.data.remoteIn;

        // Launch the first task:
        data.memFileSystem = (MemFileSystem) fileSystem1;
        launch1.launch(data, "TestApp", new String[] {"write"});

        // Launch the second task:
        data.memFileSystem = (MemFileSystem) fileSystem2;
        launch2.launch(data, "TestApp", new String[] {"read"});
    }
}

To summarize, these are the files necessary for this example:

memfile/MemFileSystem.java

memfile/MemFileSystemImpl.java

memfile/MemFileSystemAdaptor.java

memfile/MemFileInputStream.java

memfile/MemFileOutputStream.java

App.java

TestApp.java

Launch.java

LaunchData.java

LaunchImpl.java

Hello.java

When Hello is run, it should produce the following output:

Hello.
writing a message
reading a message:
Buzz buzz.