From 9fce1f83d5533296236c41a8e136764b9e381038 Mon Sep 17 00:00:00 2001 From: retrocpugeek Date: Sun, 21 Jun 2026 19:45:36 +1000 Subject: [PATCH] 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) --- qiling/os/linux/linux.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/qiling/os/linux/linux.py b/qiling/os/linux/linux.py index 0313218d..8e53868a 100644 --- a/qiling/os/linux/linux.py +++ b/qiling/os/linux/linux.py @@ -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)