Handle ARM/ARM64 Linux undefined-instruction exception

ARM and ARM64 Linux only hooked the svc syscall trap (exception 2), so any
other CPU exception fell through to the unhandled-interrupt dispatcher and
raised QlErrorCoreHook, crashing the framework.

This surfaces in examples/shellcode_run.py: since execve was patched to
return -1 for paths outside the rootfs, rootfs-less shellcode no longer
stops at execve and instead runs into its trailing data. On ARM64 that data
decodes as an undefined instruction (exception 1), which a real kernel would
deliver as SIGILL and terminate the process.

Hook EXCP.UDEF and emulate that termination by stopping cleanly, and replace
the magic syscall-trap number with the named EXCP.SWI constant.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
retrocpugeek
2026-09-06 20:50:01 +10:00
co-authored by Claude Opus 4.8
parent 19d33b65ff
commit 9fce1f83d5
+22 -2
View File
@@ -12,6 +12,7 @@ from qiling import Qiling
from qiling.arch.x86_const import GS_SEGMENT_ADDR, GS_SEGMENT_SIZE
from qiling.arch.x86_utils import GDTManager, SegmentManager86, SegmentManager64
from qiling.arch import arm_utils
from qiling.arch.cortex_m_const import EXCP
from qiling.cc import QlCC, intel, arm, mips, riscv, ppc
from qiling.const import QL_ARCH, QL_OS
from qiling.os.fcall import QlFunctionCall
@@ -56,7 +57,8 @@ class QlOsLinux(QlOsPosix):
# ARM
if self.ql.arch.type == QL_ARCH.ARM:
self.ql.arch.enable_vfp()
self.ql.hook_intno(self.hook_syscall, 2)
self.ql.hook_intno(self.hook_syscall, EXCP.SWI)
self.ql.hook_intno(self.hook_cpu_exception, EXCP.UDEF)
self.thread_class = thread.QlLinuxARMThread
arm_utils.init_linux_traps(self.ql, {
'memory_barrier': 0xffff0fa0,
@@ -72,7 +74,8 @@ class QlOsLinux(QlOsPosix):
# ARM64
elif self.ql.arch.type == QL_ARCH.ARM64:
self.ql.arch.enable_vfp()
self.ql.hook_intno(self.hook_syscall, 2)
self.ql.hook_intno(self.hook_syscall, EXCP.SWI)
self.ql.hook_intno(self.hook_cpu_exception, EXCP.UDEF)
self.thread_class = thread.QlLinuxARM64Thread
# X86
@@ -137,6 +140,23 @@ class QlOsLinux(QlOsPosix):
def hook_syscall(self, ql, intno = None):
return self.load_syscall()
def hook_cpu_exception(self, ql, intno = None):
# A cpu exception the kernel would turn into a fatal signal that
# terminates the process (e.g. SIGILL on an undefined instruction).
# Emulate that termination by stopping cleanly instead of letting the
# unhandled-interrupt dispatcher raise QlErrorCoreHook. This commonly
# happens with shellcode that falls through into trailing data once a
# terminal syscall (e.g. a denied execve) returns instead of replacing
# the image.
signame = {
EXCP.UDEF: 'SIGILL',
}.get(intno, f'exception {intno:#x}')
pc = ql.arch.regs.arch_pc
ql.log.debug(f'CPU raised {signame} at {pc:#x}; terminating emulated process')
ql.stop()
def register_function_after_load(self, function):
if function not in self.function_after_load_list:
self.function_after_load_list.append(function)