2 years ago 4 views

Random notes on system calls

Sample system calls

A sketch of event based VM interaction

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

// These look like Rust signatures, and indeed that's not a coincidence.
// Handle<T> would be Copy+Clone, and interiorly mutable. (Whether a
// function reads, writes or even consumes a handle is not described in
// this idiom, although it may make sense to add that metadata, even if
// most languages would still discard the information and just pass the
// handle)

random_u16() -> u16
random_u32() -> u32
fill_random_buffer(buffer: &mut [u8])

// For some reason I'm assuming here that we're more often need the
// full list of SAUL devices, and less often the saul_reg_find_* --
// but both could be wrapped.
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
// structs being passed here are probably rare; some VMs might be usable even w/o implementing support for them?
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
// you could also do it like this (but really I'd rather have a better phydat)
// saul_read(Handler<saul reg>) -> Handler<phydat>
// phydat_get_ndim(Handler<phydat>) -> u8

// CoAP: This is a higher-level API than we have in GCoAP / nanocoap, but
// still not as high-level as I'd prefer it to (more REST-engine-like)
//
// (And it's simplified :-( )
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>)
// But I'd rather have a callback interface in the other direction


// we've not discussed events in detail, this is based on much guesswork
timer_set_ms(time: u32) -> Handle<wakeup timer event>
timer_cancel(Handle<wakeup timer event>) -> bool
// usage: pause_vm(), and if you're waiting for any of multiple events,
// just check whether handler_is_valid() has gone to false on any of the
// events?
// (or we could really block, I don't care so much)
pause_vm()
// or do we do callbacks? (that'd only work well if the VM's main function
// returned, and the VM were called again whenever any handler thought it'd
// be a good time to call a CB?)
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>)
// access vfs dirent by dedicated calls, or just return the full value in next?
vfs_opendir_ent(Handle<vfs dirent>) -> Handle<vfs dir>
vfs_openfile_ent(Handle<vfs dirent>) -> Handle<vfs file>
// How do we want to do error handling?
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 {
    /* No (valid) file or other structure open at this index */
    HT_INVALID = 0,
    /* The saul_reg union field is valid and not null. */
    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;
/* A handler index that is never used, and thus always of type HT_INVALID */
const handler_index_t HANDLER_NONE = 0xff;

uint16_t syscall_random_u16(vm_index_t vm) {
    (void) vm; /* Every VM is eligible for RNG access */
    return random_uint32();
}
uint32_t syscall_random_u32(vm_index_t vm) {
    (void) vm; /* Every VM is eligible for RNG access */
    return random_uint32();
}
void syscall_fill_random_buffer(vm_index_t vm, vm_ptr_t buffer__data, size_t buffer__len) {
    /* No privilege checking: Every VM is eligible for RNG access */
    uint8_t *buffer = vm_memmap_slice(vm, buffer__data, buffer__len);
    if buffer == NULL {
        /* It's important to halt the machine here: a VM with no
         * support for memmapping should not just continue silently
         * without random data. */
        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) {
    /* No need to do privilege checking: The VM has already obtained
     * a SAUL 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;
    }
    /* FIXME: I don't know how C functions work precisely when it comes
     * to trailing NUL bytes */
    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()
    
# The compiler is really doing a best-effort job
# mapping str to MemviewU8, and completely ignores
# whether anything is read or write
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:
    /* pop always returns an int16_t, but that happens to also be the pointer format we use */
    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):

/* This is assuming we only have one fixed VM */
#define OUR_VM_INDEX 1

void run_machine() {
    uint16_t our_memory[512] = { PROGRAM };
    
    /* set up VM struct */
    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:

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?