Skip to content

Security, the sandbox and capabilities

Status: Implemented for the folder rule, the WASM container, the Lua sandbox, limits and containment, the file API and permission-gated HTTPS (API 1.20). The other capability APIs are Planned.

A mod is code from someone else running on the player’s computer or the operator’s server. CLight’s security model is built around one rule: every mod runs in a sandbox and can’t harm the machine. There is no unsandboxed tier and no trust prompt. This page explains how the sandbox works, what it protects against, and what it doesn’t.

For the concepts, read Capabilities and permissions first. The API is in Permissions and security, and the capability list in Capability list.

Threat model

Threat Data mods Lua and WebAssembly mods
Read, change or delete the player’s files impossible impossible outside mods/<id>/ (read-only) and mods/<id>/data/ (read/write)
Start a program, load a DLL, plant an executable impossible impossible: no process, library or exec APIs exist; files with executable extensions can’t be written
Download something impossible only over HTTPS from hosts it declared and the player allowed (net.http:<host>), saved only into mods/<id>/data/, never run (API 1.20)
Steal the account token impossible impossible: tokens are never exposed through any API
Crash the game malformed data is rejected by the parsers no: traps and errors are contained
Freeze the game no no: callbacks are interrupted after their time limit
Exhaust memory parsers have limits no: memory is capped per mod
Cheat on servers (automation, packet tampering) n/a no input-synthesis or raw-packet APIs exist (planned behind highlighted capabilities)

The sandbox cannot make a mod honest about game behaviour (a mod can still be annoying or cheat within the game API). It makes sure a mod can’t reach outside the game.

The folder rule

Shared by both runtimes (one implementation in client/clight/core/sandbox.cpp):

  • read-only: mods/<id>/, the mod’s own unpacked package
  • read/write: mods/<id>/data/ only
  • rejected: absolute paths, drive letters, .., reserved device names, : streams, links and junctions that leave the root, and executable file types (.exe .dll .bat .cmd .ps1 .com .scr .msi .vbs .js .jar .sh .so .dylib and more)
  • the file API (API 1.20) never follows, lists or writes through a link or junction, writes atomically, and caps data/ at 256 MiB

HTTP (API 1.20)

  • Only hosts covered by a net.http: capability the mod declared and the player allowed; a grant in permissions.json for a capability the manifest doesn’t list counts for nothing.
  • https only (http just for localhost / 127.0.0.1 / [::1]); every redirect target is checked again, never https to http.
  • No cookies, a fixed User-Agent with the mod’s id, connection headers (Host, Proxy-*, …) refused.
  • Time, size and request-count limits per mod; requests run off the game threads; answers come back on the tick.
  • Downloads (save_to) land only in data/, through the same folder rule, and are never run.

A .clmod is unpacked once into mods/<id>/ (with a stamp, so an update replaces it), so a packaged mod and a mod folder behave the same.

How the sandbox is built

WebAssembly (Wasmtime)

  • Memory isolation. A module can only address its own linear memory. Every pointer it passes to the host (cl_str, struct pointers, out-buffers) is an offset into that memory, and the host bounds-checks it before reading or writing. Host memory is never mapped into the module.
  • Import allow-list. The module may import only functions from the clight module plus WASI. WASI is configured with exactly two preopened directories (the mod folder read-only, data/ read/write), no sockets, no environment variables and no arguments. A module importing anything else fails to load (CLL-3004).
  • Handles, not pointers. Every game object is a 64-bit handle with a type tag and a generation. The host validates each one; a forged or stale handle returns an error and can’t reach engine memory.
  • Time limits use Wasmtime’s epoch interruption: a call past its deadline traps, and the mod is turned off (CLL-3007).
  • Memory limits cap linear memory at limits.memory_mb (default 256 MiB).
  • Compilation is ahead of time with Cranelift on first load; the compiled code is cached as .cwasm under cache/clight/ and recompiled when the module or the engine changes.

Lua

  • One lua_State per mod. Mods cannot see each other’s globals.
  • Dangerous libraries are never loaded: no io, no package, os only with time/clock/date/difftime, debug only with traceback, no binary chunk loading. require resolves only inside the mod’s own scripts/ folder. Files only through cl.fs, which goes through the folder rule.
  • An allocator with a per-mod limit, and an instruction-count hook for the time limit.

Checks on every call

Every API entry point is generated from clight.idl with the same validation (The binding generator and CLight IDL):

  1. handle generation and type,
  2. side (@side) and, where it matters, the phase,
  3. argument ranges and the caller’s namespace (a mod registers only <its id>:...).

Traps and containment

When a callback faults (a WASM trap, an uncaught Lua error, a time-limit interrupt):

  1. The callback is aborted and its effect is “no change”: a cancellable event stays as it was, a behaviour override falls through to the default behaviour.
  2. The mod-side backtrace is logged (symbolized for WASM with debug info; a Lua traceback).
  3. Lua: after 10 errors within a minute the mod is disabled for the session. WASM: after 10 traps, or at once when a call runs past its deadline. Its subscriptions and tasks are removed; the game keeps running, and the Mods screen shows the error.
  4. A failing mixin is turned off for the session and vanilla runs (CLL-7009).

Mods must not rely on a trap to undo partial work. Writes made before the fault stay applied.

Capabilities in practice

"permissions": ["net.http:api.ruby-tools.example"],
"optional_permissions": ["os.open_url"]
  • Declare the narrowest capability that works: a host, not net.http:*.
  • Put capabilities the mod can live without in optional_permissions, check them with cl_cap_granted and ask with cl_cap_request when the player uses the feature.
  • “Warn” capabilities (net.http:*, os.clipboard, input.synthesize, packets.raw) are highlighted in yellow on the Permissions screen. Explain in your description why you need them.
  • Servers can’t push capabilities to clients. A server-required mod declares its own client capabilities, and the client’s player decides.

Security guidance for mod authors

  • Treat all network input as hostile. Payload handlers on the server receive data from untrusted clients. Validate lengths, ranges, registry ids and permissions before acting. Rate-limit expensive requests per player.
  • Check rights on the server, never trust the client’s claim (command permission levels, your own lists).
  • Don’t build commands from player text. cl_cmd_run runs as the console with op level 4.
  • Don’t log secrets. Logs are often uploaded for support.

What operators should do

  • Keep the default sandbox limits on public servers.
  • Review config/clight/permissions.json like a list of installed programs’ rights.