# Obfuscator correctness fixes

This file records fixes that must not be regressed by future obfuscation work.
The compiler/VM changes are semantic; they are not optional optimizations.

## 1. CLOSEUPVALS / UI Color3 corruption

Symptom:

```
sync is not a valid member of Color3
```

Reduced failure: a captured local (`VISIBLE_COLOR`) and a later local (`UI`)
were assigned the same Luau register. `CLOSEUPVALS R63` was discarded by the
compiler, so both locals shared one boxed cell. `VISCHECK.recolor` overwrote
the cell with a `Color3`; `UI.sync` then indexed a `Color3`.

Fix:

- `luraph_compiler/compiler.py` lowers `CLOSEUPVALS from` to `XCLOSECELLS from`.
- `luraph_compiler/vm_runtime.luau` `XCLOSECELLS` replaces each boxed register
  cell at or above `from` with a fresh cell containing the old value. Closures
  keep the old cell; the reused register gets a detached cell.
- `CLOSURE` kind 0 captures `cell(cellval(q[idx]))`, never a cell-of-cell.
- `XGETCELL`, `XSETCELL`, `XBOXCELL`, `CLOSURE`, `callwith`, `XFORGLOOP`,
  `XMOVETOP`, and `XGETVARARG` consistently unbox cells.

Regression tests:

- `test_compiler.py close_reuse`
- `test_hardened.py close_reuse`
- `test_binary.py close_reuse`
- `test_macros.py`

Expected output for `close_reuse`: `BLUE<TAB>SYNC`.

## 2. Table-call corruption from boxed call slots

Symptom:

```
attempt to call a table value
```

Cause: a call slot or captured value could still contain a boxed cell, or a
closure captured a cell instead of the value it contained. The VM tried to call
the cell/table instead of the function.

Fix:

- `callwith` unboxes the callable and every argument.
- `CALL0/CALL1/CALL2/CALLN/CALLD/XCALL` use unboxed function/argument values.
- `XFORGLOOP` supports generalized table iteration and unboxes callable
  iterator cells.
- `handler_runtime.py` passes the `boxed` register list into every handler
  context, so binary/handler mode can close cells and box/unbox correctly.

Regression tests:

- all compiler suites
- `close_reuse`
- `gen_iter`
- `vararg_forward`

## 3. Binary/handler mode `p.boxed`

Symptom:

```
attempt to index nil with 'boxed'
```

Cause: extracted handler bodies used the outer `p` local, which does not exist
in handler mode.

Fix:

- `handler_runtime.py` adds `boxed` to the handler context and random-name map.
- `vm_runtime.luau` stores `local boxed = p.boxed or {}` in `V.exec` and uses
  that local in `XCLOSECELLS`.

## 4. Protocol fixes that must remain

- The loader sends `key_id` and an HMAC proof, never the raw license key.
- `auth`, `auth_ok`, and `done` are transcript-bound; nonce/time/session are
  checked.
- License keys are encrypted at rest; execution logs contain key IDs only.
- Loader uses SecroLuaVM (hardened + binary graph + Secro outer) and contains
  no `SECRO_`/`LPH_` macros.

## 5. Compatibility rule

Any future obfuscation feature must keep all of these suites green:

```
python3 test_compiler.py
python3 test_hardened.py
python3 test_binary.py
python3 test_macros.py
```

and must re-run a Secro build of the Operation One source before deployment.

## 6. Obfuscation v2 (2026-10-23)

The binary graph format is now `SGB2` version 2.  Existing v1 artifacts still
run inside their own embedded VM; new builds use v2.

Added without changing VM semantics:

- **Per-prototype lazy code decryption.** Each prototype has its own code seed;
  code is decrypted/parsed on first execution, not during initial graph load.
- **Fragmented strings.** String constants are split into independently seeded
  fragments with per-fragment headers.  The VM reassembles/decodes on first
  `K()` access.  Plaintext constants no longer appear in the binary graph.
- **Per-build graph layout.** Operand field IDs are permuted, prototype ids are
  random 32-bit ids (root/closure references remapped), and each prototype gets
  a randomized register base relocation.  Call/loop windows remain contiguous.
- **Superinstruction fusion.** Safe straight-line `GETGLOBAL` + `GETTABLE`
  pairs become `GETCHAIN`; the second slot becomes `XNOP` and the handler
  advances two pc slots.  Jump-target slots are never fused.
- **Selective control-flow noise.** Unconditional jumps are routed through up
  to three trampoline hops; every prototype gets 1-3 unreachable padding
  instructions and the branch target map is preserved.
- **Fast loader profile.** The bootstrap loader uses the same SecroLuaVM
  hardened/binary outer mode but `--fast`: no graph superinstructions/CFG
  padding and eager code parse.  This keeps auth load time near the previous
  build while the protected script uses the full profile.

Loader cache invalidation is automatic: `secroauth._compiler_signature()`
hashes the compiler/VM/loader/wrapper files into the loader digest.  Any change
to those files forces a fresh loader build.

Compatibility:

- All `XCLOSECELLS`/cell/`GETCHAIN`/lazy-code/string-fragment paths are covered
  by `close_reuse`, `gen_iter`, closures, varargs, table iteration, and macro
  tests.
- Full suites: normal 26/26, hardened 26/26, binary 26/26, macros 4/4.
- Operation One full-profile artifact revision 13: 692,235 bytes.
- Fast SecroLuaVM loader: ~394 KB; phone auth load ~15.0 s.
- Inner/middle/carrier headers are now SecroLuaVM-labelled; the old inner
  Luraph v14.9 header was a nested carrier artifact, not a stale script.

## 7. Staged loader (v2 micro + ticket-gated full loader)

New distribution path:

```lua
script_key = "CUSTOMER_KEY"
loadstring(game:HttpGet("https://.../api/secroauth/stager?id=SCRIPT_ID"))()
```

- The public `/api/secroauth/stager` is a **SecroLuaVM v2 micro** chunk
  (Secro outer/layers only, no full VM). It is ~14 KB.
- The micro stager computes `key_id`, a timestamped nonce/HMAC key proof, and
  POSTs it to `/api/secroauth/ticket`.
- `/api/secroauth/ticket` verifies the key (decrypted server-side), HWID,
  expiry, ban state, timestamp and replay nonce. It returns a short-lived,
  one-time ticket and an authenticated response.
- `/api/secroauth/loader?id=...&ticket=...` returns the real loader only with a
  valid unused ticket. Without a ticket it returns the stager, preserving old
  Get Script snippets.
- The real loader is ticket-gated and uses the script's `loader_mode`:
  - `v2` (default): full SecroLuaVM v2 loader.
  - `fast`: SecroLuaVM v2 fast bootstrap profile.
- The artifact remains full SecroLuaVM v2.

`stager_guard.luau` uses no raw key on the wire, no key in the URL, and signs
both the ticket request and response. The ticket is HMAC-signed with the
server secret, bound to script/key/HWID/executor, expires in 60 seconds, and is
consumed once.

Current measurements:

- Stager: 13,882 bytes, negligible startup.
- Full v2 loader: ~391 KB.
- Artifact revision 13: 692,235 bytes.
- Phone end-to-end (stager + full loader + artifact): **15.36 s**.

SecroAuth UI/API now exposes separate upload settings:

- Protection: `secroluavm` / `luraph_hardened`
- Loader: `v2` (full) / `fast`
