Search code examples
luasandbox

lua environments and modules


Lets assume I have a module:

-- env.lua

local env = {}

function env.resolve(str)
  print("mod", _ENV)

  if _resolve_path ~= nil then
    return _resolve_path(str)
  else
    error("bad env")
  end
end

return env

and some code using it:

-- sandbox demo
-- run as: lua env-test.lua

env = require('env')

function _resolve_path(path)
  return "/" .. path
end

print("before main()")
print("", _ENV)
print("", env.resolve("test"))

local sandbox
do
  local _ENV = {
    print = print,
    env = env,
    _resolve_path = function (path)
      return "/chroot/" .. path
    end
  }
  function sandbox()
    print("from sandbox()")
    print("", _ENV)
    print("", env.resolve("test"))
  end
end

sandbox()

print("after main()")
print("", _ENV)
print("", env.resolve("test"))

What I'd like to achieve is that env.resolve() from sandbox() would use the custom _resolve_path function from the environment. It see that the environment is not applied to code called from the sandboxed function though. The goal is to augument how some modules behave depending on where they are called from. E.g. having sandbox{1,2,3}() with different local _resolve_path() functions.


Solution

  • When you load your module with require it is bound to the global environment. Once a function is created in an environment it has that environment for its entire lifetime.

    Prior to Lua 5.2 you could use set/getfenv to alter the environment, but environments are now lexical. The environment can only be changed with the debug library by changing the _ENV upvalue.

    So, how can you run the same function within different environments? You can pass in the environment as a parameter:

    function env.resolve(str, _ENV)
      print("mod", _ENV)
      if _resolve_path ~= nil then
        return _resolve_path(str)
      else
        error("bad env")
      end
    end
    

    Where you then call resolve like:

    env.resolve('test', _ENV)
    

    Or, if you would prefer the environment didn't have to be specified for every resolve call you can bind a resolve function to each new environment:

    -- env.lua
    local print = print
    local error = error
    local env = {}
    
    -- this is the actual resolve function that takes the environment as a parameter
    local function resolve_env(str, _ENV)
      print("mod", _ENV)
      if _resolve_path ~= nil then
        return _resolve_path(str)
      else
        error("bad env")
      end
    end
    
    -- this is the module (ie. global) resolve
    function env.resolve(str)
      return resolve_env(str, _ENV)
    end
    
    -- this function binds a resolve function to a sandbox environment
    function env.bind(_ENV)
      _ENV.env = {
        resolve = function(str)
          return resolve_env(str, _ENV)
        end
      }
      return _ENV
    end
    
    return env
    

    The sandbox can now setup a bound resolve:

    -- sandbox.lua
    env = require 'env'
    
    function _resolve_path(path)
      return "/" .. path
    end
    
    print("before main()")
    print("", _ENV)
    print("", env.resolve("test"))
    
    local sandbox; do
      local _ENV = env.bind{
        print = print,
        _resolve_path = function (path)
          return "/chroot/" .. path
        end
      }
    
      function sandbox()
        print("from sandbox()")
        print("", _ENV)
        print("", env.resolve("test"))
      end
    end
    
    sandbox()
    
    print("after main()")
    print("", _ENV)
    print("", env.resolve("test"))
    

    Which will yield the result:

    $ lua sandbox.lua
    before main()
            table: 00612f40
    mod     table: 00612f40
            /test
    from sandbox()
            table: 0061c7a8
    mod     table: 0061c7a8
            /chroot/test
    after main()
            table: 00612f40
    mod     table: 00612f40
            /test