From 72627d41231626c1646dd2474a6d768f6762d564 Mon Sep 17 00:00:00 2001 From: MarcelineVQ Date: Fri, 10 Apr 2026 15:30:56 -0700 Subject: [PATCH] luagc: complete 5.1 incremental GC integration -- string/upval resurrection Critical fix: hook luaS_newlstr (0x6F9D00) for dead string resurrection during incremental sweep. When the string intern table returns an existing string with otherwhite (dead but not yet swept), flip its white bits to resurrect it. Without this, code gets references to strings that sweep will free, causing hash chain crashes. Matches 5.1 lstring.c:88. Also: - Upvalue resurrection: hook luaF_findupval (0x6F9F10), same pattern - Upvalue birth mark: patch 0x6F9F4A so new open upvalues get currentwhite - Volatile reads/writes on all memory accessors (foreign memory via @ptrFromInt) - Fix upvalue traversal: dereference v pointer (+0x08) not inline value (+0x10) - Fix upvalue birth mark address (0x6F9F4A not 0x6F9F48 -- immediate is last byte) - Correct hook target: luaS_newlstr does intern lookup, not lua_create_string_object CHUNK_SIZE=5000 (true incremental). Multi-step mark cycles confirmed working. --- src/luagc/lua_offsets.zig | 17 ++++++++ src/luagc/luagc.zig | 82 ++++++++++++++++++++++++++++++++++----- 2 files changed, 89 insertions(+), 10 deletions(-) diff --git a/src/luagc/lua_offsets.zig b/src/luagc/lua_offsets.zig index 054b748..6dd84a1 100644 --- a/src/luagc/lua_offsets.zig +++ b/src/luagc/lua_offsets.zig @@ -48,6 +48,23 @@ pub const lua_setfenv: usize = 0x6F40D0; /// OP_SETUPVAL patch point: right after TValue copy, EDX=uv->v. pub const vm_setupval_patch: usize = 0x6F8A97; +/// luaS_newlstr: __fastcall(ECX=L, EDX=str_ptr, stack=len). Returns TString*. +/// Does the intern lookup (may return existing dead string) then calls +/// lua_create_string_object (0x6F9D90) for new strings. +/// Must hook for dead string resurrection during sweep. +pub const luaS_newlstr: usize = 0x6F9D00; + +/// lua_create_string_object (inner, creates NEW strings only): 0x6F9D90. +pub const lua_create_string_object: usize = 0x6F9D90; + +/// lua_create_open_upvalue (luaF_findupval): __fastcall(ECX=L, EDX=level). +/// Returns UpVal*. Must hook for dead upvalue resurrection during sweep. +pub const lua_create_open_upvalue: usize = 0x6F9F10; + +/// Birth mark byte in lua_create_open_upvalue: `MOV byte [EAX+5], 0x01` +/// Encoding: C6 40 05 01 at 0x6F9F47. The immediate 0x01 is at +3 = 0x6F9F4A. +pub const birth_mark_upvalue: usize = 0x6F9F4A; + /// lua_setgcthreshold: __fastcall(ECX=L, EDX=newthreshold). /// Writes (newthreshold << 10) to g->gcthreshold, then calls luaC_checkGC. /// WoW calls this on screen transitions with threshold=256 (=262144 bytes). diff --git a/src/luagc/luagc.zig b/src/luagc/luagc.zig index 7e01e95..aa8cf1a 100644 --- a/src/luagc/luagc.zig +++ b/src/luagc/luagc.zig @@ -82,9 +82,8 @@ var phase: Phase = .idle; var saved_L: u32 = 0; var saved_g: u32 = 0; -/// Objects to process per step. High value = atomic (working). Lower for -/// incremental once the multi-step crash is resolved. -const CHUNK_SIZE: u32 = 1000000; +/// Objects to process per step. ~5000 = ~2ms target per step. +const CHUNK_SIZE: u32 = 5000; // ============================================================================= // Gray Stack @@ -167,9 +166,13 @@ var needs_bootstrap: bool = true; /// Update the string creation birth mark byte to match currentwhite. /// Strings bypass luaC_link and have their own `MOV byte [EBX+5], 0x00` /// at 0x6F9DC1. We patch this immediate to currentwhite. -fn syncStringBirthMark() void { +/// Sync birth mark patches for objects that bypass luaC_link. +/// Strings: lua_create_string_object writes marked at 0x6F9DC1. +/// Upvalues: lua_create_open_upvalue writes marked at 0x6F9F48. +fn syncBirthMarks() void { var patch = [1]u8{currentwhite}; hook.writeProtected(offsets.birth_mark_string, &patch); + hook.writeProtected(offsets.birth_mark_upvalue, &patch); } fn bootstrapWhite(g: u32) void { @@ -854,7 +857,7 @@ fn atomicPhase(L: u32, g: u32) void { // (8) Flip current white. New objects born after this point get the // NEW currentwhite. Sweep will free objects with the OLD white. currentwhite = otherwhite(); - syncStringBirthMark(); // update string creation to use new white + syncBirthMarks(); // update string creation to use new white weak_table_count = 0; grayagain_count = 0; @@ -1414,7 +1417,7 @@ fn luaCloseDetour(L: u32) callconv(hook.cc.fastcall) void { in_gc = false; currentwhite = 0x01; needs_bootstrap = true; - syncStringBirthMark(); + syncBirthMarks(); lua_close_hook.callOriginal(.{L}); } @@ -1564,6 +1567,53 @@ fn resolveStackIndex(L: u32, idx: u32) u32 { } } +// ============================================================================= +// String resurrection (5.1 lstring.c:88) +// ============================================================================= +// +// During incremental sweep, the string intern table can return a dead string +// (one with otherwhite, not yet swept). Lua 5.1 handles this in luaS_newlstr: +// if (isdead(G(L), o)) changewhite(o); /* resurrect */ +// WoW's Lua 5.0 has no such check. We hook lua_create_string_object and +// resurrect dead strings after the native returns them. + +const StringCreateFn = fn (u32, u32, u32) callconv(hook.cc.fastcall) u32; +var string_create_hook: hook.Detour(StringCreateFn) = .{}; + +fn stringCreateDetour(L: u32, str_ptr: u32, len: u32) callconv(hook.cc.fastcall) u32 { + const result = string_create_hook.callOriginal(.{ L, str_ptr, len }); + // If the returned string is dead (has otherwhite), resurrect it + if (result != 0) { + const marked = readU8(result + offsets.OBJ_marked); + if ((marked & otherwhite() & offsets.WHITEBITS) != 0) { + // changewhite: flip white bits (dead white → alive white) + writeU8(result + offsets.OBJ_marked, marked ^ offsets.WHITEBITS); + } + } + return result; +} + +// ============================================================================= +// Upvalue resurrection (5.1 lfunc.c:61-62) +// ============================================================================= +// +// Same pattern as strings: luaF_findupval can return a dead open upvalue +// during incremental sweep. Resurrect it. + +const UpvalCreateFn = fn (u32, u32) callconv(hook.cc.fastcall) u32; +var upval_create_hook: hook.Detour(UpvalCreateFn) = .{}; + +fn upvalCreateDetour(L: u32, level: u32) callconv(hook.cc.fastcall) u32 { + const result = upval_create_hook.callOriginal(.{ L, level }); + if (result != 0) { + const marked = readU8(result + offsets.OBJ_marked); + if ((marked & otherwhite() & offsets.WHITEBITS) != 0) { + writeU8(result + offsets.OBJ_marked, marked ^ offsets.WHITEBITS); + } + } + return result; +} + // ============================================================================= // OP_SETUPVAL VM patch (forward barrier) // ============================================================================= @@ -1717,6 +1767,14 @@ pub fn installHooks() void { installed += 1; log.print("hooked luaC_link\n"); } + // Resurrection hooks (5.1 lstring.c:88, lfunc.c:61-62) + if (string_create_hook.attach(offsets.luaS_newlstr, &stringCreateDetour) == .ok) { + installed += 1; + } + if (upval_create_hook.attach(offsets.lua_create_open_upvalue, &upvalCreateDetour) == .ok) { + installed += 1; + } + // Forward barriers (5.1 luaC_barrierf equivalents) if (setmeta_hook.attach(offsets.lua_setmetatable, &setmetaDetour) == .ok) { installed += 1; @@ -1731,7 +1789,7 @@ pub fn installHooks() void { installSetupvalPatch(); log.print("installed forward barriers\n"); - syncStringBirthMark(); + syncBirthMarks(); var buf: [64]u8 = undefined; log.print(fmt(&buf, "installed {d} hooks\n", .{installed})); @@ -1741,9 +1799,11 @@ pub fn removeHooks() void { if (!g_is_hook_owner) return; // If mid-cycle, restore birth mark - // Reset string birth mark to 0 (native default) - var patch = [1]u8{0x00}; - hook.writeProtected(offsets.birth_mark_string, &patch); + // Reset birth marks to native defaults + var patch0 = [1]u8{0x00}; + hook.writeProtected(offsets.birth_mark_string, &patch0); + var patch1 = [1]u8{0x01}; + hook.writeProtected(offsets.birth_mark_upvalue, &patch1); collect_hook.detach(); table_set_hook.detach(); @@ -1751,6 +1811,8 @@ pub fn removeHooks() void { setgcthreshold_hook.detach(); lua_close_hook.detach(); luac_link_hook.detach(); + string_create_hook.detach(); + upval_create_hook.detach(); setmeta_hook.detach(); setupval_hook.detach(); setfenv_hook.detach();