Comment by jkhdigital
2 days ago
I’m not going to argue against much of the content of this paper, but it should be pointed out that their argument in the middle section against the “confinement myth” seems pretty bogus. They say that you can isolate the capability read/write resource from the data read/write resource, but… this makes absolutely no sense. Bits are bits. If you assume some out-of-band isolation of capability distribution then you’ve changed the game, but even that isn’t enough for me to believe that isolation is possible.
Early thinking was in terms of capability handles. As with file descriptors, the handle is only meaningful when passed across a protection boundary to something which can check if the handle is valid.
Later, there were encrypted capabilities, which are signed data, like TLS certs. These get kind of bulky. And hardware support, in a few machines.
alternate futures where the 33bit versions of the i960 became the processor family of choice.
Consider two processes on a *nix system, and for the sake of argument let's say they're sufficiently isolated from each other as to have only one communications channel between them. If that communications channel is a unix domain socket, one process can send a file descriptor (effectively a capability) to the other over the socket. Each process has a file descriptor table in the kernel whose integer keys are only meaningful to that process in particular, and the kernel provides a mechanism to transmit file descriptors across a socket. The kernel mediates in this case.
If the communications channel is not a unix domain socket and is instead something like a TCP connection, you don't have this option available to you.
You aren't always just sending bits from one process to another!
No, you’re using the same sleight of hand as the paper.
Boebert’s objection is about whether Alice can transmit unauthorized authority to Bob across a security boundary that’s supposed to prevent that flow. Your SCM_RIGHTS example is a case where the kernel is deliberately providing a sanctioned channel for authority transfer, with the kernel’s blessing, between two processes that the kernel does not consider to be on opposite sides of a mandatory access control boundary. Unix has no (*)-property. There is no “high” and “low” in the Bell-LaPadula sense on a standard Unix system. So of course the kernel mediates the transfer cleanly; it’s not enforcing any policy that would be violated by the transfer.
The moment you try to extend this to the actual case under dispute—Alice is “high,” Bob is “low,” and the security policy says high-to-low information flow is forbidden—then if the kernel refuses to deliver the fd across the boundary, the security property was enforced by the separate MAC layer, not the capability mechanism.
The conflation which is endemic in this whole debate is between “capabilities as a kernel-mediated authority mechanism” and “capabilities as a property that holds across all observable behavior of the system.” Unix file descriptors are the former. Boebert’s objection is about the latter.
Your communication channel between Alice and Bob is, itself, a capability (or a collection of capabilitys) that grants Bob memory write, Alice memory read, but does not grant the ability to transmit a capability from Bob to Alice.
Absent a misunderstanding on your part, the only way I can coherently interpret your argument is that you are arguing that the presence of kernel data structures mediating the handles somehow makes it not a capability system. That there is some background element mediating the validity of your capability representation and thus that is just a MAC layer; unless you can write the byte representation of your handle into memory and somebody else can read it out and then have access to that resource it is not a capability.
One, that allows forging capabilitys unless they are cryptographically secure against collisions.
Two, the actual essence of capabilitys is not being bearer tokens, it is non-construction. Capabilitys are derived from existing capabilitys, not manifested into existence. They have provenance. It is the OS equivalent of not allowing programs to cast arbitrary integers to pointers and thus manifesting pointers into existence which breaks basically every high level memory safety guarantee. You do not allow programs to cast arbitrary data into handles to resources which is what ambient authority systems effectively require.
1 reply →
That argument assumes that the delegation of a capability to another process must happen through a path of interprocess communication that can be established only by the operating system, if the processes that want to communicate have the capabilites for this.
I have not studied to see how the existing capability-based operating systems solve this problem, because it seems that this is not a simple solution. If the capabilities are very fine-grained, to make certain that IPC really cannot happen, that might be cumbersome to use, while coarse-grained capabilities could be circumvented. To really prevent IPC without appropriate capabilities, a lot of the convenient features of a UNIX-like system must be forbidden, like the existence of files that can be read by any user, or directories like /tmp , where anyone can write a file.
> If the capabilities are very fine-grained, to make certain that IPC really cannot happen, that might be cumbersome to use, while coarse-grained capabilities could be circumvented.
In SeL4 it’s kinda like this: A capability is an opaque handle you can invoke to RPC into some other process or into the kernel. There’s no worry about how fine grained capabilities are because there’s no global table of permission bits or anything like that. Processes can invent capabilities whenever they want. Because caps just let other processes call your code, you can programmatically make them do anything.
Suppose I want to give a process read only access to a file I have RW access to. The OS doesn’t need a special “read only capability” type. It doesn’t need to. Instead, my process just makes capabilites for whatever I actually want on the fly. In this case, I just make a new capability. When it’s invoked I see the associated request, if the caller is making a read request, I proxy that request to the file handle I have. (Also another cap). And if it’s a write request, I can reject it. Voila!
This is how you can write the filesystem and drivers in userland. One process can be in charge of the block devices. That process creates some caps for reading and writing raw bytes to disk. It passes the “client side” of that cap to a filesystem process, which can produce its own file handle caps for interacting with directories and files, which can be passed to userland processes in turn. Its capabilities all the way down.
This kind of proxy capabilities has other benefits as well, e.g. you can implement a disk quota, or transparent compression, or logging, or ask the user (if you have a capability which can do that), or provide access to a part of the file as though it is the entire file, etc.
Or, if a program requests access to a camera, you can provide a capability with a still picture, a video file, a filter (e.g. that resizes the picture or modifies the colour) from some source (including, but not limited to, a camera), etc; this can be helpful in case e.g. you do not have a camera on your computer, or for testing.
(Other people have similar ideas, sometimes independently than I do.)
There is also a way to transmit capabilities across a network; I had thought of how a protocol would be made to do such a thing.
That works perfectly fine for an embedded computer, which is where systems like SeL4 are used.
On the other hand, I cannot see how this approach can be scaled to something like a personal computer.
For some programs that I run, e.g. for an Internet browser, I may want to not authorize them to access anything on SSDs/HDDs, except for a handful of configuration files and for any files they may create in some cache directories.
For other programs that I run, I may want to let them access most or all files in certain file systems. Any file system that I use contains typically many millions of files.
Therefore it is obvious that using one capability per file is not acceptable. Moreover, such programs may need to access immediately many thousands of files that have been just created by some other program that has run before them, for instance a linker running after a compiler.
Assuming that a pure capability-based system is used, not some hybrid between ACLs and capabilities, there must be some more general kinds of capabilities, that would grant access simultaneously to a great number of resources, based on some rules, e.g. all files in some directory can be read, all files with a certain extension or some other kind of name pattern from some directory may be written or deleted or renamed, and so on.
3 replies →
>Its capabilities all the way down.
IIUC one problem with such layering of capability processing is that each passed layer results in a context switch (i.e. switch of memory mappings, thrashing of caches, etc.) and its on top of the cost of passing through the kernel. In other words, you may need to pay cost of N syscalls for one multi-layered capability-based operation.
1 reply →
Doesn't that mean that your process is then responsible for ensuring that an app with a read-only capability cannot do a write ?
You're moving the burden of enforcement from the kernel to the user level ?
1 reply →
Covert channels are a thing. Shared access to resources always opens the possibility of covert information passing through e.g. modulation of resource usage. This isn’t even out-of-band, it’s just a hard fact that a shared resource always creates a potential covert channel (source: Lampson 1972, A Note on the Confinement Problem).
How I had idea of a computer and operating system design, measuring time also requires a capability. Creating files (including temporary files) also requires capabilities. Shared memory is read-only by everyone; to be able to write to memory you must have exclusive access. All of these capabilities are not necessarily what the program using them intended them to be; they may also be proxies, or capabilities of the wrong type (the kernel does not know anything about the types of capabilities except those it created itself), etc. A proxy may limit communication from one program to a service. Using these as well as other things (including, but not limited to, the CPU design), there are things that can be done to mitigate these problems (including things necessary to mitigate other kind of timing attacks based on other capabilities, e.g. slowing down network access for purpose of testing its working on slow networks).