fix(core): harden ABI cancellation contracts

This commit is contained in:
sechmachine
2026-08-12 15:43:20 +07:00
parent 67510b65b4
commit 72b3c54ed9
3 changed files with 561 additions and 128 deletions
+154 -59
View File
@@ -9,7 +9,7 @@ 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::sync::{Condvar, Mutex};
use std::thread;
use std::time::{Duration, Instant};
@@ -18,10 +18,13 @@ 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;
@@ -148,6 +151,8 @@ 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>>,
@@ -160,6 +165,8 @@ struct Context {
callback_active: AtomicUsize,
callback_max: AtomicUsize,
cancel_on_connecting: AtomicBool,
reentry_target: AtomicUsize,
reentry_results: Mutex<Vec<u32>>,
}
impl Default for Context {
@@ -168,6 +175,8 @@ impl Default for Context {
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()),
@@ -180,6 +189,8 @@ impl Default for Context {
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()),
}
}
}
@@ -197,7 +208,7 @@ unsafe extern "C" fn sign_admission(raw: *mut c_void, input: BytesView, output:
*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
ctx.admission_status.load(Ordering::SeqCst)
}
unsafe extern "C" fn sign_tls(raw: *mut c_void, input: BytesView, output: *mut u8) -> u32 {
@@ -208,7 +219,57 @@ unsafe extern "C" fn sign_tls(raw: *mut c_void, input: BytesView, output: *mut u
*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
ctx.tls_status.load(Ordering::SeqCst)
}
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) {
@@ -561,6 +622,96 @@ fn signer_callbacks_cannot_reenter_even_cancel() {
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_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);
ctx.core.store(core as usize, Ordering::SeqCst);
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!(connect(other, MANIFEST, CREDENTIAL), OK);
assert_eq!(destroy(core), 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);
ctx.core.store(core as usize, Ordering::SeqCst);
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!(connect(other, MANIFEST, CREDENTIAL), OK);
assert_eq!(destroy(core), 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();
@@ -616,65 +767,9 @@ fn destroy_timeout_keeps_ownership_suppresses_late_callbacks_and_allows_retry()
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();