Johnathan,
Thanks for this very detailed writeup!
My general take is that this is probably the right direction for the
next version of the ABI.
A few high level comments, partially based on one-on-one discussion with
you:
First, good to separate the high level approach from the specific
details of each system call. High level, this is basically defining a
set of possible types that can be passed across the ABI, how those types
are laid out in registers/memory when passed in various configurations
and orders, and a way of communicating those which types are being
passed.
To that extent, one question is whether this covers all the right types
and not more. My intuition is yes, it includes all of the typical
primitive types we care about.
There are two more types I came up with that could fit into this design; I omitted them because:
- They are more complex to explain, and I didn't feel the extra paragraphs would contribute to the goals of the document.
- They are a greater deviation from the design of Tock 2.0, and I wanted to keep this design Tock-2.0-ish to keep it easily digestable and limit the number of ideas we're discussing concurrently.
- I ran out of 4-bit type IDs (!).
For completeness, the extra types I came up with are "read-only slice" and "read-write slice". These types are effectively &[u8] and &mut [u8] from Rust, and would provide access to the buffer they point to for the duration of the system call. They would enable you to avoid an Allow and un-Allow pair when you're calling a system call that only needs synchronous access to the buffer (e.g. if we add a blocking Command syscall).
It also includes "CHERI" capabilities as
a "special" type, which is problematic on non-CHERI platforms, but I
think this is easily captured by some general name like "descriptors" or
something, that fit in a register, and refer to structure the kernel
keeps track of (e.g. file descriptors---these are not meant to be CHERI
pointers).
We could do that, but that does give up the main benefit of using CHERI capabilities for handles: userspace cannot forge CHERI capabilities. I don't see much advantage in having a separate "descriptor" type rather than just using u32.
If we remove the "fit in a register" part though, then we could still have an unforgeable handle type. On CHERI systems handles would be a capability, and on non-CHERI systems handles would be (128-bit?) cryptographically secure random numbers.
Second, it's not totally obvious we need or want this first register
that communicates the types across the ABI. In particular, for each call
and response there _has_ to be a specific contract between the driver
and process, as neither will deal with _all_ possible variants
reasonable. We need a way to "communicate" or check that contract, but
probably doing so at load time or installation time is better than
dynamically. E.g., perhaps a TBF header that describes the expected
interface from drivers and a loader (either on the host, at
compile/composition-time, or in the kernel at dynamic installation time)
ensures the kernel current drivers actually expose that interface.
There is a large spectrum of designs between "types are checked only at runtime" (the design I proposed) and "types are not checked but breaking changes are disallowed by CI" (mentioned in
possible improvement #4). Personally, I'm a fan of moving checks to build time (and where possible automating backwards-compatibility checks). Here's another idea that's between the two extremes: