Secure Calls

“Secure calls” are the current name for Cheriette inter-compartment calls. They are not in principle a new idea, every CHERI OS has a notion of a “compartment” and a CHERI capability-secured way of context switching between them. Cross-compartment calls are a CHERI-enforced RPC mechanism..

Cheriette borrows the caller-callee stack sharing from CompartOS & CHERIoT, and the general idea from CheriOS.

In Cheriette, secure calls are especially fast, because Cheriette uses a single global address space, and all process context is held in CHERI capabilities, explicitly none in the MMU. That means that switching between compartments using Secure Calls is as fast as setting up the call, saving registers to the stack and calling cinvoke, and zeroing the stack on return.

From an engineering perspective, secure calls in Cheriette have interfaces that mimick function calls. The fact that secure calls cross trust-domain boundaries means that the caller has to save and zero all non-argument registers before crossing the trust domain boundary and the callee has to zero out the stack it used before returning.

Instead of zeroing out the “shared” stack before returning to the caller, the handler stack could also

  • be allocated on-demand by an intermediary in-between the call. This design requires a trusted intermediary, and begs the question of where the intermediary gets its stack from. Depending on the design, the intermediary becomes a point of serialization for all secure calls in the system.

  • be pre-allocated and associated with the “channel” used for the secure call. This design requires no runtime allocation or sealing, but limits the number of concurrent calls for all tasks that share a channel to the number of pre-allocated stacks (In the current Cheriette, all tasks share the same “channel”, because a channel so far only had to be sealed pointers to the handler) .

What these alternatives have in common is that the handler stack is kept secret either by being sealed and “locked” to the channel or by being managed by an intermediary. The intermediary may have to zero out stacks for re-use on occasion, but that could happen asynchronously.

For now, Cheriette re-uses a restricted subset of the caller stack as the handler stack. This design is simplest, and requires no additional allocation decisions. Philosophically, this design decision follows the principle that the caller must provide the resources for the handler to run, not the other way around. In this way, the caller and handler merge into a single, continuous stream of execution that can be arbitrarly nested or even recursive.

Theory of Operation

High-level:

  1. Task begins secure call

  2. Create continuation cap that handler can return to

  3. Create continuation data cap that handler passes back

  4. Seal continuation cap

  5. Seal complete stack cap

  6. Store sealed complete stack cap in continuation data

  7. Create restricted handler stack cap

  8. Set stack pointer to restricted stack cap

  9. cinvoke(sealed pc, sealed data)

  10. Extract call arguments from register values

  11. Call handler

  12. Store return value in continuation data cap

  13. Zero out restricted stack capability

  14. Return through continuation cap

  15. Recover complete stack from sealed cap

  16. Return to task

CheriOS

CheriOS keeps a dedicated stack per compartment that a thread could enter and switches to that stack when the thread enters the compartment. This requires creating and maintaining stacks per thread.

See: cherios/system/dylink/src/platform/riscv/stubs.S

CompartOS

CompartOS uses a per-thread stack, which is donated to the called compartment and reset on return.

See: https://www.cl.cam.ac.uk/techreports/UCAM-CL-TR-976.pdf

CherIoT

Uses restricted caller stack in callee.

See: cheriot-rtos/core/switcher/tstack.h