gdb: accept vCont C/S actions carrying any signal

A client that resumes while a signal is pending sends a vCont action of the
form 'C<sig>' (continue and deliver) or 'S<sig>' (step and deliver), e.g.
'vCont;S0f:pa410.1996;c:pa410.-1' -- the exact packet reported in issue
#1377. handle_v matched only 'c'/'C05' and 's'/'S05', so any other signal
fell through to an empty reply and clients aborted the session with
'Invalid remote reply:'.

We do not deliver host signals to the guest, so the signal value carries no
meaning for us: accept any of them and carry the action out as a plain
resume or step, which is what the client asked for. This matters more now
that the stub can stop with SIGINT on an async interrupt, since a client
may well resume from such a stop with 'C02'.

Also stop assuming os.exit_code exists when reporting termination: bare-metal
os layers (QlOsMcu) do not define it, which turns the exit path into an
AttributeError (seen in issue #1276). The underlying MCU interrupt handling
of #1276 is out of scope here.

Add a regression test asserting the '#1377' packet is answered with a
SIGTRAP stop-reply and that a signalled continue runs the guest to
termination; both replies are empty without the fix.

Fixes #1377

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
retrocpugeek
2026-08-23 22:15:04 +10:00
co-authored by Claude Opus 5
parent c5a8a9b73e
commit a13a8c8c1f
2 changed files with 62 additions and 5 deletions
+13 -5
View File
@@ -261,8 +261,9 @@ class QlGdb(QlDebugger):
# emulation stopped because it hit a breakpoint
reply = f'S{SIGTRAP:02x}'
else:
# emulation has completed successfully
reply = f'W{self.ql.os.exit_code:02x}'
# emulation has completed successfully. note bare-metal os layers
# do not have an exit code (issue #1276)
reply = f'W{getattr(self.ql.os, "exit_code", 0):02x}'
return reply
@@ -672,10 +673,17 @@ class QlGdb(QlDebugger):
for grp in groups:
cmd, *tid = grp.split(':', maxsplit=1)
if cmd in ('c', f'C{SIGTRAP:02x}'):
# 'C sig' and 'S sig' resume or step while delivering a signal
# to the guest. we do not deliver signals, so the signal value
# is ignored and the action is carried out as a plain resume or
# step. matching only 'C05' and 'S05' here made clients that
# resume with any other pending signal (e.g. 'S0f' after a stop
# reply we sent) receive an empty reply and bail out with
# 'Invalid remote reply' (issue #1377)
if cmd[:1] in ('c', 'C'):
return handle_c('')
elif cmd in ('s', f'S{SIGTRAP:02x}'):
elif cmd[:1] in ('s', 'S'):
return handle_s('')
# FIXME: not sure how to handle multiple command
@@ -704,7 +712,7 @@ class QlGdb(QlDebugger):
# when the step carried pc all the way to the emulation exit point.
if getattr(self.ql.arch, 'effective_pc', self.ql.arch.regs.arch_pc) == self.gdb.exit_point:
# program terminated; report its exit code
return f'W{self.ql.os.exit_code:02x}'
return f'W{getattr(self.ql.os, "exit_code", 0):02x}'
# otherwise, this is just single stepping
return f'S{SIGTRAP:02x}'
+49
View File
@@ -260,6 +260,55 @@ class DebuggerTest(unittest.TestCase):
self.assertEqual(replies, ['S05', 'S05', 'S05'])
def test_gdbdebug_vcont_signal_actions(self):
# regression for issue #1377: a client resuming with a pending signal sends
# a 'vCont;S<sig>' or 'vCont;C<sig>' action (e.g. 'S0f' after a stop-reply we
# sent). the stub only recognized 'S05' and 'C05', replied empty to anything
# else, and the client bailed out with 'Invalid remote reply'. any signal must
# be accepted and carried out as a plain step or resume.
ql = Qiling(["../examples/rootfs/x8664_linux/bin/x8664_hello"], "../examples/rootfs/x8664_linux", verbose=QL_VERBOSE.OFF)
ql.debugger = 'gdb:127.0.0.1:9995'
replies = []
def gdb_test_client():
# yield to allow ql to launch its gdbserver
time.sleep(1.337 * 2)
with ReadingGdbClient('127.0.0.1', 9995) as client:
client.send('qSupported:multiprocess+;swbreak+;hwbreak+;vContSupported+;xmlRegisters=i386')
client.read_packet()
client.send('QStartNoAckMode')
client.read_packet()
client.send('vCont?')
replies.append(client.read_packet())
# step while delivering SIGTERM; this is the packet reported in #1377
client.send('vCont;S0f:p1.1;c:p1.-1')
replies.append(client.read_packet())
# resume while delivering SIGTERM; runs the guest to completion
client.send('vCont;C0f:p1.1')
replies.append(client.read_packet())
client.send('k')
thread = threading.Thread(target=gdb_test_client, daemon=True)
thread.start()
ql.run()
thread.join(timeout=30)
del ql
self.assertEqual(replies[0], 'vCont;c;C;s;S')
# the signalled step is an ordinary step: SIGTRAP, not an empty reply
self.assertEqual(replies[1], 'S05')
# the signalled resume ran to termination and reported an exit code
self.assertTrue(replies[2].startswith('W'), f'unexpected reply to signalled resume: {replies[2]!r}')
def test_gdbdebug_async_interrupt(self):
# a free-running guest could not be interrupted at all: the stub is blocked
# inside emu_start while the target runs, so the bare '\x03' break byte a