Skip to content

Instantly share code, notes, and snippets.

@appgurueu
Last active December 16, 2024 14:42
Show Gist options
  • Save appgurueu/4f419f4b4c10dc850cc4049962cb101a to your computer and use it in GitHub Desktop.
Save appgurueu/4f419f4b4c10dc850cc4049962cb101a to your computer and use it in GitHub Desktop.
Server-required client side scripting proposal for Luanti

Introduction

SSCSM is brought up regularly as the proper way to implement many features without excessive jank (and implementation hardship, since many things simply aren't available and shouldn't be available on the server). The client needs to become more than a "sock puppet" of the server. We need client scripting. This has been clear for a while now.

I was reminded of this recently due to a brief discussion on Mastodon.

Prior Art

Multicraft has had a simple SSCSM implementation for 3-4 years now and has, to my knowledge, not suffered any widely known issues.

Roblox uses its own scripting language, Luau, which has a hardened interpreter so code (including client "local scripts") can run "side by side" with the engine:

Luau limits the set of standard libraries exposed to the users and implements extra sandboxing features to be able to run unprivileged code (written by our game developers) side by side with privileged code (written by us).

Scripting in games is often hardly sandboxed at all and you are expected to simply trust the authors of scripts.

If we're looking at "real" game engines, which we are (supposedly) striving to be, those tend to make matters much simpler: If you run a game using that engine, you must trust it like you would any other piece of software. The engine does no sandboxing at all. Examples here would be LÖVE or the 3d pendant, LÖVR.

Now, Luanti isn't quite an engine, but more of a platform presently, and we do want users to be able to try out games and mods with basic protection; in particular, we want users to just be able to join a server relatively seamlessly without incurring terrible risks.

Luanti is sometimes also compared to something like a "browser" (for voxel games). Mainstream browsers have process-level isolation of JavaScript (but they also have vastly more development resources, and a vastly larger attack surface via the complexity of web APIs and JS itself). I think we should view Luanti as more of a game engine / platform than a "browser".

Implementation

The implementation of SRCSM I propose would be relatively straightforward and analogeous to SSM:

  • Enable client modding by default. Make CSMs as easy to manage in the main menu as SSMs.
  • Have a "builtin" CSM which is loaded by default to set up some basic wrappers.
  • Allow distribution of CSMs via CDB. CSMs should be able to depend on SSMs
  • Servers (via the serverlist) advertise the CSMs they require, identified as author/name:version. 1

(This is only the basic starting point: The CSM API would need to be vastly extended to be really useful. 2)

Since we're writing fresh APIs here, we can also set some design choices right. We can start with a very restrictive Lua sandbox (as it already is). We could give different mods different Lua environments in case we plan to have more "privileged" CSMs eventually, or we could have an entirely separate "secure" environment. Mods should obtain references to the public API tables of each other via local mod = require"mod". These tables should be returned from each mod's init.lua. (We could also allow recursive require more generally. But in the bigger picture it doesn't matter much.)

Simplicity

I think such an implementation would be likely to be much more simple and hence much more likely to land anytime soon because it doesn't depend on implementing a process sandbox (and along with it, a way for the client to call Client Lua API functions via IPC and vice versa; though at least we already have a packer for (de)serialization of Lua values).

Security

First, let me suggest that we don't need that security. It can't be a "must have", otherwise SSMs as they are would be unacceptable. If users may trust SSMs enough to risk them escaping the sandbox, they may do the same for CSMs.

Additionally, the Luanti community has, to my knowledge, hardly ever seen any attempts at (sophisticated) "malicious" mods. The "worst" I've seen so far was a silly attempt by HybridDog to get a more predictable RNG "easter egg" (presumably so they could have in-game economy advantages) into techage some years ago, and that was met with a lot of backlash. What is the realistic attacker model supposed to be here that justifies the concerns? I simply don't see any bigger actors attacking Luanti anytime soon.

Second, I do not think that we stand to gain much security by adding a process sandbox.

We still have SSMs, and users happily downloading them from CDB and running them. If there is a malicious mod which escapes the (SSM) sandbox or exploits a Lua implementation bug, I would assume the vast majority of users to already be vulnerable this way. (Also, realistically, who would waste a Lua implementation exploit on Luanti? Furthermore, isn't Luanti much more likely to be vulnerable than both PUC Lua 5.1 (+ patches) and LuaJIT, given that it is less widespread and has a massive codebase (at least compared to PUC)?)

Notably, a process-level sandbox can not protect against any kind of vulnerability that is triggered by malicious data / commands being processed in the client C++ code, of which I assume there will still be a fair amount of.

We probably have a decent attack surface already via our current networking API and dubious file readers already (though those have improved since sfan's fuzzing efforts a while ago).

Overall I think security absolutism isn't the way to go here. I don't think we'll ever get to the point where we our users would be fine with running completely untrusted code; we will always need to ask for consent and have to make explicit which code from where the user is trusting.

Hence trust becomes a central part either way. I think with CDB and the serverlist, which are both accessed over HTTPS, we're already in a decent position. However, we should probably be additionally authenticating servers. This is possible if we identify them with HTTPS websites. I understand some form of server authentification will be required for QUIC anyways.

We also have CDB as another layer of defense in this scenario. If a CSM was found to be malicious, it could be pulled from CDB rather quickly, limiting the damage.

"Paranoid" users can (and should) still configure their Luanti to be restricted to Luanti-related files at an operating system level or via containerization.

Performance

A process-level sandbox is pretty much obviously worse for performance. The question just is how much. My estimate would be order(s) of magnitude, but of course this depends a lot on the hypothetical API calls and chosen implementation of IPC we'd be looking at.

Even if we assume that throughput would be okay, latency is still an issue: Consider all those API functions that are only somewhat okay in terms of performance because a fast ping-pong with the engine is currently possible, for example core.get_node. Such functions would get prohibitively slow. This would force us into designing APIs that do "batching" everywhere, such as VoxelManips. But such APIs can be both (1) more awkward, more work to use and implement; (2) simply less flexible: Consider e.g. the case of a pathfinder.

VoxelManip is not very suitable here, since the nodes you explore tend to be rather sparse. You don't want to load dense cuboids. Which nodes you explore next depends on the nodes you explored previously, so there is simply no way to batch this. 3

The cost of IPC has been compared to that of "some form of IPC" used for "input and OpenGL rendering". I don't think this is a comparison that lets us draw solid conclusions. First, I'd like to throw out input: The throughput you need for that is minuscule. And even if you have 1ms latency, it wouldn't be noticeable (and 1ms is on the high end; you might very well have 10ms and not notice). If however CSMs were limited to 1000 or 100 sequential API calls per second, that would be very noticeable depending on the CSM.

Now onto OpenGL. First, I think there are many confounding factors here, and second, depending on the API usage, it simply doesn't run very smooth. In particular, issue too many (thousands) of drawcalls, and you get performance problems for workloads the graphics card could (if properly batched) very well handle. This is a problem we have, and it is at least facilitated by OpenGL design. Now this does not suffice to conclude that IPC has a performance problem, but I do want to point out that "OpenGL is fast enough" (if used properly) simply does not imply "IPC is fast enough" (for the kind of API we want to design).

I would still estimate that a "Client API over IPC" will be order(s) of magnitude slower if you issue many sequential API calls (of which you need to get the results back) in a "ping-pong" fashion.

Cheating

This is something I personally consider pretty obvious, but to alleviate the concerns:

CSM restriction flags are the best you can do in open source software and must be good enough. 4 If people want to cheat, they can already download readily available cheat clients that don't have CSM restrictions (and often even have extended CSM APIs). (Heck, people might be inclined to install these clients just because they offer them more powerful APIs, which even if all restrictions are off they can't get on current Luanti.)

User Experience

Users should simply be able to install and play games as is the case already. These games may include CSMs now. The only really interesting part is if a user joins a server which requires the user to install CSMs which the user doesn't have.

In that case, a popup should offer the user to install these CSMs from CDB - making it clear what they are installing and from whom (such that they can e.g. recognize trusted community members or established packages).

Users want to be able to see the code running on their machines, hence we want to force required CSMs to be installed, and not to just live somewhere in memory. 5

And again CDB serves as a good place where CSMs can undergo community scrutiny. To quote Sumi:

possibly, CSMs are hosted externally in a repo, and cannot be configured / sent from within the running server; that is, you have to use a public facing git repo / whatever else in order to send CSMs, that anyone can review, and the server just tells you where to find it. Basically, so that the server doesn't actually have the ability to send any actual code, only tell you where to find that code (this is actually how I assumed it would be done) With that, code can be scrutinised freely and people can just decline to run any CSMs. It's sketch, but a lot less so than many situations. I think best case is just have people try to hack it for a while and plug the holes they find. Having normal CSMs is a good way to test it I guess.

Conclusion

I think we should implement CSMs largely analogeous to SSMs for a first implementation, with users choosing which mods to trust (enough) to risk putting them in a pretty much Lua-only sandbox. Why not do what already worked well for SSMs for CSMs as well?

More extensive security measures, e.g. in the form of a process sandbox, can, but need not come later, and should be optional to avoid unnecessarily compromising on performance.

Footnotes

  1. Currently, we only keep a single version of a given mod installed. Ultimately I think we'll have to maintain multiple versions. Ideally we would even let mods specify which versions they depend on, the way good package managers these days work.

  2. The CSM API is currently experimental and we should not immediately declare all of it stable. Rather, we should start out by declaring some parts of it which look well-designed and are battle-tested "stable", and then slowly moving APIs to the "stable" section as they stabilize.

  3. In this specific case, an engine pathfinder could be considered an option, but is likely to turn out too inflexible. Pathfinding maybe isn't the best example for client mods, but you get the idea: Anything that requires low-latency API calls might suffer greatly from IPC.

  4. We need to give game developers the tools to implement proper server-side anticheat, but that's a different topic.

  5. This obviously can't really fulfill the desires of some proprietary game authors, but I think that's fine.

@grorp
Copy link

grorp commented Dec 16, 2024

Something that's missing from your proposal, which needs to be thought about now imo, is how to deal with different levels of trust for CSMs from a server / "cheating" perspective. As you say yourself

The CSM API would need to be vastly extended to be really useful.

For example, the CSM API should be extended so that games can completely replace player physics with their own implementation via a required CSM. However, this would be a free cheating feature built into the official client, since player-chosen CSMs would also be able to use it.

How do you fix this? You could put the new API behind csm_restriction_flags, so that servers can disable it. However, if a server disables it, the CSMs required by the server won't be able to use it either. So there needs to be a way for server-required CSMs to get access to more APIs/privileges than player-chosen CSMs.

A possible solution would be "only apply csm_restriction_flags to player-chosen CSMs, ignore them for server-required CSMs". Servers could then set very restricted csm_restriction_flags to prevent cheating without problems.

However, smart cheaters could simply locally copy their cheating code into a server-required CSM to circumvent csm_restriction_flags, making them completely useless. So you'd have to do checksum verification of server-required CSM contents or something (and of builtin, for that matter)...

And all of this is without cheaters needing to modify the engine...

Servers (via the serverlist) advertise the CSMs they require, identified as author/name:version.

Unlisted/local servers need to be able to require CSMs too, so servers sending their "required CSMs" list via the serverlist isn't enough. The server needs to send a list of required CSMs at early connection stage. The serverlist could also provide that list, but only for additional convenience / earlier availability.

@Desour
Copy link

Desour commented Dec 16, 2024

The argument about security of SRCSM because it's hosted on contendb was brought up before, and then it was brought up that one could just make a mod that loads arbitrary code sent by the server at runtime, so it doesn't work.

(And I don't want to repeat in detail here the arguments against SRCSM that were brought up before, like loosing decentralization, users not being able to judge what is secure, ...)

About the performance part: I don't trust estimates that are not based on real world data.
FYI, in luanti-org/luanti#15246, it's about 0.36 us (360 ns) per call (real world results may differ).

Btw, please let's not mix the SSCSM (or SRCSM, if we decide for that) environment with the CPCSM environment, and also not mix the APIs. They are vastly different in terms of what they should be capable of.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment