diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/focaccia/deterministic.py | 2 | ||||
| -rw-r--r-- | src/focaccia/qemu/syscall.py | 4 | ||||
| -rw-r--r-- | src/focaccia/qemu/target.py | 105 | ||||
| -rw-r--r-- | src/focaccia/qemu/x86.py | 194 |
4 files changed, 282 insertions, 23 deletions
diff --git a/src/focaccia/deterministic.py b/src/focaccia/deterministic.py index 4070504..c4d37ad 100644 --- a/src/focaccia/deterministic.py +++ b/src/focaccia/deterministic.py @@ -338,7 +338,7 @@ finally: return None def match_pair(self, event: Event | None): - if event is None or not isinstance(event, SyscallEvent): + if event is None: return None assert(self.matched_count is not None) post_event = self.events[self.matched_count] diff --git a/src/focaccia/qemu/syscall.py b/src/focaccia/qemu/syscall.py index 90016f1..6f54641 100644 --- a/src/focaccia/qemu/syscall.py +++ b/src/focaccia/qemu/syscall.py @@ -3,7 +3,8 @@ class SyscallInfo: name: str, patchup_registers: list[str] | None = None, patchup_address_registers: list[str] | None = None, - creates_thread: bool = False): + creates_thread: bool = False, + return_from_signal: bool = False): """Describes a syscall by its name and outputs. :param name: The name of a system call. @@ -16,4 +17,5 @@ class SyscallInfo: self.patchup_registers = patchup_registers self.patchup_address_registers = patchup_address_registers self.creates_thread = creates_thread + self.return_from_signal = return_from_signal diff --git a/src/focaccia/qemu/target.py b/src/focaccia/qemu/target.py index 790249c..3079e18 100644 --- a/src/focaccia/qemu/target.py +++ b/src/focaccia/qemu/target.py @@ -1,12 +1,14 @@ import re import gdb import socket +import struct import logging from typing import Optional from focaccia.deterministic import ( DeterministicLog, Event, + SignalEvent, EventMatcher, SyscallEvent, MemoryMapping, @@ -19,6 +21,7 @@ from focaccia.snapshot import ( ) from focaccia.arch import supported_architectures, Arch from focaccia.qemu.deterministic import emulated_system_calls, passthrough_system_calls, vdso_system_calls +from focaccia.qemu.x86 import SigContext, SigInfo, UContext, SigFrame logger = logging.getLogger('focaccia-qemu-target') debug = logger.debug @@ -245,6 +248,8 @@ class GDBServerStateIterator(GDBServerConnector): for idx in skipped_events: debug(f'Skip {events[idx]}') + self._signal_frames = [] + first_state = self.current_state() self._events = EventMatcher(events, match_event, @@ -292,6 +297,11 @@ class GDBServerStateIterator(GDBServerConnector): data[hole.offset:hole.offset] = b'\x00' * hole.size self._process.write_memory(addr, data) + if syscall.return_from_signal: + # TODO: restore frame + frame = self._signal_frames.pop() + info(f'Handling return from signal with frame: {frame}') + syscall = passthrough_system_calls[self.arch.archname].get(call, None) if syscall is not None: info(f'System call number {hex(call)} passed through') @@ -321,6 +331,75 @@ class GDBServerStateIterator(GDBServerConnector): return next_state + def _handle_signal(self, event: SignalEvent, post_event: SignalEvent): + info('Handling signal event') + sighandler_pc = post_event.pc + + state = self.current_state() + rsp = state.read_register('rsp') + + sc = SigContext() + sc.r8 = state.read_register('r8') + sc.r9 = state.read_register('r9') + sc.r10 = state.read_register('r10') + sc.r11 = state.read_register('r11') + sc.r12 = state.read_register('r12') + sc.r13 = state.read_register('r13') + sc.r14 = state.read_register('r14') + sc.r15 = state.read_register('r15') + sc.rdi = state.read_register('rdi') + sc.rsi = state.read_register('rsi') + sc.rbp = state.read_register('rbp') + sc.rbx = state.read_register('rbx') + sc.rdx = state.read_register('rdx') + sc.rax = state.read_register('rax') + sc.rcx = state.read_register('rcx') + sc.rsp = state.read_register('rsp') + sc.rip = state.read_register('rip') + sc.eflags = state.read_register('eflags') + + sigmask = 0 + uctx = UContext(sigmask=sigmask, mcontext=sc) + si_signo, si_errno, si_code = struct.unpack_from("<iii", event.signal_number.siginfo, 0) + siginfo = SigInfo(si_signo=si_signo, si_errno=si_errno, si_code=si_code, + si_pid=post_event.tid, si_uid=0) + frame = SigFrame(sp_new=rsp - 0xd78, pretcode=0x401824, uctx=uctx, siginfo=siginfo) + self._process.write_memory(rsp - 0xd78, frame.to_bytes()) + + gdb.execute(f'set $pc = {hex(sighandler_pc)}') + patchup_regs = ['rdi'] + for reg in patchup_regs: + gdb.parse_and_eval(f'${reg}={post_event.registers.get(reg)}') + + gdb.parse_and_eval('$rsp = $rsp - 0xd78') + gdb.parse_and_eval('$rdx = $rsp + 0x8') + gdb.parse_and_eval('$rsi = $rsp + 0x2c8') + + self._signal_frames.append(frame) + return self.current_state() + + def _handle_context_switch(self, event: SyscallEvent, post_event: SyscallEvent): + # Context switch + # TODO: handle return from pre-empt + self._thread_context[self._current_event_id] = event + self._current_event_id = post_event.tid + tid, num = self._thread_map[self._current_event_id] + self.context_switch(tid) + state = self.current_state() + debug(f'Scheduled {hex(tid)} that corresponds to native {hex(post_event.tid)}') + + if self._current_event_id in self._thread_context: + event = self._thread_context.pop(self._current_event_id) + elif match_event(post_event, state): + event = post_event + post_event = self._events.match_pair(event) + else: + debug(f'New thread {hex(tid)} started at non-event instruction') + self._events.unmatch() + self._step() + print(hex(self.current_state().read_pc())) + return self.current_state() + def _handle_event(self) -> ReadableProgramState | None: event = self._events.match(self.current_state()) @@ -331,30 +410,16 @@ class GDBServerStateIterator(GDBServerConnector): post_event = self._events.match_pair(event) assert(post_event is not None) - # Context switch - # TODO: handle return from pre-empt if post_event.tid != self._current_event_id: - self._thread_context[self._current_event_id] = event - self._current_event_id = post_event.tid - tid, num = self._thread_map[self._current_event_id] - self.context_switch(tid) - state = self.current_state() - debug(f'Scheduled {hex(tid)} that corresponds to native {hex(post_event.tid)}') - - if self._current_event_id in self._thread_context: - event = self._thread_context.pop(self._current_event_id) - elif match_event(post_event, state): - event = post_event - post_event = self._events.match_pair(event) - else: - debug(f'New thread {hex(tid)} started at non-event instruction') - self._events.unmatch() - self._step() - print(hex(self.current_state().read_pc())) - return self.current_state() + self._handle_context_switch(event, post_event) return self._handle_syscall(event, post_event) + if isinstance(event, SignalEvent): + post_event = self._events.match_pair(event) + assert(post_event is not None) + return self._handle_signal(event, post_event) + warn(f'Event handling for events of type {event.event_type} not implemented') return None diff --git a/src/focaccia/qemu/x86.py b/src/focaccia/qemu/x86.py index 3136fce..25ab874 100644 --- a/src/focaccia/qemu/x86.py +++ b/src/focaccia/qemu/x86.py @@ -1,3 +1,7 @@ +import struct +from typing import Optional +from dataclasses import dataclass, field + from focaccia.qemu.syscall import SyscallInfo # Incomplete, only the most common ones @@ -12,7 +16,7 @@ emulated_system_calls = { 8: SyscallInfo('lseek'), 13: SyscallInfo('rt_sigaction', patchup_address_registers=['rdx']), 14: SyscallInfo('rt_sigprocmask', patchup_address_registers=['rdx']), - 15: SyscallInfo('rt_sigreturn'), + 15: SyscallInfo('rt_sigreturn', return_from_signal=True), 16: SyscallInfo('ioctl', patchup_address_registers=['rdx']), 17: SyscallInfo('pread64', patchup_address_registers=['rsi']), 18: SyscallInfo('pwrite64'), @@ -109,3 +113,191 @@ vdso_system_calls = { 309: SyscallInfo('getcpu', patchup_address_registers=['rdi', 'rsi', 'rdx']) } +@dataclass +class SigContext: + """ + Represents struct sigcontext on Linux x86-64. + You fill these like ctx.r8 = 123, ctx.rip = 0x400abc, etc. + """ + + # GPRs in kernel-defined order + r8: int = 0 + r9: int = 0 + r10: int = 0 + r11: int = 0 + r12: int = 0 + r13: int = 0 + r14: int = 0 + r15: int = 0 + rdi: int = 0 + rsi: int = 0 + rbp: int = 0 + rbx: int = 0 + rdx: int = 0 + rax: int = 0 + rcx: int = 0 + rsp: int = 0 # OLD rsp before signal + rip: int = 0 # OLD rip before signal + + eflags: int = 0 + cs: int = 0x33 + ss: int = 0x2b + + err: int = 0 + trapno: int = 0 + oldmask: int = 0 + cr2: int = 0 + fpstate: int = 0 # pointer (kernel uses this) + + # Reserved padding space + reserved1: int = 0 + reserved2: int = 0 + reserved3: int = 0 + + def to_bytes(self) -> bytes: + """ + Pack exactly like struct sigcontext on x86-64. + """ + fields = [ + self.r8, self.r9, self.r10, self.r11, + self.r12, self.r13, self.r14, self.r15, + self.rdi, self.rsi, self.rbp, self.rbx, + self.rdx, self.rax, self.rcx, self.rsp, + self.rip, + self.eflags, + self.cs, self.ss, + self.err, self.trapno, self.oldmask, self.cr2, + self.fpstate, + self.reserved1, self.reserved2, self.reserved3, + ] + # All fields are 64-bit except CS/SS (16-bit) + # But kernel packs everything on 8-byte boundaries anyway. + return struct.pack("<" + "Q"*len(fields), *fields) + + +# --------------------------------------------------------------------- +# 2. Minimal siginfo_t abstraction (128 bytes on x86-64) +# --------------------------------------------------------------------- + +@dataclass +class SigInfo: + """ + Minimal representation. You can fill as needed. + Layout here is fixed to 128 bytes. + Only a few useful fields are exposed. + """ + + si_signo: int = 0 + si_errno: int = 0 + si_code: int = 0 + si_pid: int = 0 + si_uid: int = 0 + + def to_bytes(self) -> bytes: + # Linux siginfo is 128 bytes; real layout is complex. + # We place the common initial fields and pad the rest. + buf = bytearray(128) + struct.pack_into("<iii", buf, 0, self.si_signo, self.si_errno, self.si_code) + struct.pack_into("<II", buf, 16, self.si_pid, self.si_uid) + return bytes(buf) + + +# --------------------------------------------------------------------- +# 3. ucontext_t wrapper (only what matters for signal return) +# --------------------------------------------------------------------- + +@dataclass +class UContext: + """ + Only the parts required for correct signal return. + """ + sigmask: int = 0 # For simplicity; real sigset_t is 8*16 bytes + mcontext: SigContext = field(default_factory=SigContext) + + UC_FLAGS: int = 1 # Usually UC_FP_XSTATE + + def to_bytes(self) -> bytes: + """ + Real ucontext_t is large. Here we pack: + - uc_flags (8 bytes) + - uc_link (8 bytes, NULL) + - stack_t (3 * 8 bytes) + - sigmask (128 bytes normally; we use 8 bytes for simplicity) + - padding up to 0x2c0 + - mcontext (struct sigcontext) + IMPORTANT: total size must be 0x2c0 on x86-64. + """ + uc_buf = bytearray(0x2c0) + + # uc_flags + uc_link(NULL) + struct.pack_into("<Q", uc_buf, 0, self.UC_FLAGS) + struct.pack_into("<Q", uc_buf, 8, 0) + + # stack_t (ss_sp, ss_flags, ss_size) + struct.pack_into("<QQQ", uc_buf, 16, 0, 0, 0) + + # sigmask (we store a minimal 8 bytes) + struct.pack_into("<Q", uc_buf, 40, self.sigmask) + + # Now embed sigcontext at the end of ucontext + mctx_bytes = self.mcontext.to_bytes() + uc_buf[-len(mctx_bytes):] = mctx_bytes + + return bytes(uc_buf) + + +# --------------------------------------------------------------------- +# 4. Full rt_sigframe abstraction +# --------------------------------------------------------------------- + +@dataclass +class SigFrame: + sp_new: int # RSP after signal delivery + pretcode: int # pointer to restorer trampoline + uctx: UContext # full ucontext (incl. sigcontext) + siginfo: SigInfo # siginfo_t + tail_size: int = 0 # optional xstate padding + + PRETCODE_SIZE: int = 8 + UCONTEXT_SIZE: int = 0x2c0 + SIGINFO_SIZE: int = 128 + + @property + def uc_addr(self) -> int: + return self.sp_new + self.PRETCODE_SIZE + + @property + def siginfo_addr(self) -> int: + return self.uc_addr + self.UCONTEXT_SIZE + + @property + def tail_addr(self) -> int: + return self.siginfo_addr + self.SIGINFO_SIZE + + @property + def rdx_uc(self) -> int: + """What to set in guest RDX.""" + return self.uc_addr + + @property + def rsi_siginfo(self) -> int: + """What to set in guest RSI.""" + return self.siginfo_addr + + def to_bytes(self) -> bytes: + buf = bytearray(self.PRETCODE_SIZE + self.UCONTEXT_SIZE + + self.SIGINFO_SIZE + self.tail_size) + + # pretcode + struct.pack_into("<Q", buf, 0, self.pretcode) + + # ucontext + buf[self.PRETCODE_SIZE : self.PRETCODE_SIZE + self.UCONTEXT_SIZE] = \ + self.uctx.to_bytes() + + # siginfo + si_off = self.PRETCODE_SIZE + self.UCONTEXT_SIZE + buf[si_off : si_off + self.SIGINFO_SIZE] = self.siginfo.to_bytes() + + return bytes(buf) + |