Passing Parameters to System Calls

Part of 22C:169, Computer Security Notes
by Douglas W. Jones
THE UNIVERSITY OF IOWA Department of Computer Science

General

Parameter passing is easy if the parameters are passed in registers, and this even applies when passing parameters to system calls. Consider a system where some specific trap is used for system calls.

On entry to the trap service routine, the trap service routine saves all of the user's registers, including the program counter, in a register save area. For example, the program counter is saved in a variable named savePC and general purpose registers are saved in an array named saveRegs[] where the array subscripts correspond to register numbers, so register 3 is saved in saveRegs[3].

While in the trap service routine, the register save area is available to the code of the trap service routine to examine parameters stored there and to change any return value. Of course, the details of what registers are used for parameters and return values must be agreed on, but this is the same no matter what kind of subroutine is being called.

Things are different when a user program must pass a pointer to a system call. In this case, the details of the system's memory protection structure and how the system uses the memory management unit become critical. In general, operating systems fall into two broad classes with regard to their memory models and how these models interact with passing pointer parameters to the operating system. These models will be dealt with separately below.

In short, in one model the operating system allocates an entire virtual address space to each user, entirely separate from the virtual address space used by the operating system itself. Memory segments may be shared between users or between the system and individual users, but it is up to the users to request such sharing. As a result, if a user passes a pointer to a system call, that pointer has no immediately useful value to the system, since it can only be interpreted in the user's address space.

In the second model, the operating system's address space includes the user address space, or, from the user's viewpoint, every user has the entire operating system mapped into their address space. Having the system present in the user's address space does not confer access -- the user has no rights to read, write or execute system pages, but those pages are present in the user address space. As a result, if the user passes a pointer to a system call, the pointer is entirely valid and may be used directly by the system.

These two models of memory use have vastly different security consequences.

Separate Address Spaces for User and System

If the user and the operating system have separate address spaces, then the operating system must do some hard work whenever the system needs to use a pointer passed by the operating system. We need to make some assumptions about the memory management unit in order to explore the kind of code required to do this. Here, we will assume that:

space->page_table[ page_number ]
Each address space (space) has a page table that is an array, indexed by page numbers.

user_space
system_space
The operating system is able to access its own address space and it is able to access the address spaces of each user. The user_space pointer refers to the current user's address space.

spare_page
The operating system has a spare page in its address space that it uses when it needs to reach into a user's address space to follow a pointer.

address = make_address( page_number, byte_in_page )
page_number = page_number_field( address )
byte_in_page = byte_in_page_field( address )
Virtual addresses are composed of two fields, the page number and the byte in page.
Given these, we can now write a little function that takes a pointer passed by a user and fiddles with the page tables to make a pointer that the system can use to refer to the same byte that the user's pointer referenced. What the system does is map the page of the user's address space holding the byte in question into the system's spare page, and then create a pointer to the right byte of this page.
void * make_system_pointer( void * user_pointer ) {
        /* first fix the page table */
        system_space->page_table[ spare_page ]
        = user_space->page_table[ page_number_field( user_pointer ) ];
        /* then compute and return the pointer */
        return make_address( spare_page, byte_in_page_field( user_pointer ) );
}

This little routine creates a pointer that is only guaranteed to be valid for a single byte of memory (what if the user's pointer pointed to the very last byte of a page?). On the other hand, the pointer may be valid for quite a few bytes. Many system calls involve passing pointers to buffers, arrays of bytes used to read or write blocks of data. If the operating system had to call make_system_pointer over and over, once for each buffer, input-output would be very inefficient. Therefore, we add a helper routine that tells us how many bytes, from theh pointer onward, are valid:

int safe_bytes( void * user_pointer ) {
        void * first_byte_of_next_page
        = make_address( page_number_field( user_pointer ) + 1, 0 );
	return first_byte_of_next_page - user_pointer;
}

If the user attempts to pass something as trivial as a pointer to an integer, the system must be prepared to follow that pointer to the successive bytes of the integer one byte at a time, calling safe_bytes to find out how many bytes it can copy before it needs to call make_system_pointer again.

Operating System and User Share an Address Space

In this case, pointers passed to the system are trivially valid. The system may directly follow any user pointer to the item referenced by that pointer, so long as the pointer belongs to the current user. Obviously, if the system needs to change to a different user, it must manipulate the memory management unit to make that new user's address space addressable, part of making the new user the current user.

Adding Complexity

Many hardware architects over the years have noticed the complexity of the trap mechanism for gate crossing for system services, and attempted to speed this up by building special hardware to detect system calls more quickly and transfer control more promptly to the operating system. Similarly, many architects have added mechanisms that potentially speed up the transmission of parameters to system programs or that potentially speed up the process of reaching into the user's address space to follow a user-supplied pointer.

Unfortunately, many of these mechanisms are less than total successes. Special instructions, for example, to manipulate the previous address space have been proposed, for example. The instructions "move to previous address space" and "move from previous address space" are move instructions that take a source pointer in one address space and a destination pointer in another. These can speed getting data right after a system call, when the user's address space is the previous address space. The problem is, later, the previous address space might be irrelevant. The problem is, right before a return from a system call, it isn't the previous address space we want, it's the next one.