luagc: complete Lua 5.1 incremental GC -- no crashes
Full 5.1 incremental GC port with CHUNK_SIZE=5000. All crash scenarios
resolved: enter world, logout/login, /reload, /gcstress.
Compiler barriers (verified from Ghidra):
- Hook lua_parser_create_constant (0x6FD400) and
lua_parser_finish_function (0x6FCB00)
- ECX = LexState in all callers (not FuncState)
- Proto via: LexState+0x30 -> FuncState, FuncState+0x00 -> Proto
- Barrier-back: if proto is BLACK during mark, gray + grayagain
String/upvalue resurrection:
- Hook luaS_newlstr (0x6F9D00) -- resurrect dead strings from intern
- Hook luaF_findupval (0x6F9F10) -- resurrect dead open upvalues
- Birth mark patches for strings (0x6F9DC1) and upvalues (0x6F9F4A)
Other:
- Volatile reads/writes for all foreign memory access
- Fix upvalue traversal: dereference v pointer not inline value
- Remove bootstrap (luaC_link hook covers all objects except mainthread)
- Mainthread makewhite in markroot
- Remove 5.1 removeentry from traverseTable (5.0 tables don't expect TNONE)
- Clear stale weak flags when metatable is NULL
- fullgc abandon when lua_setgcthreshold forces GC mid-cycle
Remaining: /reload causes addon errors ("attempt to call a number value").
Not a crash -- investigation needed for the specific collection difference.
This commit is contained in:
@@ -48,6 +48,13 @@ pub const lua_setfenv: usize = 0x6F40D0;
|
||||
/// OP_SETUPVAL patch point: right after TValue copy, EDX=uv->v.
|
||||
pub const vm_setupval_patch: usize = 0x6F8A97;
|
||||
|
||||
/// Compiler functions that add children to protos (need forward barriers).
|
||||
/// lua_parser_create_constant: __fastcall(ECX=FuncState, EDX=TValue*, stack=type).
|
||||
/// FuncState+0x30 = proto pointer. Adds constant to proto->k.
|
||||
pub const lua_parser_create_constant: usize = 0x6FD400;
|
||||
/// lua_parser_finish_function: __fastcall(ECX=LexState). Finalizes proto arrays.
|
||||
pub const lua_parser_finish_function: usize = 0x6FCB00;
|
||||
|
||||
/// 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.
|
||||
|
||||
+93
-27
@@ -176,16 +176,18 @@ fn syncBirthMarks() void {
|
||||
}
|
||||
|
||||
fn bootstrapWhite(g: u32) void {
|
||||
// Walk rootgc and set all objects to currentwhite
|
||||
// Walk rootgc and set currentwhite, preserving non-color flags.
|
||||
// Uses makewhite pattern: (marked & MASKMARKS) | currentwhite
|
||||
// This preserves KEYWEAK, VALUEWEAK, FINALIZED, FIXED bits.
|
||||
var obj = readU32(g + offsets.GS_rootgc);
|
||||
while (obj != 0) {
|
||||
writeU8(obj + offsets.OBJ_marked, currentwhite);
|
||||
writeU8(obj + offsets.OBJ_marked, (readU8(obj + offsets.OBJ_marked) & offsets.MASKMARKS) | currentwhite);
|
||||
obj = readU32(obj + offsets.OBJ_next);
|
||||
}
|
||||
// Walk rootudata
|
||||
obj = readU32(g + offsets.GS_rootudata);
|
||||
while (obj != 0) {
|
||||
writeU8(obj + offsets.OBJ_marked, currentwhite);
|
||||
writeU8(obj + offsets.OBJ_marked, (readU8(obj + offsets.OBJ_marked) & offsets.MASKMARKS) | currentwhite);
|
||||
obj = readU32(obj + offsets.OBJ_next);
|
||||
}
|
||||
// Walk string hash table
|
||||
@@ -497,9 +499,18 @@ fn detectAndSetWeakFlags(table: u32, mt: u32) void {
|
||||
/// Matches Lua 5.1 traversetable (lgc.c:158-196).
|
||||
fn traverseTable(table: u32) bool {
|
||||
const mt = readU32(table + offsets.TABLE_metatable);
|
||||
if (mt != 0) _ = markObject(mt);
|
||||
|
||||
if (mt != 0) detectAndSetWeakFlags(table, mt);
|
||||
if (mt != 0) {
|
||||
_ = markObject(mt);
|
||||
detectAndSetWeakFlags(table, mt);
|
||||
} else {
|
||||
// No metatable: clear any stale weak flags from previous cycles.
|
||||
// Without this, a table that lost its metatable (setmetatable(t, nil))
|
||||
// would keep old KEYWEAK/VALUEWEAK bits and be treated as weak.
|
||||
const m = readU8(table + offsets.OBJ_marked);
|
||||
if ((m & (offsets.KEYWEAK | offsets.VALUEWEAK)) != 0) {
|
||||
writeU8(table + offsets.OBJ_marked, m & ~(offsets.KEYWEAK | offsets.VALUEWEAK));
|
||||
}
|
||||
}
|
||||
|
||||
const marked = readU8(table + offsets.OBJ_marked);
|
||||
const weakkey = (marked & offsets.KEYWEAK) != 0;
|
||||
@@ -542,14 +553,7 @@ fn traverseTable(table: u32) bool {
|
||||
while (ni < sizenode) : (ni += 1) {
|
||||
const node = node_ptr + ni * offsets.NODE_size;
|
||||
const val_tt = readU8(node + offsets.NODE_value_tt);
|
||||
if (val_tt == 0) {
|
||||
// 5.1: remove dead entries during traversal (removeentry)
|
||||
const key_tt = readU8(node + offsets.NODE_key_tt);
|
||||
if (key_tt >= offsets.LUA_TSTRING) {
|
||||
writeU8(node + offsets.NODE_key_tt, offsets.LUA_TNONE);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (val_tt == 0) continue; // dead node, skip (5.0 leaves key as-is)
|
||||
|
||||
if (!weakkey) {
|
||||
const key_tt = readU8(node + offsets.NODE_key_tt);
|
||||
@@ -735,10 +739,6 @@ fn markroot(g: u32) void {
|
||||
grayagain_count = 0;
|
||||
weak_table_count = 0;
|
||||
|
||||
// Bootstrap: on first cycle, existing objects from native GC have marked=0.
|
||||
// Set them to currentwhite so our mark can find them.
|
||||
if (needs_bootstrap) bootstrapWhite(g);
|
||||
|
||||
// defaultmeta
|
||||
if (readU8(g + 0x40) >= offsets.LUA_TTABLE) {
|
||||
const dm = readU32(g + offsets.GS_defaultmeta);
|
||||
@@ -751,8 +751,10 @@ fn markroot(g: u32) void {
|
||||
if (reg != 0) _ = markObject(reg);
|
||||
}
|
||||
|
||||
// mainthread
|
||||
// mainthread -- not created via luaC_link (lua_open assigns directly).
|
||||
// Ensure it has currentwhite so markObject can find it.
|
||||
const mt = readU32(g + offsets.GS_mainthread);
|
||||
if (!isWhite(mt)) makewhite(mt);
|
||||
_ = markObject(mt);
|
||||
|
||||
// Make global table be traversed before main stack (5.1 line 508)
|
||||
@@ -762,8 +764,9 @@ fn markroot(g: u32) void {
|
||||
if (gt_gc != 0) _ = markObject(gt_gc);
|
||||
}
|
||||
|
||||
// current L if different
|
||||
// current L if different (also may bypass luaC_link)
|
||||
if (saved_L != 0 and saved_L != mt) {
|
||||
if (!isWhite(saved_L)) makewhite(saved_L);
|
||||
_ = markObject(saved_L);
|
||||
}
|
||||
}
|
||||
@@ -1380,19 +1383,24 @@ const SetGcThresholdFn = fn (u32, u32) callconv(hook.cc.fastcall) void;
|
||||
var setgcthreshold_hook: hook.Detour(SetGcThresholdFn) = .{};
|
||||
|
||||
fn setgcthresholdDetour(L: u32, newthreshold: u32) callconv(hook.cc.fastcall) void {
|
||||
// The caller wants to set gcthreshold = newthreshold << 10.
|
||||
// If it's a low value (forcing GC), just run our GC cycle.
|
||||
// The original also calls luaC_checkGC after setting, which would
|
||||
// trigger our collectGarbageDetour anyway. Skip the threshold write
|
||||
// entirely -- our GC owns the threshold.
|
||||
_ = newthreshold;
|
||||
if (readU32(L + offsets.L_active_check) == 0) return;
|
||||
|
||||
// 5.1 luaC_fullgc pattern: if a forced GC arrives mid-cycle,
|
||||
// abandon the current cycle and start fresh. Without this,
|
||||
// we'd continue processing stale gray stack entries from a
|
||||
// partially-traversed state after /reload swaps in new objects.
|
||||
if (phase != .idle) {
|
||||
phase = .idle;
|
||||
gray_count = 0;
|
||||
grayagain_count = 0;
|
||||
weak_table_count = 0;
|
||||
}
|
||||
|
||||
const g = getGlobalState(L);
|
||||
const tb = readU32(g + offsets.GS_totalbytes);
|
||||
const thr = readU32(g + offsets.GS_gcthreshold);
|
||||
// If threshold is already above totalbytes, no need to force GC
|
||||
if (thr > tb) return;
|
||||
// Otherwise trigger a GC by calling our detour path
|
||||
collectGarbageDetour(L);
|
||||
}
|
||||
|
||||
@@ -1614,6 +1622,55 @@ fn upvalCreateDetour(L: u32, level: u32) callconv(hook.cc.fastcall) u32 {
|
||||
return result;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Compiler barriers (5.1 lcode.c:244, lparser.c:151,199,319)
|
||||
// =============================================================================
|
||||
//
|
||||
// The Lua compiler adds constants, nested protos, locvars, and upvalue names
|
||||
// to proto objects during compilation. If GC runs mid-compilation (multi-step
|
||||
// mark), a proto can be BLACK when the compiler adds new WHITE children.
|
||||
// 5.1 has luaC_barrier/luaC_objbarrier after each such write.
|
||||
// We hook the compiler functions and apply barrier-back on the proto.
|
||||
|
||||
const CreateConstantFn = fn (u32, u32, u32) callconv(hook.cc.fastcall) void;
|
||||
var create_constant_hook: hook.Detour(CreateConstantFn) = .{};
|
||||
|
||||
fn createConstantDetour(lexstate: u32, value: u32, typ: u32) callconv(hook.cc.fastcall) void {
|
||||
create_constant_hook.callOriginal(.{ lexstate, value, typ });
|
||||
// All callers pass LexState in ECX (verified from Ghidra: parse_string_field,
|
||||
// parseFunctionCall, parse_primary_expression all pass the same struct).
|
||||
// LexState+0x30 = FuncState, FuncState+0x00 = Proto.
|
||||
if (phase == .marking and lexstate != 0) {
|
||||
const fs = readU32(lexstate + 0x30);
|
||||
if (fs != 0) {
|
||||
const proto = readU32(fs);
|
||||
if (proto != 0 and isBlack(proto)) {
|
||||
black2gray(proto);
|
||||
grayagainAdd(proto);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const FinishFunctionFn = fn (u32) callconv(hook.cc.fastcall) void;
|
||||
var finish_function_hook: hook.Detour(FinishFunctionFn) = .{};
|
||||
|
||||
fn finishFunctionDetour(lexstate: u32) callconv(hook.cc.fastcall) void {
|
||||
// Barrier BEFORE native: proto arrays are about to be resized.
|
||||
// LexState+0x30 = FuncState, FuncState+0x00 = Proto (verified Ghidra).
|
||||
if (phase == .marking and lexstate != 0) {
|
||||
const fs = readU32(lexstate + 0x30);
|
||||
if (fs != 0) {
|
||||
const proto = readU32(fs);
|
||||
if (proto != 0 and isBlack(proto)) {
|
||||
black2gray(proto);
|
||||
grayagainAdd(proto);
|
||||
}
|
||||
}
|
||||
}
|
||||
finish_function_hook.callOriginal(.{lexstate});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// OP_SETUPVAL VM patch (forward barrier)
|
||||
// =============================================================================
|
||||
@@ -1786,6 +1843,13 @@ pub fn installHooks() void {
|
||||
installed += 1;
|
||||
}
|
||||
// Note: lua_pushcclosure doesn't need a barrier (new closures are WHITE)
|
||||
// Compiler barriers (5.1 lcode.c, lparser.c)
|
||||
if (create_constant_hook.attach(offsets.lua_parser_create_constant, &createConstantDetour) == .ok) {
|
||||
installed += 1;
|
||||
}
|
||||
if (finish_function_hook.attach(offsets.lua_parser_finish_function, &finishFunctionDetour) == .ok) {
|
||||
installed += 1;
|
||||
}
|
||||
installSetupvalPatch();
|
||||
log.print("installed forward barriers\n");
|
||||
|
||||
@@ -1816,6 +1880,8 @@ pub fn removeHooks() void {
|
||||
setmeta_hook.detach();
|
||||
setupval_hook.detach();
|
||||
setfenv_hook.detach();
|
||||
create_constant_hook.detach();
|
||||
finish_function_hook.detach();
|
||||
removeSetupvalPatch();
|
||||
luaalloc.remove();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user