Since lua interprets bytecode, it can check the arguments to the bytecode are meaningful. Point to memory lua allocated, things like that.
Turns out it doesn't do that. Feed it bytecode with invalid arguments passed to the instructions and it executes it anyway. The rest of the compromise follows.
Further, instead of fixing their interpreter, the game plan is to statically analyse bytecode. Which turns out to only work in simple cases.
For a sandbox friendly interpreted language this is pretty disappointing. I wonder if they'd take patches to stop the interpreter trusting the input - presumably performance regression is the fear there, which seems dubious when luajit is the fast option anyway.
> I wonder if they'd take patches to stop the interpreter trusting the input
The stance of the Lua developers AIUI is that processes that execute arbitrary Lua code should only accept source, and disable direct loading of byte code.
This seems reasonable to me, as it preserves the option of directly loading trusted byte code, while avoiding putting dynamic checks into the interpreter that would affect all users.
"all functions that load code are potentially insecure when loading untrusted binary data. [...] When in doubt, use the mode argument of those functions to restrict them to loading textual chunks."
I thought this was widely known within the Lua community - I'm amazed that Factorio would support loading untrusted bytecode. Was that a decision the game's developers actively made (for what reasons?) or were they simply unaware of the risks?
Lua isn't really sandbox friendly, although that's a common misconception.
Lua (by design) doesn't provide termination guarantees or a good way to force an untrusted program to terminate. If you accept untrusted lua input, be prepared for your program to halt indefinitely.
Lua is great for semi-trusted input, AKA things you download from the internet, where you do a minimal amount of due diligence, but in case the code is actually malicious, you want to severely limit (but not completely eliminate) the harm it can do.
If you actually need Javascript-style completely untrusted input,, what you want is the Roblox fork called Luau[1].
Any language can be sandboxed on the VM level. It's a property of it's implementation. So you can say that Lua has no sandbox friendly implementation right now.
For example, termination you can solve by unwinding the stack in efficiently polled safepoints. You need to take down the entire sandbox-capable Lua VM instance but you can.
Lua has debug hooks that can be used for the purpose; for example an instruction counter coupled with a pool allocator should get one quite far. I would never trust third party bytecode, only source code.
Is anyone familiar with Roblox luau security features?
It's not exactly a simple task to make safe interpreter for bytecode as some other languages have shown. It's a trade-off which also simplifies the reference implementation.
I wouldn't trust most of these interpreters when it comes to third party code execution, I barely trust my browser even with all the R&D money and attention web browsers receive.
Sandboxed in the sense that things like file i/o or network access can be easily removed and selectively reintroduced, e.g. to give an interpreter which can trash it's own heap but can't do anything to the host.
Bounds checking on instruction opcodes can absolutely be implemented in an interpreter. I suppose it's more complicated than just trusting the integer - but then the thing doesn't fall over on malformed bytecode which seems like a feature.
No, expected. Only execute bytecode that was actually generated by the correct compiler. Otherwise you get memory safety violations or sandbox escapes. (Or sandbox escapes via memory safety violations.)
Just as you don't execute arbitrary machine code.
Luau has the same thing, and you don't see Roblox suffering from sandbox escapes all the time, do you?
Java, Wasm and BPF demonstrate that it is possible to have statically-verifiable bytecode for JIT-compiled languages. Lua’s problem is that the bytecode doesn’t provide the information necessary to fully verify its safety.
All of those formats are designed to be translated to machine code when maximum performance is desired. Whereas Lua byte code is designed and optimized to be interpreted directly.
One step in Lua's evolution was to change from a stack machine to a register machine: https://www.lua.org/doc/jucs05.pdf This made the interpreter faster, but also (I suspect) more difficult to verify. I believe both Java and Wasm are stack machines (don't know about BPF).
Technically speaking, CIL is always compiled by CoreCLR (it has interpreter internally but it is never used and therefore has succumbed to bitrot), it is sometimes interpreted by Mono on certain platforms as a stand in for dynamically emitted code. A special case used to be with WASM target with Mono but supposedly that's in the past.
Luau (Roblox's variant of Lua) seems to have disabled loading bytecode from Lua completely. Per https://luau-lang.org/sandbox:
> To achieve memory safety, access to function bytecode has been removed. Bytecode is hard to validate and using untrusted bytecode may lead to exploits. Thus, loadstring doesn’t work with bytecode inputs, and string.dump/load have been removed as they aren’t necessary anymore. When embedding Luau, bytecode should be encrypted/signed to prevent MITM attacks as well, as the VM assumes that the bytecode was generated by the Luau compiler (which never produces invalid/unsafe bytecode).
>> the VM assumes that the bytecode was generated by the Luau compiler (which never produces invalid/unsafe bytecode)
Yep, to that end they also have a basic bytecode verifier (only used in debug mode / when asserts are enabled) that validates the compiler only outputs valid bytecode, and I believe they continuously fuzz the compiler to make sure those asserts can't be triggered. See https://github.com/luau-lang/luau/blob/0d2688844ab285af1ef52...
It's fairly robust (and Luau bytecode isn't _that_ complex,) but they made the right decision disallowing direct bytecode execution.
Since lua interprets bytecode, it can check the arguments to the bytecode are meaningful. Point to memory lua allocated, things like that.
Turns out it doesn't do that. Feed it bytecode with invalid arguments passed to the instructions and it executes it anyway. The rest of the compromise follows.
Further, instead of fixing their interpreter, the game plan is to statically analyse bytecode. Which turns out to only work in simple cases.
For a sandbox friendly interpreted language this is pretty disappointing. I wonder if they'd take patches to stop the interpreter trusting the input - presumably performance regression is the fear there, which seems dubious when luajit is the fast option anyway.