Fix BLP texture cleanup crash: use original CleanupFileHandleResources for fake contexts

The manual callCleanupFileContext + freeGameBuffer path crashed inside SMemFree
(EIP=0x23, heap metadata corruption). The original CleanupFileHandleResources
handles fake contexts correctly (NULL-safe on handle fields), so always delegate
to the trampoline instead.

Also adds ECX to callCleanupFileContext clobber list, adds M2 file handle cleanup
before processLoadedModelData (matching onModelLoadComplete ordering), and adds
debug logging throughout the async model load path.
This commit is contained in:
MarcelineVQ
2026-02-27 22:23:08 -08:00
parent a8ebe21b0d
commit 303dc34407
+58 -18
View File
@@ -485,7 +485,7 @@ fn callCleanupFileContext(ctx: [*]u8) void {
:
: [_] "{ecx}" (@intFromPtr(ctx)),
[func] "r" (@as(u32, 0x6472d0)),
: .{ .eax = true, .edx = true, .memory = true, .cc = true }
: .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true }
);
}
@@ -537,7 +537,7 @@ fn openFileDetour(
}
handle_out.* = @intFromPtr(ctx);
con.fmt("[file] fake ctx: {s} ({d} bytes)\n", .{ path_span, entry.data.len });
con.fmt("[file] fake ctx @0x{x}: {s} ({d} bytes)\n", .{ @intFromPtr(ctx), path_span, entry.data.len });
return 2; // success (non-zero type code)
}
@@ -555,7 +555,9 @@ fn getFileSizeDetour(
) callconv(sc) u32 {
if (isFakeFileContext(file_ctx)) {
if (high_size_out) |h| h.* = 0;
return hook.readMem(u32, file_ctx + 0x34);
const size = hook.readMem(u32, file_ctx + 0x34);
con.fmt("[file] getFileSize fake @0x{x} = {d}\n", .{ file_ctx, size });
return size;
}
const orig = get_file_size_hook.getTrampoline(
@@ -579,6 +581,8 @@ fn readFileDetour(
const data_size = hook.readMem(u32, ctx + 0x34);
const read_size = @min(size, data_size);
con.fmt("[file] readFile fake @0x{x} size={d}/{d} async=0x{x}\n", .{ ctx, read_size, data_size, async_ptr });
const src: [*]const u8 = @ptrFromInt(data_ptr);
@memcpy(buffer[0..read_size], src[0..read_size]);
@@ -657,22 +661,25 @@ fn processAsyncDetour(param1: u32, _edx: u32) callconv(.c) void {
// --- Hook 6: CleanupFileHandleResources (0x648730) ---
fn cleanupFileHandleDetour(file_ctx: u32) callconv(sc) void {
if (isFakeFileContext(file_ctx)) {
const ctx: [*]u8 = @ptrFromInt(file_ctx);
// cleanupFileContext frees +0x0C, +0x10, +0x18, +0x1C and destroys critical section.
// Do NOT manually free +0x0C here — that would be a double-free.
callCleanupFileContext(ctx);
// Free the 0x60-byte context block
freeGameBuffer(ctx);
return;
const fake = isFakeFileContext(file_ctx);
if (fake) {
const path_ptr = hook.readMem(u32, file_ctx + 0x0C);
if (path_ptr != 0) {
const path: [*:0]const u8 = @ptrFromInt(path_ptr);
con.fmt("[file] cleanup FAKE @0x{x}: {s}\n", .{ file_ctx, std.mem.span(path) });
} else {
con.fmt("[file] cleanup FAKE @0x{x}: (no path)\n", .{file_ctx});
}
}
// Always use original CleanupFileHandleResources — it handles fake contexts correctly
// (NULL-safe checks on +0x04/+0x3C/+0x40/+0x08, then cleanupFileContext + FreeMemory).
const orig = cleanup_file_handle_hook.getTrampoline(
*const fn (u32) callconv(sc) void,
);
orig(file_ctx);
if (fake) con.fmt("[file] cleanup FAKE @0x{x} done\n", .{file_ctx});
}
// --- Hook 5: loadModelFromFileAsync (0x71d4e0) ---
@@ -729,16 +736,49 @@ fn loadModelAsyncDetour(model: u32, _edx: u32, file_handle: u32, should_use_call
@as(*align(1) u32, @ptrFromInt(model + 0x0c)).* = 0;
con.print("[file] task=0 set\n");
// Skip cleanup for now (leaks 0x60 bytes) — investigate separately
// cleanupFileHandleDetour(file_handle);
con.print("[file] skipping cleanup (leak ok)\n");
// Match onModelLoadComplete ordering: clean up file handle BEFORE processing.
// The original async flow does: CleanupFileHandleResources → ReturnAsyncTaskToPool
// → model+0x0c=0 → processLoadedModelData. We must close the file context before
// processLoadedModelData runs, because initializeModelResources creates texture
// async tasks that interact with the file I/O system.
// Call original CleanupFileHandleResources through the trampoline (bypasses our
// detour, avoids the callconv crash from Issue 1). Original is __stdcall(1).
con.fmt("[file] cleanup via trampoline fh=0x{x}\n", .{file_handle});
asm volatile (
\\push %[fh]
\\call *%[func]
:
: [fh] "r" (file_handle),
[func] "r" (cleanup_file_handle_hook.trampoline),
: .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true }
);
con.print("[file] cleanup done\n");
// Dump model fields before processLoadedModelData
con.fmt("[file] PRE model+0x0c=0x{x} +0x130=0x{x} +0x134=0x{x} +0x138=0x{x}\n", .{
hook.readMem(u32, model + 0x0c),
hook.readMem(u32, model + 0x130),
hook.readMem(u32, model + 0x134),
hook.readMem(u32, model + 0x138),
});
// Call processLoadedModelData directly — __fastcall(ECX=model)
con.fmt("[file] calling processLoadedModelData(0x{x})...\n", .{model});
const result = hook.fastcall(u32, 0x71d640, model, 0);
con.fmt("[file] processLoadedModelData returned 0x{x}\n", .{result});
con.print("[file] processLoadedModelData returned\n");
con.fmt("[file] result=0x{x}\n", .{result});
con.fmt("[file] loadModelAsync: sync loaded {d} bytes\n", .{data_size});
// Dump model fields after processLoadedModelData — check if texture async task was created
con.print("[file] POST dump:\n");
con.fmt("[file] POST model+0x0c=0x{x} +0x130=0x{x} +0x134=0x{x} +0x138=0x{x}\n", .{
hook.readMem(u32, model + 0x0c),
hook.readMem(u32, model + 0x130),
hook.readMem(u32, model + 0x134),
hook.readMem(u32, model + 0x138),
});
con.fmt("[file] sync loaded {d} bytes, returning 1\n", .{data_size});
con.print("[file] === loadModelAsyncDetour EXIT ===\n");
return 1;
}