Skip to content

Commit

Permalink
libtock: add util/streaming_process_slice
Browse files Browse the repository at this point in the history
This adds userspace helpers implementing the "streaming process slice"
contract as implemented in the Tock kernel in [1]. Instead of working
with two separate buffers, these helpers present an interface similar
to a regular read-write allow based on a single buffer. Internally,
this buffer is then subdivided into an "application-owned" and
"kernel-owned" part which are swapped atomically. The caller is
responsible for keeping a `streaming_process_slice_state_t`, and is
provided with ephemeral references to the kernel-written payloads on
each swap operation. The full, original buffer can be reclaimed by
deinitializing the `streaming_process_slice_state_t` struct.

Currently, these helpers are quite minimal and do not expose the
ability to set any optional flags (such as `halt`). They are tested to
work against against the interface implemented in [1] (with the fix of
[2] included) as part of the LwIP Ethernet tap-driver userspace app.

[1]: tock/tock#4208
[2]: tock/tock#4343
  • Loading branch information
lschuermann committed Feb 16, 2025
1 parent f00ad09 commit 3f29461
Show file tree
Hide file tree
Showing 3 changed files with 230 additions and 0 deletions.
1 change: 1 addition & 0 deletions libtock/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ $(LIBNAME)_SRCS += $(wildcard $($(LIBNAME)_DIR)/sensors/*.c)
$(LIBNAME)_SRCS += $(wildcard $($(LIBNAME)_DIR)/sensors/syscalls/*.c)
$(LIBNAME)_SRCS += $(wildcard $($(LIBNAME)_DIR)/storage/*.c)
$(LIBNAME)_SRCS += $(wildcard $($(LIBNAME)_DIR)/storage/syscalls/*.c)
$(LIBNAME)_SRCS += $(wildcard $($(LIBNAME)_DIR)/util/*.c)

# Temporary hack for alarm
$(LIBNAME)_SRCS += $(wildcard $($(LIBNAME)_DIR)/internal/*.c)
Expand Down
144 changes: 144 additions & 0 deletions libtock/util/streaming_process_slice.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#include "streaming_process_slice.h"
#include <string.h>

static void streaming_process_slice_prepare_header(uint8_t* buf) {
buf[0] = 0; // version[H]
buf[1] = 0; // version[L]
buf[2] = 0; // flags[H]
buf[3] = 0; // flags[L]
buf[4] = 0; // write offset
buf[5] = 0; // write offset
buf[6] = 0; // write offset
buf[7] = 0; // write offset
}

returncode_t streaming_process_slice_init(
streaming_process_slice_state_t* state,
uint32_t driver,
uint32_t allow,
void* buffer,
size_t size) {
// Each slice's header is 8 bytes long, and we create two slices from this
// buffer. Thus ensure that the provided buffer is at least 16 bytes long:
if (size < 16) {
return RETURNCODE_ESIZE;
}

state->driver = driver;
state->allow = allow;

// We split the buffer in half, an application and kernel side. These two
// buffers are then atomically swapped with each other.
//
// Initially, the first half of this buffer is designated as the application
// buffer.
state->app_buffer_ptr = buffer;
state->app_buffer_size = size / 2;

// Write a streaming process slice header to the second half of this buffer,
// and allow it to be the kernel buffer. We currently only support version
// 0, and don't set the `halt` flag:
uint8_t* kernel_buffer_ptr = state->app_buffer_ptr + state->app_buffer_size;
size_t kernel_buffer_size = size - state->app_buffer_size;
streaming_process_slice_prepare_header(kernel_buffer_ptr);

allow_rw_return_t allow_res =
allow_readwrite(driver, allow, kernel_buffer_ptr, kernel_buffer_size);
if (!allow_res.success) {
memset(state, 0, sizeof(streaming_process_slice_state_t));
}

return tock_status_to_returncode(allow_res.status);
}

returncode_t streaming_process_slice_get_and_swap(
streaming_process_slice_state_t* state,
uint8_t** buffer,
uint32_t* size,
bool* exceeded) {
uint8_t* ret_buffer;
uint32_t ret_size;
bool ret_exceeded;

// Prepare the current app buffer to be shared with the kernel (writing a
// zeroed-out header):
streaming_process_slice_prepare_header(state->app_buffer_ptr);

// Swap the current app buffer for the kernel buffer:
allow_rw_return_t allow_res =
allow_readwrite(state->driver, state->allow, state->app_buffer_ptr,
state->app_buffer_size);

if (allow_res.success) {
// Record the new app buffer:
state->app_buffer_ptr = allow_res.ptr;
state->app_buffer_size = allow_res.size;

// Return information about the received payload:
ret_buffer = state->app_buffer_ptr + 8;
memcpy(&ret_size, state->app_buffer_ptr + 4, sizeof(uint32_t));
ret_exceeded = (state->app_buffer_ptr[3] & 0x01) == 0x01;
} else {
// Allow was not successful, return safe dummy values instead:
ret_buffer = NULL;
ret_size = 0;
ret_exceeded = false;
}

// Write return values if provided with non-NULL pointers:
if (buffer != NULL) {
*buffer = ret_buffer;
}
if (size != NULL) {
*size = ret_size;
}
if (exceeded != NULL) {
*exceeded = ret_exceeded;
}

return tock_status_to_returncode(allow_res.status);
}

returncode_t streaming_process_slice_deinit(
streaming_process_slice_state_t* state,
uint8_t** buffer,
size_t* size) {
uint8_t* ret_buffer;
size_t ret_size;

// Unallow the buffer currently allowed to the kernel:
allow_rw_return_t unallow_res =
allow_readwrite(state->driver, state->allow, NULL, 0);

if (unallow_res.success) {
// Unallow failed, don't modify the state struct.
ret_buffer = NULL;
ret_size = 0;
} else {
// The unallow worked, recreate the full, initial buffer from the app and
// kernel halves:
if ((void*)state->app_buffer_ptr < unallow_res.ptr) {
// App buffer is left half, kernel buffer is right half:
// `[ app_buffer ][ kernel_buffer ]`
ret_buffer = state->app_buffer_ptr;
ret_size = state->app_buffer_size + unallow_res.size;
} else {
// App buffer is right half, kernel buffer is left half:
// `[ kernel_buffer ][ app_buffer ]`
ret_buffer = unallow_res.ptr;
ret_size = unallow_res.size + state->app_buffer_size;
}

// Wipe the state struct:
memset(state, 0, sizeof(streaming_process_slice_state_t));
}

if (buffer != NULL) {
*buffer = ret_buffer;
}
if (size != NULL) {
*size = ret_size;
}

return tock_status_to_returncode(unallow_res.status);
}
85 changes: 85 additions & 0 deletions libtock/util/streaming_process_slice.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#include "../tock.h"

typedef struct {
uint8_t* app_buffer_ptr;
size_t app_buffer_size;
uint32_t driver;
uint32_t allow;
} streaming_process_slice_state_t;

// Initialize a "streaming process slice" read-write allow slot
//
// This method allows a userspace buffer into a "streaming process
// slice" allow slot, implementing its atomic-swap semantics and
// header layout. The streaming process slice abstraction allows a
// userspace process to lossessly receive data from a kernel
// capsule. This is done by maintaining two buffers, where at any time
// one of which is owned by the kernel (for writing new, incoming data
// into) and one by the application, to process received data. These
// buffers are atomically swapped by the application, upon receipt of
// a signal that some data has been inserted into the kernel-owned
// buffer (such as an upcall).
//
// This method abstracts this interface by consuming one buffer and
// splitting it into two halves, owned by the application and kernel
// respectively. It tracks all necessary state in the
// `streaming_process_slice_state_t` object. For this to work, the
// passed buffer must be able to hold at least two streaming process
// slice headers (8 byte each), i.e., it must be at least 16 bytes
// long.
//
// In case of an error while allowing the kernel-owned buffer to the
// specified driver and read-write allow slot, this function converts
// this error-status into a returncode using
// `tock_status_to_returncode` and returns it to the caller. When this
// method returns `RETURNCODE_SUCCESS`, the passed buffer is assumed
// to be owned by this `streaming_process_slice_state_t` and must not
// be used until after a successful call to
// `streaming_process_slice_deinit`. When the buffer is of
// insufficient size, it returns `RETURNCODE_ESIZE`.
returncode_t streaming_process_slice_init(
streaming_process_slice_state_t* state,
uint32_t driver,
uint32_t allow,
void* buffer,
size_t size);

// Swap kernel- for app-owned buffer and get received payload
//
// This method atomically swaps the kernel-owned and application-owned
// halves of the streaming process slice. This function will reset the
// application-owned buffers header, applying any flags set in the
// `streaming_process_slice_state_t` and setting the write offset to
// `0`.
//
// Following the swap operation, when returning `RETURNCODE_SUCCESS`,
// it provides the buffer's payload and any kernel-set flags to the
// caller through the `buffer`, `size`, and `exceeded` arguments
// respectively. Callers must either provide pointers to variables for
// these values, or set them to `NULL` in case they are not interested
// in any given value.
//
// This function forwards any error from the underlying `allow_readwrite`
// operation in its return value. In case of a return value other than
// `RETURNCODE_SUCCESS`, any values returned in `buffer`, `size` and `exceeded`
// must not be considered valid.
returncode_t streaming_process_slice_get_and_swap(
streaming_process_slice_state_t* state,
uint8_t** buffer,
uint32_t* size,
bool* exceeded);

// Deinitialize an initialized `streaming_process_slice_state_t`
//
// This function reconstructs the passed into `streaming_process_slice_init` and
// returns it through the `buffer` and `size` arguments (if not set to `NULL`
// respectively).
//
// This function forwards any error from the underlying `allow_readwrite`
// operation in its return value. In case of a return value other than
// `RETURNCODE_SUCCESS`, any values returned in `buffer`, `size` and `exceeded`
// must not be considered valid.
returncode_t streaming_process_slice_deinit(
streaming_process_slice_state_t* state,
uint8_t** buffer,
size_t* size);

0 comments on commit 3f29461

Please sign in to comment.