fix: health monitor stabilization pass for all pineapd crash sources

Field-verified crash stack on this firmware: SSID-pool broadcast (segfault),
wlan2mon hopping a nonexistent 6GHz iface (segfault), wlan1mon fast-hopping
6GHz channels (ASIO thread exit), and a large refilled pool. The fix path
now enforces: pool broadcast off, pool list cleared, wlan2mon off, wlan1mon
5GHz-only, wlan0mon 2.4GHz-only — idempotent, so collect refills self-heal.
8-minute continuous-PONG stability verified on-device.
This commit is contained in:
2026-08-18 20:47:43 -05:00
parent c91d970a41
commit b5d9ed39ff
2 changed files with 48 additions and 13 deletions
@@ -3341,6 +3341,39 @@ def _sigsegv_count():
return out.count('SIGSEGV')
def _stabilize_pineapd():
"""Idempotent crash-source pass. Field-verified SIGSEGV/terminate sources
on this firmware:
1. SSID-pool broadcast (segfault, ~15s cadence)
2. wlan2mon: a 6GHz monitor this hardware never creates; hopping the
missing iface segfaults pineapd
3. wlan1mon fast-hopping the 6GHz channel list (ASIO io thread exit)
The pool list itself is also cleared: collect refills it, and a large
pool was observed crashing even with broadcast disabled.
Returns a human-readable description of what was changed."""
actions = []
wanted = {
'pineapd.@ssidpool[0].disable': '1',
'pineapd.wlan2mon.disable': '1',
'pineapd.wlan2mon.hop': '0',
'pineapd.wlan1mon.bands': '5',
'pineapd.wlan0mon.bands': '2',
}
for key, value in wanted.items():
rc, out, err = device_run(['uci', 'get', key])
if rc != 0 or out.strip() != value:
device_run(['uci', 'set', '%s=%s' % (key, value)])
actions.append(key.split('.')[-1])
rc, out, err = device_run(['uci', 'get', 'pineapd.@ssidpool[0].ssid'])
if rc == 0 and out.strip():
device_run(['uci', 'delete', 'pineapd.@ssidpool[0].ssid'])
actions.append('pool-list cleared')
if actions:
device_run(['uci', 'commit', 'pineapd'])
return 'stabilized: ' + ', '.join(actions)
return 'pineapd restart'
def _iface_up(name):
"""True when the interface's admin flags contain UP. Monitor interfaces
report operstate 'unknown' even when usable, so parse `ip link` flags."""
@@ -3383,17 +3416,7 @@ def health_check():
device_run(['ip', 'link', 'set', name, 'up'], timeout=10)
h['last_action'] = 'monitor interfaces brought up'
else:
# wlan2mon is a 6GHz monitor this hardware never materializes; with
# hop enabled pineapd crashes on the missing iface (second SIGSEGV
# source after the SSID pool). Keep it disabled.
wlan2 = _uci_section('pineapd.wlan2mon')
if wlan2.get('disable') != '1':
device_run(['uci', 'set', 'pineapd.wlan2mon.disable=1'])
device_run(['uci', 'set', 'pineapd.wlan2mon.hop=0'])
device_run(['uci', 'commit', 'pineapd'])
h['last_action'] = 'wlan2mon disabled (missing 6GHz iface crash source)'
else:
h['last_action'] = 'pineapd restart'
h['last_action'] = _stabilize_pineapd()
device_run(['/etc/init.d/pineapd', 'restart'], timeout=30)
h['sigsegv_last'] = _sigsegv_count()
h['last_fix'] = now
+14 -2
View File
@@ -66,16 +66,28 @@ class HealthCheckTest(unittest.TestCase):
self.ping_ok = False
self.uci_state['pineapd.@ssidpool[0].disable'] = '1'
self.uci_state['pineapd.wlan2mon.disable'] = '1'
self.uci_state['pineapd.wlan2mon.hop'] = '0'
self.uci_state['pineapd.wlan1mon.bands'] = '5'
self.uci_state['pineapd.wlan0mon.bands'] = '2'
result = server.health_check()
self.assertEqual(result['last_action'], 'pineapd restart')
self.assertIn(['/etc/init.d/pineapd', 'restart'], [r[0] for r in self.runs])
def test_down_disables_wlan2mon_crash_source(self):
def test_down_stabilizes_known_crash_sources(self):
self.ping_ok = False
self.uci_state['pineapd.@ssidpool[0].disable'] = '1'
result = server.health_check()
self.assertEqual(result['last_action'], 'wlan2mon disabled (missing 6GHz iface crash source)')
self.assertEqual(self.uci_state['pineapd.wlan2mon.disable'], '1')
self.assertEqual(self.uci_state['pineapd.wlan1mon.bands'], '5')
self.assertIn('stabilized', result['last_action'])
def test_down_clears_refilled_pool_list(self):
self.ping_ok = False
self.uci_state['pineapd.@ssidpool[0].disable'] = '1'
self.uci_state['pineapd.@ssidpool[0].ssid'] = 'QmVlcg=='
result = server.health_check()
self.assertNotIn('pineapd.@ssidpool[0].ssid', self.uci_state)
self.assertIn('pool-list cleared', result['last_action'])
def test_down_without_crash_brings_monitors_up(self):
self.ping_ok = False