FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

KeyEventManager.handleRawKeyMessage is not exception-safe: one keyboard state desync permanently wedges all key input · Issue #191332 · flutter/flutter · GitHub

KeyEventManager.handleRawKeyMessage is not exception-safe: one keyboard state desync permanently wedges all key input #191332

Description

Summary

KeyEventManager.handleRawKeyMessage drains its pending-event queue without exception safety. A single assertion thrown while dispatching leaves the offending event at the head of the queue forever, so every subsequent key event replays it, throws again, and is never dispatched. All keyboard input in the app dies for the rest of the process.

This is not a report of a new desync trigger — there are plenty of those already (#152391, #101285, #94441, #173951, #92589). It is a report of the amplification: it explains why so many of those issues describe "keyboard input stops working entirely" rather than "one exception was logged". A transient, self-healing state discrepancy is turned into a permanent, unrecoverable one.

The defect

packages/flutter/lib/src/services/hardware_keyboard.dart, in handleRawKeyMessage:

bool handled = true;
if (shouldDispatch) {
  handled = _rawKeyboard.handleRawKeyEvent(rawEvent);

  for (final KeyEvent event in _keyEventsSinceLastMessage) {
    handled = _hardwareKeyboard.handleKeyEvent(event) || handled;   // (1) can throw
  }
  if (_transitMode == KeyDataTransitMode.rawKeyData) {
    assert(setEquals(...));                                          // (2) can throw
  }

  handled = _dispatchKeyMessage(_keyEventsSinceLastMessage, rawEvent) || handled;
  _keyEventsSinceLastMessage.clear();                                // never reached
}

return <String, dynamic>{'handled': handled};                        // never reached

HardwareKeyboard.handleKeyEvent calls _assertEventIsRegular as its first act, before recording the event and before _dispatchKeyEvent. So when the pressed-key record has drifted from the platform's — which ui.KeyData.synthesized documents as expected when "some key downs or ups might be lost when the window loses focus" — the throw at (1) skips the clear().

From that point:

  1. _keyEventsSinceLastMessage still holds the poisoned event at index 0.
  2. Every later key message appends to that list and then re-runs the loop, hitting the same event first and throwing again.
  3. _dispatchKeyMessage is never reached, so Focus, Shortcuts and EditableText receive nothing.
  4. {'handled': ...} is never returned to the embedder.

The embedder's own repair also cannot land. handleKeyData dispatches a synthesized event immediately only while the queue is empty:

if (data.synthesized && _keyEventsSinceLastMessage.isEmpty) {
  _hardwareKeyboard.handleKeyEvent(event);
  _dispatchKeyMessage(<KeyEvent>[event], null);
} else {
  _keyEventsSinceLastMessage.add(event);
}

A non-empty queue is exactly what the wedge guarantees, so the synthesized key-up that would have resynchronised the state is queued behind the event it was meant to fix. Nothing ever clears _pressedKeys outside clearState(), which is @visibleForTesting, so there is no recovery short of restarting the app.

Steps to reproduce

Any state desync will do; the amplification is independent of how the desync arose. On macOS the reliable path is the one from #89748 — type while a hot restart is still processing.

  1. Run any app with a TextField on macOS (debug).
  2. Hot restart and type into the field before the restart finishes.
  3. Keep typing after it settles.

Expected

The desync is reported once. Typing continues, and the embedder's synthesized event resynchronises the state.

Actual

The same KeyDownEvent — identical hashCode, identical timeStamp — is re-reported on every subsequent key message, and no keystroke reaches any field again. From a production log, one event replaying across 16 seconds of typing:

A KeyDownEvent is dispatched, but the state shows that the physical key is already pressed. ...
This was the event: KeyDownEvent#386e3(physicalKey: PhysicalKeyboardKey#91fcd(usbHidUsage: "0x0007000c",
debugName: "Key I"), logicalKey: ..., character: "i", timeStamp: 376:31:44.172972)
'package:flutter/src/services/hardware_keyboard.dart':
Failed assertion: line 516 pos 11: '!_pressedKeys.containsKey(event.physicalKey)'

#2  HardwareKeyboard._assertEventIsRegular (hardware_keyboard.dart:516:11)
#3  HardwareKeyboard._assertEventIsRegular (hardware_keyboard.dart:536:6)
#4  HardwareKeyboard.handleKeyEvent (hardware_keyboard.dart:660:5)
#5  KeyEventManager.handleRawKeyMessage (hardware_keyboard.dart:1185:37)

The frozen timeStamp across many seconds is the tell: it is one event being replayed, not many keystrokes failing.

Proposed fix

Drain in a finally, so one bad event costs one dropped keystroke instead of the session:

try {
  for (final KeyEvent event in _keyEventsSinceLastMessage) {
    handled = _hardwareKeyboard.handleKeyEvent(event) || handled;
  }
  ...
  handled = _dispatchKeyMessage(_keyEventsSinceLastMessage, rawEvent) || handled;
} finally {
  _keyEventsSinceLastMessage.clear();
}

With the queue drained, the next synthesized event from the embedder meets the isEmpty condition in handleKeyData and resynchronises _pressedKeys on its own, which is what the existing design already intends.

This is debug-only in effect, since _assertEventIsRegular is inside an assert — but the debug loop is where it hurts, and it makes every desync report in the tracker far worse than the underlying discrepancy.

Version

Flutter 3.38.9 • channel stable
Framework • revision 67323de285 • 2026-01-28
Engine • hash 5eb06b7ad5bb8cbc22c5230264c7a00ceac7674b (revision 587c18f873)
Tools • Dart 3.10.8 • DevTools 2.51.1

Verified identical in 3.27.3, so this is long-standing rather than a recent regression.

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs repro infoAutomated crash report whose cause isn't yet knownwaiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions


    Back | FazBrowse Home | New Git URL