diff --git a/src/Patcher.zig b/src/Patcher.zig index fe41d24..669baf5 100644 --- a/src/Patcher.zig +++ b/src/Patcher.zig @@ -39,9 +39,12 @@ const prefixes = [_]u8{ 0x36, }; -var syscall_flicken_bytes = [13]u8{ - 0x49, 0xBB, // mov r11 - 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, // 8byte immediate +/// As of the SysV ABI: 'The kernel destroys registers %rcx and %r11." +/// So we put the address of the function to call into %r11. +// TODO: Don't we need to save the red zone here, because we push the return address onto the stack +// with the `call r11` instruction? +var syscall_flicken_bytes = [_]u8{ + 0x49, 0xBB, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, // mov r11 0x41, 0xff, 0xd3, // call r11 }; @@ -65,7 +68,7 @@ pub fn init() !void { page_size / @sizeOf(Flicken), ); flicken_templates.putAssumeCapacity("nop", .{ .name = "nop", .bytes = &.{} }); - mem.writeInt(u64, syscall_flicken_bytes[2..][0..8], @intFromPtr(&syscalls.syscall_entry), .little); + mem.writeInt(u64, syscall_flicken_bytes[4..][0..8], @intFromPtr(&syscalls.syscall_entry), .little); flicken_templates.putAssumeCapacity("syscall", .{ .name = "syscall", .bytes = &syscall_flicken_bytes }); { diff --git a/src/syscalls.zig b/src/syscalls.zig index d09d9dc..f417183 100644 --- a/src/syscalls.zig +++ b/src/syscalls.zig @@ -27,19 +27,12 @@ pub const UserRegs = extern struct { /// /// 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 { +export fn syscall_handler(regs: *UserRegs) callconv(.c) 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; + const sys: linux.SYS = @enumFromInt(regs.rax); switch (sys) { .readlink => { @@ -63,43 +56,57 @@ export fn syscall_handler(regs: *UserRegs) void { return; } }, + .clone, .clone3 => { + @panic("Clone is not supported yet"); + }, + .fork, .vfork => { + // fork/vfork duplicate the stack (or share it until exec), so the return path via + // syscall_entry works fine. + }, + .rt_sigreturn => { + @panic("sigreturn is not supported yet"); + }, + .execve, .execveat => |s| { + // TODO: option to persist across new processes + std.debug.print("syscall {} called\n", .{s}); + }, + .prctl, .arch_prctl, .set_tid_address => |s| { + // TODO: what do we need to handle from these? + // process name + // fs base(gs?) + // thread id pointers + std.debug.print("syscall {} called\n", .{s}); + }, + .mmap, .mprotect => { + // TODO: JIT support + // TODO: cleanup + }, + .munmap, .mremap => { + // TODO: cleanup + }, + else => {}, } - 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; + regs.rax = executeSyscall(regs); } -fn isProcSelfExe(path: [*:0]const u8) bool { - const needle = "/proc/self/exe"; - var i: usize = 0; - while (i < needle.len) : (i += 1) { - if (path[i] != needle[i]) return false; - } - return path[i] == 0; -} - -fn handleReadlink(buf_addr: u64, buf_size: u64, regs: *UserRegs) void { - const target = Patcher.target_exec_path; - const len = @min(target.len, buf_size); - const dest = @as([*]u8, @ptrFromInt(buf_addr)); - @memcpy(dest[0..len], target[0..len]); - - // readlink does not null-terminate if the buffer is full, it just returns length. - regs.rax = len; +inline fn executeSyscall(regs: *UserRegs) u64 { + return linux.syscall6( + @enumFromInt(regs.rax), + regs.rdi, + regs.rsi, + regs.rdx, + regs.r10, + regs.r8, + regs.r9, + ); } /// Assembly trampoline that saves state and calls the Zig handler. pub fn syscall_entry() callconv(.naked) void { asm volatile ( - \\ # Respect the Red Zone (128 bytes) - \\ sub $128, %rsp - \\ \\ # Save all GPRs that must be preserved or are arguments \\ push %r15 \\ push %r14 @@ -117,6 +124,7 @@ pub fn syscall_entry() callconv(.naked) void { \\ push %rbx \\ push %rax \\ pushfq # Save Flags + \\ # TODO: save return_address \\ \\ # Align stack \\ # Current pushes: 16 * 8 = 128 bytes. @@ -148,8 +156,6 @@ pub fn syscall_entry() callconv(.naked) void { \\ pop %r14 \\ pop %r15 \\ - \\ # Restore Red Zone and Return - \\ add $128, %rsp \\ ret : // TODO: can we somehow use %[handler] in the assembly instead? @@ -157,3 +163,22 @@ pub fn syscall_entry() callconv(.naked) void { : [handler] "i" (syscall_handler), ); } + +fn isProcSelfExe(path: [*:0]const u8) bool { + const needle = "/proc/self/exe"; + var i: usize = 0; + while (i < needle.len) : (i += 1) { + if (path[i] != needle[i]) return false; + } + return path[i] == 0; +} + +fn handleReadlink(buf_addr: u64, buf_size: u64, regs: *UserRegs) void { + const target = Patcher.target_exec_path; + const len = @min(target.len, buf_size); + const dest = @as([*]u8, @ptrFromInt(buf_addr)); + @memcpy(dest[0..len], target[0..len]); + + // readlink does not null-terminate if the buffer is full, it just returns length. + regs.rax = len; +}