Add generic hook module (x86dis + auto-sizing detour), framecrash updates, marker assets
Hook lib: port HDE32 length disassembler to Zig (x86dis.zig), add GenericHook with automatic prologue sizing and relative instruction relocation, add Detour(FnType) comptime-generic type-safe hook wrapper. Update build.zig to expose x86dis, generic_hook, hook as separate modules. Framecrash: expand vtable hook research and implementation. Markers: updated raid marker M2 assets.
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
# Ground-Projected Raid Markers
|
||||
|
||||
**Goal:** Render placeable raid markers (like Wrath+) that project onto the ground as area indicators.
|
||||
|
||||
**Status:** Research complete, implementation pending
|
||||
|
||||
---
|
||||
|
||||
## What We Have
|
||||
|
||||
### D3D9 Rendering Infrastructure
|
||||
- **File:** `src/outline/d3d9_hook.zig`
|
||||
- Render target management (silhouette RT, JFA ping-pong buffers)
|
||||
- Shader assembly via D3DX (PS 3.0)
|
||||
- Fullscreen quad rendering with `DrawPrimitiveUP`
|
||||
- Complete state save/restore around custom passes
|
||||
- Hooks: EndScene, DrawIndexedPrimitive, Reset
|
||||
|
||||
### Model Rendering Hooks
|
||||
- **File:** `src/outline/model_hook.zig`
|
||||
- `CM2SceneRenderDraw` hook — batch reordering for depth control
|
||||
- `CM2Scene_DrawBatchProjected` hook — per-batch interception
|
||||
- Access to render context and model pointers
|
||||
- Batch reordering ensures outline targets render when only terrain+WMO depth exists
|
||||
|
||||
### Render Pipeline Documentation
|
||||
- **File:** `docs/RENDER_ARCHITECTURE.md`
|
||||
- Terrain renderer: `CullAndProcessWorldChunks` (0x00683040)
|
||||
- WMO renderer: `ProcessStaticObjectsCulling` (0x00683bf0)
|
||||
- M2 model pipeline fully mapped
|
||||
- Depth buffer control via hook ordering
|
||||
|
||||
### Outline Rendering Research
|
||||
- **File:** `docs/outline-rendering-research.md`
|
||||
- JFA (Jump Flood Algorithm) for distance-field outlines
|
||||
- Stencil + blur techniques (Valve L4D style)
|
||||
- Screen-space dilation approaches
|
||||
- All SM 3.0 compatible
|
||||
|
||||
### UnitXP SP3 Reference
|
||||
- **Path:** `/media/storage/projects/UnitXP_SP3_Orig/UnitXP_SP3/`
|
||||
- **Key file:** `Vanilla1121_functions.h`
|
||||
- `vanilla1121_worldToScreen(C3Vector& world)` — world → screen projection
|
||||
- `CWorld_Intersect()` — raycast through world geometry
|
||||
- `vanilla1121_unitPosition()` — unit world position
|
||||
- `vanilla1121_getCameraPosition()` — camera world position
|
||||
|
||||
### WoWee Terrain Renderer Reference
|
||||
- **Path:** `/media/storage/projects/WoWee/src/rendering/terrain_renderer.cpp`
|
||||
- Modern OpenGL terrain rendering with multi-layer textures
|
||||
- Frustum culling, LOD, shadow mapping
|
||||
- Good reference for understanding terrain data structures
|
||||
|
||||
---
|
||||
|
||||
## What's Missing
|
||||
|
||||
### 1. Terrain Height Query
|
||||
```c
|
||||
// Need: Get terrain Z at world (X, Y)
|
||||
float GetTerrainHeight(float worldX, float worldY);
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- Reverse engineer WoW's internal terrain height function
|
||||
- Use `CWorld_Intersect()` with vertical ray: `(x, y, +1000) → (x, y, -1000)`
|
||||
- ADT tile parsing (complex, requires MPQ access)
|
||||
|
||||
### 2. Terrain Normal Query
|
||||
```c
|
||||
// Need: Get terrain normal at world (X, Y) for quad orientation
|
||||
C3Vector GetTerrainNormal(float worldX, float worldY);
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- Sample height at 4 neighboring points, compute normal
|
||||
- Reverse engineer WoW's internal function
|
||||
|
||||
### 3. Depth Buffer as Texture (for projected decals)
|
||||
```c
|
||||
// Need: Access depth buffer as readable texture
|
||||
IDirect3DTexture9* GetDepthAsTexture();
|
||||
```
|
||||
|
||||
**D3D9 approaches:**
|
||||
- `INTZ` / `RAWZ` format hack (requires driver support)
|
||||
- `D3DFMT_D24S8` → `D3DFMT_D24X8` with `INTZ` fourcc
|
||||
- Render depth to separate RT during terrain pass
|
||||
- See: https://aras-p.info/texts/D3D9GPUHacks.html
|
||||
|
||||
---
|
||||
|
||||
## Implementation Options
|
||||
|
||||
### Option A: Screen-Space Marker (Simplest)
|
||||
|
||||
**Complexity:** Low
|
||||
**Visual Quality:** Basic (icon on screen, no ground conformity)
|
||||
|
||||
**Implementation:**
|
||||
1. Hook raid target array at `0x00B71368` (8 GUIDs)
|
||||
2. Get marked unit's world position via `vanilla1121_unitPosition()`
|
||||
3. Project to screen via `vanilla1121_worldToScreen()`
|
||||
4. Draw icon sprite in EndScene at screen position
|
||||
5. Optional: Depth test against terrain depth buffer
|
||||
|
||||
**Pros:**
|
||||
- Minimal implementation
|
||||
- Uses existing infrastructure
|
||||
- No terrain queries needed
|
||||
|
||||
**Cons:**
|
||||
- Marker doesn't conform to terrain
|
||||
- Looks like a floating icon, not ground projection
|
||||
- No "area on the ground" effect
|
||||
|
||||
---
|
||||
|
||||
### Option B: Projected Ground Decal (Recommended)
|
||||
|
||||
**Complexity:** Medium
|
||||
**Visual Quality:** Good (conforms to terrain, area indicator)
|
||||
|
||||
**Implementation:**
|
||||
1. Store marker world positions (from raid target array or click events)
|
||||
2. In EndScene, after terrain+WMO depth written:
|
||||
3. For each active marker:
|
||||
- Bind depth buffer as texture (INTZ hack or separate RT)
|
||||
- Render fullscreen quad with decal shader
|
||||
- Shader reconstructs world position from depth
|
||||
- If within marker radius, output marker color/texture
|
||||
- Distance fade at edges for soft boundary
|
||||
|
||||
**Decal Shader (HLSL SM 3.0):**
|
||||
```hlsl
|
||||
// Uniforms set by CPU
|
||||
float3 MarkerCenter; // World position
|
||||
float MarkerRadius; // In world units
|
||||
float4 MarkerColor; // RGBA
|
||||
float2 ScreenSize; // For UV calculation
|
||||
|
||||
// Depth buffer bound to s0
|
||||
sampler2D DepthSampler : register(s0);
|
||||
|
||||
float4 DecalPS(float2 uv : TEXCOORD0) : COLOR
|
||||
{
|
||||
// Read depth, reconstruct world position
|
||||
float depth = tex2D(DepthSampler, uv).r;
|
||||
float3 worldPos = ReconstructWorldPosition(uv, depth);
|
||||
|
||||
// Distance from marker center (XZ plane)
|
||||
float2 offset = worldPos.xz - MarkerCenter.xz;
|
||||
float dist = length(offset);
|
||||
|
||||
// Soft falloff
|
||||
float alpha = saturate(1.0 - (dist / MarkerRadius));
|
||||
alpha = smoothstep(0.0, 0.3, alpha); // Soft edge
|
||||
|
||||
// Output
|
||||
return float4(MarkerColor.rgb, MarkerColor.a * alpha);
|
||||
}
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Conforms to terrain contours
|
||||
- Looks like ground projection
|
||||
- Can support multiple markers
|
||||
- Soft edges for aesthetic
|
||||
|
||||
**Cons:**
|
||||
- Requires depth buffer access (driver-dependent)
|
||||
- More complex shader setup
|
||||
- Per-marker render pass overhead
|
||||
|
||||
---
|
||||
|
||||
### Option C: World-Space Quad (Best Conformity)
|
||||
|
||||
**Complexity:** High
|
||||
**Visual Quality:** Best (actual geometry on terrain)
|
||||
|
||||
**Implementation:**
|
||||
1. Create marker quad mesh (4 vertices, 2 triangles)
|
||||
2. Hook `CM2SceneRenderDraw` to inject custom geometry
|
||||
3. For each marker:
|
||||
- Query terrain height at marker position
|
||||
- Query terrain normal for orientation
|
||||
- Position quad at terrain height, orient to normal
|
||||
- Add to render batch with marker texture
|
||||
4. Render as part of M2 batch with depth testing
|
||||
|
||||
**Pros:**
|
||||
- Perfect terrain conformity
|
||||
- Actual geometry, not shader trick
|
||||
- Proper depth testing with other objects
|
||||
|
||||
**Cons:**
|
||||
- Requires terrain height/normal queries
|
||||
- More complex geometry management
|
||||
- Hook injection into render pipeline
|
||||
|
||||
---
|
||||
|
||||
## Recommended Path
|
||||
|
||||
### Phase 1: Proof of Concept (Screen-Space)
|
||||
1. Implement basic screen-space marker rendering
|
||||
2. Hook raid target array, project positions
|
||||
3. Draw simple circle/icon at screen position
|
||||
4. Verify hook integration works
|
||||
|
||||
**Estimated effort:** 1-2 hours
|
||||
|
||||
### Phase 2: Ground Decal (Primary Target)
|
||||
1. Implement INTZ depth texture hack
|
||||
2. Write decal projection shader
|
||||
3. Add marker position storage
|
||||
4. Render projected circles on terrain
|
||||
|
||||
**Estimated effort:** 4-6 hours
|
||||
|
||||
### Phase 3: Polish
|
||||
1. Add marker textures (skull, cross, square, etc.)
|
||||
2. Soft edge falloff
|
||||
3. Multiple marker colors
|
||||
4. Optional: Click-to-place interface
|
||||
|
||||
**Estimated effort:** 2-3 hours
|
||||
|
||||
---
|
||||
|
||||
## Key Files to Create/Modify
|
||||
|
||||
### New Files
|
||||
```
|
||||
src/marker/
|
||||
├── marker_tracker.zig # Track active markers, positions
|
||||
├── marker_decal.zig # Decal rendering shader + pipeline
|
||||
├── marker_hooks.zig # Raid target array hooks
|
||||
└── marker_types.zig # Data structures
|
||||
```
|
||||
|
||||
### Modified Files
|
||||
```
|
||||
src/outline/d3d9_hook.zig # Add depth texture creation
|
||||
src/outline/types.zig # Add depth texture format constants
|
||||
src/main.zig # Initialize marker system
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## External References
|
||||
|
||||
### D3D9 Depth Buffer Hacks
|
||||
- https://aras-p.info/texts/D3D9GPUHacks.html
|
||||
- INTZ / RAWZ format for reading depth as texture
|
||||
|
||||
### Decal Rendering Techniques
|
||||
- Valve L4D Glow Effect (stencil + blur): https://developer.valvesoftware.com/wiki/L4D_Glow_Effect
|
||||
- Unreal Engine deferred decals: https://docs.unrealengine.com/4.27/en-US/RenderingAndGraphics/DeferredRendering/
|
||||
|
||||
### WoW 1.12.1 Internals
|
||||
- wowdev.wiki for ADT terrain format
|
||||
- UnitXP_SP3 for function signatures and calling conventions
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Raid markers in Wrath+ use ground-projected circles with icon in center
|
||||
- Our outline system already has the render target / shader infrastructure
|
||||
- Depth buffer access is the main technical challenge for Option B
|
||||
- Option A is good for quick testing, Option B is the production target
|
||||
@@ -15,4 +15,21 @@ pub fn build(b: *std.Build) void {
|
||||
.optimize = optimize,
|
||||
});
|
||||
hook_mod.addOptions("config", options);
|
||||
|
||||
// x86 length disassembler — standalone, no dependencies
|
||||
const x86dis_mod = b.addModule("x86dis", .{
|
||||
.root_source_file = b.path("src/x86dis.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
// Generic hook — uses x86dis + hook for auto-sizing trampolines
|
||||
const generic_hook_mod = b.addModule("generic_hook", .{
|
||||
.root_source_file = b.path("src/generic_hook.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
generic_hook_mod.addImport("x86dis", x86dis_mod);
|
||||
generic_hook_mod.addImport("hook.zig", hook_mod);
|
||||
generic_hook_mod.addOptions("config", options);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
//! Generic x86 inline hook — no manual prologue size or fixup lists needed.
|
||||
//!
|
||||
//! Uses the x86 length disassembler (HDE32 port) to automatically determine
|
||||
//! how many prologue bytes to steal, and relocates all relative instructions.
|
||||
//!
|
||||
//! Combines MinHook's compact disassembler with HadesMem's type-safe approach:
|
||||
//! declare the function signature once at comptime, get a correctly-typed
|
||||
//! trampoline and detour with zero manual casting.
|
||||
//!
|
||||
//! ## Low-level API (GenericHook)
|
||||
//!
|
||||
//! ```zig
|
||||
//! var my_hook: GenericHook = .{};
|
||||
//! if (my_hook.install(0x401000, @intFromPtr(&myDetour)) == .ok) {
|
||||
//! const orig = my_hook.getTrampoline(OrigFnType);
|
||||
//! _ = orig();
|
||||
//! }
|
||||
//! my_hook.remove();
|
||||
//! ```
|
||||
//!
|
||||
//! ## Type-safe API (Detour) — HadesMem-inspired
|
||||
//!
|
||||
//! ```zig
|
||||
//! const MyHook = Detour(fn (u32, u32) callconv(.{ .x86_stdcall = .{} }) u32);
|
||||
//! var hook: MyHook = .{};
|
||||
//! hook.attach(0x401000, myDetour);
|
||||
//! // Inside detour: hook.callOriginal(.{arg1, arg2});
|
||||
//! ```
|
||||
|
||||
const std = @import("std");
|
||||
const x86dis = @import("x86dis");
|
||||
const hook_base = @import("hook.zig");
|
||||
|
||||
const VirtualAlloc = hook_base.VirtualAlloc;
|
||||
const VirtualFree = hook_base.VirtualFree;
|
||||
const PAGE_EXECUTE_READWRITE = hook_base.PAGE_EXECUTE_READWRITE;
|
||||
const MEM_COMMIT = hook_base.MEM_COMMIT;
|
||||
const MEM_RELEASE = hook_base.MEM_RELEASE;
|
||||
const writeProtected = hook_base.writeProtected;
|
||||
|
||||
const JMP_SIZE: usize = 5; // E9 + rel32
|
||||
const MAX_STOLEN: usize = 32;
|
||||
const TRAMPOLINE_BUF: usize = 64;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// GenericHook — low-level auto-sizing hook
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
pub const GenericHook = struct {
|
||||
mem: ?[*]u8 = null,
|
||||
trampoline: usize = 0,
|
||||
target: usize = 0,
|
||||
stolen_size: usize = 0,
|
||||
saved_bytes: [MAX_STOLEN]u8 = undefined,
|
||||
|
||||
pub const Error = enum {
|
||||
ok,
|
||||
alloc_failed,
|
||||
disasm_error,
|
||||
prologue_too_short,
|
||||
unsupported_relocation,
|
||||
};
|
||||
|
||||
/// Analyse target, build trampoline, patch target → detour. One call.
|
||||
pub fn install(self: *GenericHook, target: usize, detour_addr: usize) Error {
|
||||
const err = self.prepare(target);
|
||||
if (err != .ok) return err;
|
||||
self.activate(detour_addr);
|
||||
return .ok;
|
||||
}
|
||||
|
||||
/// Phase 1: disassemble prologue, allocate trampoline, copy + relocate.
|
||||
pub fn prepare(self: *GenericHook, target: usize) Error {
|
||||
if (self.mem != null) return .ok;
|
||||
|
||||
const src: [*]const u8 = @ptrFromInt(target);
|
||||
|
||||
// ── determine how many bytes to steal ──
|
||||
var stolen: usize = 0;
|
||||
while (stolen < JMP_SIZE) {
|
||||
const insn = x86dis.decode(src + stolen);
|
||||
if (insn.flags & x86dis.F_ERROR != 0) return .disasm_error;
|
||||
if (insn.len == 0) return .disasm_error;
|
||||
stolen += insn.len;
|
||||
if (stolen > MAX_STOLEN) return .prologue_too_short;
|
||||
}
|
||||
|
||||
// ── allocate ──
|
||||
const mem = VirtualAlloc(null, TRAMPOLINE_BUF, MEM_COMMIT, PAGE_EXECUTE_READWRITE) orelse return .alloc_failed;
|
||||
|
||||
self.mem = mem;
|
||||
self.target = target;
|
||||
self.stolen_size = stolen;
|
||||
self.trampoline = @intFromPtr(mem);
|
||||
|
||||
@memcpy(self.saved_bytes[0..stolen], src[0..stolen]);
|
||||
|
||||
// ── check if already hooked (E9 at target) — chain through ──
|
||||
if (src[0] == 0xE9) {
|
||||
const other_detour = hook_base.rel32Target(target);
|
||||
mem[0] = 0xE9;
|
||||
hook_base.writeRel32(mem + 1, self.trampoline + 1, other_detour);
|
||||
return .ok;
|
||||
}
|
||||
|
||||
// ── build trampoline: copy + relocate ──
|
||||
var t_pos: usize = 0;
|
||||
var s_pos: usize = 0;
|
||||
|
||||
while (s_pos < stolen) {
|
||||
const insn = x86dis.decode(src + s_pos);
|
||||
const op = insn.opcode;
|
||||
const src_addr = target + s_pos;
|
||||
const dst_addr = self.trampoline + t_pos;
|
||||
|
||||
if (insn.flags & x86dis.F_RELATIVE != 0) {
|
||||
if (op == 0xE8 or op == 0xE9) {
|
||||
// CALL/JMP rel32
|
||||
const abs = hook_base.rel32Target(src_addr);
|
||||
mem[t_pos] = op;
|
||||
hook_base.writeRel32(mem + t_pos + 1, dst_addr + 1, abs);
|
||||
t_pos += 5;
|
||||
} else if (op == 0x0F and insn.opcode2 >= 0x80 and insn.opcode2 <= 0x8F) {
|
||||
// Jcc rel32 (0F 80-8F)
|
||||
const abs = jcc32Target(src_addr);
|
||||
mem[t_pos] = 0x0F;
|
||||
mem[t_pos + 1] = insn.opcode2;
|
||||
hook_base.writeRel32(mem + t_pos + 2, dst_addr + 2, abs);
|
||||
t_pos += 6;
|
||||
} else if (op >= 0x70 and op <= 0x7F) {
|
||||
// Short Jcc → expand to near Jcc (0F 8x)
|
||||
const offset = @as(i8, @bitCast(src[s_pos + 1]));
|
||||
const abs: usize = @bitCast(@as(isize, @intCast(src_addr + 2)) + offset);
|
||||
mem[t_pos] = 0x0F;
|
||||
mem[t_pos + 1] = op + 0x10;
|
||||
hook_base.writeRel32(mem + t_pos + 2, dst_addr + 2, abs);
|
||||
t_pos += 6;
|
||||
} else if (op == 0xEB) {
|
||||
// Short JMP → expand to near JMP (E9)
|
||||
const offset = @as(i8, @bitCast(src[s_pos + 1]));
|
||||
const abs: usize = @bitCast(@as(isize, @intCast(src_addr + 2)) + offset);
|
||||
mem[t_pos] = 0xE9;
|
||||
hook_base.writeRel32(mem + t_pos + 1, dst_addr + 1, abs);
|
||||
t_pos += 5;
|
||||
} else {
|
||||
// LOOP/JECXZ or unknown — cannot trivially expand
|
||||
self.cleanup();
|
||||
return .unsupported_relocation;
|
||||
}
|
||||
} else {
|
||||
// Non-relative — copy verbatim
|
||||
@memcpy(mem[t_pos .. t_pos + insn.len], src[s_pos .. s_pos + insn.len]);
|
||||
t_pos += insn.len;
|
||||
}
|
||||
s_pos += insn.len;
|
||||
}
|
||||
|
||||
// ── JMP back to original code after stolen bytes ──
|
||||
mem[t_pos] = 0xE9;
|
||||
hook_base.writeRel32(
|
||||
mem + t_pos + 1,
|
||||
self.trampoline + t_pos + 1,
|
||||
target + stolen,
|
||||
);
|
||||
|
||||
return .ok;
|
||||
}
|
||||
|
||||
/// Phase 2: write the E9 JMP patch at the target.
|
||||
pub fn activate(self: *GenericHook, detour_addr: usize) void {
|
||||
var patch: [MAX_STOLEN]u8 = .{0x90} ** MAX_STOLEN;
|
||||
patch[0] = 0xE9;
|
||||
hook_base.writeRel32(patch[1..5], self.target + 1, detour_addr);
|
||||
writeProtected(self.target, patch[0..self.stolen_size]);
|
||||
}
|
||||
|
||||
/// Restore original bytes and free trampoline memory.
|
||||
pub fn remove(self: *GenericHook) void {
|
||||
if (self.mem == null) return;
|
||||
writeProtected(self.target, self.saved_bytes[0..self.stolen_size]);
|
||||
_ = VirtualFree(@ptrFromInt(@intFromPtr(self.mem.?)), 0, MEM_RELEASE);
|
||||
self.mem = null;
|
||||
}
|
||||
|
||||
/// Get trampoline as a typed function pointer.
|
||||
pub fn getTrampoline(self: *const GenericHook, comptime T: type) T {
|
||||
return @ptrFromInt(self.trampoline);
|
||||
}
|
||||
|
||||
fn cleanup(self: *GenericHook) void {
|
||||
if (self.mem) |m| {
|
||||
_ = VirtualFree(@ptrFromInt(@intFromPtr(m)), 0, MEM_RELEASE);
|
||||
self.mem = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// Detour(FnType) — HadesMem-style type-safe generic hook
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Comptime-generic typed detour. Declare the target function's type once;
|
||||
/// get type-checked attach/callOriginal with no manual pointer casts.
|
||||
///
|
||||
/// ```zig
|
||||
/// const StdcallU32x2 = fn (u32, u32) callconv(.{ .x86_stdcall = .{} }) u32;
|
||||
/// const MyHook = generic_hook.Detour(StdcallU32x2);
|
||||
/// var hook: MyHook = .{};
|
||||
/// hook.attach(0x401000, &myDetour);
|
||||
/// // in detour: hook.callOriginal(.{ a, b });
|
||||
/// hook.detach();
|
||||
/// ```
|
||||
pub fn Detour(comptime FnType: type) type {
|
||||
const FnInfo = @typeInfo(FnType).@"fn";
|
||||
const FnPtr = *const FnType;
|
||||
const ReturnType = FnInfo.return_type orelse void;
|
||||
const ParamTypes = FnInfo.params;
|
||||
|
||||
return struct {
|
||||
inner: GenericHook = .{},
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Hook the function at `target` to redirect to `detour`.
|
||||
pub fn attach(self: *Self, target: usize, detour: FnPtr) GenericHook.Error {
|
||||
return self.inner.install(target, @intFromPtr(detour));
|
||||
}
|
||||
|
||||
/// Call the original (pre-hook) function through the trampoline.
|
||||
pub fn callOriginal(self: *const Self, args: anytype) ReturnType {
|
||||
const orig: FnPtr = @ptrFromInt(self.inner.trampoline);
|
||||
return @call(.auto, orig, coerceArgs(ParamTypes, args));
|
||||
}
|
||||
|
||||
/// Unhook: restore original bytes, free trampoline.
|
||||
pub fn detach(self: *Self) void {
|
||||
self.inner.remove();
|
||||
}
|
||||
|
||||
/// Get the trampoline as the correctly-typed function pointer.
|
||||
pub fn original(self: *const Self) FnPtr {
|
||||
return @ptrFromInt(self.inner.trampoline);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Coerce a tuple of args into the exact parameter types expected.
|
||||
fn coerceArgs(comptime params: anytype, args: anytype) CoercedTuple(params) {
|
||||
var result: CoercedTuple(params) = undefined;
|
||||
inline for (0..params.len) |idx| {
|
||||
@field(result, std.fmt.comptimePrint("{d}", .{idx})) = args[idx];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
fn CoercedTuple(comptime params: anytype) type {
|
||||
var fields: [params.len]std.builtin.Type.StructField = undefined;
|
||||
inline for (0..params.len) |idx| {
|
||||
fields[idx] = .{
|
||||
.name = std.fmt.comptimePrint("{d}", .{idx}),
|
||||
.type = params[idx].type.?,
|
||||
.default_value_ptr = null,
|
||||
.is_comptime = false,
|
||||
.alignment = 0,
|
||||
};
|
||||
}
|
||||
return @Type(.{ .@"struct" = .{
|
||||
.layout = .auto,
|
||||
.fields = &fields,
|
||||
.decls = &.{},
|
||||
.is_tuple = true,
|
||||
} });
|
||||
}
|
||||
|
||||
/// Resolve absolute target of a Jcc rel32 (0F 8x xx xx xx xx) — 6 byte insn.
|
||||
fn jcc32Target(addr: usize) usize {
|
||||
const disp: u32 = @bitCast(@as(*align(1) const i32, @ptrFromInt(addr + 2)).*);
|
||||
return (addr + 6) +% disp;
|
||||
}
|
||||
@@ -0,0 +1,402 @@
|
||||
//! Minimal x86 (32-bit) length disassembler.
|
||||
//!
|
||||
//! Faithful port of Vyacheslav Patkov's Hacker Disassembler Engine 32 (HDE32),
|
||||
//! used by MinHook. Only computes instruction length + flags needed for
|
||||
//! relocation (F_RELATIVE). ~470 bytes of table data, compiles to ~1-2 KB.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
// ── public flags ───────────────────────────────────────────────────────
|
||||
pub const F_MODRM: u32 = 0x00000001;
|
||||
pub const F_SIB: u32 = 0x00000002;
|
||||
pub const F_IMM8: u32 = 0x00000004;
|
||||
pub const F_IMM16: u32 = 0x00000008;
|
||||
pub const F_IMM32: u32 = 0x00000010;
|
||||
pub const F_DISP8: u32 = 0x00000020;
|
||||
pub const F_DISP16: u32 = 0x00000040;
|
||||
pub const F_DISP32: u32 = 0x00000080;
|
||||
pub const F_RELATIVE: u32 = 0x00000100;
|
||||
pub const F_ERROR: u32 = 0x00001000;
|
||||
|
||||
// ── internal cflags ────────────────────────────────────────────────────
|
||||
const C_MODRM: u8 = 0x01;
|
||||
const C_IMM8: u8 = 0x02;
|
||||
const C_IMM16: u8 = 0x04;
|
||||
const C_IMM_P66: u8 = 0x10;
|
||||
const C_REL8: u8 = 0x20;
|
||||
const C_REL32: u8 = 0x40;
|
||||
const C_GROUP: u8 = 0x80;
|
||||
const C_ERROR: u8 = 0xff;
|
||||
|
||||
const PRE_NONE: u8 = 0x01;
|
||||
const PRE_66: u8 = 0x08;
|
||||
const PRE_67: u8 = 0x10;
|
||||
|
||||
const DELTA_OPCODES: usize = 0x4a;
|
||||
|
||||
// HDE32 opcode table — verbatim from table32.h
|
||||
const hde32_table = [_]u8{
|
||||
0xa3, 0xa8, 0xa3, 0xa8, 0xa3, 0xa8, 0xa3, 0xa8, 0xa3, 0xa8, 0xa3, 0xa8, 0xa3, 0xa8, 0xa3,
|
||||
0xa8, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xac, 0xaa, 0xb2, 0xaa, 0x9f, 0x9f,
|
||||
0x9f, 0x9f, 0xb5, 0xa3, 0xa3, 0xa4, 0xaa, 0xaa, 0xba, 0xaa, 0x96, 0xaa, 0xa8, 0xaa, 0xc3,
|
||||
0xc3, 0x96, 0x96, 0xb7, 0xae, 0xd6, 0xbd, 0xa3, 0xc5, 0xa3, 0xa3, 0x9f, 0xc3, 0x9c, 0xaa,
|
||||
0xaa, 0xac, 0xaa, 0xbf, 0x03, 0x7f, 0x11, 0x7f, 0x01, 0x7f, 0x01, 0x3f, 0x01, 0x01, 0x90,
|
||||
0x82, 0x7d, 0x97, 0x59, 0x59, 0x59, 0x59, 0x59, 0x7f, 0x59, 0x59, 0x60, 0x7d, 0x7f, 0x7f,
|
||||
0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x9a, 0x88, 0x7d,
|
||||
0x59, 0x50, 0x50, 0x50, 0x50, 0x59, 0x59, 0x59, 0x59, 0x61, 0x94, 0x61, 0x9e, 0x59, 0x59,
|
||||
0x85, 0x59, 0x92, 0xa3, 0x60, 0x60, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59,
|
||||
0x59, 0x59, 0x9f, 0x01, 0x03, 0x01, 0x04, 0x03, 0xd5, 0x03, 0xcc, 0x01, 0xbc, 0x03, 0xf0,
|
||||
0x10, 0x10, 0x10, 0x10, 0x50, 0x50, 0x50, 0x50, 0x14, 0x20, 0x20, 0x20, 0x20, 0x01, 0x01,
|
||||
0x01, 0x01, 0xc4, 0x02, 0x10, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0xc0, 0xc2, 0x10, 0x11,
|
||||
0x02, 0x03, 0x11, 0x03, 0x03, 0x04, 0x00, 0x00, 0x14, 0x00, 0x02, 0x00, 0x00, 0xc6, 0xc8,
|
||||
0x02, 0x02, 0x02, 0x02, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0xff, 0xca,
|
||||
0x01, 0x01, 0x01, 0x00, 0x06, 0x00, 0x04, 0x00, 0xc0, 0xc2, 0x01, 0x01, 0x03, 0x01, 0xff,
|
||||
0xff, 0x01, 0x00, 0x03, 0xc4, 0xc4, 0xc6, 0x03, 0x01, 0x01, 0x01, 0xff, 0x03, 0x03, 0x03,
|
||||
0xc8, 0x40, 0x00, 0x0a, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x33, 0x01, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xbf, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00,
|
||||
0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x7f, 0x00, 0x00, 0xff, 0x4a, 0x4a, 0x4a, 0x4a, 0x4b, 0x52, 0x4a, 0x4a, 0x4a, 0x4a, 0x4f,
|
||||
0x4c, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x55, 0x45, 0x40, 0x4a, 0x4a, 0x4a,
|
||||
0x45, 0x59, 0x4d, 0x46, 0x4a, 0x5d, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a,
|
||||
0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x61, 0x63, 0x67, 0x4e, 0x4a, 0x4a, 0x6b, 0x6d, 0x4a, 0x4a,
|
||||
0x45, 0x6d, 0x4a, 0x4a, 0x44, 0x45, 0x4a, 0x4a, 0x00, 0x00, 0x00, 0x02, 0x0d, 0x06, 0x06,
|
||||
0x06, 0x06, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x06, 0x06, 0x06, 0x00, 0x06, 0x06, 0x02, 0x06,
|
||||
0x00, 0x0a, 0x0a, 0x07, 0x07, 0x06, 0x02, 0x05, 0x05, 0x02, 0x02, 0x00, 0x00, 0x04, 0x04,
|
||||
0x04, 0x04, 0x00, 0x00, 0x00, 0x0e, 0x05, 0x06, 0x06, 0x06, 0x01, 0x06, 0x00, 0x00, 0x08,
|
||||
0x00, 0x10, 0x00, 0x18, 0x00, 0x20, 0x00, 0x28, 0x00, 0x30, 0x00, 0x80, 0x01, 0x82, 0x01,
|
||||
0x86, 0x00, 0xf6, 0xcf, 0xfe, 0x3f, 0xab, 0x00, 0xb0, 0x00, 0xb1, 0x00, 0xb3, 0x00, 0xba,
|
||||
0xf8, 0xbb, 0x00, 0xc0, 0x00, 0xc1, 0x00, 0xc7, 0xbf, 0x62, 0xff, 0x00, 0x8d, 0xff, 0x00,
|
||||
0xc4, 0xff, 0x00, 0xc5, 0xff, 0x00,
|
||||
};
|
||||
|
||||
pub const Insn = struct {
|
||||
len: u8,
|
||||
flags: u32,
|
||||
opcode: u8,
|
||||
opcode2: u8,
|
||||
};
|
||||
|
||||
/// Decode the instruction at `code`, returning its length and flags.
|
||||
pub fn decode(code: [*]const u8) Insn {
|
||||
var result = Insn{ .len = 0, .flags = 0, .opcode = 0, .opcode2 = 0 };
|
||||
|
||||
var p: usize = 0;
|
||||
var pref: u8 = 0;
|
||||
var disp_size: u8 = 0;
|
||||
|
||||
// ── prefixes ──
|
||||
var prefix_count: u8 = 16;
|
||||
prefix_loop: while (prefix_count > 0) : (prefix_count -= 1) {
|
||||
switch (code[p]) {
|
||||
0xf3, 0xf2 => pref |= if (code[p] == 0xf3) 0x04 else 0x02,
|
||||
0xf0 => pref |= 0x20, // PRE_LOCK
|
||||
0x26, 0x2e, 0x36, 0x3e, 0x64, 0x65 => pref |= 0x40, // PRE_SEG
|
||||
0x66 => pref |= PRE_66,
|
||||
0x67 => pref |= PRE_67,
|
||||
else => break :prefix_loop,
|
||||
}
|
||||
p += 1;
|
||||
}
|
||||
|
||||
result.flags = @as(u32, pref) << 23;
|
||||
|
||||
if (pref == 0) pref |= PRE_NONE;
|
||||
|
||||
// ── opcode ──
|
||||
var ht_base: usize = 0;
|
||||
var c = code[p];
|
||||
p += 1;
|
||||
result.opcode = c;
|
||||
|
||||
if (c == 0x0f) {
|
||||
// two-byte opcode
|
||||
result.opcode2 = code[p];
|
||||
c = code[p];
|
||||
p += 1;
|
||||
ht_base = DELTA_OPCODES;
|
||||
} else if (c >= 0xa0 and c <= 0xa3) {
|
||||
// MOV moffs — address-size prefix swaps operand-size behavior
|
||||
if (pref & PRE_67 != 0)
|
||||
pref |= PRE_66
|
||||
else
|
||||
pref &= ~PRE_66;
|
||||
}
|
||||
|
||||
const opcode = c;
|
||||
|
||||
// ── two-level table lookup: ht[ht[opcode/4] + (opcode%4)] ──
|
||||
var cflags: u8 = blk: {
|
||||
const idx1 = ht_base + @as(usize, opcode / 4);
|
||||
if (idx1 >= hde32_table.len) break :blk C_ERROR;
|
||||
const idx2 = ht_base + @as(usize, hde32_table[idx1]) + @as(usize, opcode % 4);
|
||||
if (idx2 >= hde32_table.len) break :blk C_ERROR;
|
||||
break :blk hde32_table[idx2];
|
||||
};
|
||||
|
||||
if (cflags == C_ERROR) {
|
||||
result.flags |= F_ERROR;
|
||||
cflags = 0;
|
||||
if ((opcode & 0xfd) == 0x24) // (opcode & -3) == 0x24
|
||||
cflags +%= 1;
|
||||
}
|
||||
|
||||
// ── group resolution ──
|
||||
var x: u8 = 0;
|
||||
if (cflags & C_GROUP != 0) {
|
||||
const group_idx = ht_base + @as(usize, cflags & 0x7f);
|
||||
if (group_idx + 1 < hde32_table.len) {
|
||||
const t = std.mem.readInt(u16, hde32_table[group_idx..][0..2], .little);
|
||||
cflags = @truncate(t);
|
||||
x = @truncate(t >> 8);
|
||||
}
|
||||
}
|
||||
|
||||
// ── modrm ──
|
||||
if (cflags & C_MODRM != 0) {
|
||||
result.flags |= F_MODRM;
|
||||
const modrm = code[p];
|
||||
p += 1;
|
||||
const m_mod = modrm >> 6;
|
||||
const m_rm: u8 = modrm & 7;
|
||||
const m_reg: u3 = @truncate((modrm & 0x3f) >> 3);
|
||||
|
||||
// F6 TEST imm8 / F7 TEST imm16/32
|
||||
if (m_reg <= 1) {
|
||||
if (opcode == 0xf6)
|
||||
cflags |= C_IMM8;
|
||||
if (opcode == 0xf7)
|
||||
cflags |= C_IMM_P66;
|
||||
}
|
||||
|
||||
// displacement
|
||||
switch (m_mod) {
|
||||
0 => {
|
||||
if (pref & PRE_67 != 0) {
|
||||
if (m_rm == 6) disp_size = 2;
|
||||
} else {
|
||||
if (m_rm == 5) disp_size = 4;
|
||||
}
|
||||
},
|
||||
1 => disp_size = 1,
|
||||
2 => {
|
||||
disp_size = 2;
|
||||
if (pref & PRE_67 == 0)
|
||||
disp_size = 4;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
// SIB byte
|
||||
if (m_mod != 3 and m_rm == 4 and (pref & PRE_67 == 0)) {
|
||||
result.flags |= F_SIB;
|
||||
const sib = code[p];
|
||||
p += 1;
|
||||
if ((sib & 7) == 5 and (m_mod & 1) == 0)
|
||||
disp_size = 4;
|
||||
}
|
||||
|
||||
// displacement bytes
|
||||
switch (disp_size) {
|
||||
1 => {
|
||||
result.flags |= F_DISP8;
|
||||
p += 1;
|
||||
},
|
||||
2 => {
|
||||
result.flags |= F_DISP16;
|
||||
p += 2;
|
||||
},
|
||||
4 => {
|
||||
result.flags |= F_DISP32;
|
||||
p += 4;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
// ── immediates ──
|
||||
if (cflags & C_IMM_P66 != 0) {
|
||||
if (cflags & C_REL32 != 0) {
|
||||
if (pref & PRE_66 != 0) {
|
||||
result.flags |= F_IMM16 | F_RELATIVE;
|
||||
p += 2;
|
||||
// disasm_done — skip remaining immediate checks
|
||||
result.len = @intCast(p);
|
||||
if (result.len > 15) {
|
||||
result.flags |= F_ERROR;
|
||||
result.len = 15;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// fall through to rel32_ok below
|
||||
} else {
|
||||
if (pref & PRE_66 != 0) {
|
||||
result.flags |= F_IMM16;
|
||||
p += 2;
|
||||
} else {
|
||||
result.flags |= F_IMM32;
|
||||
p += 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cflags & C_IMM16 != 0) {
|
||||
if (result.flags & F_IMM32 != 0) {
|
||||
result.flags |= F_IMM16;
|
||||
} else if (result.flags & F_IMM16 != 0) {
|
||||
// F_2IMM16
|
||||
} else {
|
||||
result.flags |= F_IMM16;
|
||||
}
|
||||
p += 2;
|
||||
}
|
||||
if (cflags & C_IMM8 != 0) {
|
||||
result.flags |= F_IMM8;
|
||||
p += 1;
|
||||
}
|
||||
|
||||
if (cflags & C_REL32 != 0) {
|
||||
result.flags |= F_IMM32 | F_RELATIVE;
|
||||
p += 4;
|
||||
} else if (cflags & C_REL8 != 0) {
|
||||
result.flags |= F_IMM8 | F_RELATIVE;
|
||||
p += 1;
|
||||
}
|
||||
|
||||
result.len = @intCast(p);
|
||||
if (result.len > 15) {
|
||||
result.flags |= F_ERROR;
|
||||
result.len = 15;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── tests ──────────────────────────────────────────────────────────────
|
||||
|
||||
test "push ebp" {
|
||||
const d = decode(&[_]u8{ 0x55, 0xCC });
|
||||
try std.testing.expectEqual(@as(u8, 1), d.len);
|
||||
}
|
||||
|
||||
test "mov ebp, esp" {
|
||||
// 8B EC (or 89 E5)
|
||||
const d = decode(&[_]u8{ 0x8B, 0xEC });
|
||||
try std.testing.expectEqual(@as(u8, 2), d.len);
|
||||
try std.testing.expect(d.flags & F_MODRM != 0);
|
||||
}
|
||||
|
||||
test "call rel32" {
|
||||
const d = decode(&[_]u8{ 0xE8, 0x78, 0x56, 0x34, 0x12 });
|
||||
try std.testing.expectEqual(@as(u8, 5), d.len);
|
||||
try std.testing.expect(d.flags & F_RELATIVE != 0);
|
||||
try std.testing.expect(d.flags & F_IMM32 != 0);
|
||||
}
|
||||
|
||||
test "jmp rel32" {
|
||||
const d = decode(&[_]u8{ 0xE9, 0x00, 0x00, 0x00, 0x00 });
|
||||
try std.testing.expectEqual(@as(u8, 5), d.len);
|
||||
try std.testing.expect(d.flags & F_RELATIVE != 0);
|
||||
}
|
||||
|
||||
test "sub esp, imm8" {
|
||||
// 83 EC 10
|
||||
const d = decode(&[_]u8{ 0x83, 0xEC, 0x10 });
|
||||
try std.testing.expectEqual(@as(u8, 3), d.len);
|
||||
try std.testing.expect(d.flags & F_MODRM != 0);
|
||||
try std.testing.expect(d.flags & F_IMM8 != 0);
|
||||
}
|
||||
|
||||
test "mov eax, [ebp+8]" {
|
||||
// 8B 45 08
|
||||
const d = decode(&[_]u8{ 0x8B, 0x45, 0x08 });
|
||||
try std.testing.expectEqual(@as(u8, 3), d.len);
|
||||
try std.testing.expect(d.flags & F_MODRM != 0);
|
||||
try std.testing.expect(d.flags & F_DISP8 != 0);
|
||||
}
|
||||
|
||||
test "jz rel32 (0F 84)" {
|
||||
const d = decode(&[_]u8{ 0x0F, 0x84, 0x10, 0x00, 0x00, 0x00 });
|
||||
try std.testing.expectEqual(@as(u8, 6), d.len);
|
||||
try std.testing.expect(d.flags & F_RELATIVE != 0);
|
||||
}
|
||||
|
||||
test "nop" {
|
||||
const d = decode(&[_]u8{0x90});
|
||||
try std.testing.expectEqual(@as(u8, 1), d.len);
|
||||
}
|
||||
|
||||
test "ret" {
|
||||
const d = decode(&[_]u8{0xC3});
|
||||
try std.testing.expectEqual(@as(u8, 1), d.len);
|
||||
}
|
||||
|
||||
test "short jmp EB" {
|
||||
const d = decode(&[_]u8{ 0xEB, 0x05 });
|
||||
try std.testing.expectEqual(@as(u8, 2), d.len);
|
||||
try std.testing.expect(d.flags & F_RELATIVE != 0);
|
||||
try std.testing.expect(d.flags & F_IMM8 != 0);
|
||||
}
|
||||
|
||||
test "short jcc 74 (jz rel8)" {
|
||||
const d = decode(&[_]u8{ 0x74, 0x0A });
|
||||
try std.testing.expectEqual(@as(u8, 2), d.len);
|
||||
try std.testing.expect(d.flags & F_RELATIVE != 0);
|
||||
}
|
||||
|
||||
test "mov eax, imm32" {
|
||||
const d = decode(&[_]u8{ 0xB8, 0x44, 0x33, 0x22, 0x11 });
|
||||
try std.testing.expectEqual(@as(u8, 5), d.len);
|
||||
}
|
||||
|
||||
test "push imm32" {
|
||||
const d = decode(&[_]u8{ 0x68, 0x44, 0x33, 0x22, 0x11 });
|
||||
try std.testing.expectEqual(@as(u8, 5), d.len);
|
||||
}
|
||||
|
||||
test "push imm8" {
|
||||
// 6A 01
|
||||
const d = decode(&[_]u8{ 0x6A, 0x01 });
|
||||
try std.testing.expectEqual(@as(u8, 2), d.len);
|
||||
}
|
||||
|
||||
test "mov [ebp-4], eax" {
|
||||
// 89 45 FC
|
||||
const d = decode(&[_]u8{ 0x89, 0x45, 0xFC });
|
||||
try std.testing.expectEqual(@as(u8, 3), d.len);
|
||||
try std.testing.expect(d.flags & F_MODRM != 0);
|
||||
try std.testing.expect(d.flags & F_DISP8 != 0);
|
||||
}
|
||||
|
||||
test "lea eax, [ecx+edx*4+8]" {
|
||||
// 8D 44 91 08
|
||||
const d = decode(&[_]u8{ 0x8D, 0x44, 0x91, 0x08 });
|
||||
try std.testing.expectEqual(@as(u8, 4), d.len);
|
||||
try std.testing.expect(d.flags & F_MODRM != 0);
|
||||
try std.testing.expect(d.flags & F_SIB != 0);
|
||||
try std.testing.expect(d.flags & F_DISP8 != 0);
|
||||
}
|
||||
|
||||
test "mov [disp32], eax" {
|
||||
// A3 xx xx xx xx
|
||||
const d = decode(&[_]u8{ 0xA3, 0x00, 0x10, 0x40, 0x00 });
|
||||
try std.testing.expectEqual(@as(u8, 5), d.len);
|
||||
}
|
||||
|
||||
test "sub esp, imm32" {
|
||||
// 81 EC 00 01 00 00
|
||||
const d = decode(&[_]u8{ 0x81, 0xEC, 0x00, 0x01, 0x00, 0x00 });
|
||||
try std.testing.expectEqual(@as(u8, 6), d.len);
|
||||
try std.testing.expect(d.flags & F_MODRM != 0);
|
||||
}
|
||||
|
||||
test "test eax, imm32 (F7 C0)" {
|
||||
// F7 C0 FF 00 00 00 = test eax, 0xFF
|
||||
const d = decode(&[_]u8{ 0xF7, 0xC0, 0xFF, 0x00, 0x00, 0x00 });
|
||||
try std.testing.expectEqual(@as(u8, 6), d.len);
|
||||
}
|
||||
|
||||
test "ret imm16" {
|
||||
// C2 04 00
|
||||
const d = decode(&[_]u8{ 0xC2, 0x04, 0x00 });
|
||||
try std.testing.expectEqual(@as(u8, 3), d.len);
|
||||
}
|
||||
@@ -343,6 +343,120 @@ but its `relativeTo` field is NULL (no target frame set).
|
||||
`[0, 3, 6]`, used by the width layout pass. `SetFrameHitTestMode` iterates these
|
||||
indices to look up anchors from the frame's anchor array.
|
||||
|
||||
---
|
||||
|
||||
## Third Crash: GetWidth/GetHeight Stack Parameter Mismatch
|
||||
|
||||
### Problem
|
||||
|
||||
After hooking vtable[1] (GetWidth) and vtable[2] (GetHeight), the game crashed
|
||||
with stack corruption. The hooks were defined with signature `fn(this: u32) f32`
|
||||
but the actual functions take an extra stack parameter.
|
||||
|
||||
### Root Cause
|
||||
|
||||
`SetFrameHitTestMode` (0x7671a0) calls `vtable[1]` with `PUSH EAX` before the
|
||||
call — passing 1 stack argument. Since GetWidth/GetHeight are `__thiscall`, the
|
||||
callee must clean up this argument (RET 4). Our hooks with no stack parameter
|
||||
used RET 0, leaving 4 bytes on the stack after every call, causing misalignment
|
||||
and eventual crash.
|
||||
|
||||
### Fix
|
||||
|
||||
Changed hook signatures to include the extra parameter:
|
||||
```zig
|
||||
fn getWidthHook(this: u32, param: u32) callconv(THISCALL) f32
|
||||
fn getHeightHook(this: u32, param: u32) callconv(THISCALL) f32
|
||||
```
|
||||
|
||||
The `param` value comes from `frame+0x58` and is passed through to the original.
|
||||
|
||||
---
|
||||
|
||||
## Frame Name Discovery
|
||||
|
||||
### CFrame + 0x98 = Name String Pointer
|
||||
|
||||
Confirmed via Ghidra decompilation of three functions:
|
||||
|
||||
**GetName virtual** at vtable[1] (0x46ff70):
|
||||
```c
|
||||
char * GetName(CFrame *this) {
|
||||
return *(char **)(this + 0x98); // simply returns the name pointer
|
||||
}
|
||||
```
|
||||
|
||||
**SetFrameName** (0x76c650):
|
||||
```c
|
||||
void SetFrameName(CFrame *this, char *name) {
|
||||
// Frees old name at +0x98 if set
|
||||
// Allocates + copies new name to +0x98
|
||||
}
|
||||
```
|
||||
|
||||
**CleanupRegion** (0x76c560):
|
||||
```c
|
||||
void CleanupRegion(CFrame *this) {
|
||||
// Frees name string at +0x98
|
||||
*(this + 0x98) = NULL;
|
||||
// ... other cleanup
|
||||
}
|
||||
```
|
||||
|
||||
### CLayoutFrame Offset
|
||||
|
||||
CLayoutFrame is an inner object within CFrame, starting at CFrame + 0x24.
|
||||
So given a CLayoutFrame pointer (e.g., relativeTo from an anchor):
|
||||
- `CFrame base = CLayoutFrame_ptr - 0x24`
|
||||
- `Frame name = *(CFrame_base + 0x98)` = `*(CLayoutFrame_ptr - 0x24 + 0x98)`
|
||||
|
||||
---
|
||||
|
||||
## Destruction Path Analysis (Ghidra)
|
||||
|
||||
### cleanup_linked_list_structures (0x767720) — The Bottleneck
|
||||
|
||||
All frame destruction goes through this function. Exactly 3 direct callers:
|
||||
|
||||
| Caller | Address | Context |
|
||||
|--------|---------|---------|
|
||||
| `destroy_object` | 0x7676f0 | Direct frame destruction |
|
||||
| `CleanupRegion` | 0x76c560 | Region cleanup (called by WorldObjectBaseDestructor) |
|
||||
| `cleanupGraphicsResources` | 0x764390 | Graphics teardown |
|
||||
|
||||
### WorldObjectBaseDestructor (0x7693b0) — Common Base
|
||||
|
||||
All frame-type destructors eventually call `WorldObjectBaseDestructor`, which:
|
||||
1. Calls `cleanup_linked_list_structures(param_1 + 9)` — **our hook fires here**
|
||||
2. Calls `CleanupRegion` — also calls `cleanup_linked_list_structures`
|
||||
3. Calls `FrameScript_Destructor` (base class cleanup, list unlinking)
|
||||
|
||||
16 callers of WorldObjectBaseDestructor (all are frame-type destructors):
|
||||
`WorldObjectDestructor`, `ChatBubbleFrame_Destructor`, `cleanup_minimap_object`,
|
||||
`CleanupLineObjectManager`, `cleanup_object_manager`, `SetAnimationRotation`,
|
||||
`CleanupPlayerModel`, `DestroyButtonResources`, `DestroyEditBoxResources`,
|
||||
`statusBarCleanupResources`, `cleanupMessageFrameResources`, `cleanupScrollFrame`,
|
||||
`CleanupObjectManager`, `CleanupColorSelectFrame`, `CleanupMovieFrame`, `luaIsVisible`
|
||||
|
||||
### DestroyFrame (0x773240) — NOT a Destruction Path
|
||||
|
||||
Only called from `InitializeFrameProperties` (0x7731d0). This is a **re-initialization**
|
||||
path, not frame destruction. No need to hook.
|
||||
|
||||
### DestroyFrameScriptObject (0x4c34a0) — Vtable Entry, Already Covered
|
||||
|
||||
Referenced only as DATA at `0x806cb8` (vtable slot). `FrameScript_Destructor` (0x4c3690)
|
||||
sets the vtable to this and does list unlinking / FreeMemory. It runs **after**
|
||||
`WorldObjectBaseDestructor`, so `cleanup_linked_list_structures` has already been
|
||||
called by the time this executes. No need to hook.
|
||||
|
||||
### Conclusion
|
||||
|
||||
**Our single hook on `cleanup_linked_list_structures` covers all frame destruction paths.**
|
||||
The vtable hooks (GetWidth/GetHeight/GetRelativeTo) remain as defense-in-depth but
|
||||
are not expected to fire during normal operation — they would only catch bugs in
|
||||
our cleanup logic or unknown destruction paths.
|
||||
|
||||
### Anchor Vtable (0x0081C44C) — Full Layout
|
||||
```
|
||||
[0] +0x00 = 0x00767d80 → GetAnimationOrder (destructor)
|
||||
|
||||
+628
-53
@@ -8,11 +8,14 @@
|
||||
//! Root cause fix: detour hook on cleanup_linked_list_structures (0x767720),
|
||||
//! the common frame destruction path. Before the original runs, we walk the
|
||||
//! dying frame's PauseAnimationGroup dependency list (frame+0x34) to find all
|
||||
//! other frames whose anchors reference the dying frame. For each, we destroy
|
||||
//! the anchor and NULL the slot, preventing any stale/NULL pointer dereferences.
|
||||
//! other frames whose anchors reference the dying frame. For each, we NULL the
|
||||
//! relativeTo field, preventing any stale pointer dereferences.
|
||||
//!
|
||||
//! Defense-in-depth: vtable[3] (GetRelativeTo) hook validates returned pointers
|
||||
//! with IsBadReadPtr, catching any cases the root cause fix misses.
|
||||
//! Defense-in-depth: anchor vtable hooks on [1] GetWidth, [2] GetHeight, and
|
||||
//! [3] GetRelativeTo. Each independently validates anchor+0x0C (relativeTo)
|
||||
//! with IsBadReadPtr before use. GetWidth/GetHeight return the layout sentinel
|
||||
//! from [0x00cf550c] when relativeTo is invalid. Catches cases the root cause
|
||||
//! fix misses (frames destroyed through paths other than cleanup_linked_list).
|
||||
//!
|
||||
//! See RESEARCH.md for full reverse engineering notes.
|
||||
|
||||
@@ -24,6 +27,15 @@ const WINAPI = std.builtin.CallingConvention.winapi;
|
||||
const THISCALL = std.builtin.CallingConvention{ .x86_thiscall = .{} };
|
||||
|
||||
extern "kernel32" fn IsBadReadPtr(lp: ?*const anyopaque, ucb: usize) callconv(WINAPI) i32;
|
||||
extern "kernel32" fn CreateMutexA(lpMutexAttributes: ?*anyopaque, bInitialOwner: i32, lpName: [*:0]const u8) callconv(WINAPI) ?*anyopaque;
|
||||
extern "kernel32" fn ReleaseMutex(hMutex: *anyopaque) callconv(WINAPI) i32;
|
||||
extern "kernel32" fn CloseHandle(hObject: *anyopaque) callconv(WINAPI) i32;
|
||||
extern "kernel32" fn GetLastError() callconv(WINAPI) u32;
|
||||
extern "kernel32" fn GetCurrentProcessId() callconv(WINAPI) u32;
|
||||
const ERROR_ALREADY_EXISTS: u32 = 183;
|
||||
|
||||
var g_mutex: ?*anyopaque = null;
|
||||
var g_is_hook_owner: bool = false;
|
||||
|
||||
// =============================================================================
|
||||
// Anchor vtable layout (20-byte object allocated in SetPoint / SetAnimationOrder)
|
||||
@@ -66,15 +78,221 @@ const GET_RELATIVE_TO_SLOT: usize = ANCHOR_VTABLE_ADDR + 0x0C; // vtable[3]
|
||||
const CLEANUP_TARGET: usize = 0x767720;
|
||||
const CLEANUP_PROLOGUE_SIZE: usize = 5;
|
||||
|
||||
/// FreeMemory — __stdcall(ptr, source_string, flags)
|
||||
const FreeMemory = @as(*const fn (u32, u32, u32) callconv(WINAPI) void, @ptrFromInt(0x646430));
|
||||
const CLAYOUT_FRAME_STR: u32 = 0x878540; // "...CLayoutFrame..."
|
||||
|
||||
var cleanup_hook: hook.Hook = .{};
|
||||
|
||||
// =============================================================================
|
||||
// Second destruction path: destroyUIElement (0x7645a0)
|
||||
//
|
||||
// Called from cleanupGraphicsResources (UI teardown/reload) via the strata loop.
|
||||
// Frees frames WITHOUT calling cleanup_linked_list_structures — just unlinks from
|
||||
// lists, cleans up sub-regions, and calls FreeMemory. The dependency list at
|
||||
// frame+0x34 is never walked, so other frames' anchors are left dangling.
|
||||
//
|
||||
// Signature: void* __thiscall destroyUIElement(void* this, byte free_flag)
|
||||
// Prologue: 55 8B EC 56 8B F1 (6 bytes, no rel32)
|
||||
// =============================================================================
|
||||
|
||||
const DESTROY_UI_TARGET: usize = 0x7645a0;
|
||||
const DESTROY_UI_PROLOGUE_SIZE: usize = 6;
|
||||
|
||||
var destroy_ui_hook: hook.Hook = .{};
|
||||
|
||||
// =============================================================================
|
||||
// Third destruction path: ProcessUIUpdateEvent (0x772ec0)
|
||||
//
|
||||
// Virtual function (vtable entry at 0x81c7ac), called via vtable dispatch.
|
||||
// Calls CleanupUIElement + FreeMemory without cleanup_linked_list_structures.
|
||||
// Signature: void* __thiscall ProcessUIUpdateEvent(void* this, byte free_flag)
|
||||
// Prologue: 55 8B EC 56 8B F1 (6 bytes, no rel32)
|
||||
// =============================================================================
|
||||
|
||||
const PROCESS_UI_TARGET: usize = 0x772ec0;
|
||||
const PROCESS_UI_PROLOGUE_SIZE: usize = 6;
|
||||
|
||||
var process_ui_hook: hook.Hook = .{};
|
||||
|
||||
// =============================================================================
|
||||
// Priority 1: Hook PauseAnimationGroup (0x767ee0) — dependency registration
|
||||
//
|
||||
// Records every dependency registration so we can later determine whether a
|
||||
// stale relativeTo pointer was ever registered through PauseAnimationGroup.
|
||||
// If PauseAnimationGroup was never called for an address, the dependency was
|
||||
// never created — pointing to a race condition or unknown creation path.
|
||||
//
|
||||
// Signature: void __thiscall PauseAnimationGroup(ECX=relativeTo_frame, owner_frame, bitmask)
|
||||
// Prologue: 55 8B EC 53 8B D9 (6 bytes, no rel32)
|
||||
// RET 0x8 (callee cleans 2 stack args)
|
||||
// =============================================================================
|
||||
|
||||
const PAUSE_ANIM_TARGET: usize = 0x767ee0;
|
||||
const PAUSE_ANIM_PROLOGUE_SIZE: usize = 6;
|
||||
|
||||
var pause_anim_hook: hook.Hook = .{};
|
||||
|
||||
// =============================================================================
|
||||
// Priority 2: Hook SetAnimationOrder (0x767c70) — anchor creation validation
|
||||
//
|
||||
// Validates the relativeTo param with IsBadReadPtr BEFORE the original runs.
|
||||
// If relativeTo is already freed when the anchor is created, the dependency
|
||||
// list node goes on dead frame memory (which may be reused). Detects race
|
||||
// conditions at anchor creation time.
|
||||
//
|
||||
// Signature: void __thiscall SetAnimationOrder(ECX=frame, point_enum, relativeTo,
|
||||
// relPoint, xOfs, yOfs, param_6)
|
||||
// Prologue: 55 8B EC 8B 45 0C (6 bytes, no rel32)
|
||||
// RET 0x18 (callee cleans 6 stack args)
|
||||
// =============================================================================
|
||||
|
||||
const SET_ANIM_TARGET: usize = 0x767c70;
|
||||
const SET_ANIM_PROLOGUE_SIZE: usize = 6;
|
||||
|
||||
var set_anim_hook: hook.Hook = .{};
|
||||
|
||||
// =============================================================================
|
||||
// Dependency registration ring buffer — track PauseAnimationGroup calls
|
||||
//
|
||||
// When vtable hooks detect a stale pointer, we look up this buffer to answer:
|
||||
// "was PauseAnimationGroup ever called for this address?"
|
||||
// =============================================================================
|
||||
|
||||
const REG_HISTORY_SIZE = 2048;
|
||||
|
||||
const DepRegistration = struct {
|
||||
relativeTo: u32 = 0, // the frame being depended upon (ECX of PauseAnimationGroup)
|
||||
owner: u32 = 0, // the frame that owns the anchor
|
||||
bitmask: u32 = 0, // which anchor slots (OR of 1<<point_enum)
|
||||
seq: u32 = 0, // monotonic sequence number for ordering
|
||||
};
|
||||
|
||||
var reg_history: [REG_HISTORY_SIZE]DepRegistration = @splat(.{});
|
||||
var reg_idx: u32 = 0;
|
||||
|
||||
fn recordRegistration(relativeTo: u32, owner: u32, bitmask: u32) void {
|
||||
const seq = reg_idx;
|
||||
reg_history[reg_idx % REG_HISTORY_SIZE] = .{
|
||||
.relativeTo = relativeTo,
|
||||
.owner = owner,
|
||||
.bitmask = bitmask,
|
||||
.seq = seq,
|
||||
};
|
||||
reg_idx +%= 1;
|
||||
}
|
||||
|
||||
/// Look up whether PauseAnimationGroup was ever called for a given relativeTo address.
|
||||
/// Returns the most recent registration entry if found, null otherwise.
|
||||
fn lookupRegistration(relativeTo: u32) ?DepRegistration {
|
||||
// Search backwards from most recent for best chance of finding it
|
||||
var best: ?DepRegistration = null;
|
||||
for (®_history) |*entry| {
|
||||
if (entry.relativeTo == relativeTo) {
|
||||
if (best == null or entry.seq > best.?.seq) {
|
||||
best = entry.*;
|
||||
}
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/// Count all registrations for a given relativeTo address.
|
||||
fn countRegistrations(relativeTo: u32) u32 {
|
||||
var count: u32 = 0;
|
||||
for (®_history) |*entry| {
|
||||
if (entry.relativeTo == relativeTo) count += 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Destruction history ring buffer — correlate stale pointers with frame names
|
||||
// =============================================================================
|
||||
|
||||
const HISTORY_SIZE = 1024;
|
||||
|
||||
const DestroyedFrame = struct {
|
||||
addr: u32 = 0,
|
||||
name: [63:0]u8 = @splat(0),
|
||||
};
|
||||
|
||||
var destroy_history: [HISTORY_SIZE]DestroyedFrame = @splat(.{});
|
||||
var history_idx: u32 = 0;
|
||||
|
||||
fn recordDestruction(layout_frame: u32) void {
|
||||
var entry = DestroyedFrame{};
|
||||
entry.addr = layout_frame;
|
||||
|
||||
if (getFrameName(layout_frame)) |name| {
|
||||
const span = std.mem.span(name);
|
||||
const len = @min(span.len, 63);
|
||||
@memcpy(entry.name[0..len], span[0..len]);
|
||||
}
|
||||
|
||||
destroy_history[history_idx % HISTORY_SIZE] = entry;
|
||||
history_idx +%= 1;
|
||||
}
|
||||
|
||||
fn lookupDestroyed(layout_frame: u32) ?[]const u8 {
|
||||
for (&destroy_history) |*entry| {
|
||||
if (entry.addr == layout_frame) {
|
||||
const span = std.mem.sliceTo(&entry.name, 0);
|
||||
return if (span.len > 0) span else null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Format info about a stale relativeTo for vtable hook logging.
|
||||
/// Checks the ring buffer first; falls back to reading (possibly garbage) memory.
|
||||
fn fmtStaleInfo(relativeTo: u32) struct { name: []const u8, saw_destroy: bool } {
|
||||
if (lookupDestroyed(relativeTo)) |name| {
|
||||
return .{ .name = name, .saw_destroy = true };
|
||||
}
|
||||
return .{ .name = fmtFrameName(relativeTo), .saw_destroy = false };
|
||||
}
|
||||
|
||||
/// Log registration status for a stale relativeTo address.
|
||||
fn logRegistrationStatus(relativeTo: u32) void {
|
||||
const reg_count = countRegistrations(relativeTo);
|
||||
if (lookupRegistration(relativeTo)) |reg| {
|
||||
con.fmt("[framecrash] DEP REGISTERED: PauseAnimGroup was called {d}x for 0x{x:0>8}, last owner=0x{x:0>8} mask=0x{x}\n", .{
|
||||
reg_count, relativeTo, reg.owner, reg.bitmask,
|
||||
});
|
||||
} else {
|
||||
con.fmt("[framecrash] DEP NEVER REGISTERED: PauseAnimGroup was NEVER called for 0x{x:0>8} (in {d}-entry buffer)\n", .{
|
||||
relativeTo, REG_HISTORY_SIZE,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Dump diagnostic info for a stale relativeTo pointer not seen in our detour.
|
||||
fn dumpStaleContext(relativeTo: u32, anchor: u32) void {
|
||||
// Anchor relPoint enum at +0x10
|
||||
if (IsBadReadPtr(@ptrFromInt(anchor + 0x10), 4) != 0) return;
|
||||
const rel_point = readAligned(anchor + 0x10);
|
||||
|
||||
// Derive owner frame: anchor lives at owner_frame + relPoint*4 + 4
|
||||
const owner_layout = anchor -% (rel_point * 4 + 4);
|
||||
const owner_name = fmtFrameName(owner_layout);
|
||||
con.fmt("[framecrash] owner=\"{s}\" (0x{x:0>8}), relPoint={d}, stale=0x{x:0>8}\n", .{
|
||||
owner_name, owner_layout, rel_point, relativeTo,
|
||||
});
|
||||
}
|
||||
|
||||
/// Detour for cleanup_linked_list_structures. Runs before the original to
|
||||
/// walk the dying frame's dependency list and destroy referencing anchors.
|
||||
fn cleanupDetour(frame: u32) callconv(THISCALL) void {
|
||||
// Record this frame in the destruction history before anything changes
|
||||
recordDestruction(frame);
|
||||
|
||||
// Count reverse dependencies for logging
|
||||
const dep_count = countReverseDependencies(frame);
|
||||
if (dep_count > 0) {
|
||||
con.fmt("[framecrash] Destroying frame \"{s}\" (0x{x:0>8}), {d} reverse dependencies\n", .{
|
||||
fmtFrameName(frame),
|
||||
frame,
|
||||
dep_count,
|
||||
});
|
||||
}
|
||||
|
||||
cleanupReverseDependencies(frame);
|
||||
|
||||
// Call original cleanup_linked_list_structures via trampoline
|
||||
@@ -82,10 +300,146 @@ fn cleanupDetour(frame: u32) callconv(THISCALL) void {
|
||||
orig(frame);
|
||||
}
|
||||
|
||||
/// Walk the PauseAnimationGroup dependency list on the dying frame and destroy
|
||||
/// all anchors from other frames that reference it.
|
||||
/// Detour for destroyUIElement. This is the second frame destruction path,
|
||||
/// called from cleanupGraphicsResources during UI teardown/reload. The original
|
||||
/// frees frames without walking the dependency list, leaving stale anchors.
|
||||
/// Signature: void* __thiscall destroyUIElement(void* this, byte free_flag)
|
||||
fn destroyUIDetour(frame: u32, free_flag: u32) callconv(THISCALL) u32 {
|
||||
// Record both possible interpretations: frame as CLayoutFrame inner,
|
||||
// and frame+0x24 in case frame is actually a CFrame base.
|
||||
// Anchors store CLayoutFrame inner ptrs as relativeTo.
|
||||
recordDestruction(frame);
|
||||
if (IsBadReadPtr(@ptrFromInt(frame + 0x24), 4) == 0) {
|
||||
recordDestruction(frame + 0x24);
|
||||
}
|
||||
|
||||
// Try cleaning deps at both offsets. cleanupReverseDependencies is
|
||||
// guarded by IsBadReadPtr so the wrong offset safely no-ops.
|
||||
const dep_count_a = countReverseDependencies(frame);
|
||||
const dep_count_b = countReverseDependencies(frame + 0x24);
|
||||
|
||||
if (dep_count_a > 0) {
|
||||
con.fmt("[framecrash] destroyUIElement frame \"{s}\" (0x{x:0>8}), {d} reverse deps (layout)\n", .{
|
||||
fmtFrameName(frame), frame, dep_count_a,
|
||||
});
|
||||
cleanupReverseDependencies(frame);
|
||||
}
|
||||
if (dep_count_b > 0) {
|
||||
con.fmt("[framecrash] destroyUIElement frame \"{s}\" (0x{x:0>8}), {d} reverse deps (inner+0x24)\n", .{
|
||||
fmtFrameName(frame + 0x24), frame + 0x24, dep_count_b,
|
||||
});
|
||||
cleanupReverseDependencies(frame + 0x24);
|
||||
}
|
||||
|
||||
// Call original destroyUIElement via trampoline
|
||||
const orig: *const fn (u32, u32) callconv(THISCALL) u32 = @ptrFromInt(destroy_ui_hook.trampoline);
|
||||
return orig(frame, free_flag);
|
||||
}
|
||||
|
||||
/// Detour for ProcessUIUpdateEvent — third destruction path, called via vtable.
|
||||
fn processUIDetour(frame: u32, free_flag: u32) callconv(THISCALL) u32 {
|
||||
recordDestruction(frame);
|
||||
if (IsBadReadPtr(@ptrFromInt(frame + 0x24), 4) == 0) {
|
||||
recordDestruction(frame + 0x24);
|
||||
}
|
||||
|
||||
const dep_count_a = countReverseDependencies(frame);
|
||||
const dep_count_b = countReverseDependencies(frame + 0x24);
|
||||
|
||||
if (dep_count_a > 0) {
|
||||
con.fmt("[framecrash] processUI frame \"{s}\" (0x{x:0>8}), {d} reverse deps (layout)\n", .{
|
||||
fmtFrameName(frame), frame, dep_count_a,
|
||||
});
|
||||
cleanupReverseDependencies(frame);
|
||||
}
|
||||
if (dep_count_b > 0) {
|
||||
con.fmt("[framecrash] processUI frame \"{s}\" (0x{x:0>8}), {d} reverse deps (inner+0x24)\n", .{
|
||||
fmtFrameName(frame + 0x24), frame + 0x24, dep_count_b,
|
||||
});
|
||||
cleanupReverseDependencies(frame + 0x24);
|
||||
}
|
||||
|
||||
const orig: *const fn (u32, u32) callconv(THISCALL) u32 = @ptrFromInt(process_ui_hook.trampoline);
|
||||
return orig(frame, free_flag);
|
||||
}
|
||||
|
||||
/// Detour for PauseAnimationGroup — records every dependency registration.
|
||||
/// This tells us whether a stale relativeTo was ever registered through the
|
||||
/// normal dependency tracking system.
|
||||
/// Signature: void __thiscall PauseAnimationGroup(ECX=relativeTo_frame, owner_frame, bitmask)
|
||||
fn pauseAnimDetour(relativeTo_frame: u32, owner_frame: u32, bitmask: u32) callconv(THISCALL) void {
|
||||
// Record this registration
|
||||
recordRegistration(relativeTo_frame, owner_frame, bitmask);
|
||||
|
||||
con.fmt("[framecrash] PauseAnimGroup: relativeTo=0x{x:0>8} \"{s}\", owner=0x{x:0>8} \"{s}\", mask=0x{x}\n", .{
|
||||
relativeTo_frame,
|
||||
fmtFrameName(relativeTo_frame),
|
||||
owner_frame,
|
||||
fmtFrameName(owner_frame),
|
||||
bitmask,
|
||||
});
|
||||
|
||||
// Call original
|
||||
const orig = pause_anim_hook.getTrampoline(*const fn (u32, u32, u32) callconv(THISCALL) void);
|
||||
orig(relativeTo_frame, owner_frame, bitmask);
|
||||
}
|
||||
|
||||
/// Detour for SetAnimationOrder — validates relativeTo param before anchor creation.
|
||||
/// Uses cdecl thunk bridge because the function has float params.
|
||||
/// cdecl args: (ecx=frame, edx=unused, point_enum, relativeTo, relPoint, xOfs_bits, yOfs_bits, param_6)
|
||||
fn setAnimOrderDetour(frame: u32, _edx: u32, point_enum: u32, relativeTo: u32, rel_point: u32, x_ofs: u32, y_ofs: u32, param_6: u32) callconv(.c) void {
|
||||
_ = _edx;
|
||||
|
||||
// Validate relativeTo BEFORE the original creates the anchor
|
||||
if (relativeTo != 0) {
|
||||
if (IsBadReadPtr(@ptrFromInt(relativeTo), 0x10) != 0) {
|
||||
con.fmt("[framecrash] RACE: SetAnimOrder creating anchor with INVALID relativeTo=0x{x:0>8}! frame=0x{x:0>8} \"{s}\", point={d}\n", .{
|
||||
relativeTo,
|
||||
frame,
|
||||
fmtFrameName(frame),
|
||||
point_enum,
|
||||
});
|
||||
} else if (relativeTo == frame) {
|
||||
// Self-reference — the original function rejects this, but log it
|
||||
con.fmt("[framecrash] SetAnimOrder: self-reference rejected, frame=0x{x:0>8}\n", .{frame});
|
||||
}
|
||||
}
|
||||
|
||||
// Call original trampoline as __thiscall(ECX=frame, 6 stack args).
|
||||
// All args are u32 — float params (xOfs, yOfs) are passed as raw bit patterns
|
||||
// which the original function reads from the stack as floats. The bit layout
|
||||
// is identical because __thiscall pushes all non-this args onto the stack.
|
||||
const orig: *const fn (u32, u32, u32, u32, u32, u32, u32) callconv(THISCALL) void =
|
||||
@ptrFromInt(set_anim_hook.trampoline);
|
||||
orig(frame, point_enum, relativeTo, rel_point, x_ofs, y_ofs, param_6);
|
||||
}
|
||||
|
||||
/// Count how many nodes are in the PauseAnimationGroup dependency list.
|
||||
fn countReverseDependencies(dying_frame: u32) u32 {
|
||||
if (IsBadReadPtr(@ptrFromInt(dying_frame + 0x34), 4) != 0) return 0;
|
||||
|
||||
var node: u32 = readAligned(dying_frame + 0x34);
|
||||
if (node == 0 or (node & 1) != 0) return 0;
|
||||
|
||||
var count: u32 = 0;
|
||||
while (node != 0 and (node & 1) == 0) {
|
||||
if (IsBadReadPtr(@ptrFromInt(node), 0x10) != 0) break;
|
||||
count += 1;
|
||||
node = readAligned(node + 0x04);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/// Walk the PauseAnimationGroup dependency list on the dying frame and NULL out
|
||||
/// the relativeTo field in any anchors from other frames that reference it.
|
||||
///
|
||||
/// Safety: purely defensive — does NOT call destructors, free nodes, or modify
|
||||
/// the dying frame's list pointers. The original cleanup_linked_list_structures
|
||||
/// handles its own data structures.
|
||||
fn cleanupReverseDependencies(dying_frame: u32) void {
|
||||
// Read first node from dying_frame+0x34
|
||||
// Validate dying_frame+0x34 is readable before dereferencing
|
||||
if (IsBadReadPtr(@ptrFromInt(dying_frame + 0x34), 4) != 0) return;
|
||||
|
||||
var node: u32 = readAligned(dying_frame + 0x34);
|
||||
|
||||
// Validate: odd pointer or zero means empty list
|
||||
@@ -94,13 +448,14 @@ fn cleanupReverseDependencies(dying_frame: u32) void {
|
||||
var cleaned: u32 = 0;
|
||||
|
||||
while (node != 0 and (node & 1) == 0) {
|
||||
// Save next pointer before we potentially free this node
|
||||
// Validate node is readable (need 0x10 bytes: link0, next, owner_frame, bitmask)
|
||||
if (IsBadReadPtr(@ptrFromInt(node), 0x10) != 0) break;
|
||||
|
||||
const next: u32 = readAligned(node + 0x04);
|
||||
const owner_frame: u32 = readAligned(node + 0x08);
|
||||
const bitmask: u32 = readAligned(node + 0x0C);
|
||||
|
||||
if (owner_frame != 0) {
|
||||
// For each bit set in bitmask, destroy the corresponding anchor
|
||||
if (owner_frame != 0 and IsBadReadPtr(@ptrFromInt(owner_frame), 0x28) == 0) {
|
||||
var bit: u5 = 0;
|
||||
while (bit < 9) : (bit += 1) {
|
||||
if ((bitmask & (@as(u32, 1) << bit)) == 0) continue;
|
||||
@@ -109,70 +464,123 @@ fn cleanupReverseDependencies(dying_frame: u32) void {
|
||||
const anchor: u32 = readAligned(anchor_slot_addr);
|
||||
if (anchor == 0) continue;
|
||||
|
||||
// Validate anchor is readable (need vtable + xOfs + yOfs + relativeTo = 0x10)
|
||||
if (IsBadReadPtr(@ptrFromInt(anchor), 0x10) != 0) continue;
|
||||
|
||||
// Verify this anchor actually references the dying frame
|
||||
const relativeTo: u32 = readAligned(anchor + 0x0C);
|
||||
if (relativeTo != dying_frame) continue;
|
||||
|
||||
// Call anchor destructor: vtable[0](1) — thiscall with flag=1 (free)
|
||||
// Verify vtable matches the known anchor vtable — reject garbage objects
|
||||
const vtable: u32 = readAligned(anchor);
|
||||
const dtor_addr: u32 = readAligned(vtable);
|
||||
const dtor: *const fn (u32, u32) callconv(THISCALL) void = @ptrFromInt(dtor_addr);
|
||||
dtor(anchor, 1);
|
||||
if (vtable != ANCHOR_VTABLE_ADDR) continue;
|
||||
|
||||
// NULL the anchor slot in the owner frame
|
||||
const slot: *align(1) u32 = @ptrFromInt(anchor_slot_addr);
|
||||
slot.* = 0;
|
||||
// NULL the relativeTo pointer so it can't dangle.
|
||||
// Don't call destructors or free the anchor — that risks cascading
|
||||
// side effects and is unnecessary. A NULL relativeTo is handled
|
||||
// gracefully by all code paths (luaGetPoint, GetWidth, GetHeight).
|
||||
const field: *align(1) u32 = @ptrFromInt(anchor + 0x0C);
|
||||
field.* = 0;
|
||||
|
||||
cleaned += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Free the dependency list node
|
||||
FreeMemory(node, CLAYOUT_FRAME_STR, 0xfffffffe);
|
||||
|
||||
node = next;
|
||||
}
|
||||
|
||||
if (cleaned > 0) {
|
||||
con.fmt("[framecrash] Cleaned {d} stale anchor(s) referencing dying frame 0x{x:0>8}\n", .{ cleaned, dying_frame });
|
||||
con.fmt("[framecrash] Nulled {d} stale relativeTo ptr(s) referencing dying frame 0x{x:0>8}\n", .{ cleaned, dying_frame });
|
||||
}
|
||||
|
||||
// Clear the list head so the original cleanup doesn't see stale nodes
|
||||
const head: *align(1) u32 = @ptrFromInt(dying_frame + 0x30);
|
||||
head.* = 0;
|
||||
const tail: *align(1) u32 = @ptrFromInt(dying_frame + 0x34);
|
||||
tail.* = 0;
|
||||
}
|
||||
|
||||
fn readAligned(addr: u32) u32 {
|
||||
return @as(*align(1) const u32, @ptrFromInt(addr)).*;
|
||||
}
|
||||
|
||||
/// Given a CLayoutFrame inner pointer, derive the CFrame base (subtract 0x24)
|
||||
/// and read the name string at CFrame+0x98. Returns null if any pointer is
|
||||
/// invalid or the name field is NULL.
|
||||
fn getFrameName(layout_frame: u32) ?[*:0]const u8 {
|
||||
if (layout_frame < 0x24) return null;
|
||||
const frame_base = layout_frame -% 0x24;
|
||||
|
||||
// Validate that frame_base+0x98 (name pointer field) is readable
|
||||
if (IsBadReadPtr(@ptrFromInt(frame_base + 0x98), 4) != 0) return null;
|
||||
|
||||
const name_ptr = readAligned(frame_base + 0x98);
|
||||
if (name_ptr == 0) return null;
|
||||
|
||||
// Validate the name string itself is readable (at least 1 byte)
|
||||
if (IsBadReadPtr(@ptrFromInt(name_ptr), 1) != 0) return null;
|
||||
|
||||
return @ptrFromInt(name_ptr);
|
||||
}
|
||||
|
||||
/// Format a frame name for logging — returns "FrameName" or "(unnamed)".
|
||||
fn fmtFrameName(layout_frame: u32) []const u8 {
|
||||
if (getFrameName(layout_frame)) |name| {
|
||||
return std.mem.span(name);
|
||||
}
|
||||
return "(unnamed)";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Defense-in-depth: GetRelativeTo vtable hook
|
||||
// Defense-in-depth: anchor vtable hooks
|
||||
//
|
||||
// Three vtable slots must be hooked because they independently read anchor+0x0C
|
||||
// (relativeTo) without going through each other:
|
||||
// [1] GetWidth — reads [this+0xC]+0x3C, crashes if relativeTo is NULL/dangling
|
||||
// [2] GetHeight — same pattern as GetWidth
|
||||
// [3] GetRelativeTo — returns *(this+0x0C), caller dereferences it
|
||||
//
|
||||
// GetRelativeTo NULLs the pointer on detection (self-heal). GetWidth/GetHeight
|
||||
// must independently handle both NULL and dangling relativeTo by returning the
|
||||
// sentinel value from [0x00cf550c] — the value SetFrameHitTestMode compares
|
||||
// against to detect "no dimension available".
|
||||
// =============================================================================
|
||||
|
||||
const GET_WIDTH_SLOT: usize = ANCHOR_VTABLE_ADDR + 0x04; // vtable[1]
|
||||
const GET_HEIGHT_SLOT: usize = ANCHOR_VTABLE_ADDR + 0x08; // vtable[2]
|
||||
|
||||
/// Sentinel float that SetFrameHitTestMode compares GetWidth/GetHeight results
|
||||
/// against. Stored at runtime in .bss at 0x00cf550c.
|
||||
const SENTINEL_ADDR: usize = 0x00cf550c;
|
||||
|
||||
var orig_get_width: usize = 0;
|
||||
var orig_get_height: usize = 0;
|
||||
var orig_get_relative_to: usize = 0;
|
||||
|
||||
/// Replacement for the anchor's GetRelativeTo virtual function.
|
||||
/// Validates the stored relativeTo pointer before returning it.
|
||||
/// If the pointer is stale (freed/decommitted memory), NULLs it out
|
||||
/// and returns 0 so the caller takes the safe "no relativeTo" code path.
|
||||
/// Check if a relativeTo pointer (CLayoutFrame inner ptr) is valid.
|
||||
/// Returns true if the pointer is non-NULL and the frame base region is readable.
|
||||
fn isRelativeToValid(relativeTo: u32) bool {
|
||||
if (relativeTo == 0) return false;
|
||||
// relativeTo is an inner offset; callers subtract 0x24 for the frame base.
|
||||
// Validate that the frame base region (vtable + lua ref + index) is readable.
|
||||
return IsBadReadPtr(@ptrFromInt(relativeTo -% 0x24), 0x10) == 0;
|
||||
}
|
||||
|
||||
/// Hook for vtable[3] GetRelativeTo. Validates the stored pointer.
|
||||
/// If stale, NULLs anchor+0x0C and returns 0 (safe "no relativeTo" path).
|
||||
fn getRelativeToHook(this: u32) callconv(THISCALL) u32 {
|
||||
// Call the original GetRelativeTo to get the stored pointer
|
||||
const orig: *const fn (u32) callconv(THISCALL) u32 = @ptrFromInt(orig_get_relative_to);
|
||||
const result = orig(this);
|
||||
|
||||
if (result == 0) return 0;
|
||||
|
||||
// Validate: the returned pointer is an inner offset into the frame object.
|
||||
// The caller (luaGetPoint) subtracts 0x24 to get the frame base, then reads
|
||||
// at +0x00 (vtable), +0x04 (lua ref), +0x08 (lua ref index).
|
||||
// Check that the frame base region is readable.
|
||||
if (IsBadReadPtr(@ptrFromInt(result -% 0x24), 0x10) != 0) {
|
||||
con.fmt("[framecrash] Stale relativeTo ptr 0x{x:0>8} in anchor 0x{x:0>8} — nulled\n", .{ result, this });
|
||||
|
||||
// Self-heal: clear the dangling pointer in the anchor object
|
||||
if (!isRelativeToValid(result)) {
|
||||
const info = fmtStaleInfo(result);
|
||||
if (info.saw_destroy) {
|
||||
con.fmt("[framecrash] STALE: frame \"{s}\" (0x{x:0>8}) went through detour but dep list missed anchor 0x{x:0>8}, detected in GetRelativeTo\n", .{
|
||||
info.name, result, this,
|
||||
});
|
||||
} else {
|
||||
con.fmt("[framecrash] STALE: frame 0x{x:0>8} NOT seen in detour, anchor 0x{x:0>8}, detected in GetRelativeTo\n", .{
|
||||
result, this,
|
||||
});
|
||||
dumpStaleContext(result, this);
|
||||
}
|
||||
logRegistrationStatus(result);
|
||||
const field: *align(1) u32 = @ptrFromInt(this + 0x0C);
|
||||
field.* = 0;
|
||||
return 0;
|
||||
@@ -181,22 +589,189 @@ fn getRelativeToHook(this: u32) callconv(THISCALL) u32 {
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Hook for vtable[1] GetWidth. Checks anchor+0x0C before calling original.
|
||||
/// Returns sentinel if relativeTo is NULL or dangling.
|
||||
/// Signature: f32 __thiscall GetWidth(this, u32 param) — callee cleans 1 stack arg.
|
||||
fn getWidthHook(this: u32, param: u32) callconv(THISCALL) f32 {
|
||||
const relativeTo: u32 = readAligned(this + 0x0C);
|
||||
if (!isRelativeToValid(relativeTo)) {
|
||||
// Self-heal if dangling (not just NULL)
|
||||
if (relativeTo != 0) {
|
||||
const info = fmtStaleInfo(relativeTo);
|
||||
if (info.saw_destroy) {
|
||||
con.fmt("[framecrash] STALE: frame \"{s}\" (0x{x:0>8}) went through detour but dep list missed anchor 0x{x:0>8}, detected in GetWidth\n", .{
|
||||
info.name, relativeTo, this,
|
||||
});
|
||||
} else {
|
||||
con.fmt("[framecrash] STALE: frame 0x{x:0>8} NOT seen in detour, anchor 0x{x:0>8}, detected in GetWidth\n", .{
|
||||
relativeTo, this,
|
||||
});
|
||||
dumpStaleContext(relativeTo, this);
|
||||
}
|
||||
logRegistrationStatus(relativeTo);
|
||||
const field: *align(1) u32 = @ptrFromInt(this + 0x0C);
|
||||
field.* = 0;
|
||||
}
|
||||
return @as(*align(1) const f32, @ptrFromInt(SENTINEL_ADDR)).*;
|
||||
}
|
||||
|
||||
const orig: *const fn (u32, u32) callconv(THISCALL) f32 = @ptrFromInt(orig_get_width);
|
||||
return orig(this, param);
|
||||
}
|
||||
|
||||
/// Hook for vtable[2] GetHeight. Same pattern as GetWidth.
|
||||
/// Signature: f32 __thiscall GetHeight(this, u32 param) — callee cleans 1 stack arg.
|
||||
fn getHeightHook(this: u32, param: u32) callconv(THISCALL) f32 {
|
||||
const relativeTo: u32 = readAligned(this + 0x0C);
|
||||
if (!isRelativeToValid(relativeTo)) {
|
||||
if (relativeTo != 0) {
|
||||
const info = fmtStaleInfo(relativeTo);
|
||||
if (info.saw_destroy) {
|
||||
con.fmt("[framecrash] STALE: frame \"{s}\" (0x{x:0>8}) went through detour but dep list missed anchor 0x{x:0>8}, detected in GetHeight\n", .{
|
||||
info.name, relativeTo, this,
|
||||
});
|
||||
} else {
|
||||
con.fmt("[framecrash] STALE: frame 0x{x:0>8} NOT seen in detour, anchor 0x{x:0>8}, detected in GetHeight\n", .{
|
||||
relativeTo, this,
|
||||
});
|
||||
dumpStaleContext(relativeTo, this);
|
||||
}
|
||||
logRegistrationStatus(relativeTo);
|
||||
const field: *align(1) u32 = @ptrFromInt(this + 0x0C);
|
||||
field.* = 0;
|
||||
}
|
||||
return @as(*align(1) const f32, @ptrFromInt(SENTINEL_ADDR)).*;
|
||||
}
|
||||
|
||||
const orig: *const fn (u32, u32) callconv(THISCALL) f32 = @ptrFromInt(orig_get_height);
|
||||
return orig(this, param);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Module API
|
||||
// =============================================================================
|
||||
|
||||
fn patchVtableSlot(slot_addr: usize, new_fn: usize, save_to: *usize) void {
|
||||
save_to.* = hook.readMem(u32, slot_addr);
|
||||
const new_val: u32 = @intCast(new_fn);
|
||||
hook.writeProtected(slot_addr, std.mem.asBytes(&new_val));
|
||||
}
|
||||
|
||||
fn restoreVtableSlot(slot_addr: usize, saved: *usize) void {
|
||||
if (saved.* != 0) {
|
||||
const orig: u32 = @intCast(saved.*);
|
||||
hook.writeProtected(slot_addr, std.mem.asBytes(&orig));
|
||||
saved.* = 0;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn installHooks() void {
|
||||
// Root cause fix: detour cleanup_linked_list_structures to clean up
|
||||
// Multi-DLL safety: only one instance per process should hook
|
||||
var mutex_name_buf: [64]u8 = undefined;
|
||||
const mutex_name = std.fmt.bufPrint(&mutex_name_buf, "Local\\FramecrashHook_{d}", .{GetCurrentProcessId()}) catch return;
|
||||
mutex_name_buf[mutex_name.len] = 0;
|
||||
|
||||
g_mutex = CreateMutexA(null, 1, @ptrCast(mutex_name_buf[0..mutex_name.len :0]));
|
||||
if (g_mutex == null) return;
|
||||
|
||||
if (GetLastError() == ERROR_ALREADY_EXISTS) {
|
||||
_ = CloseHandle(g_mutex.?);
|
||||
g_mutex = null;
|
||||
g_is_hook_owner = false;
|
||||
con.print("[framecrash] Another DLL owns hooks (mutex taken), skipping\n");
|
||||
return;
|
||||
}
|
||||
g_is_hook_owner = true;
|
||||
|
||||
// Root cause fix #1: detour cleanup_linked_list_structures to clean up
|
||||
// reverse anchor references before the frame is destroyed.
|
||||
// Prologue: 53 56 8B F1 57 (5 bytes, no rel32 fixups needed)
|
||||
// if (!cleanup_hook.install(CLEANUP_TARGET, CLEANUP_PROLOGUE_SIZE, @intFromPtr(&cleanupDetour), &.{})) {
|
||||
// con.print("[framecrash] ERROR: Failed to install frame cleanup detour\n");
|
||||
// } else {
|
||||
// con.print("[framecrash] Frame cleanup detour installed\n");
|
||||
// }
|
||||
if (!cleanup_hook.install(CLEANUP_TARGET, CLEANUP_PROLOGUE_SIZE, @intFromPtr(&cleanupDetour), &.{})) {
|
||||
con.print("[framecrash] ERROR: Failed to install frame cleanup detour\n");
|
||||
} else {
|
||||
con.print("[framecrash] Frame cleanup detour installed\n");
|
||||
}
|
||||
|
||||
// Root cause fix #2: detour destroyUIElement — the second destruction path
|
||||
// used by cleanupGraphicsResources during UI teardown/reload. This path
|
||||
// frees frames without walking the dependency list.
|
||||
// Prologue: 55 8B EC 56 8B F1 (6 bytes, no rel32 fixups needed)
|
||||
if (!destroy_ui_hook.install(DESTROY_UI_TARGET, DESTROY_UI_PROLOGUE_SIZE, @intFromPtr(&destroyUIDetour), &.{})) {
|
||||
con.print("[framecrash] ERROR: Failed to install destroyUIElement detour\n");
|
||||
} else {
|
||||
con.print("[framecrash] destroyUIElement detour installed\n");
|
||||
}
|
||||
|
||||
// Root cause fix #3: detour ProcessUIUpdateEvent — virtual function that
|
||||
// calls CleanupUIElement + FreeMemory without layout cleanup.
|
||||
// Prologue: 55 8B EC 56 8B F1 (6 bytes, no rel32 fixups needed)
|
||||
if (!process_ui_hook.install(PROCESS_UI_TARGET, PROCESS_UI_PROLOGUE_SIZE, @intFromPtr(&processUIDetour), &.{})) {
|
||||
con.print("[framecrash] ERROR: Failed to install ProcessUIUpdateEvent detour\n");
|
||||
} else {
|
||||
con.print("[framecrash] ProcessUIUpdateEvent detour installed\n");
|
||||
}
|
||||
|
||||
// Defense-in-depth: patch anchor vtable[1]/[2]/[3] to validate relativeTo
|
||||
// before use. Catches dangling pointers from any destruction path.
|
||||
patchVtableSlot(GET_WIDTH_SLOT, @intFromPtr(&getWidthHook), &orig_get_width);
|
||||
patchVtableSlot(GET_HEIGHT_SLOT, @intFromPtr(&getHeightHook), &orig_get_height);
|
||||
patchVtableSlot(GET_RELATIVE_TO_SLOT, @intFromPtr(&getRelativeToHook), &orig_get_relative_to);
|
||||
con.print("[framecrash] Anchor vtable hooks installed (GetWidth/GetHeight/GetRelativeTo)\n");
|
||||
|
||||
// Diagnostic: hook PauseAnimationGroup to track dependency registrations.
|
||||
// Answers: "was a dependency ever registered for this stale address?"
|
||||
// Prologue: 55 8B EC 53 8B D9 (6 bytes, no rel32)
|
||||
if (!pause_anim_hook.install(PAUSE_ANIM_TARGET, PAUSE_ANIM_PROLOGUE_SIZE, @intFromPtr(&pauseAnimDetour), &.{})) {
|
||||
con.print("[framecrash] ERROR: Failed to install PauseAnimationGroup detour\n");
|
||||
} else {
|
||||
con.print("[framecrash] PauseAnimationGroup detour installed\n");
|
||||
}
|
||||
|
||||
// Diagnostic: hook SetAnimationOrder to detect race conditions.
|
||||
// Validates relativeTo param BEFORE anchor creation.
|
||||
// Prologue: 55 8B EC 8B 45 0C (6 bytes, no rel32)
|
||||
// Uses fastcall-to-cdecl thunk because of float stack params.
|
||||
if (set_anim_hook.prepare(SET_ANIM_TARGET, SET_ANIM_PROLOGUE_SIZE, &.{})) {
|
||||
const thunk = set_anim_hook.mem.? + 32;
|
||||
_ = hook.buildFastcallToCdeclThunk(thunk, @intFromPtr(&setAnimOrderDetour), 6);
|
||||
set_anim_hook.activate(@intFromPtr(thunk));
|
||||
con.print("[framecrash] SetAnimationOrder detour installed\n");
|
||||
} else {
|
||||
con.print("[framecrash] ERROR: Failed to install SetAnimationOrder detour\n");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn removeHooks() void {
|
||||
cleanup_hook.remove();
|
||||
con.print("[framecrash] Frame cleanup detour removed\n");
|
||||
if (g_is_hook_owner) {
|
||||
// Remove diagnostic hooks first (reverse install order)
|
||||
set_anim_hook.remove();
|
||||
con.print("[framecrash] SetAnimationOrder detour removed\n");
|
||||
|
||||
pause_anim_hook.remove();
|
||||
con.print("[framecrash] PauseAnimationGroup detour removed\n");
|
||||
|
||||
// Restore original vtable pointers (reverse order)
|
||||
restoreVtableSlot(GET_RELATIVE_TO_SLOT, &orig_get_relative_to);
|
||||
restoreVtableSlot(GET_HEIGHT_SLOT, &orig_get_height);
|
||||
restoreVtableSlot(GET_WIDTH_SLOT, &orig_get_width);
|
||||
con.print("[framecrash] Anchor vtable hooks removed\n");
|
||||
|
||||
process_ui_hook.remove();
|
||||
con.print("[framecrash] ProcessUIUpdateEvent detour removed\n");
|
||||
|
||||
destroy_ui_hook.remove();
|
||||
con.print("[framecrash] destroyUIElement detour removed\n");
|
||||
|
||||
cleanup_hook.remove();
|
||||
con.print("[framecrash] Frame cleanup detour removed\n");
|
||||
}
|
||||
|
||||
if (g_is_hook_owner) {
|
||||
if (g_mutex) |m| {
|
||||
_ = ReleaseMutex(m);
|
||||
_ = CloseHandle(m);
|
||||
g_mutex = null;
|
||||
}
|
||||
}
|
||||
g_is_hook_owner = false;
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user