-----Original Message----- From: Jonathan S. Shapiro [mailto:shap@eros-os.org] Sent: Monday, January 05, 2004 4:56 PM To: Volkmar Uhlig Cc: rudykoot@mithrill.org; L4 Hackers List Subject: L4, High Assurance, and Protection
Issue 1: Need for Protected IPC
On Thu, 2004-01-01 at 18:09, Volkmar Uhlig wrote:
If it does not matter where the check code is executed (client/kernel/server) but only who gets accounted for the used resources (CPU, cache, whatever) this is not the case. (I'm not claiming L4 can do that (yet).)
In answer to Volkmar's question: it matters very fundamentally where the check is performed. It is the difference between mandatory and discretionary controls.
The current L4 IPC is discretionary: the sender can invoke anyone, and the recipient checks the sender identity and decides whether to accept the IPC.
In such systems, it is a fundamental requirement that the reference monitor be able to PREVENT (absolutely) communication between processes that are under its control. If a sender S is not permitted to send to a recipient R, the behavior must be exactly as if the send was performed to a non-existent ID.
Ok, I see that point.
The main change that is needed in L4 to resolve this is to define the recipient-id and sender-id fields as opaque fields. Under high-assurance requirements, the sender is not entitled to know how many threads execute within the recipient. The sender-id and recipient-id therefore must not encode "thread within process". Similarly, it must not encode "process ID".
I think there is some misconception around about L4's thread ids. In L4 V4 (X.2 is going to become V4 after maturing) threads and address spaces are completely independent of each other. Threads execute in an address spaces, but they can migrate. A thread ID is a communication-endpoint place holder. If you have, say, 100 threads in an address space and you want to hide that fact you can use a single thread serving as a distributer. Since we have local IPC the distribution can completely take place in user land without a kernel invocation. The worker can impersonate the distributer for the reply.
While I do not advocate any particular implementation strategy, let me give an *example* of one that might suffice: a simple hash table. Instead of using the PCB address as the process ID, the kernel could use H(sender kernel ID, requested-recipient-ID) as an index into a hash table and perform a single indirection (and possible hash bucket chasing) to find the process address. This is fundamentally the design proposed by Trent several years ago.
You immediately run into replacement policy issues.
2: Restricted Mapping
This is a very small issue, and there may be some way around it.
In general, I like the map/grant model very much, but in some systems it is necessary for the manager to know who has what mappings.
In these systems, having applications perform mappings directly to
each
other creates a consistency problem.
Here the argument is around IPC redirection (which is arguably expensive). By sticking an interceptor in between you know which mappings go in and out. That way you can control it. But I suspect that fine-grain control over mapping privileges makes sense. The question is what this granularity should be--page or mapping in general.
My preference would be to have *both* controls, because they serve slightly different purposes. The first lets me virtualize the map/grant operations (in order to keep manager metadata updated), while the second prevents map recursion.
The problem with the first method is that it requires protected recipient descriptors.
You can transparently drop the mapping if it is not allowed. Since the guarantees whether a page is mapped or not come from the pager hierarchy it is only as strong as all combined pagers. By adding a non-guaranteeing pager in the hierarchiy you are set.
The problem with implementing only the second method is: what thread id should a page fault handler receive from a faulting thread? Restricted or non-restricted? I can argue for either depending on what
kind of system I am trying to build.
That raises the question whether the pager is privileged or not. Since the pager can be set by the thread the pager can be changed and the thread could elevate its privileges. Or did I misunderstand you?
- Volkmar
On Mon, 2004-01-05 at 12:56, Volkmar Uhlig wrote:
I think there is some misconception around about L4's thread ids. In L4 V4 (X.2 is going to become V4 after maturing) threads and address spaces are completely independent of each other....
Volkmar:
I believe that I have now asked twice for a URL to this specification. Can you send me a URL? Otherwise we will continue to waste each other's time. Please accept my apologies that, in the absence of this document, I may have done so.
If it was on one of the L4 web sites, I failed to see it.
Threads execute in an address spaces, but they can migrate. A thread ID is a communication-endpoint place holder. If you have, say, 100 threads in an address space and you want to hide that fact you can use a single thread serving as a distributer. Since we have local IPC the distribution can completely take place in user land without a kernel invocation. The worker can impersonate the distributer for the reply.
This sound like a very good thing, and it almost certainly addresses the issue of "revealed thread ids".
While I do not advocate any particular implementation strategy, let me give an *example* of one that might suffice: a simple hash table. Instead of using the PCB address as the process ID, the kernel could use H(sender kernel ID, requested-recipient-ID) as an index into a hash table and perform a single indirection (and possible hash bucket chasing) to find the process address. This is fundamentally the design proposed by Trent several years ago.
You immediately run into replacement policy issues.
If there is a faster way, great! But protection is not merely a desire. It is a hard requirement. *Every* mechanism I know about for achieving a protected interface carries a fundamental cost somewhere. You can do it with address space traversal, or you can accept the need for a replacement policy, or you can do something else, but you are going to pay *somewhere*.
My problem is that performance cannot be used to justify fundamental insecurity. Speed at the cost of correctness is simply unacceptable.
In general, I like the map/grant model very much, but in some systems it is necessary for the manager to know who has what mappings.
In these systems, having applications perform mappings directly to
each
other creates a consistency problem.
Here the argument is around IPC redirection (which is arguably expensive). By sticking an interceptor in between you know which mappings go in and out. That way you can control it. But I suspect that fine-grain control over mapping privileges makes sense. The question is what this granularity should be--page or mapping in general.
From a security perspective, the problem is with the presence of mapping
operations at all. Mapping establishes a high-speed, mutable channel, so it tends to undermine reference monitors.
My preferred model is that performing a send with a mapping on a disabled descriptor would be treated as an access violation, and a suitable exception would be delivered to fault handler. I don't want to pay any indirection cost for the legal (non-mapping) IPCs.
The problem with the first method is that it requires protected recipient descriptors.
You can transparently drop the mapping if it is not allowed. Since the guarantees whether a page is mapped or not come from the pager hierarchy it is only as strong as all combined pagers. By adding a non-guaranteeing pager in the hierarchiy you are set.
From a security perspective, failing silently is *always* a mistake. It
also makes virtualization difficult if somebody wishes to be backward compatible with the interface later. You are describing a situation in which a sender has done a known-illegal operation, but no fault is generated? This doesn't seem like a good design.
The problem with implementing only the second method is: what thread id should a page fault handler receive from a faulting thread? Restricted or non-restricted? I can argue for either depending on what
kind of system I am trying to build.
That raises the question whether the pager is privileged or not. Since the pager can be set by the thread the pager can be changed and the thread could elevate its privileges. Or did I misunderstand you?
It is a big problem for the pager to be settable by the thread. There are some systems in which this can be permitted, and other systems in which it **must not** be permitted. Because there exists systems in which this must not be permitted, the operation must (at a minimum) involve invocation of a trusted party.
Perhaps there is some third design possibility that I have not considered carefully enough, but it appears to me that a high-assurance system constructed on top of L4 must be built exclusively using trusted pagers. While it is possible, in abstract, to use untrusted pagers in a descriptor-based system (as EROS does), the L4 map/grant operations don't appear to provide enough book-keeping controls to be trusted.
shap
l4-hackers@os.inf.tu-dresden.de