UART identity: never match a clone-serial sibling during reconnect (v0.31.9)#115
Conversation
There was a problem hiding this comment.
Code Review
This pull request addresses a bug where UART reconnection could incorrectly attach to a look-alike sibling adapter sharing a clone serial. It introduces a uniqueness check for USB serials, recording duplicate serials as null to force resolution via the physical port path. Additionally, unit tests are added to cover clone-serial scenarios, and the project version is bumped to 0.31.8. Feedback suggests wrapping the sysfs directory iteration in a try-except OSError block to prevent crashes in environments where /sys/class/tty is inaccessible.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| seen = set() | ||
| for tty_dev in _SYS_TTY.iterdir(): | ||
| if not tty_dev.name.startswith(("ttyUSB", "ttyACM")): | ||
| continue | ||
| usb_dir = _usb_device_dir_for_tty(tty_dev) | ||
| if usb_dir is None or usb_dir.name == own_dir_name or usb_dir.name in seen: | ||
| continue | ||
| seen.add(usb_dir.name) | ||
| if ((_read_sysfs_text(usb_dir / "idVendor") or "").lower() == vid | ||
| and (_read_sysfs_text(usb_dir / "idProduct") or "").lower() == pid | ||
| and _read_sysfs_text(usb_dir / "serial") == serial): | ||
| return False | ||
| return True |
There was a problem hiding this comment.
In environments where /sys/class/tty is missing, restricted, or inaccessible (such as non-Linux development environments or highly sandboxed containers), calling _SYS_TTY.iterdir() can raise OSError (e.g., FileNotFoundError or PermissionError). Wrapping the directory iteration in a try-except OSError block ensures the function fails gracefully and returns True (assuming the serial is unique) rather than crashing the application.
| seen = set() | |
| for tty_dev in _SYS_TTY.iterdir(): | |
| if not tty_dev.name.startswith(("ttyUSB", "ttyACM")): | |
| continue | |
| usb_dir = _usb_device_dir_for_tty(tty_dev) | |
| if usb_dir is None or usb_dir.name == own_dir_name or usb_dir.name in seen: | |
| continue | |
| seen.add(usb_dir.name) | |
| if ((_read_sysfs_text(usb_dir / "idVendor") or "").lower() == vid | |
| and (_read_sysfs_text(usb_dir / "idProduct") or "").lower() == pid | |
| and _read_sysfs_text(usb_dir / "serial") == serial): | |
| return False | |
| return True | |
| seen = set() | |
| try: | |
| for tty_dev in _SYS_TTY.iterdir(): | |
| if not tty_dev.name.startswith(("ttyUSB", "ttyACM")): | |
| continue | |
| usb_dir = _usb_device_dir_for_tty(tty_dev) | |
| if usb_dir is None or usb_dir.name == own_dir_name or usb_dir.name in seen: | |
| continue | |
| seen.add(usb_dir.name) | |
| if ((_read_sysfs_text(usb_dir / "idVendor") or "").lower() == vid | |
| and (_read_sysfs_text(usb_dir / "idProduct") or "").lower() == pid | |
| and _read_sysfs_text(usb_dir / "serial") == serial): | |
| return False | |
| except OSError: | |
| pass | |
| return True |
Many USB-serial adapters ship with a non-unique programmed serial (e.g. CP210x units all reading "0001"). With several such adapters on one box, a v0.31.5 reconnect could resolve a dropped device's identity to a look-alike sibling while the real device was still off the bus - the healed session came back attached to the wrong hardware (found during field validation when a session landed on a different device's console). Two changes in serial_id: - resolve_identity: a serial matched by several live devices proves nothing; the physical port is then required, and resolution fails (callers keep retrying) while the true device is absent. - identity_for_tty: a serial shared with another live device is recorded as null at snapshot time, pinning the net to its physical port outright - which also covers resolving a legacy snapshot while exactly one sibling is present. Snapshots taken under v0.31.5 on clone-serial adapters pick up the correction on their next re-save.
ddea8a1 to
1de6761
Compare
Review feedback: _serial_is_unique iterated /sys/class/tty without the missing-sysfs guard every other walker in this module has. The path is unreachable in practice (the caller has already resolved the tty's own device dir through sysfs), but the walk is best-effort by design - an OSError now degrades to assuming the serial is unique instead of propagating.
Problem
Found during field validation of the v0.31.5 UART re-enumeration work, on a bench with several USB-serial adapters that share a non-unique programmed serial (CP210x units all reading
0001). When one of them dropped mid-session, the reconnect resolved the device's identity to a look-alike sibling with the same serial while the real device was still off the bus — the healed session came back attached to the wrong hardware (a different device's console).Root cause
serial_id.resolve_identitymatched by USB serial first and used the physical port only as a tie-break: when the serial matched several live devices but none of them sat on the recorded port (because the true device was absent), it fell back to the first serial match instead of failing.Fix
resolve_identity: a serial matched by several live devices proves nothing — the physical port is then required, and resolution returns None (callers keep retrying with backoff) until the real device returns. A unique serial match still follows the cable across ports as before.identity_for_tty: a serial shared with another live device is recorded as null at snapshot time, pinning the net to its physical port outright. Multi-interface chips (one device, several ttys) are not counted as duplicates. This also covers resolving while exactly one sibling is present.Includes the v0.31.9 release prep (version bump, CHANGELOG, release-notes page, nav).
Validation
serial: nullwith distinct port paths; the previously failing mid-session power-cycle now waits through the gap, reconnects to the correct device, and the DUT answers commands through the healed session — verified against the DUT's own command set.Notes for reviewers
serial_id.pyand its tests): the two documented in UART nets survive USB re-enumeration (v0.31.5) #111 (test_box_authorize.py::test_missing_key_runs_ssh_keygen,test_monitor_state.py::test_missing_protection_falls_back_to_none) plus a new one introduced with 0.31.6:test_lock_state.py::TestAcquire::test_unknown_holder_type_falls_back_to_ephemeralstill asserts the pre-0.31.6 fallback behavior, while 0.31.6 deliberately changed unknown holder types to be preserved verbatim — the test needs updating to match the shipped behavior.