Skip to content

UAPI: architecture of the kernel interfaces for Rust and C

Philippe Thierry edited this page Oct 7, 2024 · 28 revisions

NOTE: THIS PAGE IS STILL IN DRAFT


About UAPI

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.

General kernel/user exchange model

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.

Today's architecture

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.

Today's architecture limitations

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.

Target architecture

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.

UAPI architecture

About ABI compliance with multi-repo

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.

Rust objects and C access to the same ressource

Example with task handles

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);
}

SHM handles

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();
}