diff --git a/src/Patcher.zig b/src/Patcher.zig index e967680..fe41d24 100644 --- a/src/Patcher.zig +++ b/src/Patcher.zig @@ -52,6 +52,9 @@ pub var address_allocator: AddressAllocator = .empty; pub var allocated_pages: std.AutoHashMapUnmanaged(u64, void) = .empty; pub var mutex: std.Thread.Mutex = .{}; +pub var target_exec_path_buf: [std.fs.max_path_bytes]u8 = @splat(0); +pub var target_exec_path: []const u8 = undefined; + /// Initialize the patcher. /// NOTE: This should only be called **once**. pub fn init() !void { diff --git a/src/main.zig b/src/main.zig index 5f82fa8..b48c703 100644 --- a/src/main.zig +++ b/src/main.zig @@ -49,11 +49,21 @@ pub fn main() !void { return; } - // Initialize patcher - try Patcher.init(); + const file = try lookupFile(mem.sliceTo(std.os.argv[arg_index], 0)); + + { + // Initialize patcher + try Patcher.init(); + // Resolve the absolute path of the target executable. This is needed for the + // readlink("/proc/self/exe") interception. We use the file descriptor to get the + // authoritative path. + var self_buf: [128]u8 = undefined; + const fd_path = try std.fmt.bufPrint(&self_buf, "/proc/self/fd/{d}", .{file.handle}); + Patcher.target_exec_path = try std.fs.readLinkAbsolute(fd_path, &Patcher.target_exec_path_buf); + log.debug("Resolved target executable path: {s}", .{Patcher.target_exec_path}); + } // Map file into memory - const file = try lookupFile(mem.sliceTo(std.os.argv[arg_index], 0)); var file_buffer: [128]u8 = undefined; var file_reader = file.reader(&file_buffer); log.info("--- Loading executable: {s} ---", .{std.os.argv[arg_index]}); diff --git a/src/syscalls.zig b/src/syscalls.zig index 32a2748..d09d9dc 100644 --- a/src/syscalls.zig +++ b/src/syscalls.zig @@ -1,5 +1,6 @@ const std = @import("std"); const linux = std.os.linux; +const Patcher = @import("Patcher.zig"); /// Represents the stack layout pushed by `syscall_entry` before calling the handler. pub const UserRegs = extern struct { @@ -40,6 +41,31 @@ export fn syscall_handler(regs: *UserRegs) void { const arg5 = regs.r8; const arg6 = regs.r9; + switch (sys) { + .readlink => { + // readlink(const char *path, char *buf, size_t bufsiz) + const path_ptr = @as([*:0]const u8, @ptrFromInt(regs.rdi)); + // TODO: handle relative paths with cwd + if (isProcSelfExe(path_ptr)) { + handleReadlink(regs.rsi, regs.rdx, regs); + return; + } + }, + .readlinkat => { + // readlinkat(int dirfd, const char *pathname, char *buf, size_t bufsiz) + // We only intercept if pathname is absolute "/proc/self/exe". + // TODO: handle relative paths with dirfd pointing to /proc/self + // TODO: handle relative paths with dirfd == AT_FDCWD (like readlink) + // TODO: handle empty pathname + const path_ptr = @as([*:0]const u8, @ptrFromInt(regs.rsi)); + if (isProcSelfExe(path_ptr)) { + handleReadlink(regs.rdx, regs.r10, regs); + return; + } + }, + 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. @@ -49,6 +75,25 @@ export fn syscall_handler(regs: *UserRegs) void { regs.rax = result; } +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; +} + /// Assembly trampoline that saves state and calls the Zig handler. pub fn syscall_entry() callconv(.naked) void { asm volatile (