What protects players and servers#
A game on Moud runs code on other people's computers. Players run your client scripts the moment they join, your server runs whatever a player's client sends it, and in a Collaborate session several people write into the same folder. This page is about the checks Moud makes on the way, so you know what the engine already refuses for you and what is still yours to check in your own scripts.
Most of it never shows. An honest game passes every check without noticing, and the limits sit far above what a real game does.
What a script can reach#
A Luau script reaches the engine through its globals and nothing else: the instance tree, signals,
task, the libraries in the reference, and the place's own files through require
and files.read. It has no os.execute, no io, no debug library and no way to load bytecode.
Objects from the engine reach a script as handles it can index, never as Java objects it could
reflect on.
The one way out of the sandbox is Java: the java and mixin globals, the mixins/ folder and the
languages that are not sandboxed, Java place scripts and Revo (see Other languages).
Those only exist for code the machine's owner trusts.
Where code comes from#
Every script host knows where its place came from:
| The place was | It counts as |
|---|---|
| opened from the project hub or the editor | local |
given at launch with -Pplace, or the place folder next to the game |
local |
| the game inside an exported game's jar | local |
| a game joined from Moud's catalog, a friend or a link | downloaded |
| a place the server moved you to inside a downloaded game | downloaded |
| a Collaborate working copy, which the host writes | downloaded |
Packages#
A package is Luau source pinned by a SHA-256 hash in place.toml. It runs in the same
sandbox as the place's own scripts, with nothing extra. Installing runs no code and nothing updates on
its own. A package that calls http or http.websocket has to declare it: the editor shows it
before you install, and the engine checks it while the package runs. A package whose files no
longer match their pin does not load.
Java access#
A place asks for Java in place.toml:
[trust]
java = trueWhat it gets depends on where it runs:
- A project you opened yourself gets
java,mixin, itsmixins/files and the unsandboxed languages, on the client and on the server the editor runs for it. - A downloaded game never gets them, whatever its
place.tomlsays. Its scripts seejavaandmixinasnil, itsmixins/files are not run, and a place written in Java or Revo does not start. The log says the game asked and was refused. - A dedicated server also needs whoever runs it to allow it, because a hosted server often runs a
place someone else wrote. Start it with
-Dmoud.trust.java=true. Without it the server logs thatplace.tomlasked and runs the place without Java. - Editor plugins never get them, in any project. A plugin from the catalog runs in every project you open, so it can't borrow the trust of one.
The setting is read when the place opens. Changing it later, by hand or by a Collaborate upload, takes
effect the next time you open the project. Without it a local place has no Java either, and a script
that uses it fails with java is nil, it only exists in a trusted local place.
Turning java on runs the place's scripts with everything your account can do. Only turn it on for
code you wrote or read. Opening a project someone sent you with java = true already in it is the
same as running a program they sent you.
Bytecode is never run#
Luau does not check the bytecode it loads, so a crafted chunk could corrupt the game's memory. Moud
compiles every script itself, from source, each time it loads one, and publishing ships client
scripts as source (see Publishing a game). A file that starts
with --!moud-bytecode, the marker older versions of Moud wrote, is refused with an error that names
it:
res://client/main.luau is precompiled bytecode from an older Moud. Bytecode cannot be checked, so it is not run. The game has to be published againA version published before scripts shipped as source keeps its compiled scripts, and they stop with that error until the game is published again. Nothing in the place has to change; publishing it once more is enough.
Limits#
Every VM has two limits, on the client and on the server, for place scripts, parallel actors, editor plugins, the loading screen and generation scripts alike:
| Limit | Default | Changed with |
|---|---|---|
| time a script runs before it waits | 10 seconds | -Dmoud.script.seconds=20 |
| memory one VM holds | 512 MB | -Dmoud.script.megabytes=1024 |
The limits belong to the machine, not to the game, so a downloaded game cannot raise them. Each parallel actor and each generation thread is its own VM with its own memory limit.
Time. The clock starts when the engine hands a script control, through a resume, a signal or a
require, and stops when the script waits or returns. A script that runs past the limit stops at the
line it reached:
server/main.luau:2: this script ran for more than 10 seconds without waiting and was stopped, give long loops a task.wait() (-Dmoud.script.seconds raises the limit)Only that thread stops. Every other thread, connection and script keeps running, and the error shows
in the Output panel like any other. A pcall around the loop does not save it: once the time is up,
the thread stops again at its next call or loop until it waits or returns.
Memory. An allocation that would take the VM past its limit is refused, and the script that asked
for it stops with this script went over the 512 MB of memory a vm may hold and was stopped. The
collector frees what it held, and the rest of the game goes on.
A game with real work to do in one go, such as building a large map at start, spreads it over frames
with task.wait() rather than raising the limit.
What reviewers read is what players run#
A published version waits for a moderator before anyone can play it. The moderator reads your scripts on the review page, which shows the files from the version's client bundle. Players never download that bundle: they fetch the version's files one by one, each by its hash.
So the catalog ties the two together when you publish. Every file players would get has to be in the
client bundle with exactly the same bytes, or the publish is refused with unreviewed_content and
the names of the files that did not match. Pictures, sounds and videos are the only files allowed to
stay out of the bundle, since they never run as code. A bundle that lists the same file twice is
refused too, because the review page could show one copy while players got the other.
The review page also lists what reviewers should look at closely:
- Scripts reach for Java or the mixin hooks. A script that names the
javaormixinglobals, or reaches the globals by name withgetfenvor_G[...], is pointed out with the file and the name it uses. - Contains compiled scripts nobody can read. A script has to be source. A file that starts with the bytecode marker, or holds bytes no text file does, is pointed out wherever it is, in the players' files or the server's.
Neither stops a publish on its own, a reviewer decides, but a version that hides what it does is not going to be accepted.
Remotes only reach what the player was sent#
Every instance has an id, and ids count up as instances are made. A player's client names an
instance by its id when it fires a remote or passes an instance along, so a modified client could
try ids it was never sent, such as a remote you keep in ServerStorage for your own server scripts.
The server answers only for instances that were replicated to that player. A remote they were not
sent does not exist as far as their messages go, and an instance argument they were not sent arrives
as nil.
-- server/main.luau
local storage = game.world:add("ServerStorage")
local restart = storage:add("RemoteFunction", { name = "restartRound" })
restart.onServerInvoke = function(player)
startRound()
return true
end
local inspect = game.world:add("Remote", { name = "inspect" })
inspect.onServer:connect(function(body, part)
if part == nil then return end
print(body.name, "looked at", part.name)
end)restartRound waits in server storage, so no client has been sent it. A client that invokes its id anyway
gets "that remote function no longer exists", the same answer as for a remote that was destroyed.
inspect is in the world, so players can fire it, but the part they name has to be one they were
sent. With streaming on, that means it is near them; a part across the map, or one inside server
storage, comes through as nil. That is why the handler checks for nil before using it. Any value
a client sends is still the client's word, so check it the way you would check a number or a string.
Chat#
Chat has a flood check: a player can send a few messages at once and then about one and a half a second. The check happens as a message comes in, before it is queued, so a client that sends thousands at once costs the server nothing and hears "You're sending messages too quickly" once.
What a server can make a player keep#
A server pushes changed .anim and .bbmodel files to its players while a place is edited live.
Each player keeps at most 1024 of those copies and 128 MB in all, and drops what comes past that.
The server stops taking new ones from editors at the same limit, since it keeps them to send to
everyone who joins later.
Joining from outside the game#
A moud:// link opened from a browser or another program, a play click on the website and a party
leader taking the party into a game all start a game without the player pressing anything in Moud.
Each of them asks first, with the game's name and who invited the player, and nothing happens until
they press Join.
A join made on the website while Moud is closed waits on the player's account for three minutes and
is used once. The moud:// handler is only registered for launchers that can start an instance from
a command line, and it only takes an id from the link: the address of the server always comes from
the catalog.
The catalog also checks the place name every fleet reports for its servers. Players' clients use it
as a folder name, so it is letters, digits, _ and - only, and a server that reports anything else
is not listed.
Collaborate#
A Collaborate session hands real power to the people in it. An editor can change every script, server scripts included, and the host's computer runs them. Add people as Can edit only when you would let them run code on your machine; everyone else is View only.
Collaborators are accounts, not names#
The list in .moud/team.json keeps each collaborator's Minecraft account id next to their name:
{
"collaborators": [
{ "id": "069a79f4-44e9-4726-a5be-fca90e38aaf5", "name": "Alex", "role": "edit" },
{ "id": "61699b2e-d327-4a01-9f1e-0ea8c3f06bc6", "name": "Sam", "role": "view" }
]
}A player is let in by the account they joined with, so someone who picks Alex's name gets nothing.
When you add a name in Permissions, the host looks up the account the way the server whitelist
does. A file from before ids were kept is moved over the first time the session opens; a name with
no account behind it stays in the file but lets nobody in. A role other than edit reads as view
only, so a typo never gives edit rights.
What travels#
Only the kinds of file a place is made of are shared, both ways:
| Scripts | .luau, .lua |
| Data | .scene, .json, .toml, .txt, .md, .csv, .anim, .bbmodel, .pose |
| Pictures, sound and video | .png, .jpg, .webp, .gif, .bmp, .hdr, .exr, .tga, .ogg, .wav, .mp3, .flac, .opus, .mp4, .webm, .mkv, .mov, .m4v |
| Models and fonts | .glb, .gltf, .bin, .obj, .ttf, .otf |
Hidden files and folders such as .moud, .git and editor settings stay on each computer, and so
do build and node_modules. Java, jars, native libraries and shell scripts never travel, and a
compiled script is refused. A place holds at most 10,000 shared files and 1 GB; a save past that is
refused and the editor keeps your copy in drafts.
Your working copy#
A collaborator's working copy only takes the host's files from that list. When you join and your copy
has files the host does not, they are moved to .moud/drafts instead of being deleted, and the Output
panel says how many. When the host removes a file you have edited since it arrived, your version goes
to drafts before the file goes.
Viewers and busy editors#
A view only member can not hold parts, so they can not lock anyone out of editing. Presence, the selection and camera every editor sends, is limited to ten updates a second, and a joining collaborator can ask for the file list and the files a few times, not endlessly.
The block tools have limits of their own: one change touches at most 4,000,000 blocks, a stack makes at most 64 copies, and an extrude goes at most 64 layers deep.
