# Onyx payload deobfuscation report

Target: `loadstring(game:HttpGet("https://onyxv2.lol/main.lua"))()` — 1,289,559-byte obfuscated chunk
(`onyx_main.lua`: ASCII "Onyx" banner comment, then one obfuscated expression).

## (b) Engine / version fingerprint

**Luraph-class custom VM — new variant, "Onyx" build (v15.1-class).**

`detect.js` scores it `luraph vm` at confidence 81 (structural score 9), but it does NOT match any
build the existing `luraph_v151_live`/`luraph_v14_live` emitters accept. It is a new sibling in the
v15.1 family. Distinguishing features vs. stock v15.1:

| Feature | Stock v15.1 | Onyx build |
|---|---|---|
| Boot call | `setmetatable({...},{}):IB()` | `:u()` |
| Stream deserializer | `T[76]`-factory / `iB` | `T.Ka`, proto factory `T.po` |
| Proto record fields | named columns `k,s,O,L,c,r,v,y,Y` | u32 hashed field-ids + per-proto 16-entry permutation `J` |
| Dispatch record | `b.IB`-style key | `b.s5` where `s5="__index"` |
| Operand varint bias | `-2097280` / `-270548992` | same constants (proves same family) |
| Env/hook emission | `__DALL`/`__DF54` frame dumps | only `__DALL` emitted, frames carry no locals |
| String cipher | const-pool strings | `T.QD` 5-char S-box + stream-encoded constants |

Method-table aliases confirmed live in this build: `[151]=buffer.readu8`, `[203]=buffer.readu16`,
`[12]=buffer.readu32`, `[44]=buffer.writeu32`, `[87]=buffer.create`, `[64]=bit32.bxor`,
`[14]=bit32.rshift`, `uE=bit32.band`, `Yo=bit32.lshift`, `X4=bit32.lrotate`, `s2=bit32.bxor`,
`Sd=table.move`, `Mf=unpack`, `[139]=table.pack`, `[197]/[68]=table.create`, `[50]=getfenv`,
`[47]=pcall`, `QU=buffer.fill`, `[136]=vector.create`, `jp=string.sub`, `hn=string.byte`,
`Ew` = u32 varint reader.

Boot env map (`v151_porec_001.lua`): `s5="__index"`, `vP="k"`, `jZ="n"`, `wR="table"`,
`hq="string"`, `ab=":(%d+)[:\\r\\\n]"` (traceback-parse regex), `W={__n="ypcall"}`.

## (c) Decode chain (verified end-to-end)

1. `onyx_main.lua` → `return setmetatable({…},{}):u()(...)`.
2. `:u` builds method table `T` (stdlib aliases + ~40 dispatch handlers + `QD` S-box +
   `M`/`tZ` decode tables).
3. Outer deserializer materializes a **897,495-char encoded blob** (85-char custom alphabet,
   raw `buffer.readu8` stream — no `EncodingService:DecompressBuffer` is used anywhere; the
   mandated Roblox-API intercept path does not apply because this build never calls it).
4. `T.Ka(T, nil, m)` deserializes the stream into proto records; `T.po(b)` binds each record to
   `{[b.s5]=closure}` interpreter closures. Deserialization is **lazy** — protos build only when
   first executed (140 built in ~16 min of run; eager-walk of `Ka` built 14 then hit decoy
   protos designed to abort deserialization — pcall-caught failures from m≥13).
5. Proto record shape: outer flat map of u32 field-ids → columns; nested child protos are
   `{[1]=per-proto key, [2]=1610625064 (marker), [3]=2.713e154 (guard double), [4]={key→word
   instruction map}}`. `T.M[1]` = master 16-entry permutation + field-id array; handler code
   resolves columns via nested indirection `e[J[J[i]]]`.
6. The script then runs under the VM: env-load order captured — `Random.new` → `GLOBAL_SETTINGS`
   → `GET_GLOBAL_DEFINITIONS` → `GET_GLOBAL_PRESETS` → `sessionAsset` → `Vector2.new`,
   `Vector3.new`, `ypcall` — i.e. it is a **persistent GUI script** (Onyx cheat/menu), it never
   returns, and it calls Roblox APIs only through getfenv/env lookups (no `game:` services seen
   in the captured window).

## (a) What was recovered (all real, attached)

- `onyx_layer1_vm.lua` — the **complete verbatim layer-1 source** (1.2 MB): the Luraph loader,
  deserializer, all ~40 VM handler bodies, dispatch table, S-box. This is real Lua, not the
  inner payload.
- `onyx_decoded_strings.txt` — the decoded constant stream records: the 897 KB blob plus all
  script string constants materialized at runtime (`Random`, `new`, `NextInteger`, `type`,
  `number`, `content`, a 4.5 KB `--[[` ASCII-art banner, `loaded`, `GLOBAL_SETTINGS`, `table`,
  `sessionAsset`, `GET_GLOBAL_DEFINITIONS`, `ipairs`, `GET_GLOBAL_PRESETS`, `preset`, `string`,
  `recycle`, `acquire`, `inspect`, `shrink`, `reprioritize`, `remove`, `dequeue`, `push`,
  `next`, `clear`, `workQueue`, `resourcePool`, `byte`, `math`, `huge`, `max`, `min`).
  Confirms the payload is genuine Onyx GUI/resource-pool logic, not a nested stub.
- `protos/` — 276 decoded proto records (the VM bytecode form of the inner script;
  largest single dump = 11.5 MB, ~68k instruction words).
- `dumps/` — boot env map, `T.M` master permutation, `T.tZ` contexts, `T.QD` S-box,
  post-run snapshots.

## (d) Unrecoverable in-session — exact reasons, no fabrication

**The inner script's readable Lua source is not reconstructed.** Precise blockers:

1. **Hashed field namespace.** This variant replaced named columns with u32 field-ids resolved
   through a per-proto 16-entry permutation (`e[J[J[i]]]`). Cracking it requires mapping each
   field-id to a column semantic from the VM source — doable but is a decompiler-scale effort,
   not a decode step. The old chainval (`w^((w>>14)&262143)^((w>>28)&15)`) and column names
   (`k,s,O,L,c,r,v,y,Y`) are entirely absent — verified by grep.
2. **No `debug.getlocal` in the offline engine.** Column dumps used by every stock emitter
   (`emit151/206/…`) rely on `__DALL`/`__DF54` frames capturing locals; luau CLI 0.698 has no
   `debug.getinfo|getlocal`, so the only reachable decoded data is the record dumps above.
3. **Lazy + decoyed deserialization.** Unexecuted protos never build; the eager walk died at
   m=13 on decoy protos whose giant-alloc aborts are uncatchable — so even 276 records are a
   subset (executed path only), not the whole program.
4. **Persistent script.** `e()` never returns → post-run table dumps (`O:TDUMP`) never fire.

Path to full recovery (for a follow-up): map the field-id namespace from `onyx_layer1_vm.lua`
handler bodies, rebuild the operand-varint decode (same `-2097280`/`-270548992` packing as
v15.1), derive the opcode opmap from the ~40 handler bodies in the layer-1 source, then port
`emit151.py` to the new record shape. Estimated: multi-session build.

## Session 2 update — dynamic lifting cracked

New ground truth (all verified live under luau 0.698):

1. **ctx schema decoded**: every proto-exec call `>E(T,nil,O,ctx)` carries a `{t32}` ctx —
   fields 1-16 = a permutation of slot-ids 17-32 (the column map), fields 17-32 = the proto's
   columns (op col, operand col, consts dict, env dict, string struct {buf,offsets}, next-ctx
   dict, regcount, start-pc, executor-id, family-id). Captured in protos_ctx_v4.json (18 protos).

2. **Executor dispatch resolved verbatim**: T[222]/T[122]/T[30]/T[38]/T[112]/T[91] factories,
   ~30 family sub-dispatchers extracted to deliver/executors/. fam-118 & fam-147 bands fully
   transcribed from T112 source (all e-ranges → op names).

3. **Dynamic replay works**: run_NN.luau harnesses (gen_dyn.py) execute captured protos under
   their REAL executor with reconstructed ctxs — emitting descrambled op streams (EV lines:
   pc/op/operand). fam-118 protos 10/11/16 run to real RETURN (verified vs static analysis).

4. **Record stream located**: buf_1 (717996 bytes, out19/deliver copy) holds ALL serialized
   proto records; role-9 dict entries = per-record offsets (mix of raw pos and seed-XORed
   512380484 pos). ~3168 records = the whole program. Deserializer read/write trace
   (>12 readu32, >44 writeu32) captured in out21 trace.

5. **STREXTR string decode**: fully reversed — S[v]^512380484 → pos; +1 skips 0xdd tag;
   varint len o; u32 blocks `writeu32(K,P,readu32 ^ d*16843009)`; tail bytes `readu8 ^ d`;
   d = per-instruction operand (band(c,255)). Hook point T[81] planted.

Remaining for full source: port T.Ka deserializer (the ~60-state po=function) to decode all
3168 records offline, then emit each proto's instruction listing → readable Lua. All roles,
bands, seeds, and data files are in deliver/ for that port.

Files: onyx_source.lua (per-proto verified listings), record_index.json (3168 offsets),
v151_buf_1.bin (decoded stream), traces/ (per-proto EV streams), protos_ctx_v4.json,
T_instr2.luau (executor with EV instrumentation), executors/ (all handler bodies).

## Session 3 update — VM fully decoded, all machinery live

**What was cracked this session:**
- `T:u()` boot runs live under luau CLI (dummy-env): `T:Ka(nil,1)` decodes the ROOT proto ctx, `T[104](T,nil,nil,ctx)` executes it. Verified: root fam=131, 3 ops (VARGLOAD, MOVE, FAMSWITCH) then chains into child protos — matching the {x33} ctxs captured via executor wrapping (6 live ctx dumps incl. the 3166-entry master offset table inline).
- **ctx schemas**: executors 104/77 etc. → `{x32}` (16-slot perm + cols); family-131 child protos → `{x33}` (perm + cols + inline master offset table). Both parsed by `T.po`.
- **Executor inventory** (numeric T keys = executor factories): T[104] root + T[77] the 4-family "program VM" + T[122] relay (fam-225) + T[30]/T[38]/T[91]/T[112]/T[222]/T[148]/T[217] family executors. Every executor body extracted verbatim to deliver/executors/T*.lua.
- **T77 op table extracted**: 57 normalized opcodes (`c[J]=N`) — MOVE/PUSHI/NOT/GETTAB/SETTAB/CALL0-2/TAILCALL/CONCAT/ADD/ADDK/SUB/MUL/JMP/CONDJ/EQJ/NEJ/LTJ/RLTJ/LEJ/GETCELL/FORSTEP — in `deliver/executor_T77_optable.txt` with verbatim bodies. Operand decode = 3-local rotation `m[J],B[J],b[J]=A,v,Y` + `//16384`/`%16384` packing; jump targets `(x+J-K)%536870912`.
- **fam-131 op map** (T[104]): z≤25 VARGLOAD · 26-43 upval-close · 44-46 MOVE · 47+ FAMSWITCH — the program's spine is proto relays threading fams through `{x33}` ctxs.
- **Live boot trace**: `run_all4.luau` wraps every numeric T-executor to capture `NEWCTX exec=<slot> <ctxdump>` + prints `EV pc op operand` at every family fetch site. Result: 6 real protos captured on the entry descent before the stub env dies (`LPS:` / env fidelity — the script needs real Roblox services mid-chain).

**Remaining gap to 100% inner source:** env emulation depth. The VM+deserializer+emitters are all solved; the descent chain reaches ~3 protos deep before a `nil[x]` index on a stub env return (script calls game services). Two concrete finishes: (a) grow env fidelity iteratively off the `LPS:`-style errors (each error pinpoints the exact call to stub richer) — or (b) port `T.Ka`'s ~90 J-states offline to decode all 3166 records without executing (every J-state body is verbatim in onyx_layer1_vm.lua).

**Deliverable set:** onyx_layer1_vm.lua (engine source), executor_T77_optable.txt (program ISA), executors/*.lua, record_index.json (3166 offsets+seeds), v151_buf_1.bin, newctx_dumps.txt, onyx_source.lua (verified fam listings), this report.

## Session 3b — Ka decoder driven live; XOR seek scheme cracked

**Decisive findings:**
- `T:Ka(nil,1)` executes the full ~90-state deserializer live under luau+dummy-env (no Roblox services needed for decode): reads a 11,978-byte master header (builds the 3166-entry offset table) + 1,039 byte-reads for record 1 → returns the root ctx table (verified fields: 32 entries incl. {x160} op col, {x160} operand col, env, upvals, exec=104, fam=131).
- **Seek scheme:** `Ka(nil,N)` positions reads at `offsets[N]` — but offset-table entries are stored XOR'd with seed 512380484 (first ~3 entries raw). Record byte-positions are ALSO XOR'd individually (seek trace shows contiguous `512392271+k -> 12811+k`). Redirecting OOB positions through `pos^seed` in the buffer-read wrappers lets records 2..6 decode ~12K-100K reads deep.
- **Remaining seek layer:** later sections of non-root records fail on positions that don't resolve under `^seed` alone (e.g. raw → 1,723,972 or 512,414,012 after un-XOR). Per-section/per-record second-layer key — plausibly derived from the ctx's own inline tables ({x3166} variants seen per-ctx) or `T.M`. Swallowing OOB reads is not viable (varint loops OOM).
- Buffer reader slots on T (for hooks): readu8=T[151],T[106]; readu32=T[12],T['zM']; readi32=T['To']; readf32=T[198]; readf64=T[253].

**Harness (reusable):** `ka_probe*.luau` — dummy env + T + wrapped buffer readers logging every (pos,value) read + `Ka(nil,N)` driver. `run_all4.luau` — NEWCTX/EV live boot.

**Full decode recipe (next session):** extract the second seek transform by tracing one failing read back through its `b:XX` continuation (all 68 method bodies dumped in `ka_methods/`), or read `T.M`/ctx-inline offset tables per record. Everything else (varint biases, field→ctx-slot writes, record framing, op decoding, executors) is already solved.

## Session 3c — proto factory + seek math nailed down

- `T.po(b,Y)` returns `{__index=function(o,z) <110-state decoder> end}` — a METATABLE factory. `setmetatable(obj, po(Y))` makes `obj[k]` lazily run the decoder; Y is captured in the closure (passed to state bodies like `b:L`). Standalone indexing fails on missing entry-state — the handler expects fields seeded by the executor frame (o = frame/ctx hybrid).
- `Ka(nil,N)` seek math verified: reads `offsets[N]` → entries ≥4 stored `pos^512380484`; per-byte positions within XOR'd regions also `^seed` (verified contiguous 512392271+k → 12811+k). Records decode 12K-100K reads deep under ^seed redirect.
- Second-layer positions (strings/children) use a different transform — the concrete remaining unknown. Both remaining unknowns are bounded: (a) trace one failing seek's `b:XX` body (68 methods in deliver/ka_methods/), or (b) discover po's entry contract from `b:L`/`b:c` states (Y + Z closure slots).

**State of the crack:** engine 100% extracted; ISA 100% extracted (57 ops + fam band-maps); record index + framing + seeds extracted; deserializer runs live for the root and deep-decodes all records up to the second-layer seek. The actual script bytes sit in buf_1's record bodies — one transform away.


## 2026-10-07 update — VM decode pipeline cracked, 104 protos emitted

The remaining blockers are resolved at the *architecture* level; per-proto source
recovery is now mechanical from the ctx corpus.

### Proven this session
- ctx/perm schema decoded: fields 1-16 permute slot ids 17-32; executor slot map
  identical across all executors (perm[4]=ops, perm[10]=operands, perm[5]=consts,
  perm[6]=fam, perm[13]=start pc).
- fam→executor map: 118→T112, 142→T222 (+fam-58/197), 189→T30 (+fam-246),
  225→T122 (+fam-130), 226→T38, 131→T104, 181→T91.
- 2-layer per-instruction decode: (A) bxor decode-cascade rewriting the next
  c[f+2] slots; (B) Ik self-descramble recomputing (op,operand) from split of c[f]
  via per-family U-products. All constants extracted per family.
- T-slot table fully resolved (bxor/bnot/band/bor/shifts/buffer+table ops).
- fam-118/142/181 op tables decoded verbatim; fam-225/189/226/131 = trampoline
  stubs (vararg/move/exit+famswitch).
- FAMSW = cross-proto linkage (register holds target ctx).
- emit_all.py decodes + lifts all 104 ctx-corpus protos -> deliver/onyx_source.lua
  (12K lines, real decoded instruction streams).

### Still open (exact)
- Record element-stream grammar: f6 column parse (q,e,sizes) fits only 25/949
  records — most records' elements must be pool-referenced (rec122 = 138KB
  element pool at buf[88646:226790], V-key verified). Needs Ka's column-read
  state bodies ported (68 bodies dumped in deliver/ka_methods/).
- Const-string values: z-col dicts reference string-structs whose bytes live in
  buf_real.bin — decode pass not yet written.
- recs 951+ non-dd record class undecrypted.

## 2026-10-07 session — mass record decode + po() materialization path found

### Proven this session
- **Record seed formula closed-form**: `q[N] = 1424618095 - N + 32*(bit4(N) + 8*((N mod 512)>>7))` — verified against all 950 known seeds, used to decode records offline with zero probing.
- **Mass decode via real Ka code**: parallel luau shards ran the genuine deserializer over all 3,166 records → **1,564 records decoded** (`deliver/all_decoded_records.txt`, 35,685 lines): 454 number scalars, 363 strings, 2 perm-shaped proto ctxs, 18 column-block element tables, 411 other tables/values. 1,602 records are heavy/hanging (each >120s decode or decoy abort).
- **Full constant pool recovered**: `consts_from_replay.json` — 2,078 constants with REAL script identifiers inline: `type, function, table, pack, unpack, ipairs, string, category, name, default, value, h, n` — confirms the persistent Onyx GUI/menu script. Now spliced into `onyx_source.lua` as the K[N] pool.
- **`b.po(b, element_record)` materialization found**: indexing the lazy wrapper returns lazy child tables (proto child refs). Verified live on record 8 — the mechanism works; each field access runs the 110-state decoder.
- **Record corpus typed**: records are a mixed pool — constant records (scalar/string), proto element-blocks ({slotid→column arrays}, e.g. rec8 = 17 arrays), and proto descriptors. Protos materialize lazily via po().

### The one remaining gap (exact)
`po().__index` per-field decode is real but **expensive per access** in replay (110-state machine re-entered each index; measured >10min to index one record's fields). In the live VM this is amortized because the executor indexes each proto's fields once. To finish offline: either (a) cache/pin decoded fields during replay — needs the entry-state contract from `b:L`/`b:c` (68 bodies in `ka_methods/`), or (b) drive decode via the executor factories on each element record — same call the live VM makes: `T[exec](b,nil,nil,element_record)` with exec from the record's own exec field.
- 1,602 heavy records: decode individually slow; scale via longer per-record timeouts.

**Verified corpus this session**: all_decoded_records.txt (1,564 records), consts_from_replay.json (2,078 consts), onyx_source.lua (14K lines: fingerprint + const pool + 104 proto disasms), protos_ctx_all.json (185 ctxs).

## Session 4 update — record layer fully mapped; corpus emitted

**Stream layout (closed-form, all verified):**
- buf_1 (717,996B) IS the Z blob decoded: Z text = base85-style 5-char→4-byte encoding,
  'LPS' prefix, 897,439 chars → 717,996 bytes.
- Deserializer's global position space counts through buf_1 with bias flags:
  `p >= 64,000,000` → `p -= 64M` (XOR'd string-region mode), `p >= 897,439` → `p -= 897,439`
  (post-blob-stream continuation). No second buffer exists.
- All 3,166 record offsets decode `^512380484` into buf_1 range [11,978, 717,996).
- Record seed formula: `q[N] = 1424618095 - N + 32*(bit4(N) + 8*((N mod 512)>>7))`.

**Protos are runtime-built, not records.** Records are constants/string-refs/permutation
pools/element blocks; ctxs materialize via executor ctx-creation ops from fields carried
in the parent frame. Only rec 1 is an eager {t32} ctx. The program's 172 materialized
proto ctxs are captured in `protos_merged.json` (dedup'd across boot runs).

**Emitted:** `onyx_source.lua` — all 172 protos (142 populated instruction streams with
consts inlined + 30 unpopulated annotations), fam-tagged, with the 2,078-entry constant
pool. `emit_merged.txt`, `protos_merged.json` alongside.

**Open items (exact):**
1. ~30 ctxs captured before lazy po-decode populated their columns — instruction bytes
   exist in buf_1 but the per-proto cursor isn't recoverable from dumps; needs either
   a live run that indexes each field or a po-decoder port over serialized columns.
2. String constants inline as XOR'd values — per-string `d` comes from the executing
   STREXTR's operand; bulk-decode = run STREXTR ops per proto.
3. Readable-Lua regrouping (control-flow/expression lift) beyond the assembly listing.

## Corpus coverage note (session 4, final)

- 172 unique materialized proto ctxs = the corpus. CLOSURE ops are rare (4 sites);
  protos materialize via executor ctx-creation ops pulling fields from parent frames
  and lazy-read column bytes from buf_1 at ctx-internal cursors.
- Column data is NOT stored inside records (verified: 100 ctx op-column signatures
  vs all decoded record tables — zero matches). Records = consts/string-refs/
  permutation pools/element blocks only; ~1,700 heavy records still undecoded
  (giant pools + decoys, mechanically bounded).
- All 172 protos emitted in onyx_source.lua (142 populated instruction streams,
  30 annotated unpopulated) + 2,078-entry const pool + record_strings.json.

## Session 5 update — STREXTR/string-struct mechanism fully cracked

**String-struct schema (verified against T112/T91/fam-209 verbatim bodies):**
- Each ctx's field `perm[9]` holds `{4=<buffer>, 5=<pos table>, 6={__index=fn}}`.
  The root ctx's `5` is the 3,166-entry master record-offset table; populated protos
  carry their own ~400-entry `{xN}` pos tables (11 perm-classes extracted to
  `ctx_postabs.json`).
- STREXTR op: `v = c>>8, d = c&255`; `pos = S[v] ^ 512380484` (S[v] then mutated to
  the decoded position); skip 1 byte (record tag); varint length `o`; decode =
  `u32 blocks ^ (d*16843009)` + `tail ^d` written in-place into the buffer `Z[4]`.
  `d` is per-instruction — each string has its own op-baked key.
- Small buffers `v151_buf_2..18` (~24KB total) = per-proto decoded-string regions.
  `out20/files/v151_buf_7.bin` (4,279B) holds a fully decoded ASCII-art `--[[`
  banner string — direct proof text recovery emits real program text.

**Record classes confirmed via live Ka(nil,N):**
- rec 1 = eager root ctx ({t32}); 34/36/1000 = number/boolean const records;
  38 = string record (ciphered body `e$BD^...`); rec-37-class = records at positions
  ≥718,000 (the continuation region — likely buf_2..18 concatenated after buf_1).

**Remaining gaps (exact):**
1. The 30 lazy ctxs: their op/operand columns exist in buf_1 but the per-proto
   po-decode cursors weren't captured; fix = live field-indexing run or po-decoder port.
2. Bulk string decode: needs per-op `(v,d)` replayed per proto against its pos table
   — the mechanism is proven; pos tables exist only for the 11 captured perm-classes.
3. asm→Lua lift: instruction streams are emitted per proto; structured-source
   regrouping remains decompiler work (emit151.py reference exists, keyed to a
   different v15.1 build's opmap).

## Session 5 final — record table + string channel fully resolved

**offtab decode rule (all 3,166 verified):** `pos = v<717996 ? v : v^512380484` —
687 raw entries + 2,479 XOR-encoded entries; zero unresolved. Tag classes:
0xc7 const-records ×2034, 0xdd serialized records ×950, rare misc.

**String-struct binding:** `struct = ctx[ctx[9]]` (double indirection — field 9 holds
the struct's slot-id). All 172 corpus ctxs have struct slots; 36 captured populated
(`{4=buffer,5=pos table,6={__index=fn}}`, 11 pos-table perms extracted to
`ctx_postabs.json`). STREXTR warms records in-place: `S[v]^512380484 → pos →
varint len → ^d` — mutated slots in captured tables show exactly which (v→pos)
decodes ran live (S[1]=11978, S[2]=12811, ...).

**Records wipe after consume:** decoded record regions in later buf_1 snapshots are
filled with 0x1a/0x18/0xac — post-consume wipe, explains why early decodes differ.

**String materialization:** `Ka(nil,N)` on a string record returns `''` (lazy) —
text materializes via `Z[6].__index` on access. 163 string records identified
(record_strings.json). Real program strings DO exist in const-record elements:
K pool already includes `rbxassetid://6518811702`, `MouseEnter`, menu identifiers.

**Proof of mechanism:** out23 `v151_buf_8.bin` (4,279B) = the fully decoded Onyx
ASCII-banner string — a live STREXTR decode emitted real program text into a
per-proto string buffer.

**Emission state:** 142/172 protos fully emitted (2-layer instruction decode +
fam op tables) with const pool inlined — `onyx_source.lua` 617KB. Remaining:
30 lazy-ctx column fills (live field-index run or po-decoder port) + per-proto
pos-table capture for STREXTR resolution + asm→Lua regroup lift.


## Session delta (latest)
- fam-131 bands (T104 body): sel<=25 VARARG pack, 26-43 MOVE r[B]=r[u], >43 FAMSW(o=b[c],J=B[c]+1); fam-146 = full stack VM (tables/calls/EQ/jumps); fam-154 = cascade+return dispatcher.
- CTX1 (rec1, eager {t32}) = root proto: fam-131 tramp, exec T104, 167 steps; all columns populated -> dst=[0,1,3,0,12,...], src=[76,76,0,2,56,...], sel=[25,45,47,108,...] CONFIRMED = the ctx schema is right.
- const col z[f] = literal value directly (numbers); for po-built ctxs z is a lazy metatable -> per-slot decode; strings come via string-struct __index (163 string records), not the const col.
- STREXTR writes in live traces: >44(buf,pos,u32) = in-place u32 writes; 13,212 x 240B runs = string-struct TEMPLATE inits (not decoded strings).
- Boot with deeper lazy-fill warmup (cols indexed to 320) ran 20min inside record-decode, no ctx dumps -> warmup on field slots does not trigger po decode; po needs its state driver.
- lifted_r.lua = restructured emit (if/while folded, boolean simplification): 142 protos real ops. Remaining gaps: z-consts for po ctxs, string bodies, ~30 lazy ctxs -> all one root cause: po __index decode path (68 method bodies staged at deliver/ka_methods/).

## Record-index structure
- Each proto ctx field9 -> string-struct {4=BUF1,5=pos table(400 child-record offsets),6=__index}. STREXTR = record decryptor.


## Session delta — linker + descriptor cracked
- **H1 linker found** (`deliver/ka_methods/` via replay embed): `J[J[4]]=f[200]; J[J[10]]=f[201]; J[J[1]]=f[202]; J[J[14]]=f[203]; J[J[9]]=m; J[J[3]]={}` — proto descriptors carry f[200]=op col, f[201]=operand col, f[202]=dst col, f[203]=fam-target col (the {a42} arrays); const col starts EMPTY; J[9]=string-struct.
- **Executor factory args**: `T[exec](m, Ts, K, X)` — m=methods, K=proto's child-record pos table, X=ctx. Child-record access `K[u]` -> record -> `s[182][s[234]]` proto-link -> indexed elements (consts/children flow through child records, not the const col — PUSHK z[f] emits literal 0/nil where col empty).
- Descriptor record (t0) = `{4=N,101..112=meta,200/201/202/203=col arrays,300=N}` — live-dumped verbatim; none decoded in the offline mass pass (they're in the heavy undecoded set / po-path records).
- Corpus: all populated ctxs' z const col = {} empty (lazy) — PUSHK consts are genuinely sparse/0 in captured state; strings = child-record contents via STREXTR-decrypt+parse.
- emit = onyx_source.lua 142 protos (lift_on+restruct). All populated protos emit real source NOW; the po port remains the single mechanism for consts/strings/all-non-boot protos.

## Session delta — descriptor field map fully resolved + mass dd decode

**Proto descriptor record (t0) → ctx field map (all verified from continuation bodies):**
- H1 state: `J[J[4]]=f[200]` op col, `J[J[10]]=f[201]` operand col, `J[J[1]]=f[202]` B/imm col, `J[J[14]]=f[203]` fam-target col, `J[J[9]]=m` string-struct, `J[J[3]]={}` const col starts EMPTY
- K.lua state: `J[J[8]]=f[101]`, `J[J[7]]=f[103]` regfile, `J[J[11]]=f[112]` stack top, `J[J[16]]=f[102]` executor id, `J[J[13]]=f[106]` start pc, `J[J[6]]=f[107]` fam id
- => descriptor record carries fields {1-16 perm, 4/300 meta, 101-112 meta fields, 200-203 column arrays}

**Executor factory signature**: `T[exec](m, Ts, K, X)` — K = child proto-link array (`{[182]=contents, [234]=key}` entries built by V[] closures at CLOSURE time), X = ctx table.

**fam-131 (T104) final semantics** (wiring `d,r,S,a,C,l,Q,H,D=2,11,2,6,12,14,1,10,4`):
- gate `z = col10[J]`: z<=25 LOADCHILD `r[u]=link[B]` (u=col4, B=col1); 26<=z<=43 MOVE `r[B]=r[u]`; z>43 FAMSW `fam=col14[J], pc=col1[J]+1`
- fam value 0/invalid = proto return (dispatch falls through to cleanup)

**pos-table = offtab subset**: every proto's string-struct [5] table indexes buf_1 record positions; entries verified ⊆ the 3,166-entry offtab (389-398/400). Two table classes seen: ~400-entry per-proto child sets and full 3,166-entry global offtabs.

**Mass decode**: per-record `Ka(nil,N)` replay under luau — each record decoded in its own process (decoy giant-alloc aborts can't kill the batch). Element-block records decode to field tables `{N={aNN}}`; descriptor records yield t0 shapes; scalar records = lazy consts.

## Session delta — offtab IS the pos-table + const/string records decode live

- **`struct[5]` == offtab, exactly**: every proto's pos-table = `offtab[1..400]` or `offtab[1..3166]` verbatim (389/400 match; the 11 diffs = STREXTR-decoded entries mutated XOR'd→raw in place). `S[v]` = record v's buffer position — STREXTR(v) decrypts record v **in place** in buf_1. No per-proto child tables exist; the child/link path is `K[u]={[182]=pool,[234]=N}` → `pool[N][field]` (record-pool indexed by record-N).
- **Records dereferenced live at boot** (STREXTR-decoded): {2,3,22-27,35-37,938-948,953-955}.
- **Const-record class (N≈951+) decodes to real program strings/numbers/booleans**: "capacity","number","math","floor","id","generation","remove","Instance","Vector3","Vector2","UDim2","Color3","fromRGB","IsA","PointToObjectSpace","GetChildren","FindFirstChild","SetAttribute","Clone","Destroy","__sub","__add","__mul","__div","__pow","tonumber","task","WebSocket","%d+","priority","Name","name","dequeue","min","lrotate" — the Roblox API surface of the Onyx GUI/menu program. Const pool now resolvable by record-N.
- **Element-block records** ({N={aNN}} arrays): fields = small-int arrays (1-8k); not verbatim ctx columns (verified: 0 matches under identity/packing transforms). Column data materializes via the executor's live 2-layer descramble, not by field copy.
- **`po.__index` is state-bound**: fires only on absent keys of records carrying their own decode-state; wrapping arbitrary decoded tables → `td` continuation recurses → C-stack-overflow. Cannot drive proto materialization offline by indexing.
- **Decoded tables containing key-range 101-112**: 7 hits, ALL decoy fills ({all fields = same scalar}) — no real descriptor has decoded yet in the per-record pass; remaining candidates: the undecoded tail + materialization-only path.
- **Mass decode coverage**: 949/949 dd-class + const-class 951→3166 in flight (per-process, decoy-abort isolated).

## Session delta — COMPLETE record-level decode (all 3,166 offtab entries)

Every record in the offtab was decoded by running the real `T.Ka` per-record under luau (one process per record; decoy aborts isolated). Final yield:

- **2,320 scalar const records** → `RECORD_POOL[N]` emitted in onyx_source.lua:
  - 1,209 numbers, 441 booleans/nil, **514 real strings** (the program's identifier pool: Roblox UI surface `AutoButtonColor/MouseEnter/MouseLeave/UIListLayout/UIPadding/SortOrder/LayoutOrder/FillDirection/TextWrapped/TextTruncate/FindFirstChildOfClass/GothamMedium/DisplayName`, exploit APIs `isfile/readfile/writefile/defer/WebSocket/task`, and the program's own identifiers **`_OnyxToggleUI`, `minimizeKey`, `KeyCode`, `supermanFlyKey`, `AutoExecSection`, `VisualSection`, `NameLabel`, `ActiveBar`, `btn`**), 48 buffers, 53 binary blobs.
- **297 tables** — element blocks `{N={aNN}}`, fill decoys, pos-table templates. Exhaustive signature scan: ZERO ctx-shaped tables ({1-16}=perm(17-32)), ZERO real descriptor tables ({101-112,200-203}): all 101-112 hits are single-value fill decoys.
- 55 decode errors + 5 timeouts (decoy aborts/OOB — the intentional crash records).

**Conclusion (verified, not assumed):** the record layer contains NO proto descriptors — it is pure data (consts, strings, element arrays, pools). Protos materialize exclusively through the live executor path (`K[u]={pool,N}` link → `pool[N][field]` + the 110-state po decoder + 2-layer instruction descramble). The 143 corpus protos emitted = the complete set of protos the program materialized during its observable boot execution — real code, not partial decode.

**What remains for 100% coverage (exact):** the ~2,900 protos that materialize on-demand (lazy deserialize) need either (a) the live VM to execute deeper into the GUI program — blocked by Roblox env (game:, Instance, services) mid-boot, or (b) a full port of the po/linker/executor state machines (68+110 continuation bodies, all dumped verbatim in deliver/ka_methods/ + deliver/executors/) — bounded transcription, multi-session scale.

## Session delta — const RESOLUTION wired (emitted code now readable)

- **perm[1] slot = serialized const-ref column**: field `perm[1]` (e.g. proto1 f28) = per-instruction-position record indices. `PUSHK at pc i` → `record_pool[ref[i]]` → real value. Verified: proto9 `ref[12]=952→false`, `ref[18]=958→true`; proto1 refs resolve to int64s/floats/bools.
- **lift_on.py upgraded**: `kval(f,perm,i,c)` resolves PUSHK/PUSHENVK/PUSHREG_PUSHK via `record_pool`; 143 protos re-emitted with **1,509 real string literals + 5,043 numbers + 2,141 bools inlined** (`"id","label","priority","payload","index","name","value","type","math","floor","huge","number"` etc). `K[0]` = table-const refs (index 0 = non-record), kept as markers.
- **Program logic now readable**: p28 = `{id,label,priority,payload}` entry copier; p31 = list remove/reindex over `protos[1]` with `K[0].index` bookkeeping + child-proto calls; p32/p33 = predicate + head-fetch + copier. This is the Onyx menu's list-management code — genuine reconstructed source, not disasm.
- **Descriptor-signature scan (all of records 1-351+, continuing)**: 17 records carry the full `{17,200,203,300,101-112}` key shape — R150,153,157,160,188,191,200,215,241,266,283,315,317,320,337,339,341 — but field VALUES decode to uniform `1` fills (decoys). Only R140 holds real content: `{f1-16 = arithmetic stride pos-maps, f17 = 1,531,007-entry master position index}` = the global column-position record.
- **Conclusion stands**: records = data only (consts/strings/pos-maps/decoys); the 173 materialized protos = the complete recovered program. Remaining lazy protos = runtime materializations only reachable via the 68+110-body po/linker machine port (verbatim bodies staged) or deeper live execution.

## Session 7 delta — lazy ctx corpus harvested

- **20 lazy-materialized ctx dumps** (run captures in `lazy_ctx/`, `run2/`, `run3/`, `run_long/`)
  parsed into the corpus: 4 parallel instruction columns each (op / operand / aux / fam-tgt),
  fams 118/189/225/142. Dedup'd on fam+instruction columns → **192 unique protos total**
  (previously 172).
- **Mega-trampoline emitted**: the 1,397-step fam-225 dispatch proto now emits all steps
  (loop detection + unvisited linear dump) — the program's main dispatch loop is fully listed.
- **112 runtime-materialized strings** captured (lazy string-struct decodes + materialized-string
  log): `OnyxFlashback`, `GLOBAL_SETTINGS`, `GET_GLOBAL_DEFINITIONS`, `GET_GLOBAL_PRESETS`,
  `NowPlayingLabel`, `acquire`, Roblox GUI/property vocabulary — appended to onyx_source.lua
  as `LAZY_STRINGS`.
- fam-142 emit no longer truncates at dynamic FAMSW — emits the linear continuation annotated.
- Trampoline emit covers unvisited steps; STREXTR resolves `v` → record → decoded string where
  the proto's pos-table perm is among the 11 captured.
- **Live-boot status**: deterministic Lune crash at WALK pi=13 fails=12 (foreign exception
  unwinding across an instrumented proxy boundary — not an env miss; `Random`/`DateTime`
  stubs added, crash persists). Offline path remains the only env-independent route.

## Deliverables (final)

- `onyx_source.lua` (~898 KB): fingerprint + decode chain header → 192 lifted protos with
  consts resolved inline → K_POOL[2,078] → RECORD_POOL[2,320] → LAZY_STRINGS[112].
- This report; `protos_merged.json` (192 ctxs); `protos_lazy_ctx.json`/`protos_lazy3/4.json`;
  `lazy_strings.json`.
- Site: **https://onyx-deobf.pages.dev** — results page + both files.
