/proc/self/exe support

This commit is contained in:
2025-12-11 12:25:52 +01:00
parent 9ac107b398
commit da69c60ffd
3 changed files with 61 additions and 3 deletions

View File

@@ -52,6 +52,9 @@ pub var address_allocator: AddressAllocator = .empty;
pub var allocated_pages: std.AutoHashMapUnmanaged(u64, void) = .empty; pub var allocated_pages: std.AutoHashMapUnmanaged(u64, void) = .empty;
pub var mutex: std.Thread.Mutex = .{}; 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. /// Initialize the patcher.
/// NOTE: This should only be called **once**. /// NOTE: This should only be called **once**.
pub fn init() !void { pub fn init() !void {

View File

@@ -49,11 +49,21 @@ pub fn main() !void {
return; return;
} }
// Initialize patcher const file = try lookupFile(mem.sliceTo(std.os.argv[arg_index], 0));
try Patcher.init();
{
// 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 // Map file into memory
const file = try lookupFile(mem.sliceTo(std.os.argv[arg_index], 0));
var file_buffer: [128]u8 = undefined; var file_buffer: [128]u8 = undefined;
var file_reader = file.reader(&file_buffer); var file_reader = file.reader(&file_buffer);
log.info("--- Loading executable: {s} ---", .{std.os.argv[arg_index]}); log.info("--- Loading executable: {s} ---", .{std.os.argv[arg_index]});

View File

@@ -1,5 +1,6 @@
const std = @import("std"); const std = @import("std");
const linux = std.os.linux; const linux = std.os.linux;
const Patcher = @import("Patcher.zig");
/// Represents the stack layout pushed by `syscall_entry` before calling the handler. /// Represents the stack layout pushed by `syscall_entry` before calling the handler.
pub const UserRegs = extern struct { pub const UserRegs = extern struct {
@@ -40,6 +41,31 @@ export fn syscall_handler(regs: *UserRegs) void {
const arg5 = regs.r8; const arg5 = regs.r8;
const arg6 = regs.r9; 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)}); std.debug.print("Got syscall {s}\n", .{@tagName(sys)});
// For now, we just pass through everything. // For now, we just pass through everything.
// In the future, we will switch on `sys` to handle mmap, mprotect, etc. // 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; 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. /// Assembly trampoline that saves state and calls the Zig handler.
pub fn syscall_entry() callconv(.naked) void { pub fn syscall_entry() callconv(.naked) void {
asm volatile ( asm volatile (