25. POSIX implementation of protection module

25.1. Introduction

.readership: Any MPS developer

.intro: This is the design of the POSIX implementation of the protection module. It makes use of various services provided by POSIX. It is intended to work with POSIX Threads.

25.2. Requirements

.req.general: Required to implement the general protection interface defined in design.mps.prot.if.

25.3. Data structures

.data.signext: If the SIGSEGV signal is not handled by any MPS arena, sigHandle() needs to forward the signal to the next signal handler in the chain (the signal handler that was installed when the ProtSetup() was called), by temporarily reinstalling the old signal handler and calling kill(). The only way to pass the next signal handler to the current signal handler is via a global variable, in this case the variable sigNext.

25.4. Functions

.fun.setup: ProtSetup() installs a signal handler for the signal SIGSEGV to catch and handle protection faults (this handler is the function sigHandle()). The previous handler is recorded (in the variable sigNext, see .data.signext) so that it can be reached from sigHandle() if it fails to handle the fault.

.fun.set: ProtSet() uses mprotect() to adjust the protection for pages.

.fun.set.convert: The requested protection (which is expressed in the mode parameter, see design.mps.prot.if.set) is translated into an operating system protection. If read accesses are to be forbidden then all accesses are forbidden, this is done by setting the protection of the page to PROT_NONE. If write accesses are to be forbidden (and not read accesses) then write accesses are forbidden and read accesses are allowed, this is done by setting the protection of the page to PROT_READ|PROT_EXEC. Otherwise (all access are okay), the protection is set to PROT_READ|PROT_WRITE|PROT_EXEC.

.fun.set.assume.mprotect: We assume that the call to mprotect() always succeeds.

.fun.set.assume.mprotect: This is because we should always call the function with valid arguments (aligned, references to mapped pages, and with an access that is compatible with the access of the underlying object).

.fun.sync: ProtSync() does nothing in this implementation as ProtSet() sets the protection without any delay.

25.5. Threads

.threads: The design must operate in a multi-threaded environment (with POSIX Threads) and cooperate with the POSIX support for locks (see design.mps.lock) and the thread suspension mechanism (see design.mps.pthreadext ).

.threads.suspend: The SIGSEGV signal handler does not mask out any signals, so a thread may be suspended while the handler is active, as required by the design (see design.mps.pthreadext.req.suspend.protection). The signal handlers simply nest at top of stack.

.threads.async: POSIX imposes some restrictions on signal handler functions (see design.mps.pthreadext.anal.signal.safety). Basically the rules say the behaviour of almost all POSIX functions inside a signal handler is undefined, except for a handful of functions which are known to be “async-signal safe”. However, if it’s known that the signal didn’t happen inside a POSIX function, then it is safe to call arbitrary POSIX functions inside a handler.

.threads.async.protection: If the signal handler is invoked because of an MPS access, then we know the access must have been caused by client code, because the client is not allowed to permit access to protectable memory to arbitrary foreign code. In these circumstances, it’s OK to call arbitrary POSIX functions inside the handler.

Note

Need a reference for “the client is not allowed to permit access to protectable memory to arbitrary foreign code”.

.threads.async.other: If the signal handler is invoked for some other reason (that is, one we are not prepared to handle) then there is less we can say about what might have caused the SIGSEGV. In general it is not safe to call arbitrary POSIX functions inside the handler in this case.

.threads.async.choice: The signal handler calls ArenaAccess() to determine whether the segmentation fault was the result of an MPS access. ArenaAccess() will claim various MPS locks (that is, the arena ring lock and some arena locks). The code calls no other POSIX functions in the case where the segmentation fault is not an MPS access. The locks are implemented as mutexes and are claimed by calling pthread_mutex_lock(), which is not defined to be async-signal safe.

.threads.async.choice.ok: However, despite the fact that POSIX Threads documentation doesn’t define the behaviour of pthread_mutex_lock() in these circumstances, we expect the POSIX Threads implementation will be well-behaved unless the segmentation fault occurs while while in the process of locking or unlocking one of the MPS locks. But we can assume that a segmentation fault will not happen then (because we use the locks correctly, and generally must assume that they work). Hence we conclude that it is OK to call ArenaAccess() directly from the signal handler.

.threads.async.improve: In future it would be preferable to not have to assume reentrant mutex locking and unlocking functions. An alternative approach would be necessary anyway when supporting another platform which doesn’t offer reentrant locks (if such a platform does exist).

.threads.async.improve.how: We could avoid the assumption if we had a means of testing whether an address lies within an arena chunk without the need to claim any locks. Such a test might actually be possible. For example, arenas could update a global datastructure describing the ranges of all chunks, using atomic updates rather than locks; the handler code would be allowed to read this without locking. However, this is somewhat tricky; a particular consideration is that it’s not clear when it’s safe to deallocate stale portions of the datastructure.

.threads.sig-stack: We do not handle signals on a separate signal stack. Separate signal stacks apparently don’t work properly with POSIX Threads.