From d25cf59380753ba83b7a6a1e39a2847649f7ff44 Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Wed, 17 Dec 2025 16:15:41 +0100 Subject: [PATCH] fix SIGBUS due to mapping being larger than file --- docs/TODO.md | 6 +++++- src/Patcher.zig | 4 ++++ src/main.zig | 4 ++-- src/syscalls.zig | 18 ++++++++++++++++-- 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/docs/TODO.md b/docs/TODO.md index 0468f93..cb0f0f6 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -19,7 +19,7 @@ IvyBridge(2012) and AMD Zen 2 Family 17H(2019) and Linux 5.9(2020). the stack, so `ucontext` isn't on top anymore. - [x] `/proc/self/exe`: intercept calls to `readlink`/`readlinkat` with that as argument - [x] `auxv`: check if that is setup correctly and completely -- [ ] JIT support: intercept `mmap`, `mprotect` and `mremap` that change pages to be executable +- [x] JIT support: intercept `mmap`, `mprotect` and `mremap` that change pages to be executable - [ ] `SIGILL` patching fallback - [x] `vdso` handling - [x] check why the libc tests are flaky @@ -31,6 +31,8 @@ IvyBridge(2012) and AMD Zen 2 Family 17H(2019) and Linux 5.9(2020). - [ ] Ghost page edge case: In all patch strategies, if a range spans multiple pages and we `mmap` the first one but can't `mmap` the second one we just let the first one mapped. It would be better to unmap them +- [ ] Right now when patching we mmap a page and may not use it, but we still leave it mapped. This + leaks memory. If we fix this correctly the Ghost page issue is also fixed - [ ] Re-entrancy for `patchRegion` - when a signal comes, while we are in that function, and we need to patch something due to the signal we will deadlock @@ -46,3 +48,5 @@ IvyBridge(2012) and AMD Zen 2 Family 17H(2019) and Linux 5.9(2020). - [ ] `modify_ldt`: check what we need to intercept and change - [ ] `set_tid_address`: check what we need to intercept and change - [ ] performance optimizations for patched code? Peephole might be possible +- [ ] maybe add a way to run something after the client is finished + - could be useful for statistics, cleanup(if necessary), or notifying of suppressed warnings diff --git a/src/Patcher.zig b/src/Patcher.zig index 1a2995a..c95736d 100644 --- a/src/Patcher.zig +++ b/src/Patcher.zig @@ -212,6 +212,10 @@ pub const Statistics = struct { /// NOTE: This function leaves the region as R|W and the caller is responsible for changing it to /// the desired protection pub fn patchRegion(region: []align(page_size) u8) !void { + log.info( + "Patching region: 0x{x} - 0x{x}", + .{ @intFromPtr(region.ptr), @intFromPtr(®ion[region.len - 1]) }, + ); // For now just do a coarse lock. // TODO: should we make this more fine grained? mutex.lock(); diff --git a/src/main.zig b/src/main.zig index 19eb358..b86726e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -256,10 +256,10 @@ fn patchLoadedElf(base: usize) !void { const page_start = mem.alignBackward(usize, vaddr, page_size); const page_end = mem.alignForward(usize, vaddr + memsz, page_size); + const size = page_end - page_start; - const region = @as([*]align(page_size) u8, @ptrFromInt(page_start))[0 .. page_end - page_start]; + const region = @as([*]align(page_size) u8, @ptrFromInt(page_start))[0..size]; - log.info("Patching segment: 0x{x} - 0x{x}", .{ page_start, page_end }); try Patcher.patchRegion(region); try posix.mprotect(region, elfToMmapProt(phdr.p_flags)); } diff --git a/src/syscalls.zig b/src/syscalls.zig index 50ea7d8..0e780bf 100644 --- a/src/syscalls.zig +++ b/src/syscalls.zig @@ -6,6 +6,8 @@ const assert = std.debug.assert; const page_size = std.heap.pageSize(); +const log = std.log.scoped(.syscalls); + /// Represents the stack layout pushed by `syscallEntry` before calling the handler. pub const SavedContext = extern struct { padding: u64, // Result of `sub $8, %rsp` for alignment @@ -90,13 +92,25 @@ export fn syscall_handler(ctx: *SavedContext) callconv(.c) void { // Execute the syscall first to get the address (rax) ctx.rax = executeSyscall(ctx); const addr = ctx.rax; - const len = ctx.rsi; + var len = ctx.rsi; + const flags: linux.MAP = @bitCast(@as(u32, @intCast(ctx.r10))); + const fd: linux.fd_t = @bitCast(@as(u32, @truncate(ctx.r8))); + const offset = ctx.r9; const is_error = @as(i64, @bitCast(ctx.rax)) < 0; if (is_error) return; if ((prot & posix.PROT.EXEC) == 0) return; - if (len <= 0) return; + // If file-backed (not anonymous), clamp len to file size to avoid SIGBUS + if (!flags.ANONYMOUS) { + var stat: linux.Stat = undefined; + if (0 == linux.fstat(fd, &stat) and linux.S.ISREG(stat.mode)) { + const file_size: u64 = @intCast(stat.size); + len = if (offset >= file_size) 0 else @min(len, file_size - offset); + } + } + + if (len <= 0) return; // mmap addresses are always page aligned const ptr = @as([*]align(page_size) u8, @ptrFromInt(addr)); // Check if we can patch it