在Lua 5.2中重新创建setfenv()函数

11

我如何在Lua 5.2中重新创建setfenv的功能?我有些困惑,不太确定应该如何使用新的_ENV变量。



在Lua 5.1中,您可以轻松地使用setfenv来沙盒化任何函数。

--# Lua 5.1

print('_G', _G)             -- address of _G

local foo = function()  
    print('env', _G)        -- address of sandbox _G
    bar = 1
end

-- create a simple sandbox
local env = { print = print }
env._G = env

-- set the environment and call the function
setfenv(foo, env)
foo()

-- we should have global in our environment table but not in _G
print(bar, env.bar)

运行此示例将显示输出:

_G    table: 0x62d6b0
env   table: 0x635d00
nil   1



我想在 Lua 5.2 中重现这个简单的例子。以下是我的尝试,但它不像上面的例子那样工作。

--# Lua 5.2

local function setfenv(f, env)
    local _ENV = env or {}       -- create the _ENV upvalue
    return function(...)
        print('upvalue', _ENV)   -- address of _ENV upvalue
        return f(...)
    end
end

local foo = function()
    print('_ENV', _ENV)          -- address of function _ENV
    bar = 1
end

-- create a simple sandbox
local env = { print = print }
env._G = env

-- set the environment and call the function
foo_env = setfenv(foo, env)
foo_env()

-- we should have global in our envoirnment table but not in _G
print(bar, env.bar)

运行此示例将显示输出:

upvalue    table: 0x637e90
_ENV       table: 0x6305f0
1          nil



我知道有几个关于这个主题的问题,但它们大多涉及加载动态代码(文件或字符串),使用Lua 5.2中提供的新的load函数非常有效。在这里,我特别询问如何在沙盒中运行任意函数的解决方案。我希望不使用debug库来完成这个任务。根据Lua 文档,我们不应该依赖它。

4个回答

19

在Lua 5.2中,如果没有使用Lua的debug库,就无法更改函数的环境。一旦创建了一个函数,它就拥有了它所在的环境。修改此环境的唯一方法是通过修改其第一个upvalue,这需要使用debug库。

Lua 5.2中环境的基本思想是,除了特殊手段(即debug库)外,环境应被视为不可变的。您可以在一个环境中创建一个函数;一旦在此环境中创建了函数,那么它所拥有的环境就是此环境。永远不会改变。

这是在Lua 5.1中经常使用环境的方式,但轻松且允许修改任何内容的环境是被认可的。如果您的Lua解释器删除了setfenv(以防止用户破坏沙盒),则用户代码无法在内部为其自己的函数设置环境。因此,外部世界得到了一个沙盒,但内部世界不能在沙盒内拥有沙盒。

Lua 5.2机制使得在函数创建后更改环境变得更加困难,但它确实允许您在创建期间设置环境。这样可以让您在沙盒内进行嵌套。

因此,您真正想要的是像这样重新排列代码:

local foo;

do
  local _ENV = { print = print }

  function foo()
    print('env', _ENV)
    bar = 1
  end
end

foo现在被隔离了。现在,别人要破坏这个隔离就更加困难了。

可以想象,这引起了Lua开发者之间的争议。


10
Lua 5.2拥有词法环境,而不是Lua 5.1中的神奇动态环境。正如Nicol所提到的,如果知道函数闭包中的变量位置,仍然可以通过改变upvalue来改变函数的环境。 - lhf

12

虽然有点贵,但如果对你来说很重要的话...

为什么不使用string.dump,并将函数重新加载到正确的环境中呢?

function setfenv(f, env)
    return load(string.dump(f), nil, nil, env)
end
function foo()
    herp(derp)
end

setfenv(foo, {herp = print, derp = "Hello, world!"})()

直到现在,我阅读手册时总是跳过了string.dump。 - mkluwe

12
为了在Lua 5.2中重新创建 setfenv/getfenv,您可以采取以下措施:
if not setfenv then -- Lua 5.2
  -- based on http://lua-users.org/lists/lua-l/2010-06/msg00314.html
  -- this assumes f is a function
  local function findenv(f)
    local level = 1
    repeat
      local name, value = debug.getupvalue(f, level)
      if name == '_ENV' then return level, value end
      level = level + 1
    until name == nil
    return nil end
  getfenv = function (f) return(select(2, findenv(f)) or _G) end
  setfenv = function (f, t)
    local level = findenv(f)
    if level then debug.setupvalue(f, level, t) end
    return f end
end

RPFeltz的回答(load(string.dump(f)...))很聪明,可能适用于您,但它不能处理具有闭包变量(除_ENV以外)的函数。
还有一个compat-env模块,可以在Lua 5.2和Lua 5.1之间实现函数。

我一直在寻找一种不使用调试库的方法,但很高兴看到应该如何完成。谢谢。 - Adam

4
在Lua5.2中,可沙盒化的函数需要指定自身。您可以使用一个简单的模式,让它接收_ENV作为参数。
function(_ENV)
    ...
end

或者将其包装在定义环境的某个东西内。
local mk_func(_ENV)
    return function()
        ...
    end
end

local f = mk_func({print = print})

然而,显式使用_ENV在沙箱中的效果较差,因为您不能总是假设其他函数将配合具有_ENV变量。在这种情况下,取决于您要做什么。如果您只想从其他文件加载代码,则诸如loadloadfile之类的函数通常接收可选环境参数,可以用于沙箱。此外,如果您尝试加载的代码是字符串格式,则可以使用字符串操作自己添加_ENV变量(例如,通过围绕具有env参数的函数包装它)。
local code = 'return function(_ENV) return ' .. their_code .. 'end'

最后,如果你真的需要动态功能环境操作,你可以使用debug库来更改函数内部_ENV上值。虽然通常不鼓励使用debug库,但我认为如果所有其他替代方案都不适用(我认为在这种情况下更改函数的环境已经是深奥神秘的了,因此使用debug库并没有多大问题)。


谢谢您提供的信息。这是一个不错的方法,但似乎没有使用调试库无法对我没有编写的函数进行沙盒化。 - Adam
@Adam:你到底从哪里获取这些函数的?正如我所说,你应该仍然能够在模块级别而不是在函数级别上进行沙盒化。 - hugomg

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接