Commit graph

5 commits

Author SHA1 Message Date
Stephen Dennis
6f9e325083 fix(lua/jit): decode int as varint in the bytecode loader (#1278)
DO NOT MERGE ALONE -- see below.

Lua 5.4 encodes `int` with the same base-128 varint as `size_t`:
lundump.c defines loadInt() as cast_int(loadUnsigned(S, INT_MAX)),
sharing loadUnsigned() with loadSize().  read_int() instead consumed
four raw bytes, so `linedefined` ate 4 bytes where the stream held 1,
desynchronising every field after it.

Effect: load_proto() always failed, so lua_bc_load() always failed, so
CompileLuaBytecode() always failed, so the Lua JIT compiled nothing.
Ever.  read_int() has exactly one commit in its history -- 21243e831
(2026-03-18), the commit that introduced the file -- so this is not a
regression: the pipeline has never executed a single chunk.

Traced by probe, header outward:

    dump: status=0 size=106 bytes=1B 4C 75 61 54 00 19 93 0D 0A ...
      (valid Lua 5.4: signature, version 0x54, format 0, LUAC_DATA)
    bc_load: failed at proto
    load_proto: failed at ncode
    fields: src='@#1/LJ' linedef=16810112 lastline=5342978
            nparams=0 vararg=0 maxstack=11 ncode=0 ok=0

`src` reads correctly -- read_string()/read_size() handle the varint
properly -- and the corruption starts precisely at the first read_int().

With this applied the loader parses, hir_lower_lua_proto is reached for
the first time, and `lua(me/LJ,5)` -> -5, `lua(me/LA,3,4)` -> 7.

WHY THIS MUST NOT LAND ON ITS OWN: it activates a large body of code
that has never executed.  Full smoke then reports 35 failures, all in
lua_fn.mux, most `#-1 NO ATTRIBUTE SPECIFIED`.  The lowering and codegen
are not correct yet; fixing the loader only reveals that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 22:41:59 -06:00
Stephen Dennis
630b6fb552 fix: overflow-safe bounds check + recursion cap in Lua undumper (#833)
The Lua 5.4 bytecode deserializer (lua_bytecode.cpp) had two latent
memory-safety gaps in its reader:

1. bc_reader::has(n) computed pos + n <= len, which wraps when n is a
   near-2^64 value. read_string() derives its size from read_size() (a varint
   that can decode any 64-bit value), so a crafted/corrupt size makes pos + sz
   overflow and falsely pass the bound, then std::string(data+pos, sz) reads /
   allocates ~2^64 bytes. Verified: a 42-byte crafted dump (size = 2^64-41)
   throws std::length_error -> terminate() with the old check; the fixed check
   rejects it cleanly. Fix: test n <= len - pos (pos <= len is an invariant, so
   no underflow), which can't overflow.

2. load_proto() -> load_protos() -> load_proto() recursion had no depth bound;
   a pathologically nested dump overflows the C stack before the caller's
   eligibility check (which rejects nested protos) runs. Fix: thread a depth
   parameter and cap at MAX_PROTO_DEPTH (200; real compiler output nests far
   below this).

Both are latent (the bytecode comes from the trusted lua_dump path, not
player-injectable through the text-only sandbox), so this is defense-in-depth
hardening of the deserialization boundary. Smoke 1252/1252.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 10:02:50 -06:00
Stephen Dennis
57d520f927 engine/lua: cap read_size() varint length to prevent size_t overflow
read_size() decoded a Lua base-128 size varint (lundump.c LoadUnsigned format:
continuation bytes have the high bit clear, the terminator sets it) with an
unbounded loop. A malformed bytecode stream with a long run of continuation
bytes would keep shifting the accumulator left by 7 per byte, overflowing size_t
and wrapping to a small bogus length that then slips past the caller's bounds
check.

Cap the byte count at sizeof(size_t)*8/7 + 1 (10 on a 64-bit build) — the most
groups a size_t can legitimately need — and fail the read (ok = false) past
that, matching how the upstream loader guards the same loop against overflow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 18:41:38 -06:00
Stephen Dennis
5e05f5472b Fix SIGABRT crash in Lua bytecode deserializer
read_size() returns size_t but was cast to int, causing negative
values when the bytecode reader gets out of sync.  The negative int
was then promoted back to a huge size_t by vector::resize(), throwing
std::length_error which escaped to std::terminate and killed the
server via SIGABRT on every smoke test run during lua() evaluation.

Use size_t throughout and add a 1M sanity cap on all deserialized
array sizes so malformed bytecode fails gracefully instead of crashing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 13:07:19 -06:00
Stephen Dennis
21243e8312 Add Lua bytecode → HIR → RV64 → x86-64 JIT pipeline (Phase 2)
Lua scripts compiled by lua_mod.so can now be JIT-compiled through
the existing HIR/RV64/x86-64 pipeline in engine.so. The bytecode
deserializer reads lua_dump() output without requiring Lua headers.

New COM interface mux_IJITCompile on engine.so with CompileLuaBytecode,
RunCompiled, IsCompiled, and Invalidate methods. lua_mod.so acquires
this interface and transparently attempts JIT before falling back to
the Lua VM.

Opcode coverage: data movement (MOVE/LOADI/LOADK/LOADNIL/etc.),
integer arithmetic (ADD/SUB/MUL/IDIV/MOD/UNM + immediate/constant
variants), comparisons (EQ/LT/LE/EQI/LTI/LEI/GTI/GEI), control flow
(JMP/TEST/TESTSET/FORPREP/FORLOOP), returns, and mux.* bridge calls
pattern-matched from GETTABUP+GETFIELD+CALL to engine API ECALLs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 09:59:45 -06:00