feat(core): add versioned fake-session C ABI

This commit is contained in:
sechmachine
2026-08-12 15:08:09 +07:00
parent cb94f4ad5d
commit 67510b65b4
5 changed files with 1754 additions and 1 deletions
+175
View File
@@ -0,0 +1,175 @@
#ifndef VERSEVDI_CORE_H
#define VERSEVDI_CORE_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define VERSE_CORE_ABI_VERSION_1 UINT32_C(1)
typedef uint32_t verse_status_t;
#define VERSE_STATUS_OK UINT32_C(0)
#define VERSE_STATUS_INVALID_ARGUMENT UINT32_C(1)
#define VERSE_STATUS_INVALID_STATE UINT32_C(2)
#define VERSE_STATUS_UNSUPPORTED_ABI UINT32_C(3)
#define VERSE_STATUS_AUTHORITY_REJECTED UINT32_C(4)
#define VERSE_STATUS_TLS UINT32_C(5)
#define VERSE_STATUS_TRANSPORT UINT32_C(6)
#define VERSE_STATUS_PROTOCOL UINT32_C(7)
#define VERSE_STATUS_EXPIRED UINT32_C(8)
#define VERSE_STATUS_QUEUE_FULL UINT32_C(9)
#define VERSE_STATUS_CANCELLED UINT32_C(10)
#define VERSE_STATUS_REENTRANT UINT32_C(11)
#define VERSE_STATUS_BUSY UINT32_C(12)
#define VERSE_STATUS_INTERNAL UINT32_C(13)
#define VERSE_STATE_CONNECTING UINT32_C(1)
#define VERSE_STATE_CONNECTED UINT32_C(2)
#define VERSE_STATE_CANCELLED UINT32_C(3)
#define VERSE_INPUT_KEYBOARD UINT32_C(1)
#define VERSE_INPUT_MOUSE_BUTTON UINT32_C(2)
#define VERSE_INPUT_RELATIVE_MOUSE UINT32_C(3)
#define VERSE_INPUT_TEXT UINT32_C(4)
#define VERSE_INPUT_CONTROLLER UINT32_C(5)
#define VERSE_INPUT_ABSOLUTE_MOUSE UINT32_C(6)
#define VERSE_INPUT_SCROLL UINT32_C(7)
typedef struct verse_core verse_core_t;
typedef struct verse_bytes_view {
const uint8_t *data;
size_t length;
} verse_bytes_view_t;
typedef struct verse_state_event_v1 {
uint32_t struct_size;
uint32_t abi_version;
uint32_t state;
uint32_t reason;
} verse_state_event_v1_t;
typedef struct verse_error_event_v1 {
uint32_t struct_size;
uint32_t abi_version;
uint32_t code;
uint32_t retryable;
uint32_t phase;
uint32_t reserved;
} verse_error_event_v1_t;
typedef struct verse_stats_event_v1 {
uint32_t struct_size;
uint32_t abi_version;
uint64_t dropped_callbacks;
uint64_t dropped_media_units;
uint64_t dropped_input_events;
} verse_stats_event_v1_t;
typedef struct verse_media_event_v1 {
uint32_t struct_size;
uint32_t abi_version;
uint32_t channel;
uint32_t sequence;
uint64_t timestamp_ms;
verse_bytes_view_t encoded_unit;
} verse_media_event_v1_t;
typedef struct verse_control_event_v1 {
uint32_t struct_size;
uint32_t abi_version;
uint32_t kind;
uint32_t reserved;
verse_bytes_view_t payload;
} verse_control_event_v1_t;
typedef verse_status_t (*verse_sign_admission_v1_fn)(
void *signer_context,
verse_bytes_view_t transcript,
uint8_t signature_out[64]);
typedef verse_status_t (*verse_sign_tls_ed25519_v1_fn)(
void *signer_context,
verse_bytes_view_t tls_message,
uint8_t signature_out[64]);
typedef void (*verse_state_event_v1_fn)(
void *context,
const verse_state_event_v1_t *event);
typedef void (*verse_error_event_v1_fn)(
void *context,
const verse_error_event_v1_t *event);
typedef void (*verse_stats_event_v1_fn)(
void *context,
const verse_stats_event_v1_t *event);
typedef void (*verse_media_event_v1_fn)(
void *context,
const verse_media_event_v1_t *event);
typedef void (*verse_control_event_v1_fn)(
void *context,
const verse_control_event_v1_t *event);
typedef struct verse_core_config_v1 {
uint32_t struct_size;
uint32_t abi_version;
void *context;
verse_sign_admission_v1_fn sign_admission;
verse_sign_tls_ed25519_v1_fn sign_tls_ed25519;
verse_state_event_v1_fn on_state;
verse_error_event_v1_fn on_error;
verse_stats_event_v1_fn on_stats;
verse_media_event_v1_fn on_media;
verse_control_event_v1_fn on_control;
} verse_core_config_v1_t;
typedef struct verse_connect_request_v1 {
uint32_t struct_size;
uint32_t abi_version;
verse_bytes_view_t manifest_json;
verse_bytes_view_t tunnel_credential_json;
} verse_connect_request_v1_t;
/* values are kind-specific signed fields; every unused field and flags must be zero. */
typedef struct verse_input_event_v1 {
uint32_t struct_size;
uint32_t abi_version;
uint32_t kind;
uint32_t flags;
int32_t values[12];
} verse_input_event_v1_t;
uint32_t verse_core_abi_version(void);
verse_status_t verse_core_create_v1(
const verse_core_config_v1_t *config,
verse_core_t **out_core);
verse_status_t verse_core_connect_v1(
verse_core_t *core,
const verse_connect_request_v1_t *request);
verse_status_t verse_core_send_input_v1(
verse_core_t *core,
const verse_input_event_v1_t *event);
verse_status_t verse_core_request_idr_v1(verse_core_t *core);
verse_status_t verse_core_cancel_v1(verse_core_t *core);
verse_status_t verse_core_destroy_v1(verse_core_t *core, uint32_t timeout_ms);
#if defined(__APPLE__) && defined(__aarch64__)
_Static_assert(sizeof(verse_bytes_view_t) == 16, "verse_bytes_view_t arm64 layout");
_Static_assert(sizeof(verse_core_config_v1_t) == 72, "verse_core_config_v1_t arm64 layout");
_Static_assert(offsetof(verse_core_config_v1_t, sign_admission) == 16, "config callback offset");
_Static_assert(sizeof(verse_connect_request_v1_t) == 40, "verse_connect_request_v1_t arm64 layout");
_Static_assert(offsetof(verse_connect_request_v1_t, manifest_json) == 8, "request view offset");
_Static_assert(sizeof(verse_input_event_v1_t) == 64, "verse_input_event_v1_t arm64 layout");
_Static_assert(sizeof(verse_state_event_v1_t) == 16, "verse_state_event_v1_t arm64 layout");
_Static_assert(sizeof(verse_error_event_v1_t) == 24, "verse_error_event_v1_t arm64 layout");
_Static_assert(sizeof(verse_stats_event_v1_t) == 32, "verse_stats_event_v1_t arm64 layout");
_Static_assert(sizeof(verse_media_event_v1_t) == 40, "verse_media_event_v1_t arm64 layout");
_Static_assert(sizeof(verse_control_event_v1_t) == 32, "verse_control_event_v1_t arm64 layout");
#endif
#ifdef __cplusplus
}
#endif
#endif
+786
View File
@@ -0,0 +1,786 @@
use std::cell::Cell;
use std::collections::HashMap;
use std::ffi::c_void;
use std::mem::size_of;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::ptr::{self, NonNull};
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender, SyncSender, TrySendError};
use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
use crate::error::CoreError;
use crate::input::{encode_input, ControllerState, InputEvent};
use crate::session::SessionCore;
use crate::wire::{ConnectionManifest, NativeTunnelCredential};
const ABI_V1: u32 = 1;
const OK: u32 = 0;
const INVALID_ARGUMENT: u32 = 1;
const INVALID_STATE: u32 = 2;
const UNSUPPORTED_ABI: u32 = 3;
const QUEUE_FULL: u32 = 9;
const CANCELLED: u32 = 10;
const REENTRANT: u32 = 11;
const BUSY: u32 = 12;
const INTERNAL: u32 = 13;
const STATE_CONNECTING: u32 = 1;
const STATE_CONNECTED: u32 = 2;
const STATE_CANCELLED: u32 = 3;
const INPUT_KEYBOARD: u32 = 1;
const INPUT_MOUSE_BUTTON: u32 = 2;
const INPUT_RELATIVE_MOUSE: u32 = 3;
const INPUT_TEXT: u32 = 4;
const INPUT_CONTROLLER: u32 = 5;
const INPUT_ABSOLUTE_MOUSE: u32 = 6;
const INPUT_SCROLL: u32 = 7;
const MAX_CONNECT_BYTES: usize = 1024 * 1024;
const CALLBACK_QUEUE_CAPACITY: usize = 64;
const CONTROL_QUEUE_CAPACITY: usize = 64;
static NEXT_ID: AtomicUsize = AtomicUsize::new(1);
static HANDLES: OnceLock<Mutex<HashMap<usize, Arc<CoreInner>>>> = OnceLock::new();
thread_local! {
static ACTIVE_CALLBACK: Cell<usize> = const { Cell::new(0) };
static ACTIVE_SIGNER: Cell<bool> = const { Cell::new(false) };
}
#[repr(C)]
#[derive(Clone, Copy)]
struct BytesView {
data: *const u8,
length: usize,
}
// Safety: this copied pair is only retained as an ABI value in records whose backing bytes are
// owned by the same queued event; it is never shared independently.
unsafe impl Send for BytesView {}
#[repr(C)]
struct StateEvent {
struct_size: u32,
abi_version: u32,
state: u32,
reason: u32,
}
#[repr(C)]
struct ErrorEvent {
struct_size: u32,
abi_version: u32,
code: u32,
retryable: u32,
phase: u32,
reserved: u32,
}
#[repr(C)]
struct StatsEvent {
struct_size: u32,
abi_version: u32,
dropped_callbacks: u64,
dropped_media_units: u64,
dropped_input_events: u64,
}
#[repr(C)]
struct MediaEvent {
struct_size: u32,
abi_version: u32,
channel: u32,
sequence: u32,
timestamp_ms: u64,
encoded_unit: BytesView,
}
#[repr(C)]
struct ControlEvent {
struct_size: u32,
abi_version: u32,
kind: u32,
reserved: u32,
payload: BytesView,
}
type SignFn = unsafe extern "C" fn(*mut c_void, BytesView, *mut u8) -> u32;
type StateFn = unsafe extern "C" fn(*mut c_void, *const StateEvent);
type ErrorFn = unsafe extern "C" fn(*mut c_void, *const ErrorEvent);
type StatsFn = unsafe extern "C" fn(*mut c_void, *const StatsEvent);
type MediaFn = unsafe extern "C" fn(*mut c_void, *const MediaEvent);
type ControlFn = unsafe extern "C" fn(*mut c_void, *const ControlEvent);
#[repr(C)]
struct Config {
struct_size: u32,
abi_version: u32,
context: *mut c_void,
sign_admission: Option<SignFn>,
sign_tls_ed25519: Option<SignFn>,
on_state: Option<StateFn>,
on_error: Option<ErrorFn>,
on_stats: Option<StatsFn>,
on_media: Option<MediaFn>,
on_control: Option<ControlFn>,
}
#[repr(C)]
struct ConnectRequest {
struct_size: u32,
abi_version: u32,
manifest_json: BytesView,
tunnel_credential_json: BytesView,
}
#[repr(C)]
struct AbiInputEvent {
struct_size: u32,
abi_version: u32,
kind: u32,
flags: u32,
values: [i32; 12],
}
#[derive(Clone, Copy)]
struct Callbacks {
context: usize,
sign_admission: SignFn,
sign_tls_ed25519: SignFn,
on_state: Option<StateFn>,
on_error: Option<ErrorFn>,
on_stats: Option<StatsFn>,
on_media: Option<MediaFn>,
on_control: Option<ControlFn>,
}
enum CallbackEvent {
State(u32),
}
struct SessionState {
session: SessionCore,
lifecycle: Lifecycle,
idr_queue: usize,
}
#[derive(Clone, Copy, Eq, PartialEq)]
enum Lifecycle {
Created,
Connecting,
Connected,
Cancelled,
Destroying,
}
struct CoreInner {
id: usize,
state: Mutex<SessionState>,
callbacks: Callbacks,
callback_tx: SyncSender<CallbackEvent>,
callbacks_closed: Arc<AtomicBool>,
stop_tx: Sender<()>,
worker_done: Mutex<Receiver<()>>,
worker: Mutex<Option<JoinHandle<()>>>,
in_flight: AtomicUsize,
freeing: AtomicBool,
destroying: AtomicBool,
dropped_callbacks: AtomicU64,
}
#[repr(C)]
struct CoreHandle {
marker: u8,
}
struct CallGuard {
inner: Arc<CoreInner>,
}
impl Drop for CallGuard {
fn drop(&mut self) {
self.inner.in_flight.fetch_sub(1, Ordering::AcqRel);
}
}
fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn handles() -> &'static Mutex<HashMap<usize, Arc<CoreInner>>> {
HANDLES.get_or_init(|| Mutex::new(HashMap::new()))
}
fn callback_id() -> usize {
ACTIVE_CALLBACK.with(Cell::get)
}
fn status(error: CoreError) -> u32 {
match error {
CoreError::QueueFull => QUEUE_FULL,
CoreError::Cancelled => CANCELLED,
_ => INVALID_ARGUMENT,
}
}
fn ffi_boundary(action: impl FnOnce() -> u32) -> u32 {
catch_unwind(AssertUnwindSafe(action)).unwrap_or(INTERNAL)
}
fn ffi_boundary_core(core: *mut CoreHandle, action: impl FnOnce() -> u32) -> u32 {
if let Ok(result) = catch_unwind(AssertUnwindSafe(action)) {
result
} else {
if let Some(inner) = lock(handles()).get(&(core as usize)).cloned() {
cancel_inner(&inner);
}
INTERNAL
}
}
unsafe fn header(pointer: *const u8) -> Result<(u32, u32), u32> {
if pointer.is_null() {
return Err(INVALID_ARGUMENT);
}
// Invariant: the caller must provide the ABI-mandated readable 8-byte prefix.
let struct_size = unsafe { ptr::read_unaligned(pointer.cast::<u32>()) };
// Invariant: the caller must provide the ABI-mandated readable 8-byte prefix.
let abi_version = unsafe { ptr::read_unaligned(pointer.add(4).cast::<u32>()) };
if struct_size < 8 {
return Err(INVALID_ARGUMENT);
}
if abi_version != ABI_V1 {
return Err(UNSUPPORTED_ABI);
}
Ok((struct_size, abi_version))
}
unsafe fn read_table<T>(pointer: *const T) -> Result<T, u32> {
// Invariant: every sized table begins with the common readable 8-byte prefix.
let (struct_size, _) = unsafe { header(pointer.cast()) }?;
if usize::try_from(struct_size).map_err(|_| INVALID_ARGUMENT)? < size_of::<T>() {
return Err(INVALID_ARGUMENT);
}
// Invariant: struct_size proves the complete v1 table is readable; trailing bytes are ignored.
Ok(unsafe { ptr::read_unaligned(pointer) })
}
unsafe fn copy_view(view: BytesView) -> Result<Vec<u8>, u32> {
if view.length > MAX_CONNECT_BYTES || (view.data.is_null() && view.length != 0) {
return Err(INVALID_ARGUMENT);
}
if view.length == 0 {
return Ok(Vec::new());
}
// Invariant: non-null pointer plus caller-provided length is readable for this synchronous call.
Ok(unsafe { std::slice::from_raw_parts(view.data, view.length) }.to_vec())
}
unsafe fn begin_call(core: *mut CoreHandle) -> Result<(Arc<CoreInner>, CallGuard), u32> {
let core = NonNull::new(core).ok_or(INVALID_ARGUMENT)?;
let inner = {
let live = lock(handles());
let inner = live
.get(&(core.as_ptr() as usize))
.cloned()
.ok_or(INVALID_ARGUMENT)?;
inner.in_flight.fetch_add(1, Ordering::AcqRel);
inner
};
if inner.freeing.load(Ordering::Acquire) {
inner.in_flight.fetch_sub(1, Ordering::AcqRel);
return Err(INVALID_ARGUMENT);
}
Ok((Arc::clone(&inner), CallGuard { inner }))
}
fn enqueue_callback_under_state_lock(inner: &CoreInner, event: CallbackEvent) {
if inner.callbacks_closed.load(Ordering::Acquire) {
return;
}
if let Err(TrySendError::Full(_)) = inner.callback_tx.try_send(event) {
inner.dropped_callbacks.fetch_add(1, Ordering::Relaxed);
}
}
#[allow(clippy::needless_pass_by_value)]
fn callback_worker(
id: usize,
callbacks: Callbacks,
receiver: Receiver<CallbackEvent>,
stop: Receiver<()>,
done: Sender<()>,
callbacks_closed: Arc<AtomicBool>,
callback_gate: Arc<Mutex<()>>,
) {
loop {
if stop.try_recv().is_ok() {
break;
}
match receiver.recv_timeout(Duration::from_millis(2)) {
Ok(CallbackEvent::State(state)) => {
if let Some(callback) = callbacks.on_state {
let _gate = lock(&callback_gate);
if callbacks_closed.load(Ordering::Acquire) {
continue;
}
let event = StateEvent {
struct_size: u32::try_from(size_of::<StateEvent>()).expect("ABI size fits"),
abi_version: ABI_V1,
state,
reason: 0,
};
ACTIVE_CALLBACK.with(|active| active.set(id));
// Invariant: copied callback/context remain caller-owned and valid until destroy succeeds;
// event is readable for this synchronous callback only.
unsafe { callback(callbacks.context as *mut c_void, &raw const event) };
ACTIVE_CALLBACK.with(|active| active.set(0));
}
}
Err(RecvTimeoutError::Timeout) => {}
Err(RecvTimeoutError::Disconnected) => break,
}
}
let _ = done.send(());
}
fn cancel_inner(inner: &CoreInner) {
let mut state = lock(&inner.state);
let already_cancelled = state.session.is_cancelled();
state.session.cancel();
state.lifecycle = Lifecycle::Cancelled;
if !already_cancelled {
enqueue_callback_under_state_lock(inner, CallbackEvent::State(STATE_CANCELLED));
}
}
fn call_signer(id: usize, callback: SignFn, context: usize, bytes: &[u8]) -> u32 {
let mut signature = [0_u8; 64];
let view = BytesView {
data: bytes.as_ptr(),
length: bytes.len(),
};
ACTIVE_CALLBACK.with(|active| active.set(id));
ACTIVE_SIGNER.with(|active| active.set(true));
// Invariant: bytes and the writable 64-byte signature buffer live for the synchronous callback.
let result = unsafe { callback(context as *mut c_void, view, signature.as_mut_ptr()) };
ACTIVE_SIGNER.with(|active| active.set(false));
ACTIVE_CALLBACK.with(|active| active.set(0));
result
}
fn convert_input(event: &AbiInputEvent) -> Result<InputEvent, u32> {
if event.flags != 0 {
return Err(INVALID_ARGUMENT);
}
let values = event.values;
let unused_are_zero = |used: usize| values[used..].iter().all(|value| *value == 0);
let value_u8 = |index: usize| u8::try_from(values[index]).map_err(|_| INVALID_ARGUMENT);
let unsigned_16 = |index: usize| u16::try_from(values[index]).map_err(|_| INVALID_ARGUMENT);
let signed_16 = |index: usize| i16::try_from(values[index]).map_err(|_| INVALID_ARGUMENT);
let pressed = || match values[0] {
0 => Ok(false),
1 => Ok(true),
_ => Err(INVALID_ARGUMENT),
};
match event.kind {
INPUT_KEYBOARD if unused_are_zero(3) => Ok(InputEvent::Keyboard {
pressed: pressed()?,
modifiers: value_u8(1)?,
scancode: unsigned_16(2)?,
}),
INPUT_MOUSE_BUTTON if unused_are_zero(2) => Ok(InputEvent::MouseButton {
pressed: pressed()?,
button: value_u8(1)?,
}),
INPUT_RELATIVE_MOUSE if unused_are_zero(2) => Ok(InputEvent::RelativeMouse {
delta_x: signed_16(0)?,
delta_y: signed_16(1)?,
}),
INPUT_TEXT if unused_are_zero(1) => Ok(InputEvent::Text(
char::from_u32(u32::try_from(values[0]).map_err(|_| INVALID_ARGUMENT)?)
.ok_or(INVALID_ARGUMENT)?,
)),
INPUT_CONTROLLER if unused_are_zero(10) => Ok(InputEvent::Controller(ControllerState {
controller: value_u8(0)?,
active_mask: unsigned_16(1)?,
button_flags: unsigned_16(2)?,
left_trigger: value_u8(3)?,
right_trigger: value_u8(4)?,
left_x: signed_16(5)?,
left_y: signed_16(6)?,
right_x: signed_16(7)?,
right_y: signed_16(8)?,
extra_button_flags: unsigned_16(9)?,
})),
INPUT_ABSOLUTE_MOUSE if unused_are_zero(4) => Ok(InputEvent::AbsoluteMouse {
x: unsigned_16(0)?,
y: unsigned_16(1)?,
viewport_width: unsigned_16(2)?,
viewport_height: unsigned_16(3)?,
}),
INPUT_SCROLL if unused_are_zero(2) => Ok(InputEvent::Scroll {
vertical_delta: signed_16(0)?,
horizontal_delta: signed_16(1)?,
}),
_ => Err(INVALID_ARGUMENT),
}
}
#[no_mangle]
pub extern "C" fn verse_core_abi_version() -> u32 {
ffi_boundary(|| ABI_V1)
}
#[no_mangle]
unsafe extern "C" fn verse_core_create_v1(
config: *const Config,
out_core: *mut *mut CoreHandle,
) -> u32 {
ffi_boundary(|| {
if out_core.is_null() {
return INVALID_ARGUMENT;
}
// Invariant: caller supplies a writable out pointer for this synchronous call.
unsafe { out_core.write(ptr::null_mut()) };
// Invariant: config points to a readable sized/versioned table.
let config = match unsafe { read_table(config) } {
Ok(config) => config,
Err(error) => return error,
};
let (Some(sign_admission), Some(sign_tls_ed25519)) =
(config.sign_admission, config.sign_tls_ed25519)
else {
return INVALID_ARGUMENT;
};
let callbacks = Callbacks {
context: config.context as usize,
sign_admission,
sign_tls_ed25519,
on_state: config.on_state,
on_error: config.on_error,
on_stats: config.on_stats,
on_media: config.on_media,
on_control: config.on_control,
};
let (callback_tx, callback_rx) = mpsc::sync_channel(CALLBACK_QUEUE_CAPACITY);
let (stop_tx, stop_rx) = mpsc::channel();
let (done_tx, done_rx) = mpsc::channel();
let callbacks_closed = Arc::new(AtomicBool::new(false));
let callback_gate = Arc::new(Mutex::new(()));
let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
let worker_callbacks_closed = Arc::clone(&callbacks_closed);
let worker_callback_gate = Arc::clone(&callback_gate);
let worker = thread::Builder::new()
.name("verse-core-callback".to_owned())
.spawn(move || {
callback_worker(
id,
callbacks,
callback_rx,
stop_rx,
done_tx,
worker_callbacks_closed,
worker_callback_gate,
);
});
let Ok(worker) = worker else {
return INTERNAL;
};
let inner = Arc::new(CoreInner {
id,
state: Mutex::new(SessionState {
session: SessionCore::new(),
lifecycle: Lifecycle::Created,
idr_queue: 0,
}),
callbacks,
callback_tx,
callbacks_closed,
stop_tx,
worker_done: Mutex::new(done_rx),
worker: Mutex::new(Some(worker)),
in_flight: AtomicUsize::new(0),
freeing: AtomicBool::new(false),
destroying: AtomicBool::new(false),
dropped_callbacks: AtomicU64::new(0),
});
let handle = Box::new(CoreHandle { marker: 0 });
let raw = Box::into_raw(handle);
lock(handles()).insert(raw as usize, inner);
// Invariant: out_core is writable and receives the newly owned opaque handle.
unsafe { out_core.write(raw) };
OK
})
}
#[no_mangle]
unsafe extern "C" fn verse_core_connect_v1(
core: *mut CoreHandle,
request: *const ConnectRequest,
) -> u32 {
ffi_boundary_core(core, || {
// Invariant: caller owns a live opaque handle for the complete call.
let (handle, _guard) = match unsafe { begin_call(core) } {
Ok(value) => value,
Err(error) => return error,
};
if callback_id() == handle.id {
return REENTRANT;
}
// Invariant: request points to a readable sized/versioned table.
let request = match unsafe { read_table(request) } {
Ok(request) => request,
Err(error) => return error,
};
// Invariant: views obey the pointer/length rules for this synchronous copy.
let manifest = match unsafe { copy_view(request.manifest_json) } {
Ok(bytes) => bytes,
Err(error) => return error,
};
// Invariant: views obey the pointer/length rules for this synchronous copy.
let credential = match unsafe { copy_view(request.tunnel_credential_json) } {
Ok(bytes) => bytes,
Err(error) => return error,
};
if manifest.is_empty() || credential.is_empty() {
return INVALID_ARGUMENT;
}
if ConnectionManifest::decode(&manifest).is_err()
|| NativeTunnelCredential::decode(&credential).is_err()
{
return INVALID_ARGUMENT;
}
{
let mut state = lock(&handle.state);
if state.session.is_cancelled() {
return CANCELLED;
}
if state.lifecycle != Lifecycle::Created {
return INVALID_STATE;
}
state.lifecycle = Lifecycle::Connecting;
enqueue_callback_under_state_lock(&handle, CallbackEvent::State(STATE_CONNECTING));
}
let admission = call_signer(
handle.id,
handle.callbacks.sign_admission,
handle.callbacks.context,
&manifest,
);
if admission != OK {
cancel_inner(&handle);
return admission;
}
if lock(&handle.state).session.is_cancelled() {
return CANCELLED;
}
let tls = call_signer(
handle.id,
handle.callbacks.sign_tls_ed25519,
handle.callbacks.context,
&credential,
);
if tls != OK {
cancel_inner(&handle);
return tls;
}
{
let mut state = lock(&handle.state);
if state.session.is_cancelled() || state.lifecycle == Lifecycle::Destroying {
state.lifecycle = Lifecycle::Cancelled;
return CANCELLED;
}
state.lifecycle = Lifecycle::Connected;
enqueue_callback_under_state_lock(&handle, CallbackEvent::State(STATE_CONNECTED));
}
OK
})
}
#[no_mangle]
unsafe extern "C" fn verse_core_send_input_v1(
core: *mut CoreHandle,
event: *const AbiInputEvent,
) -> u32 {
ffi_boundary_core(core, || {
// Invariant: caller owns a live opaque handle for the complete call.
let (handle, _guard) = match unsafe { begin_call(core) } {
Ok(value) => value,
Err(error) => return error,
};
if callback_id() == handle.id {
return REENTRANT;
}
// Invariant: event points to a readable sized/versioned table.
let event = match unsafe { read_table(event) } {
Ok(event) => event,
Err(error) => return error,
};
let mut state = lock(&handle.state);
if state.session.is_cancelled() {
return CANCELLED;
}
if state.lifecycle != Lifecycle::Connected {
return INVALID_STATE;
}
let event = match convert_input(&event) {
Ok(event) => event,
Err(error) => return error,
};
let features = ["input.absolute.v1", "input.scroll.v1"];
let bytes = match encode_input(&event, &features) {
Ok(bytes) => bytes,
Err(error) => return status(error),
};
match state.session.enqueue_input(bytes, &features) {
Ok(()) => OK,
Err(error) => status(error),
}
})
}
#[no_mangle]
unsafe extern "C" fn verse_core_request_idr_v1(core: *mut CoreHandle) -> u32 {
ffi_boundary_core(core, || {
// Invariant: caller owns a live opaque handle for the complete call.
let (handle, _guard) = match unsafe { begin_call(core) } {
Ok(value) => value,
Err(error) => return error,
};
if callback_id() == handle.id {
return REENTRANT;
}
let mut state = lock(&handle.state);
if state.session.is_cancelled() {
return CANCELLED;
}
if state.lifecycle != Lifecycle::Connected {
return INVALID_STATE;
}
if state.idr_queue == CONTROL_QUEUE_CAPACITY {
return QUEUE_FULL;
}
state.idr_queue += 1;
OK
})
}
#[no_mangle]
unsafe extern "C" fn verse_core_cancel_v1(core: *mut CoreHandle) -> u32 {
ffi_boundary_core(core, || {
// Invariant: caller owns a live opaque handle for the complete call.
let (handle, _guard) = match unsafe { begin_call(core) } {
Ok(value) => value,
Err(error) => return error,
};
if callback_id() == handle.id && ACTIVE_SIGNER.with(Cell::get) {
return REENTRANT;
}
cancel_inner(&handle);
OK
})
}
#[no_mangle]
unsafe extern "C" fn verse_core_destroy_v1(core: *mut CoreHandle, timeout_ms: u32) -> u32 {
ffi_boundary_core(core, || {
let Some(core) = NonNull::new(core) else {
return INVALID_ARGUMENT;
};
let inner = {
let live = lock(handles());
let Some(inner) = live.get(&(core.as_ptr() as usize)).cloned() else {
return INVALID_ARGUMENT;
};
inner
};
if callback_id() == inner.id {
return REENTRANT;
}
if inner.destroying.swap(true, Ordering::AcqRel) {
return BUSY;
}
let deadline = Instant::now() + Duration::from_millis(u64::from(timeout_ms));
inner.callbacks_closed.store(true, Ordering::Release);
{
let mut state = lock(&inner.state);
state.lifecycle = Lifecycle::Destroying;
state.session.cancel();
}
let _ = inner.stop_tx.send(());
while inner.in_flight.load(Ordering::Acquire) != 0 {
if Instant::now() >= deadline {
inner.destroying.store(false, Ordering::Release);
return BUSY;
}
thread::yield_now();
}
let worker_finished = lock(&inner.worker).is_none()
|| lock(&inner.worker_done)
.recv_timeout(deadline.saturating_duration_since(Instant::now()))
.is_ok();
if !worker_finished {
inner.destroying.store(false, Ordering::Release);
return BUSY;
}
if let Some(worker) = lock(&inner.worker).take() {
if worker.join().is_err() {
inner.destroying.store(false, Ordering::Release);
return INTERNAL;
}
}
{
let mut live = lock(handles());
if inner.in_flight.load(Ordering::Acquire) != 0 {
inner.destroying.store(false, Ordering::Release);
return BUSY;
}
inner.freeing.store(true, Ordering::Release);
live.remove(&(core.as_ptr() as usize));
}
// Invariant: all other calls and callback work have ended; this is the sole successful free.
unsafe { drop(Box::from_raw(core.as_ptr())) };
OK
})
}
const _: () = {
assert!(size_of::<BytesView>() == 16);
assert!(size_of::<Config>() == 72);
assert!(size_of::<ConnectRequest>() == 40);
assert!(size_of::<AbiInputEvent>() == 64);
assert!(size_of::<StateEvent>() == 16);
assert!(size_of::<ErrorEvent>() == 24);
assert!(size_of::<StatsEvent>() == 32);
assert!(size_of::<MediaEvent>() == 40);
assert!(size_of::<ControlEvent>() == 32);
};
// Keep provider-free callback slots part of the copied v1 table even before the fake session emits
// these record classes.
const _: fn(Callbacks) = |callbacks| {
let _ = (
callbacks.on_error,
callbacks.on_stats,
callbacks.on_media,
callbacks.on_control,
);
};
#[cfg(test)]
mod tests {
use super::{ffi_boundary, INTERNAL};
#[test]
fn panic_boundary_maps_to_internal_status() {
assert_eq!(ffi_boundary(|| panic!("contained test panic")), INTERNAL);
}
}
+5 -1
View File
@@ -1,4 +1,5 @@
#![forbid(unsafe_code, unsafe_op_in_unsafe_fn)]
#![deny(unsafe_code)]
#![forbid(unsafe_op_in_unsafe_fn)]
//! Provider-free wire codecs and bounded session primitives for `VerseVDI` clients.
//!
@@ -16,3 +17,6 @@ pub mod input;
pub mod media;
pub mod session;
pub mod wire;
#[allow(unsafe_code)]
mod abi;
+692
View File
@@ -0,0 +1,692 @@
#![allow(
unsafe_code,
clippy::borrow_as_ptr,
clippy::cast_possible_truncation,
clippy::items_after_statements
)]
use std::ffi::c_void;
use std::mem::{offset_of, size_of};
use std::ptr;
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
use std::sync::{Arc, Barrier, Condvar, Mutex};
use std::thread;
use std::time::{Duration, Instant};
const ABI_V1: u32 = 1;
const OK: u32 = 0;
const INVALID_ARGUMENT: u32 = 1;
const INVALID_STATE: u32 = 2;
const UNSUPPORTED_ABI: u32 = 3;
const QUEUE_FULL: u32 = 9;
const CANCELLED: u32 = 10;
const REENTRANT: u32 = 11;
const BUSY: u32 = 12;
const STATE_CONNECTING: u32 = 1;
const STATE_CONNECTED: u32 = 2;
const STATE_CANCELLED: u32 = 3;
const INPUT_KEYBOARD: u32 = 1;
#[repr(C)]
struct Core {
_private: [u8; 0],
}
#[repr(C)]
#[derive(Clone, Copy)]
struct BytesView {
data: *const u8,
length: usize,
}
#[repr(C)]
struct StateEvent {
struct_size: u32,
abi_version: u32,
state: u32,
reason: u32,
}
#[repr(C)]
struct ErrorEvent {
struct_size: u32,
abi_version: u32,
code: u32,
retryable: u32,
phase: u32,
reserved: u32,
}
#[repr(C)]
struct StatsEvent {
struct_size: u32,
abi_version: u32,
dropped_callbacks: u64,
dropped_media_units: u64,
dropped_input_events: u64,
}
#[repr(C)]
struct MediaEvent {
struct_size: u32,
abi_version: u32,
channel: u32,
sequence: u32,
timestamp_ms: u64,
encoded_unit: BytesView,
}
#[repr(C)]
struct ControlEvent {
struct_size: u32,
abi_version: u32,
kind: u32,
reserved: u32,
payload: BytesView,
}
type SignFn = unsafe extern "C" fn(*mut c_void, BytesView, *mut u8) -> u32;
type StateFn = unsafe extern "C" fn(*mut c_void, *const StateEvent);
type ErrorFn = unsafe extern "C" fn(*mut c_void, *const ErrorEvent);
type StatsFn = unsafe extern "C" fn(*mut c_void, *const StatsEvent);
type MediaFn = unsafe extern "C" fn(*mut c_void, *const MediaEvent);
type ControlFn = unsafe extern "C" fn(*mut c_void, *const ControlEvent);
#[repr(C)]
struct Config {
struct_size: u32,
abi_version: u32,
context: *mut c_void,
sign_admission: Option<SignFn>,
sign_tls_ed25519: Option<SignFn>,
on_state: Option<StateFn>,
on_error: Option<ErrorFn>,
on_stats: Option<StatsFn>,
on_media: Option<MediaFn>,
on_control: Option<ControlFn>,
}
#[repr(C)]
struct ConnectRequest {
struct_size: u32,
abi_version: u32,
manifest_json: BytesView,
tunnel_credential_json: BytesView,
}
#[repr(C)]
struct InputEvent {
struct_size: u32,
abi_version: u32,
kind: u32,
flags: u32,
values: [i32; 12],
}
unsafe extern "C" {
fn verse_core_abi_version() -> u32;
fn verse_core_create_v1(config: *const Config, out_core: *mut *mut Core) -> u32;
fn verse_core_connect_v1(core: *mut Core, request: *const ConnectRequest) -> u32;
fn verse_core_send_input_v1(core: *mut Core, event: *const InputEvent) -> u32;
fn verse_core_request_idr_v1(core: *mut Core) -> u32;
fn verse_core_cancel_v1(core: *mut Core) -> u32;
fn verse_core_destroy_v1(core: *mut Core, timeout_ms: u32) -> u32;
}
const MANIFEST: &[u8] = br#"{
"version":"1","purpose":"launch","session_id":"session","reconnect_sequence":0,
"gateway":{"id":"gateway","addresses":["gateway.test:443"],"public_identity":"gateway.test"},
"tunnel":{"versions":["verse-gateway-v1/1"],"features":["control.v1","input.absolute.v1","input.scroll.v1"]},
"profile":{"id":"standard","bounds":{"minimum_kbps":1000,"target_kbps":5000,"maximum_kbps":10000},"display_mode":{"resolution_width":1920,"resolution_height":1080,"fps":60}},
"grant":{"opaque_value":"ggggggggggggggggggggggggggggggggggggggggggg","expires_at":"2099-01-01T00:00:00Z","audience":"audience"},
"correlation_id":"correlation"
}"#;
const CREDENTIAL: &[u8] = br#"{"client_device_id":"device","device_key_id":"key","certificate_chain_pem":"-----BEGIN CERTIFICATE-----\nAQID\n-----END CERTIFICATE-----","trust_bundle_pem":"-----BEGIN CERTIFICATE-----\nAQID\n-----END CERTIFICATE-----","expires_at":"2099-01-01T00:00:00Z"}"#;
struct Context {
core: AtomicUsize,
admission_calls: AtomicUsize,
tls_calls: AtomicUsize,
admission_input: Mutex<Vec<u8>>,
tls_input: Mutex<Vec<u8>>,
states: Mutex<Vec<u32>>,
wake: Condvar,
reentry_cancel: AtomicU32,
reentry_send: AtomicU32,
reentry_destroy: AtomicU32,
block_callbacks: AtomicBool,
release_callbacks: AtomicBool,
callback_active: AtomicUsize,
callback_max: AtomicUsize,
cancel_on_connecting: AtomicBool,
}
impl Default for Context {
fn default() -> Self {
Self {
core: AtomicUsize::new(0),
admission_calls: AtomicUsize::new(0),
tls_calls: AtomicUsize::new(0),
admission_input: Mutex::new(Vec::new()),
tls_input: Mutex::new(Vec::new()),
states: Mutex::new(Vec::new()),
wake: Condvar::new(),
reentry_cancel: AtomicU32::new(u32::MAX),
reentry_send: AtomicU32::new(u32::MAX),
reentry_destroy: AtomicU32::new(u32::MAX),
block_callbacks: AtomicBool::new(false),
release_callbacks: AtomicBool::new(false),
callback_active: AtomicUsize::new(0),
callback_max: AtomicUsize::new(0),
cancel_on_connecting: AtomicBool::new(false),
}
}
}
unsafe fn context<'a>(raw: *mut c_void) -> &'a Context {
// Test invariant: every callback receives the live Box<Context> supplied at create.
unsafe { &*raw.cast::<Context>() }
}
unsafe extern "C" fn sign_admission(raw: *mut c_void, input: BytesView, output: *mut u8) -> u32 {
// Test invariant: ABI promises input is readable for input.length during this callback.
let bytes = unsafe { std::slice::from_raw_parts(input.data, input.length) };
let ctx = unsafe { context(raw) };
ctx.admission_calls.fetch_add(1, Ordering::SeqCst);
*ctx.admission_input.lock().expect("admission lock") = bytes.to_vec();
// Test invariant: ABI promises a writable 64-byte Rust-owned signature buffer.
unsafe { ptr::write_bytes(output, 0xA5, 64) };
OK
}
unsafe extern "C" fn sign_tls(raw: *mut c_void, input: BytesView, output: *mut u8) -> u32 {
// Test invariant: ABI promises input is readable for input.length during this callback.
let bytes = unsafe { std::slice::from_raw_parts(input.data, input.length) };
let ctx = unsafe { context(raw) };
ctx.tls_calls.fetch_add(1, Ordering::SeqCst);
*ctx.tls_input.lock().expect("tls lock") = bytes.to_vec();
// Test invariant: ABI promises a writable 64-byte Rust-owned signature buffer.
unsafe { ptr::write_bytes(output, 0x5A, 64) };
OK
}
unsafe extern "C" fn on_state(raw: *mut c_void, event: *const StateEvent) {
let ctx = unsafe { context(raw) };
let active = ctx.callback_active.fetch_add(1, Ordering::SeqCst) + 1;
ctx.callback_max.fetch_max(active, Ordering::SeqCst);
// Test invariant: ABI promises a readable state record for the callback duration.
let state = unsafe { (*event).state };
ctx.states.lock().expect("states lock").push(state);
ctx.wake.notify_all();
if state == STATE_CONNECTING && ctx.cancel_on_connecting.load(Ordering::SeqCst) {
let core = ctx.core.load(Ordering::SeqCst) as *mut Core;
ctx.reentry_cancel
.store(unsafe { verse_core_cancel_v1(core) }, Ordering::SeqCst);
}
if ctx.block_callbacks.load(Ordering::SeqCst) && !ctx.release_callbacks.load(Ordering::SeqCst) {
let mut states = ctx.states.lock().expect("states lock");
while !ctx.release_callbacks.load(Ordering::SeqCst) {
states = ctx.wake.wait(states).expect("callback wait");
}
}
if state == STATE_CONNECTED && ctx.reentry_cancel.load(Ordering::SeqCst) == u32::MAX {
let core = ctx.core.load(Ordering::SeqCst) as *mut Core;
// Test invariant: the stored handle is live until this callback and its destroy complete.
ctx.reentry_cancel
.store(unsafe { verse_core_cancel_v1(core) }, Ordering::SeqCst);
let event = keyboard_event();
// Test invariant: event and handle remain valid for the synchronous call.
ctx.reentry_send.store(
unsafe { verse_core_send_input_v1(core, &event) },
Ordering::SeqCst,
);
// Test invariant: the callback intentionally probes the documented reentry rejection.
ctx.reentry_destroy
.store(unsafe { verse_core_destroy_v1(core, 1) }, Ordering::SeqCst);
}
ctx.callback_active.fetch_sub(1, Ordering::SeqCst);
}
unsafe extern "C" fn sign_admission_reenters(
raw: *mut c_void,
input: BytesView,
output: *mut u8,
) -> u32 {
let ctx = unsafe { context(raw) };
ctx.reentry_cancel.store(
unsafe { verse_core_cancel_v1(ctx.core.load(Ordering::SeqCst) as *mut Core) },
Ordering::SeqCst,
);
unsafe { sign_admission(raw, input, output) }
}
unsafe extern "C" fn sign_admission_waits_for_cancel(
raw: *mut c_void,
input: BytesView,
output: *mut u8,
) -> u32 {
let ctx = unsafe { context(raw) };
let deadline = Instant::now() + Duration::from_secs(2);
while ctx.reentry_cancel.load(Ordering::SeqCst) == u32::MAX {
assert!(
Instant::now() < deadline,
"connecting callback did not cancel"
);
thread::yield_now();
}
unsafe { sign_admission(raw, input, output) }
}
fn config(ctx: &mut Context) -> Config {
Config {
struct_size: size_of::<Config>() as u32,
abi_version: ABI_V1,
context: ptr::from_mut(ctx).cast(),
sign_admission: Some(sign_admission),
sign_tls_ed25519: Some(sign_tls),
on_state: Some(on_state),
on_error: None,
on_stats: None,
on_media: None,
on_control: None,
}
}
fn request(manifest: &[u8], credential: &[u8]) -> ConnectRequest {
ConnectRequest {
struct_size: size_of::<ConnectRequest>() as u32,
abi_version: ABI_V1,
manifest_json: BytesView {
data: manifest.as_ptr(),
length: manifest.len(),
},
tunnel_credential_json: BytesView {
data: credential.as_ptr(),
length: credential.len(),
},
}
}
fn keyboard_event() -> InputEvent {
let mut values = [0; 12];
values[0] = 1;
values[2] = 30;
InputEvent {
struct_size: size_of::<InputEvent>() as u32,
abi_version: ABI_V1,
kind: INPUT_KEYBOARD,
flags: 0,
values,
}
}
fn create(ctx: &mut Context) -> *mut Core {
let mut core = ptr::null_mut();
let config = config(ctx);
// Test invariant: config/out pointers remain valid for the synchronous create call.
assert_eq!(unsafe { verse_core_create_v1(&config, &mut core) }, OK);
assert!(!core.is_null());
ctx.core.store(core as usize, Ordering::SeqCst);
core
}
fn connect(core: *mut Core, manifest: &[u8], credential: &[u8]) -> u32 {
let request = request(manifest, credential);
// Test invariant: request and backing byte slices remain valid for the synchronous call.
unsafe { verse_core_connect_v1(core, &request) }
}
fn wait_for(ctx: &Context, predicate: impl Fn(&[u32]) -> bool) {
let deadline = Instant::now() + Duration::from_secs(2);
let mut states = ctx.states.lock().expect("states lock");
while !predicate(&states) {
let remaining = deadline.saturating_duration_since(Instant::now());
assert!(
!remaining.is_zero(),
"callback deadline exceeded: {states:?}"
);
(states, _) = ctx
.wake
.wait_timeout(states, remaining)
.expect("callback wait");
}
}
fn destroy(core: *mut Core) -> u32 {
// Test invariant: caller retains the handle until destroy reports success.
unsafe { verse_core_destroy_v1(core, 2_000) }
}
#[test]
fn arm64_c_layout_is_exact_and_version_is_fixed() {
assert_eq!(versevdi_core::session::INPUT_QUEUE_CAPACITY, 64);
assert_eq!(size_of::<BytesView>(), 16);
assert_eq!(size_of::<Config>(), 72);
assert_eq!(offset_of!(Config, sign_admission), 16);
assert_eq!(size_of::<ConnectRequest>(), 40);
assert_eq!(offset_of!(ConnectRequest, manifest_json), 8);
assert_eq!(size_of::<InputEvent>(), 64);
assert_eq!(size_of::<StateEvent>(), 16);
assert_eq!(size_of::<ErrorEvent>(), 24);
assert_eq!(size_of::<StatsEvent>(), 32);
assert_eq!(size_of::<MediaEvent>(), 40);
assert_eq!(size_of::<ControlEvent>(), 32);
// Test invariant: no pointer arguments are involved.
assert_eq!(unsafe { verse_core_abi_version() }, ABI_V1);
}
#[test]
fn create_validates_prefix_callbacks_output_and_trailing_bytes() {
let mut ctx = Context::default();
let mut core = ptr::dangling_mut::<Core>();
let mut cfg = config(&mut ctx);
cfg.struct_size = 7;
// Test invariant: config/out are readable/writable for this call.
assert_eq!(
unsafe { verse_core_create_v1(&cfg, &mut core) },
INVALID_ARGUMENT
);
assert!(core.is_null());
cfg = config(&mut ctx);
cfg.abi_version = 2;
assert_eq!(
unsafe { verse_core_create_v1(&cfg, &mut core) },
UNSUPPORTED_ABI
);
assert!(core.is_null());
cfg = config(&mut ctx);
cfg.sign_tls_ed25519 = None;
assert_eq!(
unsafe { verse_core_create_v1(&cfg, &mut core) },
INVALID_ARGUMENT
);
assert!(core.is_null());
#[repr(C)]
struct Extended {
base: Config,
ignored: [u8; 32],
}
let extended = Extended {
base: config(&mut ctx),
ignored: [0xEE; 32],
};
let mut extended = extended;
extended.base.struct_size = size_of::<Extended>() as u32;
assert_eq!(
unsafe { verse_core_create_v1(&extended.base, &mut core) },
OK
);
assert_eq!(destroy(core), OK);
}
#[test]
fn connect_copies_inputs_and_calls_purpose_specific_signers_once() {
let mut ctx = Context::default();
let core = create(&mut ctx);
let mut manifest = MANIFEST.to_vec();
let mut credential = CREDENTIAL.to_vec();
assert_eq!(connect(core, &manifest, &credential), OK);
manifest.fill(b'x');
credential.fill(b'y');
assert_eq!(ctx.admission_calls.load(Ordering::SeqCst), 1);
assert_eq!(ctx.tls_calls.load(Ordering::SeqCst), 1);
assert_eq!(&*ctx.admission_input.lock().expect("admission"), MANIFEST);
assert_eq!(&*ctx.tls_input.lock().expect("tls"), CREDENTIAL);
assert_eq!(destroy(core), OK);
}
#[test]
fn tables_and_slices_reject_short_unsupported_null_and_oversized_inputs() {
let mut ctx = Context::default();
ctx.reentry_cancel.store(OK, Ordering::SeqCst);
let core = create(&mut ctx);
let mut req = request(MANIFEST, CREDENTIAL);
req.struct_size = 7;
assert_eq!(
unsafe { verse_core_connect_v1(core, &req) },
INVALID_ARGUMENT
);
req.struct_size = size_of::<ConnectRequest>() as u32;
req.abi_version = 2;
assert_eq!(
unsafe { verse_core_connect_v1(core, &req) },
UNSUPPORTED_ABI
);
req.abi_version = ABI_V1;
req.manifest_json = BytesView {
data: ptr::null(),
length: 1,
};
assert_eq!(
unsafe { verse_core_connect_v1(core, &req) },
INVALID_ARGUMENT
);
req.manifest_json = BytesView {
data: ptr::null(),
length: 0,
};
assert_eq!(
unsafe { verse_core_connect_v1(core, &req) },
INVALID_ARGUMENT
);
req.manifest_json.length = 1_048_577;
assert_eq!(
unsafe { verse_core_connect_v1(core, &req) },
INVALID_ARGUMENT
);
let mut input = keyboard_event();
input.struct_size = 7;
assert_eq!(
unsafe { verse_core_send_input_v1(core, &input) },
INVALID_ARGUMENT
);
input.struct_size = size_of::<InputEvent>() as u32;
input.abi_version = 2;
assert_eq!(
unsafe { verse_core_send_input_v1(core, &input) },
UNSUPPORTED_ABI
);
#[repr(C)]
struct ExtendedRequest {
base: ConnectRequest,
ignored: [u8; 24],
}
let mut trailing_request = ExtendedRequest {
base: request(MANIFEST, CREDENTIAL),
ignored: [0xEE; 24],
};
trailing_request.base.struct_size = size_of::<ExtendedRequest>() as u32;
assert_eq!(
unsafe { verse_core_connect_v1(core, &trailing_request.base) },
OK
);
#[repr(C)]
struct ExtendedInput {
base: InputEvent,
ignored: [u8; 24],
}
let mut trailing_input = ExtendedInput {
base: keyboard_event(),
ignored: [0xEE; 24],
};
trailing_input.base.struct_size = size_of::<ExtendedInput>() as u32;
assert_eq!(
unsafe { verse_core_send_input_v1(core, &trailing_input.base) },
OK
);
assert_eq!(destroy(core), OK);
}
#[test]
fn callback_order_is_serial_and_only_cancel_is_reentrant() {
let mut ctx = Context::default();
let core = create(&mut ctx);
assert_eq!(connect(core, MANIFEST, CREDENTIAL), OK);
wait_for(&ctx, |states| states.contains(&STATE_CANCELLED));
assert_eq!(
ctx.states.lock().expect("states").as_slice(),
[STATE_CONNECTING, STATE_CONNECTED, STATE_CANCELLED]
);
assert_eq!(ctx.reentry_cancel.load(Ordering::SeqCst), OK);
assert_eq!(ctx.reentry_send.load(Ordering::SeqCst), REENTRANT);
assert_eq!(ctx.reentry_destroy.load(Ordering::SeqCst), REENTRANT);
assert_eq!(ctx.callback_max.load(Ordering::SeqCst), 1);
assert_eq!(destroy(core), OK);
}
#[test]
fn signer_callbacks_cannot_reenter_even_cancel() {
let mut ctx = Context::default();
let mut cfg = config(&mut ctx);
cfg.sign_admission = Some(sign_admission_reenters);
let mut core = ptr::null_mut();
assert_eq!(unsafe { verse_core_create_v1(&cfg, &mut core) }, OK);
ctx.core.store(core as usize, Ordering::SeqCst);
assert_eq!(connect(core, MANIFEST, CREDENTIAL), OK);
assert_eq!(ctx.reentry_cancel.load(Ordering::SeqCst), REENTRANT);
assert_eq!(destroy(core), OK);
}
#[test]
fn cancel_during_connect_preserves_state_order_and_stops_before_tls_signing() {
let mut ctx = Context::default();
ctx.cancel_on_connecting.store(true, Ordering::SeqCst);
let mut cfg = config(&mut ctx);
cfg.sign_admission = Some(sign_admission_waits_for_cancel);
let mut core = ptr::null_mut();
assert_eq!(unsafe { verse_core_create_v1(&cfg, &mut core) }, OK);
ctx.core.store(core as usize, Ordering::SeqCst);
assert_eq!(connect(core, MANIFEST, CREDENTIAL), CANCELLED);
wait_for(&ctx, |states| states.contains(&STATE_CANCELLED));
assert_eq!(
ctx.states.lock().expect("states").as_slice(),
[STATE_CONNECTING, STATE_CANCELLED]
);
assert_eq!(ctx.tls_calls.load(Ordering::SeqCst), 0);
assert_eq!(destroy(core), OK);
}
#[test]
fn send_input_is_nonblocking_bounded_and_cancel_is_idempotent() {
let mut ctx = Context::default();
ctx.reentry_cancel.store(OK, Ordering::SeqCst);
let core = create(&mut ctx);
assert_eq!(connect(core, MANIFEST, CREDENTIAL), OK);
let event = keyboard_event();
for _ in 0..64 {
assert_eq!(unsafe { verse_core_send_input_v1(core, &event) }, OK);
}
assert_eq!(
unsafe { verse_core_send_input_v1(core, &event) },
QUEUE_FULL
);
assert_eq!(unsafe { verse_core_request_idr_v1(core) }, OK);
assert_eq!(unsafe { verse_core_cancel_v1(core) }, OK);
assert_eq!(unsafe { verse_core_cancel_v1(core) }, OK);
assert_eq!(unsafe { verse_core_send_input_v1(core, &event) }, CANCELLED);
assert_eq!(destroy(core), OK);
}
#[test]
fn destroy_timeout_keeps_ownership_suppresses_late_callbacks_and_allows_retry() {
let mut ctx = Context::default();
ctx.reentry_cancel.store(OK, Ordering::SeqCst);
ctx.block_callbacks.store(true, Ordering::SeqCst);
let core = create(&mut ctx);
assert_eq!(connect(core, MANIFEST, CREDENTIAL), OK);
wait_for(&ctx, |states| !states.is_empty());
assert_eq!(unsafe { verse_core_destroy_v1(core, 1) }, BUSY);
let count_at_timeout = ctx.states.lock().expect("states").len();
ctx.release_callbacks.store(true, Ordering::SeqCst);
ctx.wake.notify_all();
assert_eq!(destroy(core), OK);
thread::sleep(Duration::from_millis(20));
assert_eq!(ctx.states.lock().expect("states").len(), count_at_timeout);
}
#[test]
fn concurrent_cancel_and_destroy_do_not_race_lifetime() {
let mut ctx = Context::default();
ctx.reentry_cancel.store(OK, Ordering::SeqCst);
let core = create(&mut ctx);
assert_eq!(connect(core, MANIFEST, CREDENTIAL), OK);
let barrier = Arc::new(Barrier::new(3));
let core_address = core as usize;
let cancel_barrier = Arc::clone(&barrier);
let cancel = thread::spawn(move || {
cancel_barrier.wait();
// Test invariant: the main thread retains ownership across this overlapping call.
unsafe { verse_core_cancel_v1(core_address as *mut Core) }
});
let destroy_barrier = Arc::clone(&barrier);
let destroyer = thread::spawn(move || {
destroy_barrier.wait();
// Test invariant: this is the sole successful destroy attempt in the race.
unsafe { verse_core_destroy_v1(core_address as *mut Core, 2_000) }
});
barrier.wait();
let cancel_status = cancel.join().expect("cancel thread");
let destroy_status = destroyer.join().expect("destroy thread");
assert!(matches!(cancel_status, OK | INVALID_ARGUMENT));
assert_eq!(destroy_status, OK);
}
#[test]
fn repeated_concurrent_cancel_destroy_cycles_are_safe() {
for _ in 0..100 {
let mut ctx = Context::default();
ctx.reentry_cancel.store(OK, Ordering::SeqCst);
let core = create(&mut ctx);
assert_eq!(connect(core, MANIFEST, CREDENTIAL), OK);
let barrier = Arc::new(Barrier::new(3));
let address = core as usize;
let cancel_barrier = Arc::clone(&barrier);
let cancel = thread::spawn(move || {
cancel_barrier.wait();
unsafe { verse_core_cancel_v1(address as *mut Core) }
});
let destroy_barrier = Arc::clone(&barrier);
let destroyer = thread::spawn(move || {
destroy_barrier.wait();
unsafe { verse_core_destroy_v1(address as *mut Core, 2_000) }
});
barrier.wait();
assert!(matches!(
cancel.join().expect("cancel"),
OK | INVALID_ARGUMENT
));
assert_eq!(destroyer.join().expect("destroy"), OK);
}
}
#[test]
fn preconnect_and_postcancel_state_checks_are_stable() {
let mut ctx = Context::default();
ctx.reentry_cancel.store(OK, Ordering::SeqCst);
let core = create(&mut ctx);
let event = keyboard_event();
assert_eq!(
unsafe { verse_core_send_input_v1(core, &event) },
INVALID_STATE
);
assert_eq!(unsafe { verse_core_request_idr_v1(core) }, INVALID_STATE);
assert_eq!(unsafe { verse_core_cancel_v1(core) }, OK);
assert_eq!(connect(core, MANIFEST, CREDENTIAL), CANCELLED);
assert_eq!(destroy(core), OK);
}
+96
View File
@@ -0,0 +1,96 @@
#include "versevdi_core.h"
#include <assert.h>
#include <stdatomic.h>
#include <stdlib.h>
#include <string.h>
static const char MANIFEST[] =
"{\"version\":\"1\",\"purpose\":\"launch\",\"session_id\":\"session\","
"\"reconnect_sequence\":0,\"gateway\":{\"id\":\"gateway\",\"addresses\":["
"\"gateway.test:443\"],\"public_identity\":\"gateway.test\"},\"tunnel\":{"
"\"versions\":[\"verse-gateway-v1/1\"],\"features\":[\"control.v1\"]},"
"\"profile\":{\"id\":\"standard\",\"bounds\":{\"minimum_kbps\":1000,"
"\"target_kbps\":5000,\"maximum_kbps\":10000},\"display_mode\":null},"
"\"grant\":{\"opaque_value\":\"ggggggggggggggggggggggggggggggggggggggggggg\","
"\"expires_at\":\"2099-01-01T00:00:00Z\",\"audience\":\"audience\"},"
"\"correlation_id\":\"correlation\"}";
static const char CREDENTIAL[] =
"{\"client_device_id\":\"device\",\"device_key_id\":\"key\","
"\"certificate_chain_pem\":\"-----BEGIN CERTIFICATE-----\\nAQID\\n-----END CERTIFICATE-----\","
"\"trust_bundle_pem\":\"-----BEGIN CERTIFICATE-----\\nAQID\\n-----END CERTIFICATE-----\","
"\"expires_at\":\"2099-01-01T00:00:00Z\"}";
typedef struct smoke_context {
atomic_uint admission_calls;
atomic_uint tls_calls;
atomic_uint state_calls;
} smoke_context_t;
static verse_status_t sign_admission(
void *raw,
verse_bytes_view_t input,
uint8_t signature[64]) {
smoke_context_t *context = raw;
assert(input.length == sizeof(MANIFEST) - 1U);
memset(signature, 0xA5, 64U);
atomic_fetch_add(&context->admission_calls, 1U);
return VERSE_STATUS_OK;
}
static verse_status_t sign_tls(
void *raw,
verse_bytes_view_t input,
uint8_t signature[64]) {
smoke_context_t *context = raw;
assert(input.length == sizeof(CREDENTIAL) - 1U);
memset(signature, 0x5A, 64U);
atomic_fetch_add(&context->tls_calls, 1U);
return VERSE_STATUS_OK;
}
static void on_state(void *raw, const verse_state_event_v1_t *event) {
smoke_context_t *context = raw;
assert(event->struct_size == sizeof(*event));
assert(event->abi_version == VERSE_CORE_ABI_VERSION_1);
atomic_fetch_add(&context->state_calls, 1U);
}
int main(void) {
smoke_context_t context = {0};
verse_core_config_v1_t config = {
.struct_size = sizeof(config),
.abi_version = VERSE_CORE_ABI_VERSION_1,
.context = &context,
.sign_admission = sign_admission,
.sign_tls_ed25519 = sign_tls,
.on_state = on_state,
};
verse_core_t *core = NULL;
assert(verse_core_abi_version() == VERSE_CORE_ABI_VERSION_1);
assert(verse_core_create_v1(&config, &core) == VERSE_STATUS_OK);
assert(core != NULL);
const verse_connect_request_v1_t request = {
.struct_size = sizeof(request),
.abi_version = VERSE_CORE_ABI_VERSION_1,
.manifest_json = {(const uint8_t *)MANIFEST, sizeof(MANIFEST) - 1U},
.tunnel_credential_json = {(const uint8_t *)CREDENTIAL, sizeof(CREDENTIAL) - 1U},
};
assert(verse_core_connect_v1(core, &request) == VERSE_STATUS_OK);
verse_input_event_v1_t input = {
.struct_size = sizeof(input),
.abi_version = VERSE_CORE_ABI_VERSION_1,
.kind = VERSE_INPUT_KEYBOARD,
.values = {1, 0, 30},
};
assert(verse_core_send_input_v1(core, &input) == VERSE_STATUS_OK);
assert(verse_core_request_idr_v1(core) == VERSE_STATUS_OK);
assert(verse_core_cancel_v1(core) == VERSE_STATUS_OK);
assert(verse_core_cancel_v1(core) == VERSE_STATUS_OK);
assert(verse_core_destroy_v1(core, 2000U) == VERSE_STATUS_OK);
assert(atomic_load(&context.admission_calls) == 1U);
assert(atomic_load(&context.tls_calls) == 1U);
return EXIT_SUCCESS;
}