5

After trying and witnessing the incredible ease with which I could integrate Lua and LuaJIT into my game engine, I'm convinced that that's the scripting language I want to use. I would like to use it for my AI, unit descriptions, map triggers, et cetera. (as much as possible really). This question is not just applicable to gamedev, I can imagine creating a scriptable editor or window manager which can load external scripts (case in point: sublime text with python and package control)

But now I have a conundrum: I really appreciate the ease of use that the LuaJIT FFI offers to bind to my engine, but I don't want to provide free reign of the FFI to map authors for example. The incredible speed of lua-to-c calls via the FFI (when JITted) is also something I really want.

So ideally, I would write my own wrapper Lua files that bind to my engine with the FFI and export a nice module for map authors and modders to use. My alternative to this is to write a vanilla lua module, which is possible but much more cumbersome and slower.

I can't disable the FFI when compiling luajit because obviously I want to use it myself, but I don't see how to restrict the FFI on a per-script or per-module basis. Obviously the FFI needs to be active in the lua_State where I load my modules (after which I cant start loading the user-modified scripts). So what do I do? Is it even possible?

EDIT: In my mind, the ideal workflow would be:

  1. open lua state
  2. load all modules (luaL_openlibs()), the FFI is also preloaded
  3. load my .lua modules, which use the FFI (this is the engine binding, they are trusted files so they can use the FFI)
  4. disable select native modules and functions: os, ffi, ... (this is the missing step)
  5. execute user-provided scripts (these are untrusted, I don't want them to access the FFI)
  6. optional: look for a way to reload lua modules for a fast edit cycle, this would involve re-enabling the FFI and other modules. (not sure how to do this either)

NOTE: I'm aware that this would still not be a perfect (or even good sandbox), as Mike Pall already pointed out in some of his mails, but I still don't want to give map authors access to the FFI.

Aktau
  • 1,847
  • 21
  • 30

1 Answers1

4

Maybe I do not understand the issue, but if you use a normal Lua sandbox in which the FFI is not accessible, what is the problem?

For instance:

ffi = require "ffi"

ffi.cdef([[int printf(const char *fmt, ...);]])

function say_hello()
  ffi.C.printf("%s", "hello, ");
end

my_user_script = [[
say_hello()
ffi.C.printf("%s\n", "world")
]]

f = loadstring(my_user_script)

print("=== without sandbox ===")
print(pcall(f))

print("=== with sandbox ===")
setfenv(f,{say_hello = say_hello})
print(pcall(f))

This should output:

=== without sandbox ===
hello, world
true
=== with sandbox ===
hello, false    [string "say_hello()..."]:2: attempt to index global 'ffi' (a nil value)

Note that you also need to be careful not to leak FFI cdata into the sandbox. There is a paragraph about this in the LuaJIT documentation.

catwell
  • 6,770
  • 1
  • 23
  • 21
  • But I want to write my engine bindings with the FFI, so I can't disable it entirely. I've updated my answer to better illustrate the issue. – Aktau Aug 22 '13 at 10:10
  • 3
    Not sure if it is me or you who is missing something. I have added a code example above, does it help? – catwell Aug 22 '13 at 10:27
  • Edited again, to make it even clearer than you *can* give access to the user to engine functions that do use the FFI. Here the `say_hello` function *does* work in the sandbox. – catwell Aug 22 '13 at 10:34
  • so, setfenv comes to the rescue. Why does the print statement even work in that snippet? AFAIS only say_hello() is defined in that new env, not print(). I suppose I could also take a subtractive approach and just take the current environment minus ffi and os. Is that right? (I better understand what you were talking about with normal Lua sandboxing now, thanks for that). – Aktau Aug 22 '13 at 21:04
  • 1
    @Aktau Are you talking about `print(pcall(f))` that comes after doing 'setfenv'? 'setfenv' changes the environment of the function passed in -- it isn't changing the call site's environment. – greatwolf Aug 22 '13 at 21:21
  • @greatwolf I could slap my own forehead, I should've noticed that. Answer accepted! (though I guess it would be totally cool if I could see a snippet of code that illustrates cdata abuse) – Aktau Aug 22 '13 at 21:23
  • 1
    @Aktau Note that properly sandboxing unsecure code is tricky. For example, even if you disallow 'ffi', 'os', etc. in the new env, you want to make sure they can't get it back with something like `require 'ffi'` since 'ffi' is in `package.preload`. – greatwolf Aug 22 '13 at 21:33
  • @Aktau Like @greatwolf said this is tricky, and I would not use a blacklist (substractive) approach if I were you. Better list everything you want in the sandbox explicitly. It is verbose but at least you can check what you're doing. And yes, no (unmodified) `require` in a sandbox... – catwell Aug 23 '13 at 08:51
  • @catwell you're right, I was already wondering about the require. I suppose it would be sufficient to make sure that require = nil in the new environment, no? I'll pick the modules to preload anyway :). – Aktau Aug 23 '13 at 08:56
  • Yes, you must not leak `require`, but also other things probably (the debug library, loadstring, ...). – catwell Aug 23 '13 at 10:35