29. Operating Systems, Networks and Cookies

Part of CS:2820 Object Oriented Software Development Notes, Fall 2020
by Douglas W. Jones
THE UNIVERSITY OF IOWA Department of Computer Science

 

Objects in Operating Systems

Consider what happens when you open a file in Java. You write something like this:

File f = new File( "filename" );

From a java perspective, the result is that f is the handle on or pointer to an object of class File, where methods operating on that class do things like reading or writing the file.

It is not that simple! Files are operating system objects. That means that you can open the same file (in this case, a file named filename) whether you are writing your program in machine language, FORTRAN, C, COBOL, C++, Java, Python or Perl. Some of these languages aren't at all object oriented, and some of them are not type safe. C++, for example, is object oriented but it is not type-safe. A C++ programmer can create an object and then trick the programming environment into treating it as an object of a completely different class.

Non-type-safe languages are required for some applications. For example, to implement the storage manager and garbage collector that underly Java, you need a language that allows you to do arbitrary arithmetic on pointers. You can do this in C or C++, but not Java or Python.

Operating systems on modern computers that have memory-management units (also called MMUs) offer each user process a distinct separate memory address space. In such a system, the memory used by the operating system to store system objects such as files and timers is totally inaccessible from user programs. When you open a file, the operating system allocates a file object in the system memory, and then it hands the user program a handle for this object.

For most objects, the object handle is just the memory address of the object in the program's memory address space. When a user program accesses a system object, this model does not work because the user program cannot access the system memory space.

If you are writing a Java program, the Java run-time system allocates a Java object of class File in your user memory space, but this object is not the system file object. Instead, it is an interface or wrapper around the system object. One field of this object is the handle for the system object, and the methods of Java's class File serve primarily to translate Java's file access methods into those of the underlying operating system.

So what is an operating-system-level file handle? Here is a bad idea:

BAD IDEA: A system file handle is the memory address of the file object inside the operating system's private memory.

The problem with this idea is that the file handle is in the user's address space where the user can fiddle with it. If you are writing your code in assembly language, FORTRAN, C or C++, none of which are type safe, your program could freely use ask the system to perform a file operation using any arbitrary data as a file handle. This would causing the operating system to access random areas of its own memory and try to use those areas as system file objects. Protecting against abuse of this is very difficult. What if, by chance, a malicious user manages to invent the memory address of a real file object in the operating system's address space where that file does not belong to that user?

How Unix/Linux does it: A system file handle is an integer index into a small table in the operating system's memory address space. This table contains the memory addresses of the files that the current user is entitled to access.

This scheme allows the user to mess up, but the worst that the user can do is access a file that that user already is permitted to use.

This is a very old scheme, dating back to the early days of FORTRAN. Fortran input/output was done relative to unit numbers. Originally, the unit numbers were the numbers of physical tape drives, and the operation of "opening a file" was done by outputting a message telling the computer operator to mount the requested tape on the indicated drive. Later, in the 1960s, as operating systems matured, unit numbers became indexes into an operating-system table of the files that the current application had opened.

Cookies

While most of us think of cookies as a phenomonon of the World-Wide Web, where web sites leave cookies on your machine, forcing you to carry information about yourself on behalf of the web site (such as tracking information or personal browsing histories), the term appears to have originated in the documentaiton for the standard library of the C programming language. Specifically, the library includes services called called ftell() and fseek() that can be used inquire about the current position of an input/output stream and to set the position in that stream.

In the original Unix implementation of the C stream file model, positions in the stream were simply the integer number of bytes from the start of the stream, but when C was ported other operating systems, the developers quickly learned that some file systems made it difficult to count bytes from the start of a file. Here is how they documented file positions in the Unix Programmer's Manual, Vol I, part 3, published by Bell Laboratories in 1979, revised 1983:

Ftell returns the current value of the offset relative to the beginning of the file associated with the named stream. It is measured in bytes on UNIX; on some other systems, it is a magic cookie, and the only foolproof way to obtain an offset for fseek.

The general definition of a magic cookie you can infer from this brief mention is that it is a value returned to the user by some system where the system understands the construction and use of that value but the user is not expected to be able to interpret or manipulate it. The only useful thing the user can do with a cookie is give it back to the system so that the system can interpret it.

So, how do we use cookies to represent objects in a world where we still have something akin to classes, but all fields and methods are static? We use cookies as object handles, and we have each class maintain a collection of all objects of that class. Here is an example using integer handles:

static class C {
    static private int alloc = -1;   // used to allocate instances
    static private Field f[maxSize]; // all of the instances
    static public int C() { // the initializer
        alloc = alloc + 1;
        f[alloc] = ??
        return alloc;   // return a handle for the new instance
    }
    public Field M( int handle ) { // an example method
        return f[handle];
    }
}

The above implementation assumes that the constant maxSize gives the maximum number of members of class C that will ever be needed. More sophisticated implementations will allow users to deallocate objects as well as allocate them, and will include (in the allocator) a search for a free object so that storage can be reused.

Integer handles such as the above are dangerous. A user could incrment their handle and the result would be the handle for a different object, perhaps one that that user should not be able to use. What we want is a way to give the user a handle that, despite the fact that the user can mess with it, messing will very likely be detected. Here's an example that works:

static class C {
    static private int alloc = -1;   // used to allocate instances
    static private Field f[maxSize]; // all of the instances
    static private int keys[maxSize];// the keys to the instances
    static public int C() { // the initializer
        alloc = alloc + 1;
        f[alloc] = ??
        key[alloc] = randomInt() % keyRange;
        return alloc * keyRange + key[alloc];   // return a secure cookie
    }
    public Field M( int cookie ) { // an example method
        int i = cookie / maxSize;  // the item number
        int k = cookie % maxSize;  // the key
        if (k != key[i]) throw new IllegalHandleException();
        return f[i];
    }
}

Here the cookie (object handle) given to the user has two parts, the item number and a randomly assigned key. Messing with the cookie will be very likely to be detedted because the user would have to guess the key fields of items the user has never seen. This can be made arbitrarily secure by using a larger key range.

The above implementation assumes that the constant maxSize gives the maximum number of members of class C that this mechanism can allocate, while keyRange gives the size of the key. More sophisticated implementations will allow users to deallocate objects as well as allocate them, and will include (in the allocator) a search for a free object so that storage can be reused.

In the late 1960s and early 1970s, when object-oriented programming was just being invented, people like David Parnas developed methods of modularizing programs, creating software architectures that were almost object-oriented while writing code in languages like Fortran IV. Fortran IV does not support classes, but it is straightforward to write compilation units (separately compiled pieces of a large program) where each unit contains one callable subroutine per method of a logical class and a common block holding the representations of all instances of that class.

This method remains in use today in distributed systems where code is divided between clients and servers. In such a system, it is natural to have one server to implement each class, where the server holds all values of that class in its internal memory and offers clients the ability to perform actions on members of the class.

This solution was formalized by Andrew Tannenbaum in his development of the Amoeba operating system. He referred to the keys discussed above as salt applied to the integer handle on an object.

In Amoeba, clients (user programs) communicate with servers over a network. Users or the system can write servers, but all communication from clients to servers is by network packets. To call a method of an object, you send a message to that object's server -- a server that implements all methods on all objects of that class. The message must include a cookie that identifies the object to be manipulated, and the result of that manipulation is a reply message.

The Amoeba system was designed (with funding from the European Space Agency) to create supercomputers from clusters of inexpesive small computers. Amoeba uses this framework extensively, and since it implements the basic "salted cookie" mechanism in standard "boilerplate" code that is distributed with the system, users can think in entirely object-oriented terms when developing applications on Amoeba, without ever worrying about building their own "salted cookies".

Amoeba is more complex than indicated here because it adds, to each object handle, a set of access rights. The salting scheme is further modified to prevent users with a limited-access handle from increasing their access rights, for example, given a read-only file handle, a user cannot convert it into a read-write handle without knowing the correct value of the salt for that handle, and this is cryptographically hidden in a very clever way.