908 lines
30 KiB
Rust
908 lines
30 KiB
Rust
#![allow(
|
|
unsafe_code,
|
|
clippy::borrow_as_ptr,
|
|
clippy::cast_possible_truncation,
|
|
clippy::items_after_statements,
|
|
clashing_extern_declarations
|
|
)]
|
|
|
|
use std::cell::RefCell;
|
|
use std::collections::HashMap;
|
|
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, Condvar, Mutex};
|
|
use std::thread;
|
|
use std::time::{Duration, Instant};
|
|
|
|
use rustls::sign::SigningKey;
|
|
use rustls::SignatureScheme;
|
|
|
|
#[path = "gateway_oracle.rs"]
|
|
mod gateway_oracle;
|
|
|
|
thread_local! {
|
|
static CORE_CONTEXTS: RefCell<HashMap<usize, usize>> = RefCell::new(HashMap::new());
|
|
static ORACLES: RefCell<HashMap<usize, gateway_oracle::Oracle>> = RefCell::new(HashMap::new());
|
|
}
|
|
|
|
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 AUTHORITY_REJECTED: u32 = 4;
|
|
const TLS: u32 = 5;
|
|
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;
|
|
|
|
#[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_status: AtomicU32,
|
|
tls_status: AtomicU32,
|
|
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,
|
|
reentry_target: AtomicUsize,
|
|
reentry_results: Mutex<Vec<u32>>,
|
|
admission_key: Mutex<Option<Arc<dyn SigningKey>>>,
|
|
tls_key: Mutex<Option<Arc<dyn SigningKey>>>,
|
|
}
|
|
|
|
impl Default for Context {
|
|
fn default() -> Self {
|
|
Self {
|
|
core: AtomicUsize::new(0),
|
|
admission_calls: AtomicUsize::new(0),
|
|
tls_calls: AtomicUsize::new(0),
|
|
admission_status: AtomicU32::new(OK),
|
|
tls_status: AtomicU32::new(OK),
|
|
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),
|
|
reentry_target: AtomicUsize::new(0),
|
|
reentry_results: Mutex::new(Vec::new()),
|
|
admission_key: Mutex::new(None),
|
|
tls_key: Mutex::new(None),
|
|
}
|
|
}
|
|
}
|
|
|
|
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();
|
|
let status = ctx.admission_status.load(Ordering::SeqCst);
|
|
if status != OK {
|
|
return status;
|
|
}
|
|
let key = ctx.admission_key.lock().expect("admission key");
|
|
let Some(key) = key.as_ref() else {
|
|
return INTERNAL;
|
|
};
|
|
let signer = key
|
|
.choose_scheme(&[SignatureScheme::ED25519])
|
|
.expect("Ed25519 admission signer");
|
|
let signature = signer.sign(bytes).expect("admission signature");
|
|
assert_eq!(signature.len(), 64);
|
|
// Test invariant: ABI promises a writable 64-byte Rust-owned signature buffer.
|
|
unsafe { ptr::copy_nonoverlapping(signature.as_ptr(), output, signature.len()) };
|
|
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();
|
|
let status = ctx.tls_status.load(Ordering::SeqCst);
|
|
if status != OK {
|
|
return status;
|
|
}
|
|
let key = ctx.tls_key.lock().expect("TLS key");
|
|
let Some(key) = key.as_ref() else {
|
|
return INTERNAL;
|
|
};
|
|
let signer = key
|
|
.choose_scheme(&[SignatureScheme::ED25519])
|
|
.expect("Ed25519 TLS signer");
|
|
let signature = signer.sign(bytes).expect("TLS signature");
|
|
assert_eq!(signature.len(), 64);
|
|
// Test invariant: ABI promises a writable 64-byte Rust-owned signature buffer.
|
|
unsafe { ptr::copy_nonoverlapping(signature.as_ptr(), output, signature.len()) };
|
|
OK
|
|
}
|
|
|
|
unsafe extern "C" fn sign_admission_probes_global_reentry(
|
|
raw: *mut c_void,
|
|
input: BytesView,
|
|
output: *mut u8,
|
|
) -> u32 {
|
|
let ctx = unsafe { context(raw) };
|
|
let target = ctx.reentry_target.load(Ordering::SeqCst) as *mut Core;
|
|
let results = [
|
|
unsafe { verse_core_abi_version() },
|
|
unsafe { verse_core_create_v1(ptr::null(), ptr::null_mut()) },
|
|
unsafe { verse_core_connect_v1(target, ptr::null()) },
|
|
unsafe { verse_core_send_input_v1(target, ptr::null()) },
|
|
unsafe { verse_core_request_idr_v1(target) },
|
|
unsafe { verse_core_cancel_v1(target) },
|
|
unsafe { verse_core_destroy_v1(target, 0) },
|
|
];
|
|
ctx.reentry_results
|
|
.lock()
|
|
.expect("signer reentry results")
|
|
.extend(results);
|
|
unsafe { sign_admission(raw, input, output) }
|
|
}
|
|
|
|
unsafe extern "C" fn on_state_probes_global_reentry(raw: *mut c_void, event: *const StateEvent) {
|
|
let ctx = unsafe { context(raw) };
|
|
// 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_CONNECTED {
|
|
return;
|
|
}
|
|
let origin = ctx.core.load(Ordering::SeqCst) as *mut Core;
|
|
let other = ctx.reentry_target.load(Ordering::SeqCst) as *mut Core;
|
|
let results = [
|
|
unsafe { verse_core_abi_version() },
|
|
unsafe { verse_core_create_v1(ptr::null(), ptr::null_mut()) },
|
|
unsafe { verse_core_connect_v1(origin, ptr::null()) },
|
|
unsafe { verse_core_send_input_v1(origin, ptr::null()) },
|
|
unsafe { verse_core_request_idr_v1(origin) },
|
|
unsafe { verse_core_cancel_v1(other) },
|
|
unsafe { verse_core_destroy_v1(origin, 0) },
|
|
unsafe { verse_core_cancel_v1(origin) },
|
|
];
|
|
ctx.reentry_results
|
|
.lock()
|
|
.expect("event reentry results")
|
|
.extend(results);
|
|
}
|
|
|
|
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());
|
|
register(core, ctx);
|
|
core
|
|
}
|
|
|
|
fn register(core: *mut Core, ctx: &mut Context) {
|
|
ctx.core.store(core as usize, Ordering::SeqCst);
|
|
CORE_CONTEXTS.with(|contexts| {
|
|
contexts
|
|
.borrow_mut()
|
|
.insert(core as usize, ptr::from_mut(ctx) as usize);
|
|
});
|
|
}
|
|
|
|
fn connect(core: *mut Core, manifest: &[u8], credential: &[u8]) -> u32 {
|
|
connect_mode(core, "", manifest, credential)
|
|
}
|
|
|
|
fn connect_mode(core: *mut Core, mode: &str, manifest: &[u8], credential: &[u8]) -> u32 {
|
|
connect_with_oracle(core, mode, |oracle| {
|
|
let manifest = if manifest == MANIFEST {
|
|
oracle.ready.manifest.as_bytes()
|
|
} else {
|
|
manifest
|
|
};
|
|
let credential = if credential == CREDENTIAL {
|
|
oracle.ready.credential.as_bytes()
|
|
} else {
|
|
credential
|
|
};
|
|
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 connect_with_oracle(
|
|
core: *mut Core,
|
|
mode: &str,
|
|
action: impl FnOnce(&gateway_oracle::Oracle) -> u32,
|
|
) -> u32 {
|
|
let oracle = gateway_oracle::Oracle::start(mode);
|
|
CORE_CONTEXTS.with(|contexts| {
|
|
let raw = *contexts
|
|
.borrow()
|
|
.get(&(core as usize))
|
|
.expect("registered ABI context");
|
|
// Test invariant: context outlives the core and is removed only after destroy succeeds.
|
|
let ctx = unsafe { &*(raw as *const Context) };
|
|
*ctx.admission_key.lock().expect("admission key") =
|
|
Some(gateway_oracle::test_key(&oracle.ready.admission_key));
|
|
*ctx.tls_key.lock().expect("TLS key") =
|
|
Some(gateway_oracle::test_key(&oracle.ready.client_key));
|
|
});
|
|
let status = action(&oracle);
|
|
if status == OK {
|
|
ORACLES.with(|oracles| {
|
|
oracles.borrow_mut().insert(core as usize, oracle);
|
|
});
|
|
}
|
|
status
|
|
}
|
|
|
|
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.
|
|
let status = unsafe { verse_core_destroy_v1(core, 2_000) };
|
|
if status == OK {
|
|
ORACLES.with(|oracles| {
|
|
oracles.borrow_mut().remove(&(core as usize));
|
|
});
|
|
CORE_CONTEXTS.with(|contexts| {
|
|
contexts.borrow_mut().remove(&(core as usize));
|
|
});
|
|
}
|
|
status
|
|
}
|
|
|
|
#[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);
|
|
let admission = ctx.admission_input.lock().expect("admission");
|
|
let tls = ctx.tls_input.lock().expect("tls");
|
|
assert!(admission.starts_with(b"versevdi/tunnel-admission/v1"));
|
|
assert!(!tls.is_empty());
|
|
assert_ne!(&*admission, &*tls);
|
|
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],
|
|
}
|
|
assert_eq!(
|
|
connect_with_oracle(core, "", |oracle| {
|
|
let mut trailing_request = ExtendedRequest {
|
|
base: request(
|
|
oracle.ready.manifest.as_bytes(),
|
|
oracle.ready.credential.as_bytes(),
|
|
),
|
|
ignored: [0xEE; 24],
|
|
};
|
|
trailing_request.base.struct_size = size_of::<ExtendedRequest>() as u32;
|
|
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);
|
|
register(core, &mut ctx);
|
|
assert_eq!(connect(core, MANIFEST, CREDENTIAL), OK);
|
|
assert_eq!(ctx.reentry_cancel.load(Ordering::SeqCst), REENTRANT);
|
|
assert_eq!(destroy(core), OK);
|
|
}
|
|
|
|
#[test]
|
|
fn signer_callback_rejects_every_stateful_api_across_handles() {
|
|
let mut other_ctx = Context::default();
|
|
other_ctx.reentry_cancel.store(OK, Ordering::SeqCst);
|
|
let other = create(&mut other_ctx);
|
|
|
|
let mut ctx = Context::default();
|
|
ctx.reentry_cancel.store(OK, Ordering::SeqCst);
|
|
ctx.reentry_target.store(other as usize, Ordering::SeqCst);
|
|
let mut cfg = config(&mut ctx);
|
|
cfg.sign_admission = Some(sign_admission_probes_global_reentry);
|
|
let mut core = ptr::null_mut();
|
|
assert_eq!(unsafe { verse_core_create_v1(&cfg, &mut core) }, OK);
|
|
register(core, &mut ctx);
|
|
assert_eq!(connect(core, MANIFEST, CREDENTIAL), OK);
|
|
assert_eq!(
|
|
ctx.reentry_results
|
|
.lock()
|
|
.expect("signer results")
|
|
.as_slice(),
|
|
[ABI_V1, REENTRANT, REENTRANT, REENTRANT, REENTRANT, REENTRANT, REENTRANT]
|
|
);
|
|
|
|
assert_eq!(destroy(core), OK);
|
|
assert_eq!(connect(other, MANIFEST, CREDENTIAL), OK);
|
|
assert_eq!(destroy(other), OK);
|
|
}
|
|
|
|
#[test]
|
|
fn event_callback_allows_only_originating_handle_cancel() {
|
|
let mut other_ctx = Context::default();
|
|
other_ctx.reentry_cancel.store(OK, Ordering::SeqCst);
|
|
let other = create(&mut other_ctx);
|
|
|
|
let mut ctx = Context::default();
|
|
ctx.reentry_target.store(other as usize, Ordering::SeqCst);
|
|
let mut cfg = config(&mut ctx);
|
|
cfg.on_state = Some(on_state_probes_global_reentry);
|
|
let mut core = ptr::null_mut();
|
|
assert_eq!(unsafe { verse_core_create_v1(&cfg, &mut core) }, OK);
|
|
register(core, &mut ctx);
|
|
assert_eq!(connect(core, MANIFEST, CREDENTIAL), OK);
|
|
wait_for(&ctx, |states| states.contains(&STATE_CANCELLED));
|
|
assert_eq!(
|
|
ctx.reentry_results
|
|
.lock()
|
|
.expect("event results")
|
|
.as_slice(),
|
|
[ABI_V1, REENTRANT, REENTRANT, REENTRANT, REENTRANT, REENTRANT, REENTRANT, OK]
|
|
);
|
|
|
|
assert_eq!(destroy(core), OK);
|
|
assert_eq!(connect(other, MANIFEST, CREDENTIAL), OK);
|
|
assert_eq!(destroy(other), OK);
|
|
}
|
|
|
|
#[test]
|
|
fn signer_statuses_are_purpose_specific_and_unknown_values_are_internal() {
|
|
for (admission, expected) in [
|
|
(OK, OK),
|
|
(AUTHORITY_REJECTED, AUTHORITY_REJECTED),
|
|
(CANCELLED, CANCELLED),
|
|
(INTERNAL, INTERNAL),
|
|
(TLS, INTERNAL),
|
|
(u32::MAX, INTERNAL),
|
|
] {
|
|
let mut ctx = Context::default();
|
|
ctx.reentry_cancel.store(OK, Ordering::SeqCst);
|
|
ctx.admission_status.store(admission, Ordering::SeqCst);
|
|
let core = create(&mut ctx);
|
|
assert_eq!(connect(core, MANIFEST, CREDENTIAL), expected);
|
|
assert_eq!(destroy(core), OK);
|
|
}
|
|
|
|
for (tls, expected) in [
|
|
(OK, OK),
|
|
(TLS, TLS),
|
|
(CANCELLED, CANCELLED),
|
|
(INTERNAL, INTERNAL),
|
|
(AUTHORITY_REJECTED, INTERNAL),
|
|
(u32::MAX, INTERNAL),
|
|
] {
|
|
let mut ctx = Context::default();
|
|
ctx.reentry_cancel.store(OK, Ordering::SeqCst);
|
|
ctx.tls_status.store(tls, Ordering::SeqCst);
|
|
let core = create(&mut ctx);
|
|
assert_eq!(connect(core, MANIFEST, CREDENTIAL), expected);
|
|
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);
|
|
register(core, &mut ctx);
|
|
|
|
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_mode(core, "slow-input", MANIFEST, CREDENTIAL), OK);
|
|
let event = keyboard_event();
|
|
let started = Instant::now();
|
|
let mut sent = 0;
|
|
let saturated = loop {
|
|
match unsafe { verse_core_send_input_v1(core, &event) } {
|
|
OK => sent += 1,
|
|
QUEUE_FULL => break true,
|
|
status => panic!("unexpected input status {status}"),
|
|
}
|
|
if sent == 10_000 || started.elapsed() > Duration::from_secs(1) {
|
|
break false;
|
|
}
|
|
};
|
|
assert!(saturated, "real slow consumer did not expose bounded queue");
|
|
assert!(started.elapsed() < Duration::from_secs(1));
|
|
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);
|
|
assert_eq!(ctx.states.lock().expect("states").len(), count_at_timeout);
|
|
}
|
|
|
|
#[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);
|
|
}
|