about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/focaccia/deterministic.py2
-rw-r--r--src/focaccia/qemu/syscall.py4
-rw-r--r--src/focaccia/qemu/target.py105
-rw-r--r--src/focaccia/qemu/x86.py194
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)
+