-
Notifications
You must be signed in to change notification settings - Fork 3
UAPI: architecture of the kernel interfaces for Rust and C
NOTE: THIS PAGE IS STILL IN DRAFT
UAPI is library, defined as a Rust crate responsible for delivering a programmative interface for all kernelspace userspace interactions to upper layers. The UAPI must support C FFI so that both C-based and Rust-based applications are supported in the same time.
Kernel exchange model is built so that the kernel never access the task memory to get back or to delivers data. Instead, a dedicated exchange zone, unique to each task, is uses so that both user and kernel can exchange data.
This zone is denoted SVC_EXCHANGE
.
Such a paradigm requires that when data need to be exchange between kernel and user, it must be stored in this exchange zone, requiring at least one memory copy for each direction. On the other hand, the kernel never dereference any addresses, and doesn't even need to have the whole task memory mapped, reducing the required memory mapped zone to this exchange zone only. See Sentry kernel doc for more information about this.
The Sentry kernel stays a micro-kernel, meaning that its responsibility is reduced to basic services that are:
- platform bootup
- tasks lifecycle management (start, stop restart)
- tasks scheduling
- resources (devices, shared memories, dma streams...) partitioning and assignations
- shared resources control
All other functions are supported by applications.
The Sentry UAPI is also designed in a way that all ressources that are manipulated by applications are opaque objects, that are associated to the corresponding kernel ressource through the notion of handle. By now, the objects are not structured content but instead bare scalar holding the handle numeric value.
The UAPI crate is based on 4 public modules
-
systypes: this module hold all the data types definition that are shared between userspace applications and the Sentry kernel. These types can be manipulated by both Rust and C consumers, depending on the application language
-
syscall: this moduule depends on systypes and is delivers a low level interface to the kernel syscall gate. This is the first layer above the arch-specific kernel call, controlled by the
Syscall!
proc-macro. -
svc_exchange: this module delivers primitives in order to manipulate the
SVC_EXCHANGE
access (copy_to and copy_from methods, and properties (size and address). -
uapi: upper level interface designed in order to make the syscall + svc_exchange manipulations opaque to the caller. Today this interface is very small and support only a few subset of the syscalls list.
By now, there is no FFI-C specific module, making the syscall module components implementing directly extern C
functions. In the same way, the svc_exchange primitives are also declared extern C
, so that C applications implementations can manipulate the kernel interface properly, without any abstraction.
Such an architecture causes two main problems:
- the programative interface for both C and Rust is very low level, and is hard to manipulate
- symbols behind declared as extern C, even a full Rust application will have to go forward extern C interface, loosing the language good-properties
Moreover, handles being a basic scalar instead of an object, they can't be manipulated with the notion of lifetime and borrowing.
The goal is to define a clear architecture that allow both C and Rust usage without implacting each others, in a way that do not require, in the same time, two implementations of the same subprograms. Rust implementation should be able to properly manipulate ressources with a proper object notion, including object lifetime and ownership.
In the newly defined architecture, the UAPI would be only responsible for delivering the syscall
, systypes
and svc_exhange
public interface, and ffi_c
for extern C symbols of the other modules (meaning syscall
and svc_exchange
API.
The upper Rust and C layers would then be implemented in:
- shield-rs Crate for the Rust runtime (containing below typical Rust code)
- shield library for the C runtime (containing below typical C code)
The shield library for the C runtime will then no more hold the POSIX compliance implementation, which would be pushed up to a higher level interface library responsible exclusively of the POSIX compliance.
These two libraries are then:
- responsible of the "higher level API" over the bare libUAPI interface of Sentry
- responsible of the basic runtime support, including ldscript natural and automatic delivering,
__init
and__fini
symbols, and SSP support.
Such an architecture will include three repositories:
- the Sentry kernel, that include the low level UAPI library in Rust
- the shield-rs crate
- the shield C library
Since the startup of Sentry, the kernel API (and ABI) has been designed so that ABI breaks are reduced as much as possible, meaning in exceptional cases only. Moreover:
- UAPI ABI is not SoC-specific
- UAPI API is not arch-specific
- UAPI API is not product specific
- Starting with 1.0.0, UAPI is kept retro-compatible. In case of exceptional upgrade of UAPI interface
- major version is upgraded
- the newly implemented interface is added instead alongisde of the old one
- the old interface is kept for at least 2 consecutive major versions
These requirements ensure safe usage of UAPI by other components including shield
and shield-rs
, allowing long-time upgrade time.
A typical Rust oriented implementation of task handles would be the following:
use sentry_uapi::svc_exchange;
use sentry_uapi::syscall;
struct Task<'a> {
label: &'a TaskLabel
handle: u32
}
struct TaskLabel {
label: u32,
}
impl<'a> Task<'a> {
fn send_ipc(&self, ipc: IpcMessage) => Status {
svc_exchange::copy_to(ipc)?syscall::send_ipc(self.handle, ipc.len)
}
fn send_signal(&self, sig: Signal) => Status {
syscall::send_signal(self.handle, sig)
}
fn start(&self) => Status {
syscall::start(self.handle)
}
}
impl TaskLabel {
/// get back handle from label set at object creation time
/// Returns a Task object on success, or Status from consecutively the syscall and the exchange primitive
fn get_handle(&self) => <Task, Status> {
let mut Syshandle;
match (syscall::get_task_handle(self.label)) {
Status::Ok => (svc_exchange::copy_from(u32, &SysHandle)),
any_err => (return any_err),
}
Task {
label: self,
handle: SysHandle
}
}
}
fn main() {
let task_label = TaskLabel(label: 0xf0010022);
let mut task = task_label.get_handle();
task.send_signal(Signal::Pipe);
}
As Rust object is a notion that has no C equivalent, manipulating a task handle in C would be:
Status get_task_handle(uint32_t label, taskh_t * handle)
{
Status res = sys_get_task_handle(label);
switch (res) {
case STATUS_OK:
copy_to_user(handle, sizeof(taskh_t);
break;
default:
break;
return res;
}
Status send_ipc(taskh_t target, uint8_t *msg, size_t len)
{
Status res;
if (unlikely(len > SVC_EXCHANGE_LEN) {
res = STATUS_INVALID;
goto end;
}
copy_from_user(msg, len);
res = sys_send_ipc(target, len);
end:
return res;
}
Status send_signal(taskh_t target, Signal sig)
{
return sys_send_signal(target, sig);
}
Status start(taskh_t target)
{
return sys_start(target);
}
struct Shm<'a> {
r_handle: &'a ShmHandle
}
struct ShmHandle {
c_handle: u32
}
impl<'a> Shm<'a> {
fn foo(&self){
println!("foo");
}
}
impl ShmHandle {
fn map<'a>(&'a mut self) -> Shm<'a> {
println!("mapping");
Shm {
r_handle: self
}
}
fn bar(&self){
println!("bar");
}
}
impl<'a> Drop for Shm<'a> {
fn drop(&mut self) {
println!("unmapping");
}
}
fn main() {
let mut shm_c_handle = ShmHandle { c_handle: 0xAA };
shm_c_handle.bar();
let shm = shm_c_handle.map();
shm_c_handle.bar();
shm.foo();
}