Watch
1
0
Fork
You've already forked souveraine
0

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:
Fimeg 2026-07-26 07:30:06 -04:00
commit bd8a4eca50
2 changed files with 59 additions and 5 deletions

View file

@ -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

View file

@ -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,