Skip to content

fix(bvm): SCENERYOWNER stack-imbalance — push 0 sentinel; document dead-BVM trap#297

Merged
CoreyRDean merged 2 commits into
developfrom
fix/dead-bvm-api
May 27, 2026
Merged

fix(bvm): SCENERYOWNER stack-imbalance — push 0 sentinel; document dead-BVM trap#297
CoreyRDean merged 2 commits into
developfrom
fix/dead-bvm-api

Conversation

@CoreyRDean
Copy link
Copy Markdown
Collaborator

Summary

Applied the just-learned doc-audits-find-source-bugs recon pattern to a sibling area: the invoker-contract vs. BVM-impl drift. There's known precedent — the audit-comment at ScriptingCommands.bb:2780 records that BVM_OUTPUT's Param3 = 0 impl default mismatched the invoker contract's 255, causing every 2-arg Output(player, \"hi\") call to print black-on-black. If one was wrong, others likely were too.

Sweep: 220 contract entries (<BVM_*> in RC_Standard_Invoker.bb) vs. 222 active impls (^Function BVM_* in ScriptingCommands.bb), case-insensitive. Two contract-without-impl orphans: BVM_SETOWNER and BVM_SCENERYOWNER. The 4 impl-without-contract entries (RequirePrivileged / RequireSelfOrPrivileged / ScriptPathIsSafe / SetWaitResult) are correctly-private internal helpers.

The bug

The underlying OwnedScenery Type was disabled long ago in ServerAreas.bb:53, and both BVM impls were commented out in ScriptingCommands.bb:1046 / :1071. The dispatch cases in RC_Standard_Invoker.bb:1363 / :1494 were also commented out. But the public contract entries at lines ~95-96 were left active, so the BlitzForge compiler still accepts SetOwner(...) / SceneryOwner(...) calls from scripts.

The dispatch cases pop args from the BVM stack but the (commented-out) impl calls never happen:

Case Pops Pushes Stack delta
501 SCENERYOWNER (returns %) 3 0 ← was supposed to push the int return value -3 + caller-expects-+1 = -4 off
530 SETOWNER (void) 4 0 balanced

So SETOWNER is silently no-op (annoying but safe). SCENERYOWNER is a real silent-corruption bug: the caller's expression expects an int on the stack after the call, gets a stack underflow instead, and every subsequent BVM operation in that expression pops the wrong slot.

Fix

  1. RC_Standard_Invoker.bb:501 — push BVM_PushInt(0) so SCENERYOWNER's stack stays balanced. 0 reads as "no owner" — the closest defined sentinel given the storage is gone.
  2. 5 audit comments cross-linking all dead-API sites:
    • Contract entries at RC_Standard_Invoker.bb:~95: documenting why they stay alive (removing would renumber every opcode alphabetically after them and break the fixed-Case dispatch).
    • Case 501 (SCENERYOWNER no-op-with-sentinel) and Case 530 (SETOWNER void no-op).
    • Both commented impls at ScriptingCommands.bb:1046 and :1071.

Next contributor who hits any one of these has the full story without needing to grep the other four.

Why not remove the contract entries

The BlitzForge command-set parser assigns opcodes alphabetically by function name. Removing SCENERYOWNER and SETOWNER would shift every BVM alphabetically after them down by 2, breaking the fixed-Case dispatch at lines 501+ for SCREENFLASH / SCRIPTLOG / SETACTORX / ... / WARP. That's a ~250-Case renumber-or-mapping audit — risky for a doc-only improvement. Audit comments + the sentinel fix is the surgical-safe choice.

Test plan

  • compile.bat -t clean (Server + Client + GUE + Project Manager)
  • test.bat — all 26 tests pass
  • No content scripts in data/ reference SetOwner/SceneryOwner (grep clean)
  • CI green

🤖 Generated with Claude Code

@CoreyRDean CoreyRDean requested a review from a team as a code owner May 27, 2026 00:35
CoreyRDean and others added 2 commits May 26, 2026 19:38
…ad-BVM trap

The SCENERYOWNER dispatch case in RC_Standard_Invoker.bb at Case 501
popped 3 args from the BVM stack but did not push anything (the
;BVM_PushInt(BVM_SCENERYOWNER(...)) call was commented out alongside
the impl when the OwnedScenery type was disabled in ServerAreas.bb:53).

SCENERYOWNER's contract declares it as a `%`-returning function -- the
BlitzForge compiler emits the call expecting one int on the stack
afterwards. With the case popping 3 and pushing 0, the caller's
subsequent BVM operation pops the wrong slot, silently corrupting the
expression. This is a real bug, not a hypothetical -- any script that
ever called `SceneryOwner(zone, id)` would produce stack-shifted
expression results from that point until the next stack reset.

Fix: add `BVM_PushInt(0)` to Case 501 so the caller's stack stays
balanced. 0 reads as "no owner" -- the closest defined sentinel given
that the underlying OwnedScenery storage is gone.

Sibling Case 530 (SETOWNER) is void, so its 4-arg pop self-balances
and remains a silent no-op. Added an audit comment recording that.

Also added audit comments at all FIVE dead-API sites:

  1. RC_Standard_Invoker.bb:~95 -- contract entries for SETOWNER /
     SCENERYOWNER (cannot be removed without renumbering every BVM
     opcode alphabetically after them and breaking the fixed-Case
     dispatch at 1363 / 1494).
  2. RC_Standard_Invoker.bb:501 -- SCENERYOWNER no-op-with-sentinel.
  3. RC_Standard_Invoker.bb:530 -- SETOWNER no-op.
  4. ScriptingCommands.bb:1046 -- commented BVM_SETOWNER impl.
  5. ScriptingCommands.bb:1071 -- commented BVM_SCENERYOWNER impl.

The audit-comment cluster cross-links the 5 sites and the disabled
OwnedScenery Type, so the next contributor who encounters one of them
has the full story without grepping.

Recon was a sweep of every <BVM_*> contract entry against `^Function
BVM_*` impls (220 contract vs 222 impl, case-insensitive comm). Only
these two were contract-without-impl orphans; the 4 impl-without-
contract entries (BVM_RequirePrivileged, RequireSelfOrPrivileged,
ScriptPathIsSafe, SetWaitResult) are correctly-private helpers.

Compile-verified clean across Server/Client/GUE/Project Manager.
All 26 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reviewer corrections on the dead-BVM audit-comment cluster:

1. Stale line numbers (3 of 5 audit cross-references were off after the
   comment blocks themselves displaced the dispatch cases downward).
   Rewrote as symbolic references:
   - "ServerAreas.bb:53" -> "ServerAreas.bb (the Type OwnedScenery and
     Field OwnedScenery[] declarations are commented out)" -- the Type
     name is unique enough to grep.
   - "RC_Standard_Invoker.bb:1363/1494" -> "Case 501 / Case 530" --
     Case numbers don't drift even if the surrounding code shifts.
   - "ScriptingCommands.bb:1046/1071" -> the impls are now found via
     `grep ';Function BVM_SETOWNER'` / `;Function BVM_SCENERYOWNER`
     instead of by line.
   - "RC_Standard_Invoker.bb:~95" -> "near the top of this file (grep
     'DEAD-API')" -- the unique sentinel finds the comment.

2. "Above" wording was wrong (the contract entries are *after* the
   audit-comment block, not before). Replaced with "next two lines".

3. .bcs file was not updated. Added the same audit-comment block at
   src/RC_Standard.bcs:113-121 so a maintainer reading the static
   command-set file sees the same trap warning as one reading the
   runtime-string builder.

Recon also surfaced that the just-rebased develop pulled in PR #292
(Loom Alpha) which adds src/Loom.bb + src/Modules/Loom/Theme.bb.
Verified Loom compiles cleanly alongside the BVM fix.

Compile-verified across all 5 engine targets (Server + Client + GUE +
Project Manager + Loom).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@CoreyRDean CoreyRDean merged commit cb1c7f7 into develop May 27, 2026
1 check passed
@CoreyRDean CoreyRDean deleted the fix/dead-bvm-api branch May 27, 2026 00:43
CoreyRDean added a commit that referenced this pull request May 27, 2026
Pre-commit verification (the same symbolic-refs lesson from PR #297):
two of the cited line numbers for the importance check were off:
ServerNet.bb:353 should be :356, ServerNet.bb:367 should be :370.
Replaced the line-number list with symbolic references (function /
case names) per the doc-audit lesson.

Also adjusted the prose: noted there are SIX sites (not five) -- I
under-counted by lumping the two ServerNet handlers as one.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CoreyRDean added a commit that referenced this pull request May 27, 2026
docs/modules/scripting.md (the already-existing runtime-half overview)
references docs/modules/scriptingcommands.md as the implementation-
half companion -- but the file didn't exist. Dangling link surfaced
during iteration #38 recon.

Wrote the module-level overview for ScriptingCommands.bb (~3300 lines,
222 BVM_* functions):

  * File-structure section table (~50-3300 grouped by theme: privilege
    helpers / actor lifecycle / items / spells / attributes / party /
    output / persistence / UDP). Refresh trigger when reorganization
    happens; navigation aid, not strict spec.

  * Privilege gating section consolidates the four CLAUDE.md gate
    categories, the clicker-handle trap (`SI\AI = Handle(clicker)`
    for Examine/Trade/RightClick/ItemScript), and the full
    currently-gated brick-vector cluster (11 functions with their
    threat shapes).

  * Dead-API surface (BVM_SETOWNER + BVM_SCENERYOWNER) -- references
    PR #297's stack-balance sentinel fix and the opcode-stability
    rationale for keeping the contract entries alive.

  * Float / integer hardening overview -- ClampWorldCoord# /
    ClampSaneFloat# and the bounds-check-before-array-index pattern.

  * Handle-Null discipline -- the canonical entry pattern that every
    BVM body must follow (Object.X(handle) returns Null for stale
    handles).

  * "Adding a new BVM function" three-file-lockstep procedure
    (ScriptingCommands.bb impl + RC_Standard_Invoker.bb contract +
    dispatch Case + RC_Standard.bcs compile-time twin), the
    alphabetical-opcode-shift gotcha, and the privilege-gate decision
    tree.

  * Notable historical hardening table cross-referencing PRs #260,
    #237-#239, #246-#248, #233/#234, #300, #301, #304.

  * Related-modules section linking back to scripting.md /
    bvm-reference.md / RC_Standard_Invoker.bb / RC_Standard.bcs /
    ServerNet.bb / BVMPrivilegeGateTest.bb / CLAUDE.md.

Closes the dangling-link gap; future scripting.md readers now have a
landing page for the implementation half. rc_standard_invoker.md is
still a dangling link from scripting.md -- deferred to a separate
iteration (it's more arcane; the BVM-reference auto-gen already
covers the user-facing API surface).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant