Skip to content

security(bvm): gate SETMAXATTRIBUTE / CHANGEMAXATTRIBUTE — clicker brick vector#300

Merged
CoreyRDean merged 1 commit into
developfrom
security/gate-set-max-attribute
May 27, 2026
Merged

security(bvm): gate SETMAXATTRIBUTE / CHANGEMAXATTRIBUTE — clicker brick vector#300
CoreyRDean merged 1 commit into
developfrom
security/gate-set-max-attribute

Conversation

@CoreyRDean
Copy link
Copy Markdown
Collaborator

Summary

Sibling-asymmetry gate added. BVM_SETATTRIBUTE / BVM_CHANGEATTRIBUTE have been BVM_RequirePrivileged-gated since the original privilege sweep — their HealthStat branch falls through to KillActor(Actor, Null) and a clicker exploit could one-shot the player. The MAX-counterparts (BVM_SETMAXATTRIBUTE, BVM_CHANGEMAXATTRIBUTE) sit one function block below in ScriptingCommands.bb and were never gated. Discovered during PR #299 (P_StatUpdate detail page) recon when cataloguing the four SET/CHANGE/SETMAX/CHANGEMAX importance dispatchers.

Threat

Non-priv NPC's Examine/Trade/RightClick/ItemScript can call:

Exploit Effect
SetMaxAttribute(player, "Health", 1) Permanent max HP = 1, next damage tick kills
SetMaxAttribute(player, "Speed", 0) Player can't move
SetMaxAttribute(player, "Energy", 0) Player can't cast spells
ChangeMaxAttribute(player, "Health", -big%) Same brick via the relative-mutation path

Why full-priv (not self-or-priv)

For Examine/Trade/RightClick/ItemScript spawns, ServerNet.bb calls ThreadScript(script, method, Handle(clicker), Handle(NPC)), so SI\AI = Handle(clicker). A BVM_RequireSelfOrPrivileged(Param1) gate would let SetMaxAttribute(clicker_handle, "Health", 1) reach the lethal path because Param1 = SI\AI. Full-priv (BVM_RequirePrivileged) is the correct choice — same reasoning as the SET/CHANGE pair (see CLAUDE.md "BVM clicker-handle trap" and the audit-comment cluster at ScriptingCommands.bb:2209-2222).

Test coverage

7 new tests in src/Tests/Modules/BVMPrivilegeGateTest.bb:

  • testSetMaxAttributeGateBlocksArbitraryTarget (Param1 ≠ SI\AI)
  • testSetMaxAttributeGateBlocksBrickingOwnAITarget (Param1 == SI\AI — the clicker shape that defeats self-or-priv)
  • testSetMaxAttributeGateBlocksBrickingOwnContextTarget (Param1 == SI\AIContext)
  • testSetMaxAttributeGatePassesForPrivileged
  • testChangeMaxAttributeGateBlocksArbitraryTarget
  • testChangeMaxAttributeGateBlocksBrickingOwnAITarget
  • testChangeMaxAttributeGatePassesForPrivileged

Test file header updated from "Seven BVM functions" to "Nine BVM functions" (the SETMAX/CHANGEMAX pair joins the gated set).

CLAUDE.md update

"Pairs to keep in lockstep" list at line 246 extended:

BVM_KILLACTOR / BVM_SETATTRIBUTE / BVM_CHANGEATTRIBUTE ..., BVM_SETMAXATTRIBUTE / BVM_CHANGEMAXATTRIBUTE (no KillActor fall-through but still a brick vector — SetMaxAttribute(player, \"Health\", 1) permanently nerfs max HP to 1 so the next damage tick kills...). All five families use BVM_RequirePrivileged()...

Test plan

  • compile.bat -t clean across all 5 engine targets (Server + Client + GUE + Project Manager + Loom)
  • test.bat BVMPrivilegeGateTest — all gate cases pass
  • test.bat — full suite: 26 of 26 pass
  • CI green
  • No content script in data/ currently calls SetMaxAttribute / ChangeMaxAttribute from a non-priv path (the gate would log + return False, but legitimate priv-spawn calls — quest reward scripts that adjust max-Health, etc. — keep working unchanged)

🤖 Generated with Claude Code

…ick vector

Sibling-asymmetry gate added. BVM_SETATTRIBUTE / BVM_CHANGEATTRIBUTE
have been BVM_RequirePrivileged-gated since the original privilege
sweep -- their HealthStat branch falls through to KillActor(Actor, Null)
and a clicker exploit could one-shot the player. The MAX-counterparts
sit one function block below in ScriptingCommands.bb and were never
gated. Discovered during PR #299 (P_StatUpdate detail page) recon when
auditing the four SET/CHANGE/SETMAX/CHANGEMAX mutators for the doc's
importance-check catalogue.

Threat: non-priv NPC's Examine/Trade/RightClick/ItemScript can call:
  SetMaxAttribute(player, "Health", 1) -> permanent max HP = 1, next
                                          damage tick kills
  SetMaxAttribute(player, "Speed",  0) -> player can't move
  SetMaxAttribute(player, "Energy", 0) -> player can't cast spells
  ChangeMaxAttribute(player, "Health", -big%) -> same brick via the
                                                  relative-mutation path

For Examine/Trade/RightClick/ItemScript spawns, ServerNet.bb calls
ThreadScript(script, method, Handle(clicker), Handle(NPC)), so
SI\AI = Handle(clicker). A self-or-priv gate on Param1 would let
SetMaxAttribute(clicker_handle, "Health", 1) reach the lethal path.
Full-priv (BVM_RequirePrivileged) is the correct choice -- same
reasoning as the SET/CHANGE pair (CLAUDE.md "BVM clicker-handle trap").

Test coverage in src/Tests/Modules/BVMPrivilegeGateTest.bb:
  - testSetMaxAttributeGateBlocksArbitraryTarget (Param1 != SI\AI)
  - testSetMaxAttributeGateBlocksBrickingOwnAITarget (Param1 == SI\AI,
    the clicker shape that defeats self-or-priv)
  - testSetMaxAttributeGateBlocksBrickingOwnContextTarget (Param1 ==
    SI\AIContext, NPC-self-spawn shape)
  - testSetMaxAttributeGatePassesForPrivileged
  - testChangeMaxAttributeGateBlocksArbitraryTarget
  - testChangeMaxAttributeGateBlocksBrickingOwnAITarget
  - testChangeMaxAttributeGatePassesForPrivileged

Plus updated the test file header (was "Seven BVM functions had effects
identical to already-gated peers" -- now nine, since SETMAX and
CHANGEMAX join the gated set).

CLAUDE.md "Pairs to keep in lockstep" list extended:
"...BVM_SETMAXATTRIBUTE / BVM_CHANGEMAXATTRIBUTE (no KillActor
fall-through but still a brick vector...). All five families use
BVM_RequirePrivileged()..."

Compile-verified clean across all 5 engine targets.
All 26 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@CoreyRDean CoreyRDean requested a review from a team as a code owner May 27, 2026 01:23
@CoreyRDean CoreyRDean merged commit d67b267 into develop May 27, 2026
1 check passed
@CoreyRDean CoreyRDean deleted the security/gate-set-max-attribute branch May 27, 2026 01:26
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