1. 程式人生 > >在Lua 5.2中保護全域性環境

在Lua 5.2中保護全域性環境

轉自:http://blog.csdn.net/axx1611/article/details/8121740

Lua指令碼語言十分強大,但是有一個問題就是全域性可寫,比如你定義一個全域性變數很容易不小心被另一個同名變數給覆蓋掉。

這種問題一旦出現是十分難以調查的,該文章介紹的這種機制可以解決該問題。

我已經在我自己的工程中應用了該技術,它可以達到以下目的:

1.全域性變數不能直接在Lua中被修改

2.可以創建出不能直接被修改的table

3.遮蔽一些你不想開放的Lua原生函式比如檔案操作

注:我是混合著使用C和Lua實現該機制的,但是在純Lua裡也可以同樣實現。為了便於表述,我這裡只給出純Lua版的例子。

另外該範例程式碼僅限於Lua 5.2版,但是該技巧同樣可以適用於其他版本,但可能需要修改該一部分程式碼。

首先將所有安全機制的程式碼放進一個Lua指令碼檔案safe.lua如下:

  1. -- 僅支援Lua 5.2版  
  2. assert(_VERSION == "Lua 5.2")  
  3. -- 全域性環境在登錄檔中的索引值(見lua.h)  
  4. local LUA_RIDX_GLOBALS = 2  
  5. -- 安全table的metatable標誌  
  6. local SAFE_TABLE_FLAG = ".SAFETABLE"  
  7. -- 設定全域性安全保護機制  
  8. local function SetupGlobal()  
  9.     -- 獲取登錄檔  
  10.     local reg = debug.getregistry()  
  11.     local env = {}          -- 新環境table  
  12.     local proxy = {}        -- 代理table  
  13.     local mt = {}           -- metatable  
  14.     -- 操作過載  
  15.     mt.__index = proxy  
  16.     mt.__newindex = function() print("cannot modify global enviroment!") end  
  17.     mt.__len = function() return #proxy end  
  18.     mt.__pairs = function() return pairs(proxy) end  
  19.     mt.__ipairs = function() return ipairs(proxy) end  
  20.     -- 隱藏metatable  
  21.     mt.__metatable = 0  
  22.     -- 標記為安全table  
  23.     mt[SAFE_TABLE_FLAG] = true  
  24.     -- 獲取舊環境  
  25.     local old_env = reg[LUA_RIDX_GLOBALS]  
  26.     -- 設定新環境的metatable  
  27.     setmetatable(env, mt)  
  28.     -- 啟用新環境  
  29.     _ENV = env  
  30.     -- 將全域性預設環境也改為新環境  
  31.     reg[LUA_RIDX_GLOBALS] = env  
  32.     -- 返回代理table和舊環境  
  33.     return proxy, old_env  
  34. end  
  35. -- 新建一個有安全保護的table  
  36. local function CreateSafeTable(base)  
  37.     local new = {}          -- 新table  
  38.     local mt = {}           -- metatable  
  39.     -- 如果沒有指定base則新建一個空table  
  40.     local proxy = (type(base) == "table") and base or {}  
  41.     -- 操作過載  
  42.     mt.__index = proxy  
  43.     mt.__newindex = function() print("cannot modify safe table!") end  
  44.     mt.__len = function() return #proxy end  
  45.     mt.__pairs = function() return pairs(proxy) end  
  46.     mt.__ipairs = function() return ipairs(proxy) end  
  47.     -- 隱藏metatable  
  48.     mt.__metatable = 0  
  49.     -- 標記為安全table  
  50.     mt[SAFE_TABLE_FLAG] = true  
  51.     -- 設定新table的metatable  
  52.     setmetatable(new, mt)  
  53.     -- 返回新table和對應的代理table  
  54.     return new, proxy  
  55. end  
  56. -- 開啟全域性保護  
  57. local proxy, old_env = SetupGlobal()  
  58. -- 在這裡複製需要匯出給新環境使用的Lua原生全域性變數和函式  
  59. -- 被遮蔽的原生全域性變數和函式有:  
  60. --  _G          Lua 5.2推薦使用_ENV(你可以根據需要把它定義為_ENV)  
  61. --  dofile      我的工程需要屏遮檔案系統,我沒有評估過開放它對安全性有沒有影響  
  62. --  loadfile    我的工程需要屏遮檔案系統,我沒有評估過開放它對安全性有沒有影響  
  63. --  rawequal    需要覆蓋,不應該直接操作安全table  
  64. --  rawget      需要覆蓋,不應該直接操作安全table  
  65. --  rawlen      需要覆蓋,不應該直接操作安全table  
  66. --  rawset      需要覆蓋,不應該直接操作安全table  
  67. --  require     我的工程需要屏遮檔案系統,我沒有評估過開放它對安全性有沒有影響  
  68. proxy._VERSION = old_env._VERSION  
  69. proxy.assert = old_env.assert  
  70. proxy.collectgarbage = old_env.collectgarbage  
  71. proxy.error = old_env.error  
  72. proxy.getmetatable = old_env.getmetatable  
  73. proxy.ipairs = old_env.ipairs  
  74. proxy.load = old_env.load  
  75. proxy.next = old_env.next  
  76. proxy.pairs = old_env.pairs  
  77. proxy.pcall = old_env.pcall  
  78. proxy.print = old_env.print  
  79. proxy.select = old_env.select  
  80. proxy.setmetatable = old_env.setmetatable  
  81. proxy.tostring = old_env.tostring  
  82. proxy.tonumber = old_env.tonumber  
  83. proxy.type = old_env.type  
  84. proxy.xpcall = old_env.xpcall  
  85. -- 在這裡匯出給新環境使用的Lua原生全域性table(將被設為只讀table)  
  86. -- 被遮蔽的原生全域性table有:  
  87. --  coroutine   我的工程裡不需要coroutine,我沒有評估過開放它對安全性有沒有影響  
  88. --  debug       會嚴重影響安全性,必須遮蔽  
  89. --  io          我的工程需要屏遮檔案系統,我沒有評估過開放它對安全性有沒有影響  
  90. --  os          我的工程裡不需要os,我沒有評估過開放它對安全性有沒有影響  
  91. --  package     我的工程需要屏遮檔案系統,我沒有評估過開放它對安全性有沒有影響  
  92. proxy.bit32 = CreateSafeTable(old_env.bit32)  
  93. proxy.math = CreateSafeTable(old_env.math)  
  94. proxy.string = CreateSafeTable(old_env.string)  
  95. proxy.table = CreateSafeTable(old_env.table)  
  96. -- 實現安全版的rawequal  
  97. proxy.rawequal = function(v1, v2)  
  98.     -- 獲得真實的metatable  
  99.     local mt1 = old_env.debug.getmetatable(v1)  
  100.     local mt2 = old_env.debug.getmetatable(v2)  
  101.     -- 如果是安全table則使用代理table  
  102.     if mt1 and mt1[SAFE_TABLE_FLAG] then  
  103.         v1 = mt1.__index  
  104.     end  
  105.     if mt2 and mt2[SAFE_TABLE_FLAG] then  
  106.         v2 = mt2.__index  
  107.     end  
  108.     -- 呼叫原始rawequal  
  109.     return old_env.rawequal(v1, v2)  
  110. end  
  111. -- 實現安全版的rawget  
  112. proxy.rawget = function(t, k)  
  113.     -- 獲得真實的metatable  
  114.     local mt = old_env.debug.getmetatable(t)  
  115.     -- 如果是安全table則使用代理table  
  116.     if mt and mt[SAFE_TABLE_FLAG] then  
  117.         t = mt.__index  
  118.     end  
  119.     -- 呼叫原始rawget  
  120.     return old_env.rawget(t, k)  
  121. end  
  122. -- 實現安全版的rawlen  
  123. proxy.rawlen = function(v)  
  124.     -- 獲得真實的metatable  
  125.     local mt = old_env.debug.getmetatable(v)  
  126.     -- 如果是安全table則使用代理table  
  127.     if mt and mt[SAFE_TABLE_FLAG] then  
  128.         v = mt.__index  
  129.     end  
  130.     -- 呼叫原始rawlen  
  131.     return old_env.rawlen(v)  
  132. end  
  133. -- 實現安全版的rawset  
  134. proxy.rawset = function(t, k, v)  
  135.     -- 獲得真實的metatable  
  136.     local mt = old_env.debug.getmetatable(t)  
  137.     -- 如果是安全table則使用代理table  
  138.     if mt and mt[SAFE_TABLE_FLAG] then  
  139.         t = mt.__index  
  140.     end  
  141.     -- 呼叫原始rawset  
  142.     return old_env.rawset(t, k, v)  
  143. end  
  144. -- 這裡可以自定義一些自己的內容  
  145. -- 指令碼檔案裝載列表  
  146. local loaded_proxy  
  147. proxy.LOADED, loaded_proxy = CreateSafeTable()  
  148. -- 匯入指令碼檔案  
  149. proxy.import = function(s)  
  150.     -- 如果已經被匯入則返回true  
  151.     if LOADED[s] ~= nil then  
  152.         return true  
  153.     end  
  154.     -- 裝載檔案  
  155.     local f, msg = old_env.loadfile(s)  
  156.     -- 如果裝載失敗,輸出錯誤  
  157.     if not f then  
  158.         old_env.io.stderr:write(msg)  
  159.         return false  
  160.     end  
  161.     -- 否則執行該指令碼  
  162.     local r, msg = pcall(f)  
  163.     -- 如果執行過程中出錯,輸出錯誤  
  164.     if not r then  
  165.         old_env.io.stderr:write(msg)  
  166.         return false  
  167.     end  
  168.     -- 記錄檔名到裝載列表  
  169.     loaded_proxy[s] = f  
  170.     -- 成功  
  171.     return true  
  172. end  
  173. -- 由於外界(這裡指的是main.lua)環境已經初始化過環境了,沒辦法在safe.lua裡直接更改(我沒找到辦法)  
  174. -- 因此這裡返回新環境給main.lua,main.lua需要在裝載完該檔案後把自己的環境設為該新環境  
  175. -- 對於C這一步是不需要的,本身main.lua做作的一切可以都在C裡完成  
  176. do return _ENV end  

入口指令碼main.lua:

  1. -- 開啟全域性保護,並且更新自己的環境(見safe.lua末尾的說明)  
  2. _ENV = dofile("safe.lua")  
  3. -- 裝載其他指令碼  
  4. import("test.lua")  
  5. -- 輸出已裝載指令碼  
  6. for k, v in pairs(LOADED) do  
  7.     print("["..k.."] = "..tostring(v))  
  8. end  
  9. -- 嘗試重複裝載指令碼  
  10. import("test.lua")  

測試指令碼test.lua:

  1. -- 嘗試定義全域性變數  
  2. x = 1  
  3. print(x)  
  4. -- 嘗試修改已有全域性變數  
  5. print = nil  
  6. print(print)  
  7. -- 嘗試修改安全table  
  8. math.x = 0  
  9. print(math.x)  
  10. math.sin = nil  
  11. print(math.sin)  

命令列裡敲入lua main.lua,執行結果將為:

  1. cannot modify global enviroment!  
  2. nil  
  3. cannot modify global enviroment!  
  4. function: 6D793C3C  
  5. cannot modify safe table!  
  6. nil  
  7. cannot modify safe table!  
  8. function: 6D796C34  
  9. [test.lua] = function: 003E8310  

可以看出所有寫操作都沒有成功,並且test.lua只加載了一次,在LOADED中有其記錄。