Skip to content

Commit 33704d1

Browse files
committed
firewall blocks signal during read operations
1 parent 0a67a77 commit 33704d1

2 files changed

Lines changed: 60 additions & 1 deletion

File tree

qubesagent/firewall.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,9 @@ def dns_addresses(family=None):
344344
def main(self):
345345
self.terminate_requested = False
346346
self.reload_requested = False
347+
# Block SIGHUP and SIGTERM during all qdb operations to prevent interrupting request-response pairs which corrupts protocol state.
348+
# Signals are only unblocked during read_watch() which is safe to interrupt as it's just waiting, not mid-operation.
349+
signal.pthread_sigmask(signal.SIG_BLOCK, {signal.SIGHUP, signal.SIGTERM})
347350
self.init()
348351
self.run_firewall_dir()
349352
if not self.is_custom_persist_enabled():
@@ -366,11 +369,22 @@ def main(self):
366369
self.handle_addr(source_addr)
367370
self.reload_requested = False
368371
self.sd_notify('READY=1')
372+
# Unblock signals only during read_watch()
373+
signal.pthread_sigmask(signal.SIG_UNBLOCK, {signal.SIGHUP, signal.SIGTERM})
374+
375+
# Re-check flags after unblocking, in case signal arrived
376+
if self.terminate_requested or self.reload_requested:
377+
signal.pthread_sigmask(signal.SIG_BLOCK, {signal.SIGHUP, signal.SIGTERM})
378+
continue
369379
try:
370380
watch_path = self.qdb.read_watch()
371381
except OSError: # EINTR
372-
# signal received, re-check loop condition
382+
# signal received, block signals again and re-check loop condition
383+
signal.pthread_sigmask(signal.SIG_BLOCK, {signal.SIGHUP, signal.SIGTERM})
373384
continue
385+
386+
#Block signals again before doing any qdb work
387+
signal.pthread_sigmask(signal.SIG_BLOCK, {signal.SIGHUP, signal.SIGTERM})
374388

375389
if watch_path is None:
376390
break

qubesagent/test_firewall.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
import qubesagent.firewall
99

10+
import signal
11+
1012

1113
class DummyIptablesRestore(object):
1214
# pylint: disable=too-few-public-methods
@@ -572,3 +574,46 @@ def test_is_blocked(self):
572574

573575
for server in dns_servers_ipv6:
574576
self.assertTrue(self.obj.is_blocked(rules, ("udp", server, "53"), dns))
577+
578+
def test_main_blocks_signals_during_qdb_operations(self):
579+
#Test that signals are blocked during qdb operations and only unblocked during read_watch().
580+
581+
self.obj.qdb.entries['/qubes-firewall/10.137.0.1/policy'] = b'accept'
582+
self.obj.qdb.entries['/connected-ips'] = b''
583+
self.obj.qdb.entries['/connected-ips6'] = b''
584+
585+
# Track sigmask calls
586+
sigmask_calls = []
587+
original_sigmask = signal.pthread_sigmask
588+
589+
def mock_sigmask(how, mask):
590+
sigmask_calls.append((how, mask))
591+
return original_sigmask(how, set()) # Don't actually block
592+
593+
# Make read_watch() terminate the loop after first call
594+
call_count = [0]
595+
def mock_read_watch():
596+
call_count[0] += 1
597+
if call_count[0] == 1:
598+
return '/qubes-firewall/10.137.0.1'
599+
self.obj.terminate_requested = True
600+
raise OSError("Interrupted")
601+
602+
self.obj.qdb.read_watch = mock_read_watch
603+
604+
with patch.object(signal, 'pthread_sigmask', mock_sigmask):
605+
self.obj.main()
606+
607+
# Verify signal blocking pattern:
608+
# 1. SIG_BLOCK at start
609+
# 2. SIG_UNBLOCK before read_watch
610+
# 3. SIG_BLOCK after read_watch (or in except)
611+
612+
block_calls = [c for c in sigmask_calls if c[0] == signal.SIG_BLOCK]
613+
unblock_calls = [c for c in sigmask_calls if c[0] == signal.SIG_UNBLOCK]
614+
615+
self.assertGreater(len(block_calls), 0, "Should have SIG_BLOCK calls")
616+
self.assertGreater(len(unblock_calls), 0, "Should have SIG_UNBLOCK calls")
617+
# First call should be SIG_BLOCK
618+
self.assertEqual(sigmask_calls[0][0], signal.SIG_BLOCK)
619+

0 commit comments

Comments
 (0)