Files
codex/codex-rs/user-verification/src/native_operation_tests.rs
T
ben zhang 815ef9bbc4 Fix cancellation of macOS user-verification prompts (#53790)
## Why

Keychain signing reconstructs its own authentication context, so invalidating the caller's context does not reliably cancel the context that owns the biometric prompt.

## What changed

Evaluate the stored key's access-control policy explicitly with `LAContext` before signing. On cancellation, invalidate that context and wait for its authorization callback before releasing the lifecycle lock, rejecting any late success. Reload the key with the authorized context and disable further interaction so signing cannot open another prompt.

## Testing

Update unit tests to cover waiting for cancellation completion and rejecting completed authorization after cancellation. Add an ignored macOS hardware test and setup instructions covering persisted Secure Enclave key reuse, signature verification, cancellation, retry signing, and cleanup.

GitOrigin-RevId: 4ea9756430c5649654e95e047d1666617503072c
2026-10-13 22:40:57 +00:00

71 lines
2.5 KiB
Rust

//! Exercises cancellation completion and late-result rejection without biometric hardware.
use super::*;
use crate::UserVerificationCancellationReason;
use pretty_assertions::assert_eq;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
#[test]
fn cancellation_waits_for_completion_on_the_context_owner_thread() {
let guard = UserVerificationRequestGuard::default();
let queued = guard.clone();
let (done_tx, done_rx) = mpsc::sync_channel(0);
let (cancelled_tx, cancelled_rx) = mpsc::channel();
let owner = std::thread::spawn(move || {
let owner_thread = std::thread::current().id();
wait_with_cancellation(&queued, done_rx, || {
assert_eq!(std::thread::current().id(), owner_thread);
cancelled_tx.send(()).expect("dismiss authentication");
})
});
guard.cancel();
cancelled_rx
.recv_timeout(Duration::from_secs(/*secs*/ 5))
.expect("authentication invalidated");
assert!(!owner.is_finished());
done_tx
.send(Ok("late authorization"))
.expect("complete authentication");
assert_eq!(
owner.join().expect("owner returned"),
Err(UserVerificationError::Cancelled {
reason: UserVerificationCancellationReason::Interrupted,
message: "the verification operation is no longer active".to_string(),
})
);
}
#[test]
fn completed_operation_does_not_cancel_its_context() {
let cancelled = AtomicBool::new(/*v*/ false);
let (done_tx, done_rx) = mpsc::channel();
done_tx
.send(Ok("authorization"))
.expect("complete authentication");
assert_eq!(
wait_with_cancellation(&UserVerificationRequestGuard::default(), done_rx, || {
cancelled.store(/*val*/ true, Ordering::Release)
},),
Ok("authorization")
);
assert!(!cancelled.load(Ordering::Acquire));
}
#[test]
fn cancellation_rejects_already_completed_authorization() {
let guard = UserVerificationRequestGuard::default();
let (done_tx, done_rx) = mpsc::channel();
done_tx.send(Ok(())).expect("complete authentication");
guard.cancel();
assert_eq!(
wait_with_cancellation(&guard, done_rx, || panic!(
"authentication already completed"
)),
Err(UserVerificationError::Cancelled {
reason: UserVerificationCancellationReason::Interrupted,
message: "the verification operation is no longer active".to_string(),
})
);
}