You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
bug(lua-pool): numeric-keyed globals escape pool baseline reset on all runtimes; pine-java snapshotKeys also uses coercion isstring() (#175 family) #177
Follow-up from the #175 review (PR #176, finding 175-run1-M2). Two related facts, one per layer:
Cross-runtime: numeric-keyed globals written by a script (_G[42] = ...) escape the Lua pool's baseline reset on all three runtimes. Every implementation snapshots and clears string keys only, so a numeric-keyed global set during one borrow is still visible to the next borrow of the same pooled state — a hole in the pool state-isolation contract.
pine-go / gopher-lua (pool_gopher_lua.go — snapshotGlobals/resetToBaseline iterate glua.LString keys only; numeric keys are skipped in both passes):
after cleanup, _G[42] numeric slot: numeric-slot <- leaked
after cleanup, leaked_str: nil
pine-go / wangshu (v0.2.0): documented as a deliberate limitation — MarkGlobalsBaseline godoc: 「限定:仅快照字符串 key——stdlib 与宿主自己的全局都是字符串 key,数字/表/函数等 key 跳过(非典型,实际场景不存在)」. ResetGlobalsToBaseline likewise only deletes/restores string keys.
pine-cpp (src/lua/lua_bridge.cpp:29 and :54): both LuaSnapshot's constructor and reset_to_baseline gate on lua_type(L, -2) == LUA_TSTRING; numeric keys are skipped in both the baseline set and the removal scan. Same leak by source reading (not separately executed).
Impact
Narrow. Normal operator paths only ever create string-identifier globals (function names from lua_script, input fields via set_global(field, ...)), so the only way to hit this is a script explicitly writing _G[<number>] = .... Consequences when hit:
state leaks across borrows of the same pooled VM (violates the isolation contract that baseline reset otherwise enforces — script hijack/leak of string globals is correctly wiped);
leaked values are per-state, so behavior depends on which pooled state a request happens to borrow — a nondeterminism source for diff-fuzz if a generator ever emits numeric-global writes (none do today).
Options
A (align + hygiene, cheapest): adopt wangshu's stance as the documented cross-runtime contract ("baseline reset covers string-keyed globals only; numeric/table/function keys are out of contract"), record it in llmdoc/reference/operator-contract.md next to the existing Lua bridge conventions, and fix the pine-java predicate to k.type() == LuaValue.TSTRING so the bookkeeping stops collecting phantom keys (mirrors the bug(pine-java): TransformByLua.fromLua corrupts numeric strings — isnumber() coercion turns 19-digit ID strings into precision-lost longs, diverging from Go #175 fix pattern; behavior-neutral today but removes the coercion trap from the remaining call site).
B (close the hole everywhere): extend snapshot/reset on all three runtimes to track non-string keys (needs a key-encoding scheme or a parallel non-string key set; wangshu would need an upstream API change since its baseline is string-only). Higher cost, questionable value given the trigger surface.
Option A seems right unless someone has a real script pattern that writes numeric globals.
Summary
Follow-up from the #175 review (PR #176, finding 175-run1-M2). Two related facts, one per layer:
_G[42] = ...) escape the Lua pool's baseline reset on all three runtimes. Every implementation snapshots and clears string keys only, so a numeric-keyed global set during one borrow is still visible to the next borrow of the same pooled state — a hole in the pool state-isolation contract.TransformByLua.LuaPool.snapshotKeys(pine-java/src/main/java/page/liam/pine/operators/TransformByLua.java:429) filters keys with the coercion predicatek.isstring()— the same luaj trap family as bug(pine-java): TransformByLua.fromLua corrupts numeric strings — isnumber() coercion turns 19-digit ID strings into precision-lost longs, diverging from Go #175 (LuaInteger.isstring()is always true). A numeric key42is collected as the phantom string"42", and cleanup then callsg.set("42", NIL)which clears the string slot while the numeric slot_G[42]survives. The bookkeeping touches the wrong slot; the net observable behavior happens to match Go/C++ (leak), but the predicate is wrong for the same reason bug(pine-java): TransformByLua.fromLua corrupts numeric strings — isnumber() coercion turns 19-digit ID strings into precision-lost longs, diverging from Go #175 was.Per-runtime evidence
pine-java (standalone repro, luaj-jse 3.0.1):
Output:
pine-go / gopher-lua (
pool_gopher_lua.go—snapshotGlobals/resetToBaselineiterateglua.LStringkeys only; numeric keys are skipped in both passes):pine-go / wangshu (v0.2.0): documented as a deliberate limitation —
MarkGlobalsBaselinegodoc: 「限定:仅快照字符串 key——stdlib 与宿主自己的全局都是字符串 key,数字/表/函数等 key 跳过(非典型,实际场景不存在)」.ResetGlobalsToBaselinelikewise only deletes/restores string keys.pine-cpp (
src/lua/lua_bridge.cpp:29and:54): bothLuaSnapshot's constructor andreset_to_baselinegate onlua_type(L, -2) == LUA_TSTRING; numeric keys are skipped in both the baseline set and the removal scan. Same leak by source reading (not separately executed).Impact
Narrow. Normal operator paths only ever create string-identifier globals (function names from
lua_script, input fields viaset_global(field, ...)), so the only way to hit this is a script explicitly writing_G[<number>] = .... Consequences when hit:Options
llmdoc/reference/operator-contract.mdnext to the existing Lua bridge conventions, and fix the pine-java predicate tok.type() == LuaValue.TSTRINGso the bookkeeping stops collecting phantom keys (mirrors the bug(pine-java): TransformByLua.fromLua corrupts numeric strings — isnumber() coercion turns 19-digit ID strings into precision-lost longs, diverging from Go #175 fix pattern; behavior-neutral today but removes the coercion trap from the remaining call site).Option A seems right unless someone has a real script pattern that writes numeric globals.
Refs
fromLuacoercion-dispatch fix this finding fell out of (.code-review/runs/175-run1/blind-report.mdminor-2, dispositioned pre-existing/out-of-range there)MarkGlobalsBaseline/ResetGlobalsToBaselinegodoc (documented string-key-only limitation)