From a4285496fb42eb984ae8a40ac7c19a7db163652a Mon Sep 17 00:00:00 2001 From: c4ch3c4d3 Date: Tue, 18 Aug 2026 19:32:49 -0500 Subject: [PATCH] fix: health monitor disables SSID pool immediately on PING failure Ring-buffer SIGSEGV counts were unreliable for growth detection; the pool broadcast is the only known crash cause, so disable it on first failure. --- .../user/remote_access/pager-webui/server.py | 18 ++++++++++-------- tests/test_health.py | 13 ++++++++++--- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/payload/user/remote_access/pager-webui/server.py b/payload/user/remote_access/pager-webui/server.py index 2faeaa0..9cf4842 100644 --- a/payload/user/remote_access/pager-webui/server.py +++ b/payload/user/remote_access/pager-webui/server.py @@ -3205,7 +3205,11 @@ def _monitor_down(name): def health_check(): """One health pass. Returns the health dict. Fix actions are - rate-limited by HEALTH_FIX_COOLDOWN.""" + rate-limited by HEALTH_FIX_COOLDOWN. + + The stock SSID-pool broadcast segfaults pineapd on this firmware; when + pineapd is unreachable the pool broadcast is disabled before restarting, + since it is the only known crash cause.""" h = _health rc, out, err = device_run([HAK5CMD, 'PING'], timeout=10) h['pineap_up'] = rc == 0 and 'PONG' in (out or '') @@ -3214,20 +3218,18 @@ def health_check(): now = time.time() if now - h['last_fix'] < HEALTH_FIX_COOLDOWN: return dict(h) - count = _sigsegv_count() - if h['sigsegv_last'] is not None and count > h['sigsegv_last']: - # Crash-loop signature: disable the SSID pool broadcast and restart. + pool = _uci_section('pineapd.@ssidpool[0]') + if pool.get('disable') != '1': device_run(['uci', 'set', 'pineapd.@ssidpool[0].disable=1']) device_run(['uci', 'commit', 'pineapd']) - device_run(['/etc/init.d/pineapd', 'restart'], timeout=30) - h['last_action'] = 'pool-disabled + pineapd restart (SIGSEGV crash-loop)' + h['last_action'] = 'SSID pool broadcast disabled (pineapd crash-loop fix)' elif _monitor_down('wlan1mon'): device_run(['ip', 'link', 'set', 'wlan1mon', 'up'], timeout=10) h['last_action'] = 'wlan1mon brought up' else: - device_run(['/etc/init.d/pineapd', 'restart'], timeout=30) h['last_action'] = 'pineapd restart' - h['sigsegv_last'] = count + device_run(['/etc/init.d/pineapd', 'restart'], timeout=30) + h['sigsegv_last'] = _sigsegv_count() h['last_fix'] = now h['fixes'] += 1 return dict(h) diff --git a/tests/test_health.py b/tests/test_health.py index 79eceac..b292faf 100644 --- a/tests/test_health.py +++ b/tests/test_health.py @@ -49,15 +49,22 @@ class HealthCheckTest(unittest.TestCase): def test_down_with_growing_sigsegv_disables_pool(self): self.ping_ok = False self.sigsegs = 5 - server._health['sigsegv_last'] = 3 result = server.health_check() - self.assertIn('pool-disabled', result['last_action']) + self.assertIn('pool broadcast disabled', result['last_action']) self.assertEqual(self.uci_state['pineapd.@ssidpool[0].disable'], '1') self.assertIn(['/etc/init.d/pineapd', 'restart'], [r[0] for r in self.runs]) self.assertEqual(result['fixes'], 1) + def test_down_with_pool_already_disabled_restarts_pineapd(self): + self.ping_ok = False + self.uci_state['pineapd.@ssidpool[0].disable'] = '1' + 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_without_crash_brings_monitor_up(self): self.ping_ok = False + self.uci_state['pineapd.@ssidpool[0].disable'] = '1' self.iw_out = 'wlan0mon\n' result = server.health_check() self.assertEqual(result['last_action'], 'wlan1mon brought up') @@ -67,7 +74,7 @@ class HealthCheckTest(unittest.TestCase): self.ping_ok = False server._health['sigsegv_last'] = 4 result = server.health_check() - self.assertEqual(result['last_action'], 'pineapd restart') + self.assertIn('pineapd restart', result['last_action']) self.assertIn(['/etc/init.d/pineapd', 'restart'], [r[0] for r in self.runs]) def test_fix_cooldown_prevents_thrash(self):