Watch
1
0
Fork
You've already forked souveraine
0

sessiond: the wake latch is per button, not one field for all of them

An audit found the fix from earlier tonight reintroduced through a
second button. A tap never resolves on the UP edge — the recogniser
returns None and the tap lands when the 300 ms multi-tap window closes
on a later tick, up to about 1.3 s after the press — so a single shared
latch had to survive that whole gap, and any other button's DOWN edge
inside it overwrote the answer.

Concretely: power wakes a dark phone, the volume rocker under the same
grip gets nudged within the second, and the pending power tap resolves
as "the panel was lit" and blanks the screen. That is exactly the
regression 8c7567e was written to fix.

`buttons` is already keyed per button so they cannot interfere; this
latch had reintroduced the coupling one level up. The test fails without
the fix — checked by reverting it.
This commit is contained in:
Fimeg 2026-08-03 21:12:36 -04:00
commit 133daa94ef

View file

@ -1397,7 +1397,7 @@ pub struct DeviceStateMachine {
/// makes this the sixth edge-versus-level bug here, after `locked_ack`,
/// `ChargeRate`, `bootBloomActive`, `hasLoginctl` and the dormant OR in the
/// compositor's `disclosure_locked`.
press_began_dark: bool,
press_began_dark: std::collections::HashMap<Button, bool>,
/// True once a blank has been asked for, so we ask exactly once per wake
/// instead of every tick.
blank_requested: bool,
@ -1515,7 +1515,7 @@ impl DeviceStateMachine {
observed_since: None,
// Boot comes up lit, so no press is in flight and the latch would
// only ever be read after a real DOWN edge has set it.
press_began_dark: false,
press_began_dark: std::collections::HashMap::new(),
dimmed: false,
pending_blank: None,
brightness_before_dim: None,
@ -1581,7 +1581,20 @@ impl DeviceStateMachine {
// is what lights the panel, so this is the last moment the answer is
// still about the screen the user actually pressed against.
if edge == ButtonEdge::Down {
self.press_began_dark = !self.panel_on;
// Per button, not one field for all of them.
//
// A tap never resolves on the UP edge — `ButtonRecognizer::up`
// returns `None` and the tap lands when the 300 ms multi-tap window
// closes on a later tick, up to about 1.3 s after the press. A
// single shared latch had to survive that whole gap, and any other
// button's DOWN edge inside it overwrote the answer: power wakes a
// dark phone, the volume rocker beside it gets nudged within the
// second, and the pending power tap resolves as "the panel was lit"
// and blanks the screen. That is the exact regression the latch was
// added to fix, coming back through a second button.
//
// `buttons` is keyed per button for this reason already.
self.press_began_dark.insert(button, !self.panel_on);
}
let rec = self.buttons.entry(button).or_default();
let gesture = match edge {
@ -1658,9 +1671,9 @@ impl DeviceStateMachine {
if button != Button::Power || gesture != ButtonGesture::Tap {
return Vec::new();
}
// The panel as it was when the press began, not as it is now. See
// `press_began_dark`: this same press already woke it.
if self.press_began_dark {
// The panel as it was when THIS button's press began, not as it is
// now. See `press_began_dark`: this same press already woke it.
if self.press_began_dark.remove(&button).unwrap_or(!self.panel_on) {
// Dark: the tap is a wake, and a power-button wake is never vetoed.
//
// `Unblank` *then* `Restore`, and the order is the whole of it: the
@ -3260,6 +3273,29 @@ mod tests {
);
}
#[test]
fn a_volume_nudge_does_not_make_the_power_tap_blank_the_screen() {
// The latch used to be one field for every button. A tap resolves on
// the multi-tap window closing, up to ~1.3 s after the press, so any
// other button's DOWN edge inside that gap overwrote it — and on a
// Pixel 3 the volume rocker is under the same grip as power.
let (mut sm, t0) = unlocked_and_lit();
sm.set_panel(false);
sm.button_edge_at(Button::Power, ButtonEdge::Down, t0);
sm.set_panel(true); // the wake this press caused
sm.button_edge_at(Button::Power, ButtonEdge::Up, t0 + Duration::from_millis(80));
// The rocker, well inside the multi-tap window.
sm.button_edge_at(Button::VolumeUp, ButtonEdge::Down, t0 + Duration::from_millis(120));
sm.button_edge_at(Button::VolumeUp, ButtonEdge::Up, t0 + Duration::from_millis(200));
let actions = sm.tick_at(t0 + Duration::from_secs(2));
assert!(
!actions.iter().any(|a| matches!(a, Action::Blank | Action::Lock)),
"the volume press must not decide what the power press meant: {actions:?}"
);
}
#[test]
fn a_press_against_a_lit_panel_still_blanks_it() {
// The other half — without this the fix would simply disable the power