| author | |
| committer | |
| log | 5b82b40043e3a930e6693867c83de00ab3d20ef7 |
| tree | acc4e2cec84c50612cf0dc5428ec648d32ae76dd |
| parent | c964e10821c417ceb7d0efcf1625d67484e734f7 |
| parent | 908ccce064a898d5db1d43dbdc4a3590fd84d4ba |
| signature |
coff: add hot-code swapping PoC6 files changed, 383 insertions(+), 149 deletions(-)
lib/std/os/windows.zig+199| ... | @@ -1514,6 +1514,24 @@ pub fn VirtualProtect(lpAddress: ?LPVOID, dwSize: SIZE_T, flNewProtect: DWORD, l | ... | @@ -1514,6 +1514,24 @@ pub fn VirtualProtect(lpAddress: ?LPVOID, dwSize: SIZE_T, flNewProtect: DWORD, l |
| 1514 | } | 1514 | } |
| 1515 | } | 1515 | } |
| 1516 | 1516 | ||
| 1517 | pub fn VirtualProtectEx(handle: HANDLE, addr: ?LPVOID, size: SIZE_T, new_prot: DWORD) VirtualProtectError!DWORD { | ||
| 1518 | var old_prot: DWORD = undefined; | ||
| 1519 | var out_addr = addr; | ||
| 1520 | var out_size = size; | ||
| 1521 | switch (ntdll.NtProtectVirtualMemory( | ||
| 1522 | handle, | ||
| 1523 | &out_addr, | ||
| 1524 | &out_size, | ||
| 1525 | new_prot, | ||
| 1526 | &old_prot, | ||
| 1527 | )) { | ||
| 1528 | .SUCCESS => return old_prot, | ||
| 1529 | .INVALID_ADDRESS => return error.InvalidAddress, | ||
| 1530 | // TODO: map errors | ||
| 1531 | else => |rc| return std.os.windows.unexpectedStatus(rc), | ||
| 1532 | } | ||
| 1533 | } | ||
| 1534 | |||
| 1517 | pub const VirtualQueryError = error{Unexpected}; | 1535 | pub const VirtualQueryError = error{Unexpected}; |
| 1518 | 1536 | ||
| 1519 | pub fn VirtualQuery(lpAddress: ?LPVOID, lpBuffer: PMEMORY_BASIC_INFORMATION, dwLength: SIZE_T) VirtualQueryError!SIZE_T { | 1537 | pub fn VirtualQuery(lpAddress: ?LPVOID, lpBuffer: PMEMORY_BASIC_INFORMATION, dwLength: SIZE_T) VirtualQueryError!SIZE_T { |
| ... | @@ -4457,3 +4475,184 @@ pub const MODULEENTRY32 = extern struct { | ... | @@ -4457,3 +4475,184 @@ pub const MODULEENTRY32 = extern struct { |
| 4457 | szModule: [MAX_MODULE_NAME32 + 1]CHAR, | 4475 | szModule: [MAX_MODULE_NAME32 + 1]CHAR, |
| 4458 | szExePath: [MAX_PATH]CHAR, | 4476 | szExePath: [MAX_PATH]CHAR, |
| 4459 | }; | 4477 | }; |
| 4478 | |||
| 4479 | pub const THREADINFOCLASS = enum(c_int) { | ||
| 4480 | ThreadBasicInformation, | ||
| 4481 | ThreadTimes, | ||
| 4482 | ThreadPriority, | ||
| 4483 | ThreadBasePriority, | ||
| 4484 | ThreadAffinityMask, | ||
| 4485 | ThreadImpersonationToken, | ||
| 4486 | ThreadDescriptorTableEntry, | ||
| 4487 | ThreadEnableAlignmentFaultFixup, | ||
| 4488 | ThreadEventPair_Reusable, | ||
| 4489 | ThreadQuerySetWin32StartAddress, | ||
| 4490 | ThreadZeroTlsCell, | ||
| 4491 | ThreadPerformanceCount, | ||
| 4492 | ThreadAmILastThread, | ||
| 4493 | ThreadIdealProcessor, | ||
| 4494 | ThreadPriorityBoost, | ||
| 4495 | ThreadSetTlsArrayAddress, | ||
| 4496 | ThreadIsIoPending, | ||
| 4497 | // Windows 2000+ from here | ||
| 4498 | ThreadHideFromDebugger, | ||
| 4499 | // Windows XP+ from here | ||
| 4500 | ThreadBreakOnTermination, | ||
| 4501 | ThreadSwitchLegacyState, | ||
| 4502 | ThreadIsTerminated, | ||
| 4503 | // Windows Vista+ from here | ||
| 4504 | ThreadLastSystemCall, | ||
| 4505 | ThreadIoPriority, | ||
| 4506 | ThreadCycleTime, | ||
| 4507 | ThreadPagePriority, | ||
| 4508 | ThreadActualBasePriority, | ||
| 4509 | ThreadTebInformation, | ||
| 4510 | ThreadCSwitchMon, | ||
| 4511 | // Windows 7+ from here | ||
| 4512 | ThreadCSwitchPmu, | ||
| 4513 | ThreadWow64Context, | ||
| 4514 | ThreadGroupInformation, | ||
| 4515 | ThreadUmsInformation, | ||
| 4516 | ThreadCounterProfiling, | ||
| 4517 | ThreadIdealProcessorEx, | ||
| 4518 | // Windows 8+ from here | ||
| 4519 | ThreadCpuAccountingInformation, | ||
| 4520 | // Windows 8.1+ from here | ||
| 4521 | ThreadSuspendCount, | ||
| 4522 | // Windows 10+ from here | ||
| 4523 | ThreadHeterogeneousCpuPolicy, | ||
| 4524 | ThreadContainerId, | ||
| 4525 | ThreadNameInformation, | ||
| 4526 | ThreadSelectedCpuSets, | ||
| 4527 | ThreadSystemThreadInformation, | ||
| 4528 | ThreadActualGroupAffinity, | ||
| 4529 | }; | ||
| 4530 | |||
| 4531 | pub const PROCESSINFOCLASS = enum(c_int) { | ||
| 4532 | ProcessBasicInformation, | ||
| 4533 | ProcessQuotaLimits, | ||
| 4534 | ProcessIoCounters, | ||
| 4535 | ProcessVmCounters, | ||
| 4536 | ProcessTimes, | ||
| 4537 | ProcessBasePriority, | ||
| 4538 | ProcessRaisePriority, | ||
| 4539 | ProcessDebugPort, | ||
| 4540 | ProcessExceptionPort, | ||
| 4541 | ProcessAccessToken, | ||
| 4542 | ProcessLdtInformation, | ||
| 4543 | ProcessLdtSize, | ||
| 4544 | ProcessDefaultHardErrorMode, | ||
| 4545 | ProcessIoPortHandlers, | ||
| 4546 | ProcessPooledUsageAndLimits, | ||
| 4547 | ProcessWorkingSetWatch, | ||
| 4548 | ProcessUserModeIOPL, | ||
| 4549 | ProcessEnableAlignmentFaultFixup, | ||
| 4550 | ProcessPriorityClass, | ||
| 4551 | ProcessWx86Information, | ||
| 4552 | ProcessHandleCount, | ||
| 4553 | ProcessAffinityMask, | ||
| 4554 | ProcessPriorityBoost, | ||
| 4555 | ProcessDeviceMap, | ||
| 4556 | ProcessSessionInformation, | ||
| 4557 | ProcessForegroundInformation, | ||
| 4558 | ProcessWow64Information, | ||
| 4559 | ProcessImageFileName, | ||
| 4560 | ProcessLUIDDeviceMapsEnabled, | ||
| 4561 | ProcessBreakOnTermination, | ||
| 4562 | ProcessDebugObjectHandle, | ||
| 4563 | ProcessDebugFlags, | ||
| 4564 | ProcessHandleTracing, | ||
| 4565 | ProcessIoPriority, | ||
| 4566 | ProcessExecuteFlags, | ||
| 4567 | ProcessTlsInformation, | ||
| 4568 | ProcessCookie, | ||
| 4569 | ProcessImageInformation, | ||
| 4570 | ProcessCycleTime, | ||
| 4571 | ProcessPagePriority, | ||
| 4572 | ProcessInstrumentationCallback, | ||
| 4573 | ProcessThreadStackAllocation, | ||
| 4574 | ProcessWorkingSetWatchEx, | ||
| 4575 | ProcessImageFileNameWin32, | ||
| 4576 | ProcessImageFileMapping, | ||
| 4577 | ProcessAffinityUpdateMode, | ||
| 4578 | ProcessMemoryAllocationMode, | ||
| 4579 | ProcessGroupInformation, | ||
| 4580 | ProcessTokenVirtualizationEnabled, | ||
| 4581 | ProcessConsoleHostProcess, | ||
| 4582 | ProcessWindowInformation, | ||
| 4583 | MaxProcessInfoClass, | ||
| 4584 | }; | ||
| 4585 | |||
| 4586 | pub const PROCESS_BASIC_INFORMATION = extern struct { | ||
| 4587 | ExitStatus: NTSTATUS, | ||
| 4588 | PebBaseAddress: *PEB, | ||
| 4589 | AffinityMask: ULONG_PTR, | ||
| 4590 | BasePriority: KPRIORITY, | ||
| 4591 | UniqueProcessId: ULONG_PTR, | ||
| 4592 | InheritedFromUniqueProcessId: ULONG_PTR, | ||
| 4593 | }; | ||
| 4594 | |||
| 4595 | pub const ReadMemoryError = error{ | ||
| 4596 | Unexpected, | ||
| 4597 | }; | ||
| 4598 | |||
| 4599 | pub fn ReadProcessMemory(handle: HANDLE, addr: ?LPVOID, buffer: []u8) ReadMemoryError![]u8 { | ||
| 4600 | var nread: usize = 0; | ||
| 4601 | switch (ntdll.NtReadVirtualMemory( | ||
| 4602 | handle, | ||
| 4603 | addr, | ||
| 4604 | buffer.ptr, | ||
| 4605 | buffer.len, | ||
| 4606 | &nread, | ||
| 4607 | )) { | ||
| 4608 | .SUCCESS => return buffer[0..nread], | ||
| 4609 | // TODO: map errors | ||
| 4610 | else => |rc| return unexpectedStatus(rc), | ||
| 4611 | } | ||
| 4612 | } | ||
| 4613 | |||
| 4614 | pub const WriteMemoryError = error{ | ||
| 4615 | Unexpected, | ||
| 4616 | }; | ||
| 4617 | |||
| 4618 | pub fn WriteProcessMemory(handle: HANDLE, addr: ?LPVOID, buffer: []const u8) WriteMemoryError!usize { | ||
| 4619 | var nwritten: usize = 0; | ||
| 4620 | switch (ntdll.NtWriteVirtualMemory( | ||
| 4621 | handle, | ||
| 4622 | addr, | ||
| 4623 | @ptrCast(*const anyopaque, buffer.ptr), | ||
| 4624 | buffer.len, | ||
| 4625 | &nwritten, | ||
| 4626 | )) { | ||
| 4627 | .SUCCESS => return nwritten, | ||
| 4628 | // TODO: map errors | ||
| 4629 | else => |rc| return unexpectedStatus(rc), | ||
| 4630 | } | ||
| 4631 | } | ||
| 4632 | |||
| 4633 | pub const ProcessBaseAddressError = GetProcessMemoryInfoError || ReadMemoryError; | ||
| 4634 | |||
| 4635 | /// Returns the base address of the process loaded into memory. | ||
| 4636 | pub fn ProcessBaseAddress(handle: HANDLE) ProcessBaseAddressError!HMODULE { | ||
| 4637 | var info: PROCESS_BASIC_INFORMATION = undefined; | ||
| 4638 | var nread: DWORD = 0; | ||
| 4639 | const rc = ntdll.NtQueryInformationProcess( | ||
| 4640 | handle, | ||
| 4641 | .ProcessBasicInformation, | ||
| 4642 | &info, | ||
| 4643 | @sizeOf(PROCESS_BASIC_INFORMATION), | ||
| 4644 | &nread, | ||
| 4645 | ); | ||
| 4646 | switch (rc) { | ||
| 4647 | .SUCCESS => {}, | ||
| 4648 | .ACCESS_DENIED => return error.AccessDenied, | ||
| 4649 | .INVALID_HANDLE => return error.InvalidHandle, | ||
| 4650 | .INVALID_PARAMETER => unreachable, | ||
| 4651 | else => return unexpectedStatus(rc), | ||
| 4652 | } | ||
| 4653 | |||
| 4654 | var peb_buf: [@sizeOf(PEB)]u8 align(@alignOf(PEB)) = undefined; | ||
| 4655 | const peb_out = try ReadProcessMemory(handle, info.PebBaseAddress, &peb_buf); | ||
| 4656 | const ppeb = @ptrCast(*const PEB, @alignCast(@alignOf(PEB), peb_out.ptr)); | ||
| 4657 | return ppeb.ImageBaseAddress; | ||
| 4658 | } |
lib/std/os/windows/ntdll.zig+22-108| ... | @@ -31,61 +31,10 @@ const UNWIND_HISTORY_TABLE = windows.UNWIND_HISTORY_TABLE; | ... | @@ -31,61 +31,10 @@ const UNWIND_HISTORY_TABLE = windows.UNWIND_HISTORY_TABLE; |
| 31 | const RUNTIME_FUNCTION = windows.RUNTIME_FUNCTION; | 31 | const RUNTIME_FUNCTION = windows.RUNTIME_FUNCTION; |
| 32 | const KNONVOLATILE_CONTEXT_POINTERS = windows.KNONVOLATILE_CONTEXT_POINTERS; | 32 | const KNONVOLATILE_CONTEXT_POINTERS = windows.KNONVOLATILE_CONTEXT_POINTERS; |
| 33 | const EXCEPTION_ROUTINE = windows.EXCEPTION_ROUTINE; | 33 | const EXCEPTION_ROUTINE = windows.EXCEPTION_ROUTINE; |
| 34 | 34 | const THREADINFOCLASS = windows.THREADINFOCLASS; | |
| 35 | pub const PROCESSINFOCLASS = enum(c_int) { | 35 | const PROCESSINFOCLASS = windows.PROCESSINFOCLASS; |
| 36 | ProcessBasicInformation, | 36 | const LPVOID = windows.LPVOID; |
| 37 | ProcessQuotaLimits, | 37 | const LPCVOID = windows.LPCVOID; |
| 38 | ProcessIoCounters, | ||
| 39 | ProcessVmCounters, | ||
| 40 | ProcessTimes, | ||
| 41 | ProcessBasePriority, | ||
| 42 | ProcessRaisePriority, | ||
| 43 | ProcessDebugPort, | ||
| 44 | ProcessExceptionPort, | ||
| 45 | ProcessAccessToken, | ||
| 46 | ProcessLdtInformation, | ||
| 47 | ProcessLdtSize, | ||
| 48 | ProcessDefaultHardErrorMode, | ||
| 49 | ProcessIoPortHandlers, | ||
| 50 | ProcessPooledUsageAndLimits, | ||
| 51 | ProcessWorkingSetWatch, | ||
| 52 | ProcessUserModeIOPL, | ||
| 53 | ProcessEnableAlignmentFaultFixup, | ||
| 54 | ProcessPriorityClass, | ||
| 55 | ProcessWx86Information, | ||
| 56 | ProcessHandleCount, | ||
| 57 | ProcessAffinityMask, | ||
| 58 | ProcessPriorityBoost, | ||
| 59 | ProcessDeviceMap, | ||
| 60 | ProcessSessionInformation, | ||
| 61 | ProcessForegroundInformation, | ||
| 62 | ProcessWow64Information, | ||
| 63 | ProcessImageFileName, | ||
| 64 | ProcessLUIDDeviceMapsEnabled, | ||
| 65 | ProcessBreakOnTermination, | ||
| 66 | ProcessDebugObjectHandle, | ||
| 67 | ProcessDebugFlags, | ||
| 68 | ProcessHandleTracing, | ||
| 69 | ProcessIoPriority, | ||
| 70 | ProcessExecuteFlags, | ||
| 71 | ProcessTlsInformation, | ||
| 72 | ProcessCookie, | ||
| 73 | ProcessImageInformation, | ||
| 74 | ProcessCycleTime, | ||
| 75 | ProcessPagePriority, | ||
| 76 | ProcessInstrumentationCallback, | ||
| 77 | ProcessThreadStackAllocation, | ||
| 78 | ProcessWorkingSetWatchEx, | ||
| 79 | ProcessImageFileNameWin32, | ||
| 80 | ProcessImageFileMapping, | ||
| 81 | ProcessAffinityUpdateMode, | ||
| 82 | ProcessMemoryAllocationMode, | ||
| 83 | ProcessGroupInformation, | ||
| 84 | ProcessTokenVirtualizationEnabled, | ||
| 85 | ProcessConsoleHostProcess, | ||
| 86 | ProcessWindowInformation, | ||
| 87 | MaxProcessInfoClass, | ||
| 88 | }; | ||
| 89 | 38 | ||
| 90 | pub extern "ntdll" fn NtQueryInformationProcess( | 39 | pub extern "ntdll" fn NtQueryInformationProcess( |
| 91 | ProcessHandle: HANDLE, | 40 | ProcessHandle: HANDLE, |
| ... | @@ -95,57 +44,6 @@ pub extern "ntdll" fn NtQueryInformationProcess( | ... | @@ -95,57 +44,6 @@ pub extern "ntdll" fn NtQueryInformationProcess( |
| 95 | ReturnLength: ?*ULONG, | 44 | ReturnLength: ?*ULONG, |
| 96 | ) callconv(WINAPI) NTSTATUS; | 45 | ) callconv(WINAPI) NTSTATUS; |
| 97 | 46 | ||
| 98 | pub const THREADINFOCLASS = enum(c_int) { | ||
| 99 | ThreadBasicInformation, | ||
| 100 | ThreadTimes, | ||
| 101 | ThreadPriority, | ||
| 102 | ThreadBasePriority, | ||
| 103 | ThreadAffinityMask, | ||
| 104 | ThreadImpersonationToken, | ||
| 105 | ThreadDescriptorTableEntry, | ||
| 106 | ThreadEnableAlignmentFaultFixup, | ||
| 107 | ThreadEventPair_Reusable, | ||
| 108 | ThreadQuerySetWin32StartAddress, | ||
| 109 | ThreadZeroTlsCell, | ||
| 110 | ThreadPerformanceCount, | ||
| 111 | ThreadAmILastThread, | ||
| 112 | ThreadIdealProcessor, | ||
| 113 | ThreadPriorityBoost, | ||
| 114 | ThreadSetTlsArrayAddress, | ||
| 115 | ThreadIsIoPending, | ||
| 116 | // Windows 2000+ from here | ||
| 117 | ThreadHideFromDebugger, | ||
| 118 | // Windows XP+ from here | ||
| 119 | ThreadBreakOnTermination, | ||
| 120 | ThreadSwitchLegacyState, | ||
| 121 | ThreadIsTerminated, | ||
| 122 | // Windows Vista+ from here | ||
| 123 | ThreadLastSystemCall, | ||
| 124 | ThreadIoPriority, | ||
| 125 | ThreadCycleTime, | ||
| 126 | ThreadPagePriority, | ||
| 127 | ThreadActualBasePriority, | ||
| 128 | ThreadTebInformation, | ||
| 129 | ThreadCSwitchMon, | ||
| 130 | // Windows 7+ from here | ||
| 131 | ThreadCSwitchPmu, | ||
| 132 | ThreadWow64Context, | ||
| 133 | ThreadGroupInformation, | ||
| 134 | ThreadUmsInformation, | ||
| 135 | ThreadCounterProfiling, | ||
| 136 | ThreadIdealProcessorEx, | ||
| 137 | // Windows 8+ from here | ||
| 138 | ThreadCpuAccountingInformation, | ||
| 139 | // Windows 8.1+ from here | ||
| 140 | ThreadSuspendCount, | ||
| 141 | // Windows 10+ from here | ||
| 142 | ThreadHeterogeneousCpuPolicy, | ||
| 143 | ThreadContainerId, | ||
| 144 | ThreadNameInformation, | ||
| 145 | ThreadSelectedCpuSets, | ||
| 146 | ThreadSystemThreadInformation, | ||
| 147 | ThreadActualGroupAffinity, | ||
| 148 | }; | ||
| 149 | pub extern "ntdll" fn NtQueryInformationThread( | 47 | pub extern "ntdll" fn NtQueryInformationThread( |
| 150 | ThreadHandle: HANDLE, | 48 | ThreadHandle: HANDLE, |
| 151 | ThreadInformationClass: THREADINFOCLASS, | 49 | ThreadInformationClass: THREADINFOCLASS, |
| ... | @@ -364,10 +262,26 @@ pub extern "ntdll" fn RtlQueryRegistryValues( | ... | @@ -364,10 +262,26 @@ pub extern "ntdll" fn RtlQueryRegistryValues( |
| 364 | Environment: ?*anyopaque, | 262 | Environment: ?*anyopaque, |
| 365 | ) callconv(WINAPI) NTSTATUS; | 263 | ) callconv(WINAPI) NTSTATUS; |
| 366 | 264 | ||
| 265 | pub extern "ntdll" fn NtReadVirtualMemory( | ||
| 266 | ProcessHandle: HANDLE, | ||
| 267 | BaseAddress: ?PVOID, | ||
| 268 | Buffer: LPVOID, | ||
| 269 | NumberOfBytesToRead: SIZE_T, | ||
| 270 | NumberOfBytesRead: ?*SIZE_T, | ||
| 271 | ) callconv(WINAPI) NTSTATUS; | ||
| 272 | |||
| 273 | pub extern "ntdll" fn NtWriteVirtualMemory( | ||
| 274 | ProcessHandle: HANDLE, | ||
| 275 | BaseAddress: ?PVOID, | ||
| 276 | Buffer: LPCVOID, | ||
| 277 | NumberOfBytesToWrite: SIZE_T, | ||
| 278 | NumberOfBytesWritten: ?*SIZE_T, | ||
| 279 | ) callconv(WINAPI) NTSTATUS; | ||
| 280 | |||
| 367 | pub extern "ntdll" fn NtProtectVirtualMemory( | 281 | pub extern "ntdll" fn NtProtectVirtualMemory( |
| 368 | ProcessHandle: HANDLE, | 282 | ProcessHandle: HANDLE, |
| 369 | BaseAddress: *PVOID, | 283 | BaseAddress: *?PVOID, |
| 370 | NumberOfBytesToProtect: *ULONG, | 284 | NumberOfBytesToProtect: *SIZE_T, |
| 371 | NewAccessProtection: ULONG, | 285 | NewAccessProtection: ULONG, |
| 372 | OldAccessProtection: *ULONG, | 286 | OldAccessProtection: *ULONG, |
| 373 | ) callconv(WINAPI) NTSTATUS; | 287 | ) callconv(WINAPI) NTSTATUS; |
src/link.zig+24-18| ... | @@ -379,24 +379,30 @@ pub const File = struct { | ... | @@ -379,24 +379,30 @@ pub const File = struct { |
| 379 | if (base.file != null) return; | 379 | if (base.file != null) return; |
| 380 | const emit = base.options.emit orelse return; | 380 | const emit = base.options.emit orelse return; |
| 381 | if (base.child_pid) |pid| { | 381 | if (base.child_pid) |pid| { |
| 382 | // If we try to open the output file in write mode while it is running, | 382 | if (builtin.os.tag == .windows) { |
| 383 | // it will return ETXTBSY. So instead, we copy the file, atomically rename it | 383 | base.cast(Coff).?.ptraceAttach(pid) catch |err| { |
| 384 | // over top of the exe path, and then proceed normally. This changes the inode, | ||
| 385 | // avoiding the error. | ||
| 386 | const tmp_sub_path = try std.fmt.allocPrint(base.allocator, "{s}-{x}", .{ | ||
| 387 | emit.sub_path, std.crypto.random.int(u32), | ||
| 388 | }); | ||
| 389 | try emit.directory.handle.copyFile(emit.sub_path, emit.directory.handle, tmp_sub_path, .{}); | ||
| 390 | try emit.directory.handle.rename(tmp_sub_path, emit.sub_path); | ||
| 391 | switch (builtin.os.tag) { | ||
| 392 | .linux => std.os.ptrace(std.os.linux.PTRACE.ATTACH, pid, 0, 0) catch |err| { | ||
| 393 | log.warn("ptrace failure: {s}", .{@errorName(err)}); | ||
| 394 | }, | ||
| 395 | .macos => base.cast(MachO).?.ptraceAttach(pid) catch |err| { | ||
| 396 | log.warn("attaching failed with error: {s}", .{@errorName(err)}); | 384 | log.warn("attaching failed with error: {s}", .{@errorName(err)}); |
| 397 | }, | 385 | }; |
| 398 | .windows => {}, | 386 | } else { |
| 399 | else => return error.HotSwapUnavailableOnHostOperatingSystem, | 387 | // If we try to open the output file in write mode while it is running, |
| 388 | // it will return ETXTBSY. So instead, we copy the file, atomically rename it | ||
| 389 | // over top of the exe path, and then proceed normally. This changes the inode, | ||
| 390 | // avoiding the error. | ||
| 391 | const tmp_sub_path = try std.fmt.allocPrint(base.allocator, "{s}-{x}", .{ | ||
| 392 | emit.sub_path, std.crypto.random.int(u32), | ||
| 393 | }); | ||
| 394 | try emit.directory.handle.copyFile(emit.sub_path, emit.directory.handle, tmp_sub_path, .{}); | ||
| 395 | try emit.directory.handle.rename(tmp_sub_path, emit.sub_path); | ||
| 396 | switch (builtin.os.tag) { | ||
| 397 | .linux => std.os.ptrace(std.os.linux.PTRACE.ATTACH, pid, 0, 0) catch |err| { | ||
| 398 | log.warn("ptrace failure: {s}", .{@errorName(err)}); | ||
| 399 | }, | ||
| 400 | .macos => base.cast(MachO).?.ptraceAttach(pid) catch |err| { | ||
| 401 | log.warn("attaching failed with error: {s}", .{@errorName(err)}); | ||
| 402 | }, | ||
| 403 | .windows => unreachable, | ||
| 404 | else => return error.HotSwapUnavailableOnHostOperatingSystem, | ||
| 405 | } | ||
| 400 | } | 406 | } |
| 401 | } | 407 | } |
| 402 | base.file = try emit.directory.handle.createFile(emit.sub_path, .{ | 408 | base.file = try emit.directory.handle.createFile(emit.sub_path, .{ |
| ... | @@ -437,7 +443,7 @@ pub const File = struct { | ... | @@ -437,7 +443,7 @@ pub const File = struct { |
| 437 | .macos => base.cast(MachO).?.ptraceDetach(pid) catch |err| { | 443 | .macos => base.cast(MachO).?.ptraceDetach(pid) catch |err| { |
| 438 | log.warn("detaching failed with error: {s}", .{@errorName(err)}); | 444 | log.warn("detaching failed with error: {s}", .{@errorName(err)}); |
| 439 | }, | 445 | }, |
| 440 | .windows => {}, | 446 | .windows => base.cast(Coff).?.ptraceDetach(pid), |
| 441 | else => return error.HotSwapUnavailableOnHostOperatingSystem, | 447 | else => return error.HotSwapUnavailableOnHostOperatingSystem, |
| 442 | } | 448 | } |
| 443 | } | 449 | } |
src/link/Coff.zig+110-11| ... | @@ -89,6 +89,20 @@ relocs: RelocTable = .{}, | ... | @@ -89,6 +89,20 @@ relocs: RelocTable = .{}, |
| 89 | /// this will be a table indexed by index into the list of Atoms. | 89 | /// this will be a table indexed by index into the list of Atoms. |
| 90 | base_relocs: BaseRelocationTable = .{}, | 90 | base_relocs: BaseRelocationTable = .{}, |
| 91 | 91 | ||
| 92 | /// Hot-code swapping state. | ||
| 93 | hot_state: if (is_hot_update_compatible) HotUpdateState else struct {} = .{}, | ||
| 94 | |||
| 95 | const is_hot_update_compatible = switch (builtin.target.os.tag) { | ||
| 96 | .windows => true, | ||
| 97 | else => false, | ||
| 98 | }; | ||
| 99 | |||
| 100 | const HotUpdateState = struct { | ||
| 101 | /// Base address at which the process (image) got loaded. | ||
| 102 | /// We need this info to correctly slide pointers when relocating. | ||
| 103 | loaded_base_address: ?std.os.windows.HMODULE = null, | ||
| 104 | }; | ||
| 105 | |||
| 92 | const Entry = struct { | 106 | const Entry = struct { |
| 93 | target: SymbolWithLoc, | 107 | target: SymbolWithLoc, |
| 94 | // Index into the synthetic symbol table (i.e., file == null). | 108 | // Index into the synthetic symbol table (i.e., file == null). |
| ... | @@ -772,13 +786,87 @@ fn writeAtom(self: *Coff, atom_index: Atom.Index, code: []u8) !void { | ... | @@ -772,13 +786,87 @@ fn writeAtom(self: *Coff, atom_index: Atom.Index, code: []u8) !void { |
| 772 | const sym = atom.getSymbol(self); | 786 | const sym = atom.getSymbol(self); |
| 773 | const section = self.sections.get(@enumToInt(sym.section_number) - 1); | 787 | const section = self.sections.get(@enumToInt(sym.section_number) - 1); |
| 774 | const file_offset = section.header.pointer_to_raw_data + sym.value - section.header.virtual_address; | 788 | const file_offset = section.header.pointer_to_raw_data + sym.value - section.header.virtual_address; |
| 789 | |||
| 775 | log.debug("writing atom for symbol {s} at file offset 0x{x} to 0x{x}", .{ | 790 | log.debug("writing atom for symbol {s} at file offset 0x{x} to 0x{x}", .{ |
| 776 | atom.getName(self), | 791 | atom.getName(self), |
| 777 | file_offset, | 792 | file_offset, |
| 778 | file_offset + code.len, | 793 | file_offset + code.len, |
| 779 | }); | 794 | }); |
| 780 | self.resolveRelocs(atom_index, code); | 795 | |
| 796 | const gpa = self.base.allocator; | ||
| 797 | |||
| 798 | // Gather relocs which can be resolved. | ||
| 799 | // We need to do this as we will be applying different slide values depending | ||
| 800 | // if we are running in hot-code swapping mode or not. | ||
| 801 | // TODO: how crazy would it be to try and apply the actual image base of the loaded | ||
| 802 | // process for the in-file values rather than the Windows defaults? | ||
| 803 | var relocs = std.ArrayList(*Relocation).init(gpa); | ||
| 804 | defer relocs.deinit(); | ||
| 805 | |||
| 806 | if (self.relocs.getPtr(atom_index)) |rels| { | ||
| 807 | try relocs.ensureTotalCapacityPrecise(rels.items.len); | ||
| 808 | for (rels.items) |*reloc| { | ||
| 809 | if (reloc.isResolvable(self)) relocs.appendAssumeCapacity(reloc); | ||
| 810 | } | ||
| 811 | } | ||
| 812 | |||
| 813 | if (is_hot_update_compatible) { | ||
| 814 | if (self.base.child_pid) |handle| { | ||
| 815 | const slide = @ptrToInt(self.hot_state.loaded_base_address.?); | ||
| 816 | |||
| 817 | const mem_code = try gpa.dupe(u8, code); | ||
| 818 | defer gpa.free(mem_code); | ||
| 819 | self.resolveRelocs(atom_index, relocs.items, mem_code, slide); | ||
| 820 | |||
| 821 | const vaddr = sym.value + slide; | ||
| 822 | const pvaddr = @intToPtr(*anyopaque, vaddr); | ||
| 823 | |||
| 824 | log.debug("writing to memory at address {x}", .{vaddr}); | ||
| 825 | |||
| 826 | if (build_options.enable_logging) { | ||
| 827 | try debugMem(gpa, handle, pvaddr, mem_code); | ||
| 828 | } | ||
| 829 | |||
| 830 | if (section.header.flags.MEM_WRITE == 0) { | ||
| 831 | writeMemProtected(handle, pvaddr, mem_code) catch |err| { | ||
| 832 | log.warn("writing to protected memory failed with error: {s}", .{@errorName(err)}); | ||
| 833 | }; | ||
| 834 | } else { | ||
| 835 | writeMem(handle, pvaddr, mem_code) catch |err| { | ||
| 836 | log.warn("writing to protected memory failed with error: {s}", .{@errorName(err)}); | ||
| 837 | }; | ||
| 838 | } | ||
| 839 | } | ||
| 840 | } | ||
| 841 | |||
| 842 | self.resolveRelocs(atom_index, relocs.items, code, self.getImageBase()); | ||
| 781 | try self.base.file.?.pwriteAll(code, file_offset); | 843 | try self.base.file.?.pwriteAll(code, file_offset); |
| 844 | |||
| 845 | // Now we can mark the relocs as resolved. | ||
| 846 | while (relocs.popOrNull()) |reloc| { | ||
| 847 | reloc.dirty = false; | ||
| 848 | } | ||
| 849 | } | ||
| 850 | |||
| 851 | fn debugMem(allocator: Allocator, handle: std.ChildProcess.Id, pvaddr: std.os.windows.LPVOID, code: []const u8) !void { | ||
| 852 | var buffer = try allocator.alloc(u8, code.len); | ||
| 853 | defer allocator.free(buffer); | ||
| 854 | const memread = try std.os.windows.ReadProcessMemory(handle, pvaddr, buffer); | ||
| 855 | log.debug("to write: {x}", .{std.fmt.fmtSliceHexLower(code)}); | ||
| 856 | log.debug("in memory: {x}", .{std.fmt.fmtSliceHexLower(memread)}); | ||
| 857 | } | ||
| 858 | |||
| 859 | fn writeMemProtected(handle: std.ChildProcess.Id, pvaddr: std.os.windows.LPVOID, code: []const u8) !void { | ||
| 860 | const old_prot = try std.os.windows.VirtualProtectEx(handle, pvaddr, code.len, std.os.windows.PAGE_EXECUTE_WRITECOPY); | ||
| 861 | try writeMem(handle, pvaddr, code); | ||
| 862 | // TODO: We can probably just set the pages writeable and leave it at that without having to restore the attributes. | ||
| 863 | // For that though, we want to track which page has already been modified. | ||
| 864 | _ = try std.os.windows.VirtualProtectEx(handle, pvaddr, code.len, old_prot); | ||
| 865 | } | ||
| 866 | |||
| 867 | fn writeMem(handle: std.ChildProcess.Id, pvaddr: std.os.windows.LPVOID, code: []const u8) !void { | ||
| 868 | const amt = try std.os.windows.WriteProcessMemory(handle, pvaddr, code); | ||
| 869 | if (amt != code.len) return error.InputOutput; | ||
| 782 | } | 870 | } |
| 783 | 871 | ||
| 784 | fn writePtrWidthAtom(self: *Coff, atom_index: Atom.Index) !void { | 872 | fn writePtrWidthAtom(self: *Coff, atom_index: Atom.Index) !void { |
| ... | @@ -814,19 +902,30 @@ fn markRelocsDirtyByAddress(self: *Coff, addr: u32) void { | ... | @@ -814,19 +902,30 @@ fn markRelocsDirtyByAddress(self: *Coff, addr: u32) void { |
| 814 | } | 902 | } |
| 815 | } | 903 | } |
| 816 | 904 | ||
| 817 | fn resolveRelocs(self: *Coff, atom_index: Atom.Index, code: []u8) void { | 905 | fn resolveRelocs(self: *Coff, atom_index: Atom.Index, relocs: []*const Relocation, code: []u8, image_base: u64) void { |
| 818 | const relocs = self.relocs.getPtr(atom_index) orelse return; | ||
| 819 | |||
| 820 | log.debug("relocating '{s}'", .{self.getAtom(atom_index).getName(self)}); | 906 | log.debug("relocating '{s}'", .{self.getAtom(atom_index).getName(self)}); |
| 821 | 907 | for (relocs) |reloc| { | |
| 822 | for (relocs.items) |*reloc| { | 908 | reloc.resolve(atom_index, code, image_base, self); |
| 823 | if (!reloc.dirty) continue; | ||
| 824 | if (reloc.resolve(atom_index, code, self)) { | ||
| 825 | reloc.dirty = false; | ||
| 826 | } | ||
| 827 | } | 909 | } |
| 828 | } | 910 | } |
| 829 | 911 | ||
| 912 | pub fn ptraceAttach(self: *Coff, handle: std.ChildProcess.Id) !void { | ||
| 913 | if (!is_hot_update_compatible) return; | ||
| 914 | |||
| 915 | log.debug("attaching to process with handle {*}", .{handle}); | ||
| 916 | self.hot_state.loaded_base_address = std.os.windows.ProcessBaseAddress(handle) catch |err| { | ||
| 917 | log.warn("failed to get base address for the process with error: {s}", .{@errorName(err)}); | ||
| 918 | return; | ||
| 919 | }; | ||
| 920 | } | ||
| 921 | |||
| 922 | pub fn ptraceDetach(self: *Coff, handle: std.ChildProcess.Id) void { | ||
| 923 | if (!is_hot_update_compatible) return; | ||
| 924 | |||
| 925 | log.debug("detaching from process with handle {*}", .{handle}); | ||
| 926 | self.hot_state.loaded_base_address = null; | ||
| 927 | } | ||
| 928 | |||
| 830 | fn freeAtom(self: *Coff, atom_index: Atom.Index) void { | 929 | fn freeAtom(self: *Coff, atom_index: Atom.Index) void { |
| 831 | log.debug("freeAtom {d}", .{atom_index}); | 930 | log.debug("freeAtom {d}", .{atom_index}); |
| 832 | 931 | ||
| ... | @@ -1421,7 +1520,7 @@ pub fn flushModule(self: *Coff, comp: *Compilation, prog_node: *std.Progress.Nod | ... | @@ -1421,7 +1520,7 @@ pub fn flushModule(self: *Coff, comp: *Compilation, prog_node: *std.Progress.Nod |
| 1421 | 1520 | ||
| 1422 | for (self.relocs.keys(), self.relocs.values()) |atom_index, relocs| { | 1521 | for (self.relocs.keys(), self.relocs.values()) |atom_index, relocs| { |
| 1423 | const needs_update = for (relocs.items) |reloc| { | 1522 | const needs_update = for (relocs.items) |reloc| { |
| 1424 | if (reloc.dirty) break true; | 1523 | if (reloc.isResolvable(self)) break true; |
| 1425 | } else false; | 1524 | } else false; |
| 1426 | 1525 | ||
| 1427 | if (!needs_update) continue; | 1526 | if (!needs_update) continue; |
src/link/Coff/Relocation.zig+9-7| ... | @@ -72,14 +72,18 @@ pub fn getTargetAddress(self: Relocation, coff_file: *const Coff) ?u32 { | ... | @@ -72,14 +72,18 @@ pub fn getTargetAddress(self: Relocation, coff_file: *const Coff) ?u32 { |
| 72 | } | 72 | } |
| 73 | } | 73 | } |
| 74 | 74 | ||
| 75 | /// Returns `false` if obtaining the target address has been deferred until `flushModule`. | 75 | /// Returns true if and only if the reloc is dirty AND the target address is available. |
| 76 | /// This can happen when trying to resolve address of an import table entry ahead of time. | 76 | pub fn isResolvable(self: Relocation, coff_file: *Coff) bool { |
| 77 | pub fn resolve(self: Relocation, atom_index: Atom.Index, code: []u8, coff_file: *Coff) bool { | 77 | _ = self.getTargetAddress(coff_file) orelse return false; |
| 78 | return self.dirty; | ||
| 79 | } | ||
| 80 | |||
| 81 | pub fn resolve(self: Relocation, atom_index: Atom.Index, code: []u8, image_base: u64, coff_file: *Coff) void { | ||
| 78 | const atom = coff_file.getAtom(atom_index); | 82 | const atom = coff_file.getAtom(atom_index); |
| 79 | const source_sym = atom.getSymbol(coff_file); | 83 | const source_sym = atom.getSymbol(coff_file); |
| 80 | const source_vaddr = source_sym.value + self.offset; | 84 | const source_vaddr = source_sym.value + self.offset; |
| 81 | 85 | ||
| 82 | const target_vaddr = self.getTargetAddress(coff_file) orelse return false; | 86 | const target_vaddr = self.getTargetAddress(coff_file).?; // Oops, you didn't check if the relocation can be resolved with isResolvable(). |
| 83 | const target_vaddr_with_addend = target_vaddr + self.addend; | 87 | const target_vaddr_with_addend = target_vaddr + self.addend; |
| 84 | 88 | ||
| 85 | log.debug(" ({x}: [() => 0x{x} ({s})) ({s}) ", .{ | 89 | log.debug(" ({x}: [() => 0x{x} ({s})) ({s}) ", .{ |
| ... | @@ -92,7 +96,7 @@ pub fn resolve(self: Relocation, atom_index: Atom.Index, code: []u8, coff_file: | ... | @@ -92,7 +96,7 @@ pub fn resolve(self: Relocation, atom_index: Atom.Index, code: []u8, coff_file: |
| 92 | const ctx: Context = .{ | 96 | const ctx: Context = .{ |
| 93 | .source_vaddr = source_vaddr, | 97 | .source_vaddr = source_vaddr, |
| 94 | .target_vaddr = target_vaddr_with_addend, | 98 | .target_vaddr = target_vaddr_with_addend, |
| 95 | .image_base = coff_file.getImageBase(), | 99 | .image_base = image_base, |
| 96 | .code = code, | 100 | .code = code, |
| 97 | .ptr_width = coff_file.ptr_width, | 101 | .ptr_width = coff_file.ptr_width, |
| 98 | }; | 102 | }; |
| ... | @@ -102,8 +106,6 @@ pub fn resolve(self: Relocation, atom_index: Atom.Index, code: []u8, coff_file: | ... | @@ -102,8 +106,6 @@ pub fn resolve(self: Relocation, atom_index: Atom.Index, code: []u8, coff_file: |
| 102 | .x86, .x86_64 => self.resolveX86(ctx), | 106 | .x86, .x86_64 => self.resolveX86(ctx), |
| 103 | else => unreachable, // unhandled target architecture | 107 | else => unreachable, // unhandled target architecture |
| 104 | } | 108 | } |
| 105 | |||
| 106 | return true; | ||
| 107 | } | 109 | } |
| 108 | 110 | ||
| 109 | const Context = struct { | 111 | const Context = struct { |
src/main.zig+19-5| ... | @@ -3817,11 +3817,25 @@ fn runOrTestHotSwap( | ... | @@ -3817,11 +3817,25 @@ fn runOrTestHotSwap( |
| 3817 | runtime_args_start: ?usize, | 3817 | runtime_args_start: ?usize, |
| 3818 | ) !std.ChildProcess.Id { | 3818 | ) !std.ChildProcess.Id { |
| 3819 | const exe_emit = comp.bin_file.options.emit.?; | 3819 | const exe_emit = comp.bin_file.options.emit.?; |
| 3820 | // A naive `directory.join` here will indeed get the correct path to the binary, | 3820 | |
| 3821 | // however, in the case of cwd, we actually want `./foo` so that the path can be executed. | 3821 | const exe_path = switch (builtin.target.os.tag) { |
| 3822 | const exe_path = try fs.path.join(gpa, &[_][]const u8{ | 3822 | // On Windows it seems impossible to perform an atomic rename of a file that is currently |
| 3823 | exe_emit.directory.path orelse ".", exe_emit.sub_path, | 3823 | // running in a process. Therefore, we do the opposite. We create a copy of the file in |
| 3824 | }); | 3824 | // tmp zig-cache and use it to spawn the child process. This way we are free to update |
| 3825 | // the binary with each requested hot update. | ||
| 3826 | .windows => blk: { | ||
| 3827 | try exe_emit.directory.handle.copyFile(exe_emit.sub_path, comp.local_cache_directory.handle, exe_emit.sub_path, .{}); | ||
| 3828 | break :blk try fs.path.join(gpa, &[_][]const u8{ | ||
| 3829 | comp.local_cache_directory.path orelse ".", exe_emit.sub_path, | ||
| 3830 | }); | ||
| 3831 | }, | ||
| 3832 | |||
| 3833 | // A naive `directory.join` here will indeed get the correct path to the binary, | ||
| 3834 | // however, in the case of cwd, we actually want `./foo` so that the path can be executed. | ||
| 3835 | else => try fs.path.join(gpa, &[_][]const u8{ | ||
| 3836 | exe_emit.directory.path orelse ".", exe_emit.sub_path, | ||
| 3837 | }), | ||
| 3838 | }; | ||
| 3825 | defer gpa.free(exe_path); | 3839 | defer gpa.free(exe_path); |
| 3826 | 3840 | ||
| 3827 | var argv = std.ArrayList([]const u8).init(gpa); | 3841 | var argv = std.ArrayList([]const u8).init(gpa); |