Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 32 additions & 13 deletions drivers/soc/apple/sbio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1653,7 +1653,7 @@ impl SepData {
dev_err!(self.dev, "verify: could not start the capture ({:?})\n", e);
return bio::VerifyOutcome::Failed(ENROL_STATUS_SENSOR);
}
let advertised = match self.await_capture() {
let advertised = match self.await_capture(CapturePurpose::Verification) {
CaptureWait::Ready(n) => n,
CaptureWait::Timeout => {
dev_warn!(self.dev, "verify: the capture ended without a frame\n");
Expand Down Expand Up @@ -2712,7 +2712,7 @@ impl SepData {
return ImageOutcome::Failed(ENROL_STATUS_SENSOR);
}

let available = match self.await_capture() {
let available = match self.await_capture(CapturePurpose::Enrollment) {
CaptureWait::Ready(n) => n,
CaptureWait::Timeout => {
dev_warn!(self.dev, "enrol: capture {} ended without a frame\n", counter);
Expand Down Expand Up @@ -2846,7 +2846,7 @@ impl SepData {
}
}

fn await_capture(&self) -> CaptureWait {
fn await_capture(&self, purpose: CapturePurpose) -> CaptureWait {
let mut previous: Option<[u8; sensor::STATUS_LEN]> = None;
let mut armed_reported = false;

Expand All @@ -2865,9 +2865,13 @@ impl SepData {
// attempt budget would then be spent in milliseconds and time the
// capture out before the frame lands. Time-bounding degrades, at worst,
// to the same duration as the poll path.
let deadline_ns = crate::shim::monotonic_ns().saturating_add(
u64::from(ENROL_POLL_ATTEMPTS) * u64::from(ENROL_POLL_MS) * 1_000_000,
let timeout_ns = u64::from(ENROL_POLL_ATTEMPTS) * u64::from(ENROL_POLL_MS) * 1_000_000;
let started_ns = crate::shim::monotonic_ns();
let mut deadline_ns = started_ns.saturating_add(timeout_ns);
let verification_deadline_ns = started_ns.saturating_add(
u64::from(VERIFY_CAPTURE_TIMEOUT_MS) * 1_000_000,
);
let mut waiting_for_finger = matches!(purpose, CapturePurpose::Verification);
let mut attempt: u32 = 0;
loop {
if !bio::capture_is_live(&self.bio_session.lock(), self.capture_epoch.load(Relaxed)) {
Expand All @@ -2881,6 +2885,12 @@ impl SepData {
}
};

// Cancellation or suspend may have happened during the SPI read.
// Do not carry even a complete frame into the old operation.
if !bio::capture_is_live(&self.bio_session.lock(), self.capture_epoch.load(Relaxed)) {
return CaptureWait::Abandon;
}

if attempt == 0 || previous != Some(st.raw) {
previous = Some(st.raw);
}
Expand All @@ -2902,9 +2912,24 @@ impl SepData {
}
}

if st.state == sensor::STATE_ARMED && !armed_reported {
armed_reported = true;
// Keep one idle verification armed longer than enrollment, but
// retain an absolute upper bound even if its caller never cancels.
// A transition out of ARMED still ends the idle extension.
let now_ns = crate::shim::monotonic_ns();
if matches!(purpose, CapturePurpose::Verification) && now_ns >= verification_deadline_ns {
return CaptureWait::Timeout;
}
// A late ARMED report or frame cannot revive an expired arming,
// enrollment or progress budget, even if SPI consumed the time.
if now_ns >= deadline_ns {
return CaptureWait::Timeout;
}
if waiting_for_finger && st.state == sensor::STATE_ARMED {
deadline_ns = now_ns.saturating_add(timeout_ns);
} else if st.state == sensor::STATE_READING || armed_reported {
waiting_for_finger = false;
}
armed_reported |= st.state == sensor::STATE_ARMED;

if let sensor::Offset12::Available(count) = st.offset12() {
if count == 0 {
Expand All @@ -2913,10 +2938,6 @@ impl SepData {
return CaptureWait::Ready(count);
}

if crate::shim::monotonic_ns() >= deadline_ns {
break;
}

if use_irq {
// Block on the data-ready line; the interrupt wakes this the
// instant a frame is ready, and the backstop bounds a missed
Expand All @@ -2928,8 +2949,6 @@ impl SepData {
}
attempt = attempt.saturating_add(1);
}
let _ = sensor::status();
CaptureWait::Timeout
}

pub(crate) fn on_sbio(&self, msg: Message) {
Expand Down
8 changes: 8 additions & 0 deletions drivers/soc/apple/sep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,11 @@ enum CaptureWait {
Abandon,
}

enum CapturePurpose {
Enrollment,
Verification,
}

const ENROL_STATUS_SENSOR: u32 = 1;
const ENROL_STATUS_ENCLAVE: u32 = 2;
const ENROL_STATUS_TOO_MANY: u32 = 3;
Expand Down Expand Up @@ -434,6 +439,9 @@ const ENROL_POLL_MS: u32 = 2;
const ENROL_IRQ_WAIT_MS: u32 = 250;

const ENROL_CAPTURE_TIMEOUT_MS: u32 = 60_000;
// A forgotten userspace verifier must not retain one capture indefinitely.
// Five minutes preserves a long idle prompt without the old minute-long churn.
const VERIFY_CAPTURE_TIMEOUT_MS: u32 = 300_000;
const ENROL_POLL_ATTEMPTS: u32 = ENROL_CAPTURE_TIMEOUT_MS / ENROL_POLL_MS;
static_assert!(ENROL_POLL_ATTEMPTS * ENROL_POLL_MS == ENROL_CAPTURE_TIMEOUT_MS);

Expand Down
18 changes: 18 additions & 0 deletions tools/aurora-ci/host-tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -2271,9 +2271,27 @@
"{output}"
],
"note": "Build only the host Kconfig tool in an isolated output directory; check all built-in/module dependency combinations."
},
{
"name": "tools/aurora-sep/tests/test_capture_wait.py",
"group": "aurora-sep",
"argv": [
"python3",
"-m",
"unittest",
"discover",
"-s",
"tools/aurora-sep/tests",
"-p",
"test_capture_wait.py",
"-v"
],
"rust": "1.85.0",
"timeout": 120
}
],
"helpers": {
"tools/aurora-sep/tests/capture-wait.rs": "Synthetic time/SPI/IRQ leaves and behavioral cases compiled with production await_capture by test_capture_wait.py.",
"tools/testing/selftests/drivers/gpu/apple-dcp/run.py": "the implementation behind tools/testing/selftests/drivers/gpu/apple-dcp/run.sh, which is listed",
"tools/testing/selftests/drivers/gpu/asahi-g15/uat-owner.c": "compiled and executed by the registered asahi-g15/run.py actual reservation-owner fixture",
"tools/testing/selftests/drivers/gpu/asahi-g15/pmp-bridge.c": "compiled and executed by the registered asahi-g15/run.py maintained PMP fixture",
Expand Down
23 changes: 23 additions & 0 deletions tools/aurora-sep/CAPTURE-WAIT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Bounded idle verification

An idle verification no longer tears down a healthy ARMED capture every 60 seconds. It can wait for a finger within a fixed five-minute deadline measured from the start of `await_capture`. The deadline is not refreshed by ARMED reports, transitions, IRQs or a late frame. Cancellation or suspend still invalidates the operation immediately at the next session check.

Five minutes is a proposed host policy: it leaves margin beyond the earlier combined candidate's attended 185-second idle wait, while bounding a caller that remains alive but never cancels. It is not a measured SEP nonce lifetime or a claim that a sensor remains reliable or power-efficient for five minutes. The earlier combined result does not qualify this changed branch; maintainers can choose a different finite bound after reviewing the caller policy.

Enrollment retains its absolute 60-second deadline. Verification also retains the existing 60-second bound while initially arming and after capture progress begins or an armed sensor leaves ARMED. Returning to ARMED cannot restart that progress budget. The five-minute limit overrides it if sooner, including when a status read returns a ready frame at or after the limit. Deadline expiry is now checked before any ARMED renewal or ready-frame acceptance. This tightens the old boundary ordering: a first ARMED report at or after the arming deadline cannot revive it, and a late frame cannot bypass an expired enrollment or progress deadline. A zero-length frame, patch loss or status failure retains its existing outcome. Frame processing, nonce checks, persistence and settling delays are unchanged.

## Caller lifetime

`pam_fprintd` supplies an independent timeout, 30 seconds by default, and calls D-Bus `VerifyStop` when its verification window expires. The Omarchy sudo and polkit entrypoints use `pam_fprintd.so` without a timeout override, so their ordinary no-touch wait remains governed by PAM. Administrators may change that setting, including disabling its timeout. This kernel patch does not change PAM policy or promise password fallback for every administrator-defined stack.

The Omarchy lock uses a PAM context for fingerprint verification, aborts that context on unlock or its reachability watchdog, and may schedule a new attempt after a completed one. A longer-lived lock prompt can therefore contain multiple bounded attempts. The kernel limit applies to one capture, not to the lifetime of that UI.

In fprintd, `VerifyStop`, release and disappearance of the claiming D-Bus client cancel the underlying operation. The Aurora libfprint driver's cancellation callback issues `AURORA_BIO_CANCEL`, and closing its device description also cancels the kernel session. A kernel capture timeout is a terminal `AURORA_BIO_STATUS_TIMEOUT`, not the retry status that fprintd automatically restarts. A custom client that stays connected and never cancels can no longer hold one kernel capture forever; it still owns its D-Bus claim until it releases it or disconnects. The fprintd `--no-timeout` service option concerns idle daemon exit, not the PAM verification deadline.

The wait checks session validity before and after each SPI status read. IRQ mode has a 250 ms backstop; polling mode sleeps 2 ms between reads. These bound the interval between checks when hardware calls return, not a malfunctioning SPI transaction. The patch does not establish a hard deadline for a blocked transport call.

## Offline verification

`python3 -m unittest discover -s tools/aurora-sep/tests -p test_capture_wait.py -v` compiles the production wait method with synthetic clocks and hardware leaves. Thirty-one Rust cases cover delayed touch beyond 185 seconds, both wait modes, enrollment and progress bounds, fixed verification expiry, frames just before and at expiry, a status read crossing expiry, cancellation priority, epoch changes, transport errors and spurious IRQs. Additional exact/just-before-boundary cases cover initial arming, enrollment and progress, including status reads crossing their deadlines. Four compiled negative controls restore the old minute-long idle timeout, remove cancellation checks, remove the fixed verification bound and move progress expiry behind renewal/frame acceptance; each must fail its named behavioral assertion.

These are host tests. Physical five-minute expiry, idle power, firmware or nonce lifetime, and actual suspend behavior on this changed branch remain unqualified. No sensor is opened by the tests.
Loading