From c32cd74628cebec64a9eca865610b2a5e2e3e469 Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Wed, 10 Dec 2025 10:51:52 +0100 Subject: [PATCH] syscall tracing skeleton --- src/Patcher.zig | 23 +++++++++- src/syscalls.zig | 113 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 src/syscalls.zig diff --git a/src/Patcher.zig b/src/Patcher.zig index a41ce44..b6ab63b 100644 --- a/src/Patcher.zig +++ b/src/Patcher.zig @@ -6,6 +6,7 @@ const mem = std.mem; const posix = std.posix; const zydis = @import("zydis").zydis; const dis = @import("disassembler.zig"); +const syscalls = @import("syscalls.zig"); const log = std.log.scoped(.patcher); const AddressAllocator = @import("AddressAllocator.zig"); @@ -38,11 +39,18 @@ const prefixes = [_]u8{ 0x36, }; +var syscall_flicken_bytes = [13]u8{ + 0x49, 0xBB, // mov r11 + 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, // 8byte immediate + 0x41, 0xff, 0xd3, // call r11 +}; + pub var gpa: mem.Allocator = undefined; pub var flicken_templates: std.StringArrayHashMapUnmanaged(Flicken) = .empty; pub var address_allocator: AddressAllocator = .empty; /// Tracks the base addresses of pages we have mmap'd for Flicken. pub var allocated_pages: std.AutoHashMapUnmanaged(u64, void) = .empty; +pub var mutex: std.Thread.Mutex = .{}; var init_once = std.once(initInner); pub fn init() void { @@ -55,6 +63,8 @@ fn initInner() void { page_size / @sizeOf(Flicken), ) catch @panic("failed initializing patcher"); flicken_templates.putAssumeCapacity("nop", .{ .name = "nop", .bytes = &.{} }); + mem.writeInt(u64, syscall_flicken_bytes[2..][0..8], @intFromPtr(&syscalls.syscall_entry), .little); + flicken_templates.putAssumeCapacity("syscall", .{ .name = "syscall", .bytes = &syscall_flicken_bytes }); } /// Flicken name and bytes have to be valid for the lifetime it's used. If a trampoline with the @@ -62,6 +72,7 @@ fn initInner() void { /// NOTE: The name "nop" is reserved and always has the ID 0. pub fn addFlicken(trampoline: Flicken) !FlickenId { assert(!mem.eql(u8, "nop", trampoline.name)); + assert(!mem.eql(u8, "syscall", trampoline.name)); try flicken_templates.ensureUnusedCapacity(gpa, 1); errdefer comptime unreachable; @@ -90,6 +101,8 @@ pub const FlickenId = enum(u64) { /// It also needs special handling when constructing the patches, because it's different for /// each instruction. nop = 0, + /// TODO: docs + syscall = 1, _, }; @@ -172,6 +185,11 @@ pub const Statistics = struct { /// The region is processed Back-to-Front to ensure that modifications (punning) only /// constrain instructions that have already been processed or are locked. pub fn patchRegion(region: []align(page_size) u8) !void { + // For now just do a coarse lock. + // TODO: should we make this more fine grained? + mutex.lock(); + defer mutex.unlock(); + { // Block the region, such that we don't try to allocate there anymore. const start: i64 = @intCast(@intFromPtr(region.ptr)); @@ -202,11 +220,12 @@ pub fn patchRegion(region: []align(page_size) u8) !void { const offset = instruction.address - @intFromPtr(region.ptr); instruction_starts.set(offset); - const should_patch = instruction.instruction.mnemonic == zydis.ZYDIS_MNEMONIC_SYSCALL or + const is_syscall = instruction.instruction.mnemonic == zydis.ZYDIS_MNEMONIC_SYSCALL; + const should_patch = is_syscall or instruction.instruction.attributes & zydis.ZYDIS_ATTRIB_HAS_LOCK > 0; if (should_patch) { const request: PatchRequest = .{ - .flicken = .nop, + .flicken = if (is_syscall) .syscall else .nop, .offset = offset, .size = instruction.instruction.length, .bytes = region[offset..], diff --git a/src/syscalls.zig b/src/syscalls.zig new file mode 100644 index 0000000..20f6de7 --- /dev/null +++ b/src/syscalls.zig @@ -0,0 +1,113 @@ +const std = @import("std"); +const linux = std.os.linux; + +/// Represents the stack layout pushed by `syscall_entry` before calling the handler. +pub const UserRegs = extern struct { + padding: u64, // Result of `sub $8, %rsp` for alignment + rflags: u64, + rax: u64, + rbx: u64, + rcx: u64, + rdx: u64, + rsi: u64, + rdi: u64, + rbp: u64, + r8: u64, + r9: u64, + r10: u64, + r11: u64, + r12: u64, + r13: u64, + r14: u64, + r15: u64, +}; + +/// The main entry point for intercepted syscalls. +/// +/// This function is called from `syscall_entry` with a pointer to the saved registers. +/// It effectively emulates the syscall instruction while allowing for interception. +export fn syscall_handler(regs: *UserRegs) void { + // TODO: Handle signals (masking) to prevent re-entrancy issues if we touch global state. + // TODO: Handle `clone` specially because the child thread wakes up with a fresh stack + // and cannot pop the registers we saved here. + + const sys_nr = regs.rax; + const sys: linux.SYS = @enumFromInt(sys_nr); + const arg1 = regs.rdi; + const arg2 = regs.rsi; + const arg3 = regs.rdx; + const arg4 = regs.r10; + const arg5 = regs.r8; + const arg6 = regs.r9; + + std.debug.print("Got syscall {s}\n", .{@tagName(sys)}); + // For now, we just pass through everything. + // In the future, we will switch on `sys` to handle mmap, mprotect, etc. + const result = std.os.linux.syscall6(sys, arg1, arg2, arg3, arg4, arg5, arg6); + + // Write result back to the saved RAX so it is restored to the application. + regs.rax = result; +} + +/// Assembly trampoline that saves state and calls the Zig handler. +pub fn syscall_entry() callconv(.naked) void { + asm volatile ( + \\ .global syscall_entry + \\ .type syscall_entry, @function + \\ syscall_entry: + \\ # Respect the Red Zone (128 bytes) + \\ sub $128, %rsp + \\ + \\ # Save all GPRs that must be preserved or are arguments + \\ push %r15 + \\ push %r14 + \\ push %r13 + \\ push %r12 + \\ push %r11 + \\ push %r10 + \\ push %r9 + \\ push %r8 + \\ push %rbp + \\ push %rdi + \\ push %rsi + \\ push %rdx + \\ push %rcx + \\ push %rbx + \\ push %rax + \\ pushfq # Save Flags + \\ + \\ # Align stack + \\ # Current pushes: 16 * 8 = 128 bytes. + \\ # Red zone sub: 128 bytes. + \\ # Trampoline call pushed ret addr: 8 bytes. + \\ # Total misalign: 8 bytes. We need 16-byte alignment for 'call'. + \\ sub $8, %rsp + \\ + \\ # Pass pointer to regs (current rsp) as 1st argument (rdi) and call handler. + \\ mov %rsp, %rdi + \\ call syscall_handler + \\ + \\ # Restore State + \\ add $8, %rsp + \\ popfq + \\ pop %rax + \\ pop %rbx + \\ pop %rcx + \\ pop %rdx + \\ pop %rsi + \\ pop %rdi + \\ pop %rbp + \\ pop %r8 + \\ pop %r9 + \\ pop %r10 + \\ pop %r11 + \\ pop %r12 + \\ pop %r13 + \\ pop %r14 + \\ pop %r15 + \\ + \\ # Restore Red Zone and Return + \\ add $128, %rsp + \\ ret + ); +}