Object-capabilities design might be a net performance win, despite its overhead, if it can obviate some/all layers of “workload-oblivious” sandboxing (e.g. VM hypercalls, container network sandboxing, ring0–3 context switching, separate process memory maps with associated TLB cache flushes, etc.) Capability checks can be optimized away when you (the program loader, e.g. the kernel) know they’re not necessary; while you can never transparently optimize a syscall or hypercall into a regular function call.
Ideally, under a capability-based OS, you can just have a unikernel that directly loads user-supplied(!) modules in a bytecode format where instructions have capability-checking as an intrinsic; and then, after performing static analysis on those modules to guarantee that they don’t do anything crazy, the kernel module loader can JIT them into native code that doesn’t need to do the capability checks. Basically like if the entire userland consisted of ePBF programs—but with higher-level exposed semantics that allow for rich access to kernel structures, rather than just their handles.
Plus, you also get the benefit of safe sharing of higher-level data abstractions. A “native” userland process has to make syscalls using, at most, registers containing pointers to strings or fixed-size non-polymorphic structs. A capability-based userland module, meanwhile, could interact with the kernel even through reads and writes to a shared-memory tree or hashmap.
Essentially, with capabilities, you get all the benefits of every program having the same low-overhead access to the kernel that a kernel driver does, with none of the drawbacks of having to guard against corrupting the kernel’s state.
Ideally, under a capability-based OS, you can just have a unikernel that directly loads user-supplied(!) modules in a bytecode format where instructions have capability-checking as an intrinsic; and then, after performing static analysis on those modules to guarantee that they don’t do anything crazy, the kernel module loader can JIT them into native code that doesn’t need to do the capability checks. Basically like if the entire userland consisted of ePBF programs—but with higher-level exposed semantics that allow for rich access to kernel structures, rather than just their handles.
Plus, you also get the benefit of safe sharing of higher-level data abstractions. A “native” userland process has to make syscalls using, at most, registers containing pointers to strings or fixed-size non-polymorphic structs. A capability-based userland module, meanwhile, could interact with the kernel even through reads and writes to a shared-memory tree or hashmap.
Essentially, with capabilities, you get all the benefits of every program having the same low-overhead access to the kernel that a kernel driver does, with none of the drawbacks of having to guard against corrupting the kernel’s state.