fix(core): harden ABI cancellation contracts
This commit is contained in:
+154
-59
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user