sessiond: refuse half-applied unlocks; log the real sensor source
set_session_locked(false) discarded transition()'s return. There is no Suspending/Asleep -> Active edge, so an unlock reported while asleep left the state put but voided pending_blank, blank_requested and dimmed anyway. Now refused, loudly, with the state intact. update_sensors hardcoded SensorSource::Proximity in the forensic entry, so accel/light/touch readings were written to the trail labelled as proximity. Takes the source now; update_sensors() stays as a proximity wrapper. 47 tests, 1 new.
This commit is contained in:
parent
70a05819e8
commit
bd8a4eca50
2 changed files with 59 additions and 5 deletions
|
|
@ -951,8 +951,27 @@ impl DeviceStateMachine {
|
|||
// it was locked is void: a pending blank was only legitimate because
|
||||
// the screen was a lock screen, and the dim warning belongs to that
|
||||
// same rule.
|
||||
//
|
||||
// But only if the machine can actually leave where it is. There is no
|
||||
// legal Suspending → Active or Asleep → Active edge (waking goes
|
||||
// through Locked), so an unlock reported while the device is asleep is
|
||||
// either a logind race or a genuine impossibility. Voiding lock-screen
|
||||
// intent while the state stays Asleep would leave the two disagreeing,
|
||||
// which is the whole class of bug this function exists to end.
|
||||
if !self.transition(DeviceState::Active) {
|
||||
warn!(
|
||||
"[device-state] logind says unlocked but {:?} has no edge to Active — keeping lock-screen intent",
|
||||
self.state
|
||||
);
|
||||
self.record_error(
|
||||
"device-state",
|
||||
"unlock-refused",
|
||||
&format!("no legal transition from {:?} to Active", self.state),
|
||||
);
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
info!("[device-state] session unlocked (logind) — clearing lock-screen intent");
|
||||
self.transition(DeviceState::Active);
|
||||
self.blank_requested = false;
|
||||
self.pending_blank = None;
|
||||
|
||||
|
|
@ -964,10 +983,15 @@ impl DeviceStateMachine {
|
|||
actions
|
||||
}
|
||||
|
||||
/// Proximity-shaped convenience wrapper.
|
||||
pub fn update_sensors(&mut self, evidence: SensorEvidence) {
|
||||
self.update_sensors_from(SensorSource::Proximity, evidence)
|
||||
}
|
||||
|
||||
/// Update sensor evidence and, if the current state is Locked,
|
||||
/// potentially transition to/from Observed.
|
||||
/// Emits forensic entries for every sensor input.
|
||||
pub fn update_sensors(&mut self, evidence: SensorEvidence) {
|
||||
pub fn update_sensors_from(&mut self, source: SensorSource, evidence: SensorEvidence) {
|
||||
let was_observed = matches!(self.state, DeviceState::Observed);
|
||||
let proximity_near = evidence.proximity_near;
|
||||
let should_be_observed = self.state == DeviceState::Locked && proximity_near;
|
||||
|
|
@ -993,8 +1017,21 @@ impl DeviceStateMachine {
|
|||
};
|
||||
self.forensic.append(
|
||||
ForensicEvent::SensorInput {
|
||||
source: SensorSource::Proximity,
|
||||
value: SensorValue::Near(proximity_near),
|
||||
// The reporting source, not a hardcoded Proximity. Accel,
|
||||
// light and touch readings were all being written to the trail
|
||||
// labelled as proximity, which makes post-hoc reconstruction —
|
||||
// the entire point of the trail — lie about what was measured.
|
||||
source,
|
||||
value: match source {
|
||||
SensorSource::Proximity => SensorValue::Near(proximity_near),
|
||||
SensorSource::Accelerometer => {
|
||||
SensorValue::Moving(self.sensor_evidence.accel_moving)
|
||||
}
|
||||
SensorSource::Light => {
|
||||
SensorValue::Changing(self.sensor_evidence.light_changing)
|
||||
}
|
||||
SensorSource::Touch => SensorValue::Active(self.sensor_evidence.touch_active),
|
||||
},
|
||||
confidence,
|
||||
},
|
||||
snapshot,
|
||||
|
|
@ -1163,6 +1200,23 @@ mod tests {
|
|||
(sm, t0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unlock_reported_while_asleep_is_refused_not_half_applied() {
|
||||
// There is no Asleep → Active edge. Voiding lock-screen intent while
|
||||
// the state stays Asleep would leave the two disagreeing.
|
||||
let mut sm = DeviceStateMachine::new();
|
||||
sm.transition(DeviceState::Locked);
|
||||
sm.transition(DeviceState::Suspending);
|
||||
sm.transition(DeviceState::Asleep);
|
||||
sm.dimmed = true;
|
||||
sm.blank_requested = true;
|
||||
|
||||
assert!(sm.set_session_locked(false).is_empty());
|
||||
assert_eq!(sm.state, DeviceState::Asleep, "state must not move");
|
||||
assert!(sm.dimmed, "intent must not be voided by a refused unlock");
|
||||
assert!(sm.blank_requested);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logind_unlock_releases_the_machine_from_locked() {
|
||||
// The bug this closes, measured on hardware 2026-07-26: the machine
|
||||
|
|
|
|||
|
|
@ -613,7 +613,7 @@ fn handle_request(
|
|||
input.source, input.value, conf, suppress, promote
|
||||
);
|
||||
// Let the state machine evaluate Observed transitions.
|
||||
d.device_state.update_sensors(evidence_copy);
|
||||
d.device_state.update_sensors_from(input.source, evidence_copy);
|
||||
serde_json::json!({
|
||||
"ok": true,
|
||||
"confidence": conf,
|
||||
|
|
|
|||
Loading…
Reference in a new issue