From 22e1b033607580756329879ba7158a29aca57981 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Thu, 30 Mar 2023 00:57:53 +0200 Subject: [PATCH 01/10] coff: use copy in zig-cache for child process in HCS Ideally, we would just do an atomic rename, but so far I had no luck. I have also tried marking the file to delete-on-close but then we cannot use it to spawn the process. So for now, let's just put a copy in `zig-cache` and let the user decide when to recycle the cache dir. --- src/link.zig | 40 +++++++++++++++++++++++----------------- src/link/Coff.zig | 5 +++++ src/main.zig | 24 +++++++++++++++++++----- 3 files changed, 47 insertions(+), 22 deletions(-) diff --git a/src/link.zig b/src/link.zig index 239dc646b2..d821027196 100644 --- a/src/link.zig +++ b/src/link.zig @@ -379,24 +379,30 @@ pub const File = struct { if (base.file != null) return; const emit = base.options.emit orelse return; if (base.child_pid) |pid| { - // If we try to open the output file in write mode while it is running, - // it will return ETXTBSY. So instead, we copy the file, atomically rename it - // over top of the exe path, and then proceed normally. This changes the inode, - // avoiding the error. - const tmp_sub_path = try std.fmt.allocPrint(base.allocator, "{s}-{x}", .{ - emit.sub_path, std.crypto.random.int(u32), - }); - try emit.directory.handle.copyFile(emit.sub_path, emit.directory.handle, tmp_sub_path, .{}); - try emit.directory.handle.rename(tmp_sub_path, emit.sub_path); - switch (builtin.os.tag) { - .linux => std.os.ptrace(std.os.linux.PTRACE.ATTACH, pid, 0, 0) catch |err| { - log.warn("ptrace failure: {s}", .{@errorName(err)}); - }, - .macos => base.cast(MachO).?.ptraceAttach(pid) catch |err| { + if (builtin.os.tag == .windows) { + base.cast(Coff).?.ptraceAttach(pid) catch |err| { log.warn("attaching failed with error: {s}", .{@errorName(err)}); - }, - .windows => {}, - else => return error.HotSwapUnavailableOnHostOperatingSystem, + }; + } else { + // If we try to open the output file in write mode while it is running, + // it will return ETXTBSY. So instead, we copy the file, atomically rename it + // over top of the exe path, and then proceed normally. This changes the inode, + // avoiding the error. + const tmp_sub_path = try std.fmt.allocPrint(base.allocator, "{s}-{x}", .{ + emit.sub_path, std.crypto.random.int(u32), + }); + try emit.directory.handle.copyFile(emit.sub_path, emit.directory.handle, tmp_sub_path, .{}); + try emit.directory.handle.rename(tmp_sub_path, emit.sub_path); + switch (builtin.os.tag) { + .linux => std.os.ptrace(std.os.linux.PTRACE.ATTACH, pid, 0, 0) catch |err| { + log.warn("ptrace failure: {s}", .{@errorName(err)}); + }, + .macos => base.cast(MachO).?.ptraceAttach(pid) catch |err| { + log.warn("attaching failed with error: {s}", .{@errorName(err)}); + }, + .windows => unreachable, + else => return error.HotSwapUnavailableOnHostOperatingSystem, + } } } base.file = try emit.directory.handle.createFile(emit.sub_path, .{ diff --git a/src/link/Coff.zig b/src/link/Coff.zig index 7b5539287d..240c61bffd 100644 --- a/src/link/Coff.zig +++ b/src/link/Coff.zig @@ -827,6 +827,11 @@ fn resolveRelocs(self: *Coff, atom_index: Atom.Index, code: []u8) void { } } +pub fn ptraceAttach(self: *Coff, handle: std.os.pid_t) !void { + _ = self; + log.warn("attaching to process with handle {*}", .{handle}); +} + fn freeAtom(self: *Coff, atom_index: Atom.Index) void { log.debug("freeAtom {d}", .{atom_index}); diff --git a/src/main.zig b/src/main.zig index c96fd25766..1a445107ba 100644 --- a/src/main.zig +++ b/src/main.zig @@ -3817,11 +3817,25 @@ fn runOrTestHotSwap( runtime_args_start: ?usize, ) !std.ChildProcess.Id { const exe_emit = comp.bin_file.options.emit.?; - // A naive `directory.join` here will indeed get the correct path to the binary, - // however, in the case of cwd, we actually want `./foo` so that the path can be executed. - const exe_path = try fs.path.join(gpa, &[_][]const u8{ - exe_emit.directory.path orelse ".", exe_emit.sub_path, - }); + + const exe_path = switch (builtin.target.os.tag) { + // On Windows it seems impossible to perform an atomic rename of a file that is currently + // running in a process. Therefore, we do the opposite. We create a copy of the file in + // tmp zig-cache and use it to spawn the child process. This way we are free to update + // the binary with each requested hot update. + .windows => blk: { + try exe_emit.directory.handle.copyFile(exe_emit.sub_path, comp.local_cache_directory.handle, exe_emit.sub_path, .{}); + break :blk try fs.path.join(gpa, &[_][]const u8{ + comp.local_cache_directory.path orelse ".", exe_emit.sub_path, + }); + }, + + // A naive `directory.join` here will indeed get the correct path to the binary, + // however, in the case of cwd, we actually want `./foo` so that the path can be executed. + else => try fs.path.join(gpa, &[_][]const u8{ + exe_emit.directory.path orelse ".", exe_emit.sub_path, + }), + }; defer gpa.free(exe_path); var argv = std.ArrayList([]const u8).init(gpa); From 39d63036441841fb81eebda97fb2932e5c3f79c4 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Thu, 30 Mar 2023 12:32:29 +0200 Subject: [PATCH 02/10] coff: first (not-fully-functional) PoC of HCS --- src/link.zig | 2 +- src/link/Coff.zig | 159 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 157 insertions(+), 4 deletions(-) diff --git a/src/link.zig b/src/link.zig index d821027196..dc114eb0ad 100644 --- a/src/link.zig +++ b/src/link.zig @@ -443,7 +443,7 @@ pub const File = struct { .macos => base.cast(MachO).?.ptraceDetach(pid) catch |err| { log.warn("detaching failed with error: {s}", .{@errorName(err)}); }, - .windows => {}, + .windows => base.cast(Coff).?.ptraceDetach(pid), else => return error.HotSwapUnavailableOnHostOperatingSystem, } } diff --git a/src/link/Coff.zig b/src/link/Coff.zig index 240c61bffd..c2421ee912 100644 --- a/src/link/Coff.zig +++ b/src/link/Coff.zig @@ -89,6 +89,13 @@ relocs: RelocTable = .{}, /// this will be a table indexed by index into the list of Atoms. base_relocs: BaseRelocationTable = .{}, +/// Hot-code swapping state. +hot_state: HotUpdateState = .{}, + +const HotUpdateState = struct { + loaded_base_address: ?u64 = null, +}; + const Entry = struct { target: SymbolWithLoc, // Index into the synthetic symbol table (i.e., file == null). @@ -778,9 +785,147 @@ fn writeAtom(self: *Coff, atom_index: Atom.Index, code: []u8) !void { file_offset + code.len, }); self.resolveRelocs(atom_index, code); + + if (self.base.child_pid) |handle| { + const vaddr = sym.value + (self.hot_state.loaded_base_address orelse self.getImageBase()); + log.warn("hcs: writing to memory at address {x}", .{vaddr}); + try debugMem(self.base.allocator, handle, vaddr, code); + if (section.header.flags.MEM_WRITE == 0) { + log.warn(" page not mapped for write access; re-mapping...", .{}); + try writeMemProtected(handle, vaddr, code); + } else { + try writeMem(handle, vaddr, code); + } + } + try self.base.file.?.pwriteAll(code, file_offset); } +extern "kernel32" fn ReadProcessMemory( + hProcess: std.os.windows.HANDLE, + lpBaseAddress: std.os.windows.LPCVOID, + lpBuffer: std.os.windows.LPVOID, + nSize: std.os.windows.SIZE_T, + lpNumberOfBytesRead: *std.os.windows.SIZE_T, +) std.os.windows.BOOL; + +extern "kernel32" fn WriteProcessMemory( + hProcess: std.os.windows.HANDLE, + lpBaseAddress: std.os.windows.LPVOID, + lpBuffer: std.os.windows.LPCVOID, + nSize: std.os.windows.SIZE_T, + lpNumberOfBytesWritten: *std.os.windows.SIZE_T, +) std.os.windows.BOOL; + +extern "kernel32" fn VirtualProtectEx( + hProcess: std.os.windows.HANDLE, + lpAddress: std.os.windows.LPVOID, + dwSize: std.os.windows.SIZE_T, + flNewProtect: std.os.windows.DWORD, + lpflOldProtect: *std.os.windows.DWORD, +) std.os.windows.BOOL; + +const PROCESS_BASIC_INFORMATION = extern struct { + ExitStatus: std.os.windows.NTSTATUS, + PebBaseAddress: *std.os.windows.PEB, + AffinityMask: std.os.windows.ULONG_PTR, + BasePriority: std.os.windows.KPRIORITY, + UniqueProcessId: std.os.windows.ULONG_PTR, + InheritedFromUniqueProcessId: std.os.windows.ULONG_PTR, +}; + +fn getProcessBaseAddress(handle: std.ChildProcess.Id) !u64 { + var info: PROCESS_BASIC_INFORMATION = undefined; + var nread: std.os.windows.DWORD = 0; + const rc = std.os.windows.ntdll.NtQueryInformationProcess( + handle, + .ProcessBasicInformation, + &info, + @sizeOf(PROCESS_BASIC_INFORMATION), + &nread, + ); + switch (rc) { + .SUCCESS => {}, + else => return std.os.windows.unexpectedStatus(rc), + } + + var peb_buf: [@sizeOf(std.os.windows.PEB)]u8 align(@alignOf(std.os.windows.PEB)) = undefined; + var peb_nread: usize = 0; + if (ReadProcessMemory( + handle, + info.PebBaseAddress, + &peb_buf, + @sizeOf(std.os.windows.PEB), + &peb_nread, + ) == 0) { + const err = std.os.windows.kernel32.GetLastError(); + log.warn("hcs: reading from process memory failed with err: {s}({x})", .{ @tagName(err), @enumToInt(err) }); + return error.FailedToReadPebForProcess; + } + if (peb_nread != @sizeOf(std.os.windows.PEB)) return error.InputOutput; + + const peb = @ptrCast(*const std.os.windows.PEB, &peb_buf); + return @ptrToInt(peb.ImageBaseAddress); +} + +fn debugMem(allocator: Allocator, handle: std.ChildProcess.Id, vaddr: u64, code: []const u8) !void { + var buffer = try allocator.alloc(u8, code.len); + defer allocator.free(buffer); + var nread: usize = 0; + if (ReadProcessMemory( + handle, + @intToPtr(*anyopaque, vaddr), + buffer.ptr, + code.len, + &nread, + ) == 0) { + const err = std.os.windows.kernel32.GetLastError(); + log.warn("hcs: reading from process memory failed with err: {s}({x})", .{ @tagName(err), @enumToInt(err) }); + } + if (nread != code.len) { + log.warn("hcs: reading from process memory InputOutput error: read != requested: {x} != {x}", .{ nread, code.len }); + } + + log.warn("in memory: {x}", .{std.fmt.fmtSliceHexLower(buffer)}); + log.warn("to write: {x}", .{std.fmt.fmtSliceHexLower(code)}); +} + +fn writeMemProtected(handle: std.ChildProcess.Id, vaddr: u64, code: []const u8) !void { + const pvaddr = @intToPtr(*anyopaque, vaddr); + var new_prot: std.os.windows.DWORD = std.os.windows.PAGE_EXECUTE_WRITECOPY; + var old_prot: std.os.windows.DWORD = undefined; + if (VirtualProtectEx(handle, pvaddr, code.len, new_prot, &old_prot) == 0) { + const err = std.os.windows.kernel32.GetLastError(); + log.warn("hcs: making page(s) writeable failed with error: {s}({x})", .{ @tagName(err), @enumToInt(err) }); + return; + } + log.warn("old = {x}, new = {x}", .{ old_prot, new_prot }); + try writeMem(handle, vaddr, code); + // TODO: We can probably just set the pages writeable and leave it at that without having to restore the attributes. + // For that though, we want to track which page has already been modified. + if (VirtualProtectEx(handle, pvaddr, code.len, old_prot, &new_prot) == 0) { + const err = std.os.windows.kernel32.GetLastError(); + log.warn("hcs: restoring page(s) attributes failed with error: {s}({x})", .{ @tagName(err), @enumToInt(err) }); + } +} + +fn writeMem(handle: std.ChildProcess.Id, vaddr: u64, code: []const u8) !void { + var nwritten: usize = 0; + if (WriteProcessMemory( + handle, + @intToPtr(*anyopaque, vaddr), + code.ptr, + code.len, + &nwritten, + ) == 0) { + const err = std.os.windows.kernel32.GetLastError(); + log.warn("hcs: writing to process memory failed with err: {s}({x})", .{ @tagName(err), @enumToInt(err) }); + } + if (nwritten != code.len) { + log.warn("hcs: writing to process memory InputOutput error: written != requested: {x} != {x}", .{ nwritten, code.len }); + } +} + fn writePtrWidthAtom(self: *Coff, atom_index: Atom.Index) !void { switch (self.ptr_width) { .p32 => { @@ -827,9 +972,17 @@ fn resolveRelocs(self: *Coff, atom_index: Atom.Index, code: []u8) void { } } -pub fn ptraceAttach(self: *Coff, handle: std.os.pid_t) !void { - _ = self; - log.warn("attaching to process with handle {*}", .{handle}); +pub fn ptraceAttach(self: *Coff, handle: std.ChildProcess.Id) !void { + log.warn("hcs: attaching to process with handle {*}", .{handle}); + self.hot_state.loaded_base_address = getProcessBaseAddress(handle) catch |err| { + log.warn("hcs: failed to get base address for the process with error: {s}", .{@errorName(err)}); + return; + }; +} + +pub fn ptraceDetach(self: *Coff, handle: std.ChildProcess.Id) void { + log.warn("hcs: detaching from process with handle {*}", .{handle}); + self.hot_state.loaded_base_address = null; } fn freeAtom(self: *Coff, atom_index: Atom.Index) void { From d2f013085523436b51023f7d28d52a859b70a4b3 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Thu, 30 Mar 2023 14:10:11 +0200 Subject: [PATCH 03/10] coff: make sure we correctly slide relocation target when resolving --- src/link/Coff.zig | 30 ++++++++++++++---------------- src/link/Coff/Relocation.zig | 2 +- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/link/Coff.zig b/src/link/Coff.zig index c2421ee912..8337d69088 100644 --- a/src/link/Coff.zig +++ b/src/link/Coff.zig @@ -788,10 +788,9 @@ fn writeAtom(self: *Coff, atom_index: Atom.Index, code: []u8) !void { if (self.base.child_pid) |handle| { const vaddr = sym.value + (self.hot_state.loaded_base_address orelse self.getImageBase()); - log.warn("hcs: writing to memory at address {x}", .{vaddr}); - try debugMem(self.base.allocator, handle, vaddr, code); + log.debug("writing to memory at address {x}", .{vaddr}); if (section.header.flags.MEM_WRITE == 0) { - log.warn(" page not mapped for write access; re-mapping...", .{}); + log.debug("page not mapped for write access; re-mapping...", .{}); try writeMemProtected(handle, vaddr, code); } else { try writeMem(handle, vaddr, code); @@ -859,7 +858,7 @@ fn getProcessBaseAddress(handle: std.ChildProcess.Id) !u64 { &peb_nread, ) == 0) { const err = std.os.windows.kernel32.GetLastError(); - log.warn("hcs: reading from process memory failed with err: {s}({x})", .{ @tagName(err), @enumToInt(err) }); + log.warn("reading from process memory failed with err: {s}({x})", .{ @tagName(err), @enumToInt(err) }); return error.FailedToReadPebForProcess; } if (peb_nread != @sizeOf(std.os.windows.PEB)) return error.InputOutput; @@ -880,14 +879,14 @@ fn debugMem(allocator: Allocator, handle: std.ChildProcess.Id, vaddr: u64, code: &nread, ) == 0) { const err = std.os.windows.kernel32.GetLastError(); - log.warn("hcs: reading from process memory failed with err: {s}({x})", .{ @tagName(err), @enumToInt(err) }); + log.warn("reading from process memory failed with err: {s}({x})", .{ @tagName(err), @enumToInt(err) }); } if (nread != code.len) { - log.warn("hcs: reading from process memory InputOutput error: read != requested: {x} != {x}", .{ nread, code.len }); + log.warn("reading from process memory InputOutput error: read != requested: {x} != {x}", .{ nread, code.len }); } - log.warn("in memory: {x}", .{std.fmt.fmtSliceHexLower(buffer)}); - log.warn("to write: {x}", .{std.fmt.fmtSliceHexLower(code)}); + log.debug("in memory: {x}", .{std.fmt.fmtSliceHexLower(buffer)}); + log.debug("to write: {x}", .{std.fmt.fmtSliceHexLower(code)}); } fn writeMemProtected(handle: std.ChildProcess.Id, vaddr: u64, code: []const u8) !void { @@ -896,16 +895,15 @@ fn writeMemProtected(handle: std.ChildProcess.Id, vaddr: u64, code: []const u8) var old_prot: std.os.windows.DWORD = undefined; if (VirtualProtectEx(handle, pvaddr, code.len, new_prot, &old_prot) == 0) { const err = std.os.windows.kernel32.GetLastError(); - log.warn("hcs: making page(s) writeable failed with error: {s}({x})", .{ @tagName(err), @enumToInt(err) }); + log.warn("making page(s) writeable failed with error: {s}({x})", .{ @tagName(err), @enumToInt(err) }); return; } - log.warn("old = {x}, new = {x}", .{ old_prot, new_prot }); try writeMem(handle, vaddr, code); // TODO: We can probably just set the pages writeable and leave it at that without having to restore the attributes. // For that though, we want to track which page has already been modified. if (VirtualProtectEx(handle, pvaddr, code.len, old_prot, &new_prot) == 0) { const err = std.os.windows.kernel32.GetLastError(); - log.warn("hcs: restoring page(s) attributes failed with error: {s}({x})", .{ @tagName(err), @enumToInt(err) }); + log.warn("restoring page(s) attributes failed with error: {s}({x})", .{ @tagName(err), @enumToInt(err) }); } } @@ -919,10 +917,10 @@ fn writeMem(handle: std.ChildProcess.Id, vaddr: u64, code: []const u8) !void { &nwritten, ) == 0) { const err = std.os.windows.kernel32.GetLastError(); - log.warn("hcs: writing to process memory failed with err: {s}({x})", .{ @tagName(err), @enumToInt(err) }); + log.warn("writing to process memory failed with err: {s}({x})", .{ @tagName(err), @enumToInt(err) }); } if (nwritten != code.len) { - log.warn("hcs: writing to process memory InputOutput error: written != requested: {x} != {x}", .{ nwritten, code.len }); + log.warn("writing to process memory InputOutput error: written != requested: {x} != {x}", .{ nwritten, code.len }); } } @@ -973,15 +971,15 @@ fn resolveRelocs(self: *Coff, atom_index: Atom.Index, code: []u8) void { } pub fn ptraceAttach(self: *Coff, handle: std.ChildProcess.Id) !void { - log.warn("hcs: attaching to process with handle {*}", .{handle}); + log.debug("attaching to process with handle {*}", .{handle}); self.hot_state.loaded_base_address = getProcessBaseAddress(handle) catch |err| { - log.warn("hcs: failed to get base address for the process with error: {s}", .{@errorName(err)}); + log.warn("failed to get base address for the process with error: {s}", .{@errorName(err)}); return; }; } pub fn ptraceDetach(self: *Coff, handle: std.ChildProcess.Id) void { - log.warn("hcs: detaching from process with handle {*}", .{handle}); + log.debug("detaching from process with handle {*}", .{handle}); self.hot_state.loaded_base_address = null; } diff --git a/src/link/Coff/Relocation.zig b/src/link/Coff/Relocation.zig index 37bd3e292f..d778206e60 100644 --- a/src/link/Coff/Relocation.zig +++ b/src/link/Coff/Relocation.zig @@ -92,7 +92,7 @@ pub fn resolve(self: Relocation, atom_index: Atom.Index, code: []u8, coff_file: const ctx: Context = .{ .source_vaddr = source_vaddr, .target_vaddr = target_vaddr_with_addend, - .image_base = coff_file.getImageBase(), + .image_base = coff_file.hot_state.loaded_base_address orelse coff_file.getImageBase(), .code = code, .ptr_width = coff_file.ptr_width, }; From 423b9f11144cc7f7f027db9a091174e9c752c511 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Thu, 30 Mar 2023 17:04:50 +0200 Subject: [PATCH 04/10] coff: reimplement Read/WriteProcessMemory using our own ntdll wrappers --- src/link/Coff.zig | 117 +++++++++++++++++++++------------------------- 1 file changed, 53 insertions(+), 64 deletions(-) diff --git a/src/link/Coff.zig b/src/link/Coff.zig index 8337d69088..2dd29d8022 100644 --- a/src/link/Coff.zig +++ b/src/link/Coff.zig @@ -793,28 +793,60 @@ fn writeAtom(self: *Coff, atom_index: Atom.Index, code: []u8) !void { log.debug("page not mapped for write access; re-mapping...", .{}); try writeMemProtected(handle, vaddr, code); } else { - try writeMem(handle, vaddr, code); + if (WriteProcessMemory(handle, vaddr, code)) |amt| { + if (amt != code.len) return error.InputOutput; + } else |err| { + log.warn("writing to process memory failed with error: {s}", .{@errorName(err)}); + } } } try self.base.file.?.pwriteAll(code, file_offset); } -extern "kernel32" fn ReadProcessMemory( - hProcess: std.os.windows.HANDLE, - lpBaseAddress: std.os.windows.LPCVOID, - lpBuffer: std.os.windows.LPVOID, - nSize: std.os.windows.SIZE_T, - lpNumberOfBytesRead: *std.os.windows.SIZE_T, -) std.os.windows.BOOL; +extern "ntdll" fn NtReadVirtualMemory( + ProcessHandle: std.os.windows.HANDLE, + BaseAddress: std.os.windows.PVOID, + Buffer: std.os.windows.LPVOID, + NumberOfBytesToRead: std.os.windows.SIZE_T, + NumberOfBytesRead: ?*std.os.windows.SIZE_T, +) std.os.windows.NTSTATUS; -extern "kernel32" fn WriteProcessMemory( - hProcess: std.os.windows.HANDLE, - lpBaseAddress: std.os.windows.LPVOID, - lpBuffer: std.os.windows.LPCVOID, - nSize: std.os.windows.SIZE_T, - lpNumberOfBytesWritten: *std.os.windows.SIZE_T, -) std.os.windows.BOOL; +extern "ntdll" fn NtWriteVirtualMemory( + ProcessHandle: std.os.windows.HANDLE, + BaseAddress: std.os.windows.PVOID, + Buffer: std.os.windows.LPCVOID, + NumberOfBytesToWrite: std.os.windows.SIZE_T, + NumberOfBytesWritten: ?*std.os.windows.SIZE_T, +) std.os.windows.NTSTATUS; + +fn ReadProcessMemory(handle: std.os.windows.HANDLE, base_addr: usize, buffer: []u8) ![]u8 { + var nread: usize = 0; + switch (NtReadVirtualMemory( + handle, + @intToPtr(*anyopaque, base_addr), + buffer.ptr, + buffer.len, + &nread, + )) { + .SUCCESS => return buffer[0..nread], + else => |rc| return std.os.windows.unexpectedStatus(rc), + } +} + +fn WriteProcessMemory(handle: std.os.windows.HANDLE, base_addr: usize, buffer: []const u8) !usize { + var nwritten: usize = 0; + switch (NtWriteVirtualMemory( + handle, + @intToPtr(*anyopaque, base_addr), + @ptrCast(*const anyopaque, buffer.ptr), + buffer.len, + &nwritten, + )) { + .SUCCESS => return nwritten, + else => |rc| return std.os.windows.unexpectedStatus(rc), + } +} extern "kernel32" fn VirtualProtectEx( hProcess: std.os.windows.HANDLE, @@ -849,43 +881,16 @@ fn getProcessBaseAddress(handle: std.ChildProcess.Id) !u64 { } var peb_buf: [@sizeOf(std.os.windows.PEB)]u8 align(@alignOf(std.os.windows.PEB)) = undefined; - var peb_nread: usize = 0; - if (ReadProcessMemory( - handle, - info.PebBaseAddress, - &peb_buf, - @sizeOf(std.os.windows.PEB), - &peb_nread, - ) == 0) { - const err = std.os.windows.kernel32.GetLastError(); - log.warn("reading from process memory failed with err: {s}({x})", .{ @tagName(err), @enumToInt(err) }); - return error.FailedToReadPebForProcess; - } - if (peb_nread != @sizeOf(std.os.windows.PEB)) return error.InputOutput; - - const peb = @ptrCast(*const std.os.windows.PEB, &peb_buf); + const pebout = try ReadProcessMemory(handle, @ptrToInt(info.PebBaseAddress), &peb_buf); + const peb = @ptrCast(*const std.os.windows.PEB, @alignCast(@alignOf(std.os.windows.PEB), pebout.ptr)); return @ptrToInt(peb.ImageBaseAddress); } fn debugMem(allocator: Allocator, handle: std.ChildProcess.Id, vaddr: u64, code: []const u8) !void { var buffer = try allocator.alloc(u8, code.len); defer allocator.free(buffer); - var nread: usize = 0; - if (ReadProcessMemory( - handle, - @intToPtr(*anyopaque, vaddr), - buffer.ptr, - code.len, - &nread, - ) == 0) { - const err = std.os.windows.kernel32.GetLastError(); - log.warn("reading from process memory failed with err: {s}({x})", .{ @tagName(err), @enumToInt(err) }); - } - if (nread != code.len) { - log.warn("reading from process memory InputOutput error: read != requested: {x} != {x}", .{ nread, code.len }); - } - - log.debug("in memory: {x}", .{std.fmt.fmtSliceHexLower(buffer)}); + const memread = try ReadProcessMemory(handle, vaddr, buffer); + log.debug("in memory: {x}", .{std.fmt.fmtSliceHexLower(memread)}); log.debug("to write: {x}", .{std.fmt.fmtSliceHexLower(code)}); } @@ -898,7 +903,8 @@ fn writeMemProtected(handle: std.ChildProcess.Id, vaddr: u64, code: []const u8) log.warn("making page(s) writeable failed with error: {s}({x})", .{ @tagName(err), @enumToInt(err) }); return; } - try writeMem(handle, vaddr, code); + const amt = try WriteProcessMemory(handle, vaddr, code); + if (amt != code.len) return error.InputOutput; // TODO: We can probably just set the pages writeable and leave it at that without having to restore the attributes. // For that though, we want to track which page has already been modified. if (VirtualProtectEx(handle, pvaddr, code.len, old_prot, &new_prot) == 0) { @@ -907,23 +913,6 @@ fn writeMemProtected(handle: std.ChildProcess.Id, vaddr: u64, code: []const u8) } } -fn writeMem(handle: std.ChildProcess.Id, vaddr: u64, code: []const u8) !void { - var nwritten: usize = 0; - if (WriteProcessMemory( - handle, - @intToPtr(*anyopaque, vaddr), - code.ptr, - code.len, - &nwritten, - ) == 0) { - const err = std.os.windows.kernel32.GetLastError(); - log.warn("writing to process memory failed with err: {s}({x})", .{ @tagName(err), @enumToInt(err) }); - } - if (nwritten != code.len) { - log.warn("writing to process memory InputOutput error: written != requested: {x} != {x}", .{ nwritten, code.len }); - } -} - fn writePtrWidthAtom(self: *Coff, atom_index: Atom.Index) !void { switch (self.ptr_width) { .p32 => { From 5d387742fd0f9e5c2072a2a527ccbf22044d4513 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Thu, 30 Mar 2023 17:18:34 +0200 Subject: [PATCH 05/10] coff: reimplement VirtualProtectEx using our own ntdll wrapper --- lib/std/os/windows/ntdll.zig | 2 +- src/link/Coff.zig | 44 ++++++++++++++++++++---------------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/lib/std/os/windows/ntdll.zig b/lib/std/os/windows/ntdll.zig index 429b36039e..8c31a6fcc9 100644 --- a/lib/std/os/windows/ntdll.zig +++ b/lib/std/os/windows/ntdll.zig @@ -367,7 +367,7 @@ pub extern "ntdll" fn RtlQueryRegistryValues( pub extern "ntdll" fn NtProtectVirtualMemory( ProcessHandle: HANDLE, BaseAddress: *PVOID, - NumberOfBytesToProtect: *ULONG, + NumberOfBytesToProtect: *SIZE_T, NewAccessProtection: ULONG, OldAccessProtection: *ULONG, ) callconv(WINAPI) NTSTATUS; diff --git a/src/link/Coff.zig b/src/link/Coff.zig index 2dd29d8022..97a862dfd8 100644 --- a/src/link/Coff.zig +++ b/src/link/Coff.zig @@ -820,6 +820,14 @@ extern "ntdll" fn NtWriteVirtualMemory( NumberOfBytesWritten: ?*std.os.windows.SIZE_T, ) std.os.windows.NTSTATUS; +extern "ntdll" fn NtProtectVirtualMemory( + ProcessHandle: std.os.windows.HANDLE, + BaseAddress: *std.os.windows.PVOID, + NumberOfBytesToProtect: *std.os.windows.SIZE_T, + NewAccessProtection: std.os.windows.ULONG, + OldAccessProtection: *std.os.windows.ULONG, +) std.os.windows.NTSTATUS; + fn ReadProcessMemory(handle: std.os.windows.HANDLE, base_addr: usize, buffer: []u8) ![]u8 { var nread: usize = 0; switch (NtReadVirtualMemory( @@ -848,13 +856,21 @@ fn WriteProcessMemory(handle: std.os.windows.HANDLE, base_addr: usize, buffer: [ } } -extern "kernel32" fn VirtualProtectEx( - hProcess: std.os.windows.HANDLE, - lpAddress: std.os.windows.LPVOID, - dwSize: std.os.windows.SIZE_T, - flNewProtect: std.os.windows.DWORD, - lpflOldProtect: *std.os.windows.DWORD, -) std.os.windows.BOOL; +fn VirtualProtectEx(handle: std.os.windows.HANDLE, base_addr: usize, size: usize, new_prot: u32) !u32 { + var out_paddr = @intToPtr(*anyopaque, base_addr); + var out_size = size; + var old_prot: u32 = undefined; + switch (NtProtectVirtualMemory( + handle, + &out_paddr, + &out_size, + new_prot, + &old_prot, + )) { + .SUCCESS => return old_prot, + else => |rc| return std.os.windows.unexpectedStatus(rc), + } +} const PROCESS_BASIC_INFORMATION = extern struct { ExitStatus: std.os.windows.NTSTATUS, @@ -895,22 +911,12 @@ fn debugMem(allocator: Allocator, handle: std.ChildProcess.Id, vaddr: u64, code: } fn writeMemProtected(handle: std.ChildProcess.Id, vaddr: u64, code: []const u8) !void { - const pvaddr = @intToPtr(*anyopaque, vaddr); - var new_prot: std.os.windows.DWORD = std.os.windows.PAGE_EXECUTE_WRITECOPY; - var old_prot: std.os.windows.DWORD = undefined; - if (VirtualProtectEx(handle, pvaddr, code.len, new_prot, &old_prot) == 0) { - const err = std.os.windows.kernel32.GetLastError(); - log.warn("making page(s) writeable failed with error: {s}({x})", .{ @tagName(err), @enumToInt(err) }); - return; - } + const old_prot = try VirtualProtectEx(handle, vaddr, code.len, std.os.windows.PAGE_EXECUTE_WRITECOPY); const amt = try WriteProcessMemory(handle, vaddr, code); if (amt != code.len) return error.InputOutput; // TODO: We can probably just set the pages writeable and leave it at that without having to restore the attributes. // For that though, we want to track which page has already been modified. - if (VirtualProtectEx(handle, pvaddr, code.len, old_prot, &new_prot) == 0) { - const err = std.os.windows.kernel32.GetLastError(); - log.warn("restoring page(s) attributes failed with error: {s}({x})", .{ @tagName(err), @enumToInt(err) }); - } + _ = try VirtualProtectEx(handle, vaddr, code.len, old_prot); } fn writePtrWidthAtom(self: *Coff, atom_index: Atom.Index) !void { From ba5302c4f88d2942890a5838e53eb078d61da123 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Thu, 30 Mar 2023 18:52:27 +0200 Subject: [PATCH 06/10] std: move ntdll wrappers to std.os.windows --- lib/std/os/windows.zig | 198 +++++++++++++++++++++++++++++++++++ lib/std/os/windows/ntdll.zig | 130 ++++------------------- 2 files changed, 220 insertions(+), 108 deletions(-) diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index fe0a68a13a..97462f6505 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -1514,6 +1514,23 @@ pub fn VirtualProtect(lpAddress: ?LPVOID, dwSize: SIZE_T, flNewProtect: DWORD, l } } +pub fn VirtualProtectEx(handle: HANDLE, addr: ?LPVOID, size: usize, new_prot: DWORD, old_prot: ?*DWORD) VirtualProtectError!void { + var out_addr = addr; + var out_size = size; + switch (ntdll.NtProtectVirtualMemory( + handle, + &out_addr, + &out_size, + new_prot, + old_prot, + )) { + .SUCCESS => {}, + .INVALID_ADDRESS => return error.InvalidAddress, + // TODO: map errors + else => |rc| return std.os.windows.unexpectedStatus(rc), + } +} + pub const VirtualQueryError = error{Unexpected}; pub fn VirtualQuery(lpAddress: ?LPVOID, lpBuffer: PMEMORY_BASIC_INFORMATION, dwLength: SIZE_T) VirtualQueryError!SIZE_T { @@ -4457,3 +4474,184 @@ pub const MODULEENTRY32 = extern struct { szModule: [MAX_MODULE_NAME32 + 1]CHAR, szExePath: [MAX_PATH]CHAR, }; + +pub const THREADINFOCLASS = enum(c_int) { + ThreadBasicInformation, + ThreadTimes, + ThreadPriority, + ThreadBasePriority, + ThreadAffinityMask, + ThreadImpersonationToken, + ThreadDescriptorTableEntry, + ThreadEnableAlignmentFaultFixup, + ThreadEventPair_Reusable, + ThreadQuerySetWin32StartAddress, + ThreadZeroTlsCell, + ThreadPerformanceCount, + ThreadAmILastThread, + ThreadIdealProcessor, + ThreadPriorityBoost, + ThreadSetTlsArrayAddress, + ThreadIsIoPending, + // Windows 2000+ from here + ThreadHideFromDebugger, + // Windows XP+ from here + ThreadBreakOnTermination, + ThreadSwitchLegacyState, + ThreadIsTerminated, + // Windows Vista+ from here + ThreadLastSystemCall, + ThreadIoPriority, + ThreadCycleTime, + ThreadPagePriority, + ThreadActualBasePriority, + ThreadTebInformation, + ThreadCSwitchMon, + // Windows 7+ from here + ThreadCSwitchPmu, + ThreadWow64Context, + ThreadGroupInformation, + ThreadUmsInformation, + ThreadCounterProfiling, + ThreadIdealProcessorEx, + // Windows 8+ from here + ThreadCpuAccountingInformation, + // Windows 8.1+ from here + ThreadSuspendCount, + // Windows 10+ from here + ThreadHeterogeneousCpuPolicy, + ThreadContainerId, + ThreadNameInformation, + ThreadSelectedCpuSets, + ThreadSystemThreadInformation, + ThreadActualGroupAffinity, +}; + +pub const PROCESSINFOCLASS = enum(c_int) { + ProcessBasicInformation, + ProcessQuotaLimits, + ProcessIoCounters, + ProcessVmCounters, + ProcessTimes, + ProcessBasePriority, + ProcessRaisePriority, + ProcessDebugPort, + ProcessExceptionPort, + ProcessAccessToken, + ProcessLdtInformation, + ProcessLdtSize, + ProcessDefaultHardErrorMode, + ProcessIoPortHandlers, + ProcessPooledUsageAndLimits, + ProcessWorkingSetWatch, + ProcessUserModeIOPL, + ProcessEnableAlignmentFaultFixup, + ProcessPriorityClass, + ProcessWx86Information, + ProcessHandleCount, + ProcessAffinityMask, + ProcessPriorityBoost, + ProcessDeviceMap, + ProcessSessionInformation, + ProcessForegroundInformation, + ProcessWow64Information, + ProcessImageFileName, + ProcessLUIDDeviceMapsEnabled, + ProcessBreakOnTermination, + ProcessDebugObjectHandle, + ProcessDebugFlags, + ProcessHandleTracing, + ProcessIoPriority, + ProcessExecuteFlags, + ProcessTlsInformation, + ProcessCookie, + ProcessImageInformation, + ProcessCycleTime, + ProcessPagePriority, + ProcessInstrumentationCallback, + ProcessThreadStackAllocation, + ProcessWorkingSetWatchEx, + ProcessImageFileNameWin32, + ProcessImageFileMapping, + ProcessAffinityUpdateMode, + ProcessMemoryAllocationMode, + ProcessGroupInformation, + ProcessTokenVirtualizationEnabled, + ProcessConsoleHostProcess, + ProcessWindowInformation, + MaxProcessInfoClass, +}; + +pub const PROCESS_BASIC_INFORMATION = extern struct { + ExitStatus: NTSTATUS, + PebBaseAddress: *PEB, + AffinityMask: ULONG_PTR, + BasePriority: KPRIORITY, + UniqueProcessId: ULONG_PTR, + InheritedFromUniqueProcessId: ULONG_PTR, +}; + +pub const ReadMemoryError = error{ + Unexpected, +}; + +pub fn ReadProcessMemory(handle: HANDLE, addr: ?LPVOID, buffer: []u8) ReadMemoryError![]u8 { + var nread: usize = 0; + switch (ntdll.NtReadVirtualMemory( + handle, + addr, + buffer.ptr, + buffer.len, + &nread, + )) { + .SUCCESS => return buffer[0..nread], + // TODO: map errors + else => |rc| return unexpectedStatus(rc), + } +} + +pub const WriteMemoryError = error{ + Unexpected, +}; + +pub fn WriteProcessMemory(handle: HANDLE, addr: ?LPVOID, buffer: []const u8) WriteMemoryError!usize { + var nwritten: usize = 0; + switch (ntdll.NtWriteVirtualMemory( + handle, + addr, + @ptrCast(*const anyopaque, buffer.ptr), + buffer.len, + &nwritten, + )) { + .SUCCESS => return nwritten, + // TODO: map errors + else => |rc| return unexpectedStatus(rc), + } +} + +pub const ProcessBaseAddressError = GetProcessMemoryInfoError || ReadMemoryError; + +/// Returns the base address of the process loaded into memory. +pub fn ProcessBaseAddress(handle: HANDLE) ProcessBaseAddressError!HMODULE { + var info: PROCESS_BASIC_INFORMATION = undefined; + var nread: DWORD = 0; + const rc = ntdll.NtQueryInformationProcess( + handle, + .ProcessBasicInformation, + &info, + @sizeOf(PROCESS_BASIC_INFORMATION), + &nread, + ); + switch (rc) { + .SUCCESS => {}, + .ACCESS_DENIED => return error.AccessDenied, + .INVALID_HANDLE => return error.InvalidHandle, + .INVALID_PARAMETER => unreachable, + else => return unexpectedStatus(rc), + } + + var peb_buf: [@sizeOf(PEB)]u8 align(@alignOf(PEB)) = undefined; + const peb_out = try ReadProcessMemory(handle, info.PebBaseAddress, &peb_buf); + const ppeb = @ptrCast(*const PEB, @alignCast(@alignOf(PEB), peb_out.ptr)); + return ppeb.ImageBaseAddress; +} diff --git a/lib/std/os/windows/ntdll.zig b/lib/std/os/windows/ntdll.zig index 8c31a6fcc9..6fb75ae321 100644 --- a/lib/std/os/windows/ntdll.zig +++ b/lib/std/os/windows/ntdll.zig @@ -31,61 +31,10 @@ const UNWIND_HISTORY_TABLE = windows.UNWIND_HISTORY_TABLE; const RUNTIME_FUNCTION = windows.RUNTIME_FUNCTION; const KNONVOLATILE_CONTEXT_POINTERS = windows.KNONVOLATILE_CONTEXT_POINTERS; const EXCEPTION_ROUTINE = windows.EXCEPTION_ROUTINE; - -pub const PROCESSINFOCLASS = enum(c_int) { - ProcessBasicInformation, - ProcessQuotaLimits, - ProcessIoCounters, - ProcessVmCounters, - ProcessTimes, - ProcessBasePriority, - ProcessRaisePriority, - ProcessDebugPort, - ProcessExceptionPort, - ProcessAccessToken, - ProcessLdtInformation, - ProcessLdtSize, - ProcessDefaultHardErrorMode, - ProcessIoPortHandlers, - ProcessPooledUsageAndLimits, - ProcessWorkingSetWatch, - ProcessUserModeIOPL, - ProcessEnableAlignmentFaultFixup, - ProcessPriorityClass, - ProcessWx86Information, - ProcessHandleCount, - ProcessAffinityMask, - ProcessPriorityBoost, - ProcessDeviceMap, - ProcessSessionInformation, - ProcessForegroundInformation, - ProcessWow64Information, - ProcessImageFileName, - ProcessLUIDDeviceMapsEnabled, - ProcessBreakOnTermination, - ProcessDebugObjectHandle, - ProcessDebugFlags, - ProcessHandleTracing, - ProcessIoPriority, - ProcessExecuteFlags, - ProcessTlsInformation, - ProcessCookie, - ProcessImageInformation, - ProcessCycleTime, - ProcessPagePriority, - ProcessInstrumentationCallback, - ProcessThreadStackAllocation, - ProcessWorkingSetWatchEx, - ProcessImageFileNameWin32, - ProcessImageFileMapping, - ProcessAffinityUpdateMode, - ProcessMemoryAllocationMode, - ProcessGroupInformation, - ProcessTokenVirtualizationEnabled, - ProcessConsoleHostProcess, - ProcessWindowInformation, - MaxProcessInfoClass, -}; +const THREADINFOCLASS = windows.THREADINFOCLASS; +const PROCESSINFOCLASS = windows.PROCESSINFOCLASS; +const LPVOID = windows.LPVOID; +const LPCVOID = windows.LPCVOID; pub extern "ntdll" fn NtQueryInformationProcess( ProcessHandle: HANDLE, @@ -95,57 +44,6 @@ pub extern "ntdll" fn NtQueryInformationProcess( ReturnLength: ?*ULONG, ) callconv(WINAPI) NTSTATUS; -pub const THREADINFOCLASS = enum(c_int) { - ThreadBasicInformation, - ThreadTimes, - ThreadPriority, - ThreadBasePriority, - ThreadAffinityMask, - ThreadImpersonationToken, - ThreadDescriptorTableEntry, - ThreadEnableAlignmentFaultFixup, - ThreadEventPair_Reusable, - ThreadQuerySetWin32StartAddress, - ThreadZeroTlsCell, - ThreadPerformanceCount, - ThreadAmILastThread, - ThreadIdealProcessor, - ThreadPriorityBoost, - ThreadSetTlsArrayAddress, - ThreadIsIoPending, - // Windows 2000+ from here - ThreadHideFromDebugger, - // Windows XP+ from here - ThreadBreakOnTermination, - ThreadSwitchLegacyState, - ThreadIsTerminated, - // Windows Vista+ from here - ThreadLastSystemCall, - ThreadIoPriority, - ThreadCycleTime, - ThreadPagePriority, - ThreadActualBasePriority, - ThreadTebInformation, - ThreadCSwitchMon, - // Windows 7+ from here - ThreadCSwitchPmu, - ThreadWow64Context, - ThreadGroupInformation, - ThreadUmsInformation, - ThreadCounterProfiling, - ThreadIdealProcessorEx, - // Windows 8+ from here - ThreadCpuAccountingInformation, - // Windows 8.1+ from here - ThreadSuspendCount, - // Windows 10+ from here - ThreadHeterogeneousCpuPolicy, - ThreadContainerId, - ThreadNameInformation, - ThreadSelectedCpuSets, - ThreadSystemThreadInformation, - ThreadActualGroupAffinity, -}; pub extern "ntdll" fn NtQueryInformationThread( ThreadHandle: HANDLE, ThreadInformationClass: THREADINFOCLASS, @@ -364,10 +262,26 @@ pub extern "ntdll" fn RtlQueryRegistryValues( Environment: ?*anyopaque, ) callconv(WINAPI) NTSTATUS; +pub extern "ntdll" fn NtReadVirtualMemory( + ProcessHandle: HANDLE, + BaseAddress: ?PVOID, + Buffer: LPVOID, + NumberOfBytesToRead: SIZE_T, + NumberOfBytesRead: ?*SIZE_T, +) callconv(WINAPI) NTSTATUS; + +pub extern "ntdll" fn NtWriteVirtualMemory( + ProcessHandle: HANDLE, + BaseAddress: ?PVOID, + Buffer: LPCVOID, + NumberOfBytesToWrite: SIZE_T, + NumberOfBytesWritten: ?*SIZE_T, +) callconv(WINAPI) NTSTATUS; + pub extern "ntdll" fn NtProtectVirtualMemory( ProcessHandle: HANDLE, - BaseAddress: *PVOID, + BaseAddress: *?PVOID, NumberOfBytesToProtect: *SIZE_T, NewAccessProtection: ULONG, - OldAccessProtection: *ULONG, + OldAccessProtection: ?*ULONG, ) callconv(WINAPI) NTSTATUS; From 216badef0bd90c6353bf32bd927e2c6d36b3ebf6 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Thu, 30 Mar 2023 18:52:56 +0200 Subject: [PATCH 07/10] coff: use std.os.windows wrappers; fix relocating in-file --- src/link/Coff.zig | 150 ++++++++--------------------------- src/link/Coff/Relocation.zig | 4 +- 2 files changed, 35 insertions(+), 119 deletions(-) diff --git a/src/link/Coff.zig b/src/link/Coff.zig index 97a862dfd8..87ad1085aa 100644 --- a/src/link/Coff.zig +++ b/src/link/Coff.zig @@ -93,7 +93,9 @@ base_relocs: BaseRelocationTable = .{}, hot_state: HotUpdateState = .{}, const HotUpdateState = struct { - loaded_base_address: ?u64 = null, + /// Base address at which the process (image) got loaded. + /// We need this info to correctly slide pointers when relocating. + loaded_base_address: ?std.os.windows.HMODULE = null, }; const Entry = struct { @@ -784,139 +786,53 @@ fn writeAtom(self: *Coff, atom_index: Atom.Index, code: []u8) !void { file_offset, file_offset + code.len, }); - self.resolveRelocs(atom_index, code); if (self.base.child_pid) |handle| { - const vaddr = sym.value + (self.hot_state.loaded_base_address orelse self.getImageBase()); + const slide = @ptrToInt(self.hot_state.loaded_base_address.?); + + const mem_code = try self.base.allocator.dupe(u8, code); + defer self.base.allocator.free(mem_code); + self.resolveRelocs(atom_index, mem_code, slide); + + const vaddr = sym.value + slide; + const pvaddr = @intToPtr(*anyopaque, vaddr); log.debug("writing to memory at address {x}", .{vaddr}); if (section.header.flags.MEM_WRITE == 0) { log.debug("page not mapped for write access; re-mapping...", .{}); - try writeMemProtected(handle, vaddr, code); + writeMemProtected(handle, pvaddr, mem_code) catch |err| { + log.warn("writing to protected memory failed with error: {s}", .{@errorName(err)}); + }; } else { - if (WriteProcessMemory(handle, vaddr, code)) |amt| { - if (amt != code.len) return error.InputOutput; - } else |err| { - log.warn("writing to process memory failed with error: {s}", .{@errorName(err)}); - } + writeMem(handle, pvaddr, mem_code) catch |err| { + log.warn("writing to protected memory failed with error: {s}", .{@errorName(err)}); + }; } } + self.resolveRelocs(atom_index, code, self.getImageBase()); try self.base.file.?.pwriteAll(code, file_offset); } -extern "ntdll" fn NtReadVirtualMemory( - ProcessHandle: std.os.windows.HANDLE, - BaseAddress: std.os.windows.PVOID, - Buffer: std.os.windows.LPVOID, - NumberOfBytesToRead: std.os.windows.SIZE_T, - NumberOfBytesRead: ?*std.os.windows.SIZE_T, -) std.os.windows.NTSTATUS; - -extern "ntdll" fn NtWriteVirtualMemory( - ProcessHandle: std.os.windows.HANDLE, - BaseAddress: std.os.windows.PVOID, - Buffer: std.os.windows.LPCVOID, - NumberOfBytesToWrite: std.os.windows.SIZE_T, - NumberOfBytesWritten: ?*std.os.windows.SIZE_T, -) std.os.windows.NTSTATUS; - -extern "ntdll" fn NtProtectVirtualMemory( - ProcessHandle: std.os.windows.HANDLE, - BaseAddress: *std.os.windows.PVOID, - NumberOfBytesToProtect: *std.os.windows.SIZE_T, - NewAccessProtection: std.os.windows.ULONG, - OldAccessProtection: *std.os.windows.ULONG, -) std.os.windows.NTSTATUS; - -fn ReadProcessMemory(handle: std.os.windows.HANDLE, base_addr: usize, buffer: []u8) ![]u8 { - var nread: usize = 0; - switch (NtReadVirtualMemory( - handle, - @intToPtr(*anyopaque, base_addr), - buffer.ptr, - buffer.len, - &nread, - )) { - .SUCCESS => return buffer[0..nread], - else => |rc| return std.os.windows.unexpectedStatus(rc), - } -} - -fn WriteProcessMemory(handle: std.os.windows.HANDLE, base_addr: usize, buffer: []const u8) !usize { - var nwritten: usize = 0; - switch (NtWriteVirtualMemory( - handle, - @intToPtr(*anyopaque, base_addr), - @ptrCast(*const anyopaque, buffer.ptr), - buffer.len, - &nwritten, - )) { - .SUCCESS => return nwritten, - else => |rc| return std.os.windows.unexpectedStatus(rc), - } -} - -fn VirtualProtectEx(handle: std.os.windows.HANDLE, base_addr: usize, size: usize, new_prot: u32) !u32 { - var out_paddr = @intToPtr(*anyopaque, base_addr); - var out_size = size; - var old_prot: u32 = undefined; - switch (NtProtectVirtualMemory( - handle, - &out_paddr, - &out_size, - new_prot, - &old_prot, - )) { - .SUCCESS => return old_prot, - else => |rc| return std.os.windows.unexpectedStatus(rc), - } -} - -const PROCESS_BASIC_INFORMATION = extern struct { - ExitStatus: std.os.windows.NTSTATUS, - PebBaseAddress: *std.os.windows.PEB, - AffinityMask: std.os.windows.ULONG_PTR, - BasePriority: std.os.windows.KPRIORITY, - UniqueProcessId: std.os.windows.ULONG_PTR, - InheritedFromUniqueProcessId: std.os.windows.ULONG_PTR, -}; - -fn getProcessBaseAddress(handle: std.ChildProcess.Id) !u64 { - var info: PROCESS_BASIC_INFORMATION = undefined; - var nread: std.os.windows.DWORD = 0; - const rc = std.os.windows.ntdll.NtQueryInformationProcess( - handle, - .ProcessBasicInformation, - &info, - @sizeOf(PROCESS_BASIC_INFORMATION), - &nread, - ); - switch (rc) { - .SUCCESS => {}, - else => return std.os.windows.unexpectedStatus(rc), - } - - var peb_buf: [@sizeOf(std.os.windows.PEB)]u8 align(@alignOf(std.os.windows.PEB)) = undefined; - const pebout = try ReadProcessMemory(handle, @ptrToInt(info.PebBaseAddress), &peb_buf); - const peb = @ptrCast(*const std.os.windows.PEB, @alignCast(@alignOf(std.os.windows.PEB), pebout.ptr)); - return @ptrToInt(peb.ImageBaseAddress); -} - -fn debugMem(allocator: Allocator, handle: std.ChildProcess.Id, vaddr: u64, code: []const u8) !void { +fn debugMem(allocator: Allocator, handle: std.ChildProcess.Id, pvaddr: std.os.windows.LPVOID, code: []const u8) !void { var buffer = try allocator.alloc(u8, code.len); defer allocator.free(buffer); - const memread = try ReadProcessMemory(handle, vaddr, buffer); + const memread = try std.os.windows.ReadProcessMemory(handle, pvaddr, buffer); log.debug("in memory: {x}", .{std.fmt.fmtSliceHexLower(memread)}); log.debug("to write: {x}", .{std.fmt.fmtSliceHexLower(code)}); } -fn writeMemProtected(handle: std.ChildProcess.Id, vaddr: u64, code: []const u8) !void { - const old_prot = try VirtualProtectEx(handle, vaddr, code.len, std.os.windows.PAGE_EXECUTE_WRITECOPY); - const amt = try WriteProcessMemory(handle, vaddr, code); - if (amt != code.len) return error.InputOutput; +fn writeMemProtected(handle: std.ChildProcess.Id, pvaddr: std.os.windows.LPVOID, code: []const u8) !void { + var old_prot: std.os.windows.DWORD = undefined; + try std.os.windows.VirtualProtectEx(handle, pvaddr, code.len, std.os.windows.PAGE_EXECUTE_WRITECOPY, &old_prot); + try writeMem(handle, pvaddr, code); // TODO: We can probably just set the pages writeable and leave it at that without having to restore the attributes. // For that though, we want to track which page has already been modified. - _ = try VirtualProtectEx(handle, vaddr, code.len, old_prot); + try std.os.windows.VirtualProtectEx(handle, pvaddr, code.len, old_prot, null); +} + +fn writeMem(handle: std.ChildProcess.Id, pvaddr: std.os.windows.LPVOID, code: []const u8) !void { + const amt = try std.os.windows.WriteProcessMemory(handle, pvaddr, code); + if (amt != code.len) return error.InputOutput; } fn writePtrWidthAtom(self: *Coff, atom_index: Atom.Index) !void { @@ -952,14 +868,14 @@ fn markRelocsDirtyByAddress(self: *Coff, addr: u32) void { } } -fn resolveRelocs(self: *Coff, atom_index: Atom.Index, code: []u8) void { +fn resolveRelocs(self: *Coff, atom_index: Atom.Index, code: []u8, image_base: u64) void { const relocs = self.relocs.getPtr(atom_index) orelse return; log.debug("relocating '{s}'", .{self.getAtom(atom_index).getName(self)}); for (relocs.items) |*reloc| { if (!reloc.dirty) continue; - if (reloc.resolve(atom_index, code, self)) { + if (reloc.resolve(atom_index, code, image_base, self)) { reloc.dirty = false; } } @@ -967,7 +883,7 @@ fn resolveRelocs(self: *Coff, atom_index: Atom.Index, code: []u8) void { pub fn ptraceAttach(self: *Coff, handle: std.ChildProcess.Id) !void { log.debug("attaching to process with handle {*}", .{handle}); - self.hot_state.loaded_base_address = getProcessBaseAddress(handle) catch |err| { + self.hot_state.loaded_base_address = std.os.windows.ProcessBaseAddress(handle) catch |err| { log.warn("failed to get base address for the process with error: {s}", .{@errorName(err)}); return; }; diff --git a/src/link/Coff/Relocation.zig b/src/link/Coff/Relocation.zig index d778206e60..6b35de93f4 100644 --- a/src/link/Coff/Relocation.zig +++ b/src/link/Coff/Relocation.zig @@ -74,7 +74,7 @@ pub fn getTargetAddress(self: Relocation, coff_file: *const Coff) ?u32 { /// Returns `false` if obtaining the target address has been deferred until `flushModule`. /// This can happen when trying to resolve address of an import table entry ahead of time. -pub fn resolve(self: Relocation, atom_index: Atom.Index, code: []u8, coff_file: *Coff) bool { +pub fn resolve(self: Relocation, atom_index: Atom.Index, code: []u8, image_base: u64, coff_file: *Coff) bool { const atom = coff_file.getAtom(atom_index); const source_sym = atom.getSymbol(coff_file); const source_vaddr = source_sym.value + self.offset; @@ -92,7 +92,7 @@ pub fn resolve(self: Relocation, atom_index: Atom.Index, code: []u8, coff_file: const ctx: Context = .{ .source_vaddr = source_vaddr, .target_vaddr = target_vaddr_with_addend, - .image_base = coff_file.hot_state.loaded_base_address orelse coff_file.getImageBase(), + .image_base = image_base, .code = code, .ptr_width = coff_file.ptr_width, }; From 349349fa01f77fe3bf2b57dc821f889e2e869004 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Thu, 30 Mar 2023 20:54:46 +0200 Subject: [PATCH 08/10] std: simplify VirtualProtectEx and fix ntdll signature --- lib/std/os/windows.zig | 7 ++++--- lib/std/os/windows/ntdll.zig | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 97462f6505..1e6f717bb2 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -1514,7 +1514,8 @@ pub fn VirtualProtect(lpAddress: ?LPVOID, dwSize: SIZE_T, flNewProtect: DWORD, l } } -pub fn VirtualProtectEx(handle: HANDLE, addr: ?LPVOID, size: usize, new_prot: DWORD, old_prot: ?*DWORD) VirtualProtectError!void { +pub fn VirtualProtectEx(handle: HANDLE, addr: ?LPVOID, size: SIZE_T, new_prot: DWORD) VirtualProtectError!DWORD { + var old_prot: DWORD = undefined; var out_addr = addr; var out_size = size; switch (ntdll.NtProtectVirtualMemory( @@ -1522,9 +1523,9 @@ pub fn VirtualProtectEx(handle: HANDLE, addr: ?LPVOID, size: usize, new_prot: DW &out_addr, &out_size, new_prot, - old_prot, + &old_prot, )) { - .SUCCESS => {}, + .SUCCESS => return old_prot, .INVALID_ADDRESS => return error.InvalidAddress, // TODO: map errors else => |rc| return std.os.windows.unexpectedStatus(rc), diff --git a/lib/std/os/windows/ntdll.zig b/lib/std/os/windows/ntdll.zig index 6fb75ae321..a8af39aa4d 100644 --- a/lib/std/os/windows/ntdll.zig +++ b/lib/std/os/windows/ntdll.zig @@ -283,5 +283,5 @@ pub extern "ntdll" fn NtProtectVirtualMemory( BaseAddress: *?PVOID, NumberOfBytesToProtect: *SIZE_T, NewAccessProtection: ULONG, - OldAccessProtection: ?*ULONG, + OldAccessProtection: *ULONG, ) callconv(WINAPI) NTSTATUS; From ee0c4457657523e218c1e211c447d3e196575ddc Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Thu, 30 Mar 2023 20:56:25 +0200 Subject: [PATCH 09/10] coff: due to ASLR we need to dupe the code for relocating In addition, we need to be careful not to mark the relocations as resolved prematurely as then we are risking malforming the binary as we need to resolve the relocs twice: once for in-memory writes, and once for in-file updates. --- src/link/Coff.zig | 59 ++++++++++++++++++++++++------------ src/link/Coff/Relocation.zig | 14 +++++---- 2 files changed, 48 insertions(+), 25 deletions(-) diff --git a/src/link/Coff.zig b/src/link/Coff.zig index 87ad1085aa..a7ca47c151 100644 --- a/src/link/Coff.zig +++ b/src/link/Coff.zig @@ -781,24 +781,47 @@ fn writeAtom(self: *Coff, atom_index: Atom.Index, code: []u8) !void { const sym = atom.getSymbol(self); const section = self.sections.get(@enumToInt(sym.section_number) - 1); const file_offset = section.header.pointer_to_raw_data + sym.value - section.header.virtual_address; + log.debug("writing atom for symbol {s} at file offset 0x{x} to 0x{x}", .{ atom.getName(self), file_offset, file_offset + code.len, }); + const gpa = self.base.allocator; + + // Gather relocs which can be resolved. + // We need to do this as we will be applying different slide values depending + // if we are running in hot-code swapping mode or not. + // TODO: how crazy would it be to try and apply the actual image base of the loaded + // process for the in-file values rather than the Windows defaults? + var relocs = std.ArrayList(*Relocation).init(gpa); + defer relocs.deinit(); + + if (self.relocs.getPtr(atom_index)) |rels| { + try relocs.ensureTotalCapacityPrecise(rels.items.len); + for (rels.items) |*reloc| { + if (reloc.isResolvable(self)) relocs.appendAssumeCapacity(reloc); + } + } + if (self.base.child_pid) |handle| { const slide = @ptrToInt(self.hot_state.loaded_base_address.?); - const mem_code = try self.base.allocator.dupe(u8, code); - defer self.base.allocator.free(mem_code); - self.resolveRelocs(atom_index, mem_code, slide); + const mem_code = try gpa.dupe(u8, code); + defer gpa.free(mem_code); + self.resolveRelocs(atom_index, relocs.items, mem_code, slide); const vaddr = sym.value + slide; const pvaddr = @intToPtr(*anyopaque, vaddr); + log.debug("writing to memory at address {x}", .{vaddr}); + + if (build_options.enable_logging) { + try debugMem(gpa, handle, pvaddr, mem_code); + } + if (section.header.flags.MEM_WRITE == 0) { - log.debug("page not mapped for write access; re-mapping...", .{}); writeMemProtected(handle, pvaddr, mem_code) catch |err| { log.warn("writing to protected memory failed with error: {s}", .{@errorName(err)}); }; @@ -809,25 +832,29 @@ fn writeAtom(self: *Coff, atom_index: Atom.Index, code: []u8) !void { } } - self.resolveRelocs(atom_index, code, self.getImageBase()); + self.resolveRelocs(atom_index, relocs.items, code, self.getImageBase()); try self.base.file.?.pwriteAll(code, file_offset); + + // Now we can mark the relocs as resolved. + while (relocs.popOrNull()) |reloc| { + reloc.dirty = false; + } } fn debugMem(allocator: Allocator, handle: std.ChildProcess.Id, pvaddr: std.os.windows.LPVOID, code: []const u8) !void { var buffer = try allocator.alloc(u8, code.len); defer allocator.free(buffer); const memread = try std.os.windows.ReadProcessMemory(handle, pvaddr, buffer); - log.debug("in memory: {x}", .{std.fmt.fmtSliceHexLower(memread)}); log.debug("to write: {x}", .{std.fmt.fmtSliceHexLower(code)}); + log.debug("in memory: {x}", .{std.fmt.fmtSliceHexLower(memread)}); } fn writeMemProtected(handle: std.ChildProcess.Id, pvaddr: std.os.windows.LPVOID, code: []const u8) !void { - var old_prot: std.os.windows.DWORD = undefined; - try std.os.windows.VirtualProtectEx(handle, pvaddr, code.len, std.os.windows.PAGE_EXECUTE_WRITECOPY, &old_prot); + const old_prot = try std.os.windows.VirtualProtectEx(handle, pvaddr, code.len, std.os.windows.PAGE_EXECUTE_WRITECOPY); try writeMem(handle, pvaddr, code); // TODO: We can probably just set the pages writeable and leave it at that without having to restore the attributes. // For that though, we want to track which page has already been modified. - try std.os.windows.VirtualProtectEx(handle, pvaddr, code.len, old_prot, null); + _ = try std.os.windows.VirtualProtectEx(handle, pvaddr, code.len, old_prot); } fn writeMem(handle: std.ChildProcess.Id, pvaddr: std.os.windows.LPVOID, code: []const u8) !void { @@ -868,16 +895,10 @@ fn markRelocsDirtyByAddress(self: *Coff, addr: u32) void { } } -fn resolveRelocs(self: *Coff, atom_index: Atom.Index, code: []u8, image_base: u64) void { - const relocs = self.relocs.getPtr(atom_index) orelse return; - +fn resolveRelocs(self: *Coff, atom_index: Atom.Index, relocs: []*const Relocation, code: []u8, image_base: u64) void { log.debug("relocating '{s}'", .{self.getAtom(atom_index).getName(self)}); - - for (relocs.items) |*reloc| { - if (!reloc.dirty) continue; - if (reloc.resolve(atom_index, code, image_base, self)) { - reloc.dirty = false; - } + for (relocs) |reloc| { + reloc.resolve(atom_index, code, image_base, self); } } @@ -1488,7 +1509,7 @@ pub fn flushModule(self: *Coff, comp: *Compilation, prog_node: *std.Progress.Nod for (self.relocs.keys(), self.relocs.values()) |atom_index, relocs| { const needs_update = for (relocs.items) |reloc| { - if (reloc.dirty) break true; + if (reloc.isResolvable(self)) break true; } else false; if (!needs_update) continue; diff --git a/src/link/Coff/Relocation.zig b/src/link/Coff/Relocation.zig index 6b35de93f4..2fafa0bbdc 100644 --- a/src/link/Coff/Relocation.zig +++ b/src/link/Coff/Relocation.zig @@ -72,14 +72,18 @@ pub fn getTargetAddress(self: Relocation, coff_file: *const Coff) ?u32 { } } -/// Returns `false` if obtaining the target address has been deferred until `flushModule`. -/// This can happen when trying to resolve address of an import table entry ahead of time. -pub fn resolve(self: Relocation, atom_index: Atom.Index, code: []u8, image_base: u64, coff_file: *Coff) bool { +/// Returns true if and only if the reloc is dirty AND the target address is available. +pub fn isResolvable(self: Relocation, coff_file: *Coff) bool { + _ = self.getTargetAddress(coff_file) orelse return false; + return self.dirty; +} + +pub fn resolve(self: Relocation, atom_index: Atom.Index, code: []u8, image_base: u64, coff_file: *Coff) void { const atom = coff_file.getAtom(atom_index); const source_sym = atom.getSymbol(coff_file); const source_vaddr = source_sym.value + self.offset; - const target_vaddr = self.getTargetAddress(coff_file) orelse return false; + const target_vaddr = self.getTargetAddress(coff_file).?; // Oops, you didn't check if the relocation can be resolved with isResolvable(). const target_vaddr_with_addend = target_vaddr + self.addend; log.debug(" ({x}: [() => 0x{x} ({s})) ({s}) ", .{ @@ -102,8 +106,6 @@ pub fn resolve(self: Relocation, atom_index: Atom.Index, code: []u8, image_base: .x86, .x86_64 => self.resolveX86(ctx), else => unreachable, // unhandled target architecture } - - return true; } const Context = struct { From 908ccce064a898d5db1d43dbdc4a3590fd84d4ba Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Thu, 30 Mar 2023 21:24:49 +0200 Subject: [PATCH 10/10] coff: enable hot-code swapping on a compatible host only --- src/link/Coff.zig | 51 ++++++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/src/link/Coff.zig b/src/link/Coff.zig index a7ca47c151..f3068f01a9 100644 --- a/src/link/Coff.zig +++ b/src/link/Coff.zig @@ -90,7 +90,12 @@ relocs: RelocTable = .{}, base_relocs: BaseRelocationTable = .{}, /// Hot-code swapping state. -hot_state: HotUpdateState = .{}, +hot_state: if (is_hot_update_compatible) HotUpdateState else struct {} = .{}, + +const is_hot_update_compatible = switch (builtin.target.os.tag) { + .windows => true, + else => false, +}; const HotUpdateState = struct { /// Base address at which the process (image) got loaded. @@ -805,30 +810,32 @@ fn writeAtom(self: *Coff, atom_index: Atom.Index, code: []u8) !void { } } - if (self.base.child_pid) |handle| { - const slide = @ptrToInt(self.hot_state.loaded_base_address.?); + if (is_hot_update_compatible) { + if (self.base.child_pid) |handle| { + const slide = @ptrToInt(self.hot_state.loaded_base_address.?); - const mem_code = try gpa.dupe(u8, code); - defer gpa.free(mem_code); - self.resolveRelocs(atom_index, relocs.items, mem_code, slide); + const mem_code = try gpa.dupe(u8, code); + defer gpa.free(mem_code); + self.resolveRelocs(atom_index, relocs.items, mem_code, slide); - const vaddr = sym.value + slide; - const pvaddr = @intToPtr(*anyopaque, vaddr); + const vaddr = sym.value + slide; + const pvaddr = @intToPtr(*anyopaque, vaddr); - log.debug("writing to memory at address {x}", .{vaddr}); + log.debug("writing to memory at address {x}", .{vaddr}); - if (build_options.enable_logging) { - try debugMem(gpa, handle, pvaddr, mem_code); - } + if (build_options.enable_logging) { + try debugMem(gpa, handle, pvaddr, mem_code); + } - if (section.header.flags.MEM_WRITE == 0) { - writeMemProtected(handle, pvaddr, mem_code) catch |err| { - log.warn("writing to protected memory failed with error: {s}", .{@errorName(err)}); - }; - } else { - writeMem(handle, pvaddr, mem_code) catch |err| { - log.warn("writing to protected memory failed with error: {s}", .{@errorName(err)}); - }; + if (section.header.flags.MEM_WRITE == 0) { + writeMemProtected(handle, pvaddr, mem_code) catch |err| { + log.warn("writing to protected memory failed with error: {s}", .{@errorName(err)}); + }; + } else { + writeMem(handle, pvaddr, mem_code) catch |err| { + log.warn("writing to protected memory failed with error: {s}", .{@errorName(err)}); + }; + } } } @@ -903,6 +910,8 @@ fn resolveRelocs(self: *Coff, atom_index: Atom.Index, relocs: []*const Relocatio } pub fn ptraceAttach(self: *Coff, handle: std.ChildProcess.Id) !void { + if (!is_hot_update_compatible) return; + log.debug("attaching to process with handle {*}", .{handle}); self.hot_state.loaded_base_address = std.os.windows.ProcessBaseAddress(handle) catch |err| { log.warn("failed to get base address for the process with error: {s}", .{@errorName(err)}); @@ -911,6 +920,8 @@ pub fn ptraceAttach(self: *Coff, handle: std.ChildProcess.Id) !void { } pub fn ptraceDetach(self: *Coff, handle: std.ChildProcess.Id) void { + if (!is_hot_update_compatible) return; + log.debug("detaching from process with handle {*}", .{handle}); self.hot_state.loaded_base_address = null; }