Random notes on system calls
Sample system calls
-
random
-
saul
We can probably not have SAUL devices implemented in VMs for two reasons:
- That VM can never terminate and restart (for otherwise it’d need to unregister the SAUL entry, which we can’t with the current API)
- Any SAUL read/write calls would need to complete blocking the caller, and unless we add interrupts inside the VMs with all the stack considerations they have … will the VM continue to run on the stack of the caller until it has processed the pending SAUL read or write?
- We could have “VM managed” read-only or write-only sensors, where reads are served from a stored “latest phydat” (the VM will not notice when it’s being read) or writes set a “latest phydat”, and if multiple writes arrive before the VM has worked them off, they are just ignored.
-
coap
-
trigger-me-an-event-in-this-and-that-time
-
puts
-
system information callback (get board name)
-
vfs
A sketch of event based VM interaction
-
Let’s not do interrupt-style things in VMs – one VM with one stack always runs one routine to completion.
-
Options for events:
-
main routine sets some pause-flag, which events then resume (“WFI style”, maybe with a whiff of thread flags).
The interrupts would be level triggered, and it’d be up to the application inside the VM to find which event is to be processed (doing which clears the interrupt).
-
main routine only registers callbacks to events (unless that’s done by static configuration anyway), and after it has completed, any routine may run (“everything-is-an-interrupt style”)
(I somehow like this one better … especially if we can ensure users don’t need globals to write meaningful programs.)
-
Can programs, rather than being just-code-and-please-jump-right-into-main, be described with all the handles they need, and and callbacks set in the description?
A simple example:
https://chaos.social/@overflo/110204374724327082
(This might even be self-explanatory: If the program gets uploaded to a “run our LEDs like this” endpoint, it’d see that there is a render
and beforeRender
and act accordingly)
A complex example:
This program wants to have these handles:
- a read handle on a monotonous u32 time source in seconds (
time
)
- a read handle on 2 configured 1-dimensional temperature sensors (
T_surf
“surface temp”, T_air
“air temp”)
- a read handle on 1 configured 1-dimensional humidity sensors (
RH_air
“air humidity”)
- a read/write handle on u32 storage that doesn’t need to be connected to anything else and is zero-initialized (
last_switch
)
- a write handle on 1 configured 1-dimensional on-off switch (
out
“emergency heating”)
When any input changes, run trigger(time, T_surf, T_air, RH_air, last_switch, out)
:
fn trigger(time, T_surf, T_air, RH_air, last_switch, out) {
const HYSTERESIS = 10;
if time - HYSTERESIS < last_switch {
return;
}
dew_point = calculate_dew_poin(T_air, RH_air);
if T_surf < dew_point && !out {
last_switch = time;
out = true;
}
if T_surf > dw_point && out {
last_switch = time;
out = false;
}
}
(Depending on how the reads and writes are implemented, this may get away with no syscalls at all. But to not calculate once per second, it’d be better to keep reconfiguring the time event.)
By the way, this should work for CoAP just as for SAUL ;-)
chrysn’s “how I’d implement them”
(and let’s replace this with a any established syntax if there is one)
VMs may treat any Handler just like an integer; it’s only the backend implementations that’ll need to deal with that
random_u16() -> u16
random_u32() -> u32
fill_random_buffer(buffer: &mut [u8])
saul_iter_init() -> Handler<saul reg>
saul_iter_next(Handler<saul reg>)
handler_is_valid(Handler<T>) -> bool
saul_get_type(Handler<saul reg>) -> u16
saul_read_name(Handler<saul reg>, buffer: &mut [u8]) -> usize
struct phydat {
v1: u16,
v2: u16,
v3: u16,
n_v: u8,
unit: u16,
exponent: u8,
}
saul_write(Handler<saul reg>, data: phydat)
saul_read(Handler<saul reg>) -> phydat
coap_server_resource_new(name_hint: Option<&[u8]>) -> Handler<coap server>
coap_server_resource_request_pending(Handler<coap server>) -> Option<cred_id>
coap_message_take_option_u16(Handler<coap any>, optnum: u16) -> Option<u16>
coap_message_take_body(Handler<coap any>, &mut [u8]) -> usize
coap_message_prepare_response(Handler<coap server>) -> bool
coap_message_set_option_u16(Handler<coap any>, optnum: u16, optval: u16)
coap_message_set_body(Handler<coap any>, &mut [u8])
coap_message_send_response(Handler<coap server>)
timer_set_ms(time: u32) -> Handle<wakeup timer event>
timer_cancel(Handle<wakeup timer event>) -> bool
pause_vm()
timer_set_cb(Handle<wakup timer event>, fn())
puts(buffer: &[u8])
get_board_name(buffer: &mut [u8]) -> usize
vfs_root() -> Handle<vfs dir>
vfs_opendir_at(Handle<vfs dir>, name: &[u8]) -> Handle<vfs dir>
vfs_openfile_at(Handle<vfs dir>, name: &[u8]) -> Handle<vfs file>
vfs_readdir(Handle<vfs dir>) -> Handle<vfs dirent>
vfs_next(Handle<vfs dirent>)
vfs_opendir_ent(Handle<vfs dirent>) -> Handle<vfs dir>
vfs_openfile_ent(Handle<vfs dirent>) -> Handle<vfs file>
vfs_file_readslice(Handle<vfs file>, offset: size_t, buf: &mut [u8]) -> usize
vfs_file_replace(Handle<vfs file>, buf: &[u8])
vfs_close(Handle<vfs any>)
Corresponding syscall implementations (VM independent)
header code (either hand-written, or generated just to catch if we change our syscalls):
uint16_t syscall_random_u16(vm_index_t vm);
handwritten code:
enum vm_handler_type {
HT_INVALID = 0,
HT_SAUL_REG,
}
typedef struct {
enum vm_handler_type type;
union {
saul_reg_t *saul_reg;
} value;
} vm_handler_t;
typedef uint8_t handler_index_t;
const handler_index_t HANDLER_NONE = 0xff;
uint16_t syscall_random_u16(vm_index_t vm) {
(void) vm;
return random_uint32();
}
uint32_t syscall_random_u32(vm_index_t vm) {
(void) vm;
return random_uint32();
}
void syscall_fill_random_buffer(vm_index_t vm, vm_ptr_t buffer__data, size_t buffer__len) {
uint8_t *buffer = vm_memmap_slice(vm, buffer__data, buffer__len);
if buffer == NULL {
vm_panic(vm);
return;
}
random_bytes(buffer, buffer__len);
}
handler_index_t saul_iter_init(vm_index_t vm) {
if (!vm_access_allow_saul(vm)) {
return HANDLER_NONE;
}
handler_index_t result;
handler_t *handler = vm_allocate_handler(vm, &mut result);
if (result == NULL) {
return result;
}
handler->value.saul_reg = saul_reg_find_nth(0);
if (handler->value.saul_reg != NULL) {
handler->type = HT_SAUL;
}
return result;
}
void saul_iter_next(vm_index_t vm, handler_index_t handler) {
handler_t *handler = vm_get_handler(vm, handler, HT_SAUL);
if (handler == NULL) {
return;
}
handler->value.saul_reg = handler->value.saul_reg->next;
if (handler->value.saul_reg == NULL) {
handler->type = HT_INVALID;
}
}
size_t saul_read_name(vm_index_t vm, handler_index_t handler, vm_ptr_t buffer__data, size_t buffer__len) {
handler_t *handler = vm_get_handler(vm, handler, HT_SAUL);
if (handler == NULL) {
return 0;
}
uint8_t *buffer = vm_memmap_slice(vm, buffer__data, buffer__len);
if (buffer == NULL) {
return 0;
}
if (handler->value.saul_reg->name == NULL) {
return 0;
}
return strncpy(buffer, handler->value.saul_reg->name, buffer__len);
}
VM glue code (by the imaginary example of EmbedVM)
embedVM side (generated, in a bowdlerized of the Python-ish language I wrote a compiler for Back Then)
def random_u16() -> int:
syscall1(0)
return stack_pop()
def fill_random_buffer(buffer: MemviewU8):
stack_push(buffer.ptr)
stack_push(buffer.len)
syscall1(2)
return stack_pop()
def saul_iter_init() -> Handler:
syscall1(5)
return stack_pop()
Example program:
def main():
puts("Hello, this is RIOT on ")
board_name: MemviewU8[16] = MemviewU8()
board_name_len = get_board_name(board_name)
puts(board_name[:board_name_len])
puts("!\n")
C side (generated; bowdlerized)
case 0:
embedvm_push(syscall_random_u16(VM_NUMBER));
break;
case 2:
vm_ptr_t buffer__ptr = embedvm_pop();
size_t buffer__len = embedvm_pop();
syscall_fill_random_buffer(VM_NUMBER, buffer__ptr, buffer__len);
break;
case 5:
embedvm_push(syscall_saul_iter_init(VM_NUMBER));
break;
C side (hand-written; bowdlerized):
#define OUR_VM_INDEX 1
void run_machine() {
uint16_t our_memory[512] = { PROGRAM };
vm_setup_memmaps(OUR_VM_INDEX, (uint8_t*)our_memory, sizeof(our_memory)));
vm_setup_permissions(OUR_VM_INDEX, VM_PERMISSIONS_ADMIN);
while (embedvm_running) {
embedvm_step();
switch embedvm_syscall_pending() {
#define VM_NUMBER OUR_VM_INDEX
#include "generated.c"
#undef VM_NUMBER
default:
continue;
}
}
}
Could we also use these for wrapping compiled languages?
Yes – the compiled code would be rolling both sides into one, and may need to do some tricks around handlers (they may not be movable).
Running Rust code would almost-but-not-quite be sandboxed when doing that – but we’d need to check that:
- unsafe code is only used from “blessed” crates
- it can’t contain any symbol references other than the syscall_ ones
- the entry functions’ stack usage would need to be bound
Error handling
This is getting really verbose really quickly in many cases, and different languages have completely different idioms.
Can we get away with any error condition just irrecoverably halting the VM?