From 72dbb2ecd0f72dd4f9b9ec47aec228e287103185 Mon Sep 17 00:00:00 2001 From: Me Here Date: Thu, 4 Jun 2026 11:21:06 -0500 Subject: [PATCH] pm-grid: hardening - bound the USB-MIDI TX queue + defensive set-list guard Audit for panic/brick risks (a panic = black screen on this device): - sx_send (live-sync broadcasts + 5s heartbeat) pushed to tx_q with no cap. If the editor disconnects without a BYE while sync_armed and nothing drains MIDI-IN, tx_q grows unbounded -> heap exhaustion -> brick. Now drops messages when tx_q > 256 (the heartbeat re-syncs when the host returns). Notes/clock were already capped. - build_setlists now drops empty set lists, so load()/next_track() can never hit a `% 0`. (parse guarantees >=1 lane; built-ins/parsed lists are non-empty, this is belt-and-suspenders.) Other unwrap()s are boot-time peripheral init; lanes[0]/items[0]/step[0] are all safe (parse substitutes beep:4 for empty programs; built-ins lead the list). Co-Authored-By: Claude Opus 4.8 (1M context) --- rust/pm-grid/src/main.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/rust/pm-grid/src/main.rs b/rust/pm-grid/src/main.rs index 2e5c2a9..151daf0 100644 --- a/rust/pm-grid/src/main.rs +++ b/rust/pm-grid/src/main.rs @@ -216,6 +216,7 @@ fn build_setlists(user: Vec) -> Vec { }) .collect(); v.extend(user); + v.retain(|s| !s.items.is_empty()); // never keep an empty set list (would `% 0` in load/next) v } @@ -946,6 +947,12 @@ impl App { /// Build `F0 7D F7`, packetize into 4-byte USB-MIDI events, queue them. fn sx_send(&mut self, op: u8, text: &str) { + // Bound the TX queue: if the host stopped draining MIDI-IN (e.g. editor closed without a BYE + // while sync_armed), drop the message rather than grow the heap forever. The 5 s heartbeat + // re-syncs once the host returns. + if self.tx_q.len() > 256 { + return; + } let mut f: Vec = Vec::with_capacity(text.len() + 4); f.push(0xF0); f.push(0x7D);