ArTchie Studios
ArTchie-Bedder-Suite / Docs / BedderScript

BedderScript security model

Audited 2026-08-15 (line-by-line review of the escape surface + DoS hardening pass with battery proof). This file states what is guaranteed, what is the embedder's job, and what a hostile script can still do.

Threat model

The adversary is a hostile script — a mod, a player-made level, pasted code. The script is untrusted; the embedding application and its host implementation are trusted.

Scope note (2026-08-15): everything below concerns the script sandbox. The scene-op booklet and the asset catalog are a different, wider trust tier — see "Scene mods and assets" at the end. Statements like "no reflection on script input" are about scripts, and remain true; they are deliberately NOT true of booklets.

Verified: no escape surface

A script cannot reach the CLR, the filesystem, the network, or arbitrary Unity API:

  • No reflection anywhere on script input. GetProperty/SetProperty are closed switches (name/tag/active/x/y/z — that is the entire set). CallMethod on the Unity host answers one verb. Component types resolve through an approved-name table, never Type.GetType.
  • No eval, no IL, no codegen — source compiles once to bytecode for a closed-opcode VM.
  • No IO capability exists to misuse: save_* is an in-memory dictionary owned by the host; there is no file, network, process, or thread API in the language.
  • Walking an exposed object into CLR metadata (.GetType().Assembly and friends) is impossible by construction, not by configuration — there is no member-access path from a script value to CLR metadata.

Enforced limits (each has a battery test where it matters)

Limit Enforced at Notes
MaxInstructionsPerFrame / PerCall every opcode dispatch
MaxExecutionSeconds every dispatch (wall clock, per outermost invocation)
MaxRecursionDepth / MaxStackDepth frame push / stack push script recursion
MaxEventChainDepth emit
MaxCollectionSize array/map growth + indexing
MaxScriptMemoryBytes string concat, join, replace (worst-case pre-charge) allocation budget per invocation — resets like the instruction budget
MaxSpawnedObjects spawn builtin cumulative per VM session
MaxCoroutineCount coroutine create
Parser nesting (fixed, 200) Expression/Statement/Unary a compile-time bomb (thousands of nested parens / unary chains) is a diagnostic, not a process-killing StackOverflow

Worst case for a hostile script: it hits a limit and its invocation stops with an error the host sees. (As of 2026-08-15 this sentence is true for every limit in the table.)

The embedder's duties (holes only YOU can open)

  1. ModuleResolver — you map module names to source. Never map names to file paths without canonicalization; import names are attacker-chosen strings (../../secrets must not work because of YOUR code, not ours).
  2. RegisterApi / custom hosts — every function you register is capability you grant. The sandbox's whole guarantee is the sum of what the host exposes. No reflection-based auto-binding; keep it that way.
  3. Scene authority — the default Unity host gives scripts whole-scene reach (find + destroy/set_position on anything nameable). That is machine-safe but game-integrity- trusting. If mods should not grief the scene, scope a host (ExtensibleEngineHost) that resolves names against an allow-list instead.
  4. Source size — cap script file size at your load door; the parser is robust but a 100 MB source string still costs memory to lex.

Known residual risks (accepted, documented)

  • Host-call wall time is not metered per call — a script may call find() (a scene scan) thousands of times within its instruction budget. Bounded by MaxInstructionsPerFrame; a per-frame host-call budget is the growth path if a real workload needs it.
  • The memory budget is allocation accounting at the known amplification points (concat, join, replace), not a GC-integrated heap meter. Amplifiers are pre-charged with worst-case estimates; steady accumulation across frames is bounded by MaxCollectionSize.

Scene mods and assets — the LOCAL trust tier (added with the scene-op spine)

The booklet (Documents/<product>/SceneMods/<scene>.json) and the asset catalog (Documents/<product>/Assets/) are the player modding their own machine — the same trust tier as editing a save file. Within that tier, deliberately wider powers than scripts get:

  • add_component / set_property ops resolve any loaded component type by name and set members by reflection (the in-game inspector needs full reach). This is the local tier working as designed.
  • Asset files pass an extension allow-list (image/audio/model only — never code), a 50 MB cap, and are sha-deduped copies; importer parse failures are caught, never fatal.
  • OBJ/MTL/GLB parsing and Texture2D.LoadImage run on user bytes — malformed files fail the import, they do not execute.

NOT YET HARDENED — do not ship booklet/asset SHARING without this: a booklet downloaded from another player is not save-file-tier, it is hostile-input-tier. Before any sharing feature: (1) an op allow-list mode for add_component/set_property (curated types + members, like the script-side table), (2) reject catalog fileName entries containing path separators at load, (3) cap booklet op count per file. Until then, the only supported booklet source is the local player's own machine.

Source in the box: Assets/ArTchie Studios/ArTchie-Bedder-Suite/com.bedder.script/SECURITY.md