在Lua 5.2中保護全域性環境
阿新 • • 發佈:2019-02-17
轉自: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如下:
- -- 僅支援Lua 5.2版
- assert(_VERSION == "Lua 5.2")
- -- 全域性環境在登錄檔中的索引值(見lua.h)
- local LUA_RIDX_GLOBALS = 2
- -- 安全table的metatable標誌
- local SAFE_TABLE_FLAG = ".SAFETABLE"
- -- 設定全域性安全保護機制
-
local function SetupGlobal()
- -- 獲取登錄檔
- local reg = debug.getregistry()
- local env = {} -- 新環境table
- local proxy = {} -- 代理table
- local mt = {} -- metatable
- -- 操作過載
- mt.__index = proxy
- mt.__newindex = function() print("cannot modify global enviroment!") end
-
mt.__len = function() return #proxy end
- mt.__pairs = function() return pairs(proxy) end
- mt.__ipairs = function() return ipairs(proxy) end
- -- 隱藏metatable
- mt.__metatable = 0
- -- 標記為安全table
- mt[SAFE_TABLE_FLAG] = true
- -- 獲取舊環境
- local old_env = reg[LUA_RIDX_GLOBALS]
- -- 設定新環境的metatable
- setmetatable(env, mt)
- -- 啟用新環境
- _ENV = env
- -- 將全域性預設環境也改為新環境
- reg[LUA_RIDX_GLOBALS] = env
- -- 返回代理table和舊環境
- return proxy, old_env
- end
- -- 新建一個有安全保護的table
- local function CreateSafeTable(base)
- local new = {} -- 新table
- local mt = {} -- metatable
- -- 如果沒有指定base則新建一個空table
- local proxy = (type(base) == "table") and base or {}
- -- 操作過載
- mt.__index = proxy
- mt.__newindex = function() print("cannot modify safe table!") end
- mt.__len = function() return #proxy end
- mt.__pairs = function() return pairs(proxy) end
- mt.__ipairs = function() return ipairs(proxy) end
- -- 隱藏metatable
- mt.__metatable = 0
- -- 標記為安全table
- mt[SAFE_TABLE_FLAG] = true
- -- 設定新table的metatable
- setmetatable(new, mt)
- -- 返回新table和對應的代理table
- return new, proxy
- end
- -- 開啟全域性保護
- local proxy, old_env = SetupGlobal()
- -- 在這裡複製需要匯出給新環境使用的Lua原生全域性變數和函式
- -- 被遮蔽的原生全域性變數和函式有:
- -- _G Lua 5.2推薦使用_ENV(你可以根據需要把它定義為_ENV)
- -- dofile 我的工程需要屏遮檔案系統,我沒有評估過開放它對安全性有沒有影響
- -- loadfile 我的工程需要屏遮檔案系統,我沒有評估過開放它對安全性有沒有影響
- -- rawequal 需要覆蓋,不應該直接操作安全table
- -- rawget 需要覆蓋,不應該直接操作安全table
- -- rawlen 需要覆蓋,不應該直接操作安全table
- -- rawset 需要覆蓋,不應該直接操作安全table
- -- require 我的工程需要屏遮檔案系統,我沒有評估過開放它對安全性有沒有影響
- proxy._VERSION = old_env._VERSION
- proxy.assert = old_env.assert
- proxy.collectgarbage = old_env.collectgarbage
- proxy.error = old_env.error
- proxy.getmetatable = old_env.getmetatable
- proxy.ipairs = old_env.ipairs
- proxy.load = old_env.load
- proxy.next = old_env.next
- proxy.pairs = old_env.pairs
- proxy.pcall = old_env.pcall
- proxy.print = old_env.print
- proxy.select = old_env.select
- proxy.setmetatable = old_env.setmetatable
- proxy.tostring = old_env.tostring
- proxy.tonumber = old_env.tonumber
- proxy.type = old_env.type
- proxy.xpcall = old_env.xpcall
- -- 在這裡匯出給新環境使用的Lua原生全域性table(將被設為只讀table)
- -- 被遮蔽的原生全域性table有:
- -- coroutine 我的工程裡不需要coroutine,我沒有評估過開放它對安全性有沒有影響
- -- debug 會嚴重影響安全性,必須遮蔽
- -- io 我的工程需要屏遮檔案系統,我沒有評估過開放它對安全性有沒有影響
- -- os 我的工程裡不需要os,我沒有評估過開放它對安全性有沒有影響
- -- package 我的工程需要屏遮檔案系統,我沒有評估過開放它對安全性有沒有影響
- proxy.bit32 = CreateSafeTable(old_env.bit32)
- proxy.math = CreateSafeTable(old_env.math)
- proxy.string = CreateSafeTable(old_env.string)
- proxy.table = CreateSafeTable(old_env.table)
- -- 實現安全版的rawequal
- proxy.rawequal = function(v1, v2)
- -- 獲得真實的metatable
- local mt1 = old_env.debug.getmetatable(v1)
- local mt2 = old_env.debug.getmetatable(v2)
- -- 如果是安全table則使用代理table
- if mt1 and mt1[SAFE_TABLE_FLAG] then
- v1 = mt1.__index
- end
- if mt2 and mt2[SAFE_TABLE_FLAG] then
- v2 = mt2.__index
- end
- -- 呼叫原始rawequal
- return old_env.rawequal(v1, v2)
- end
- -- 實現安全版的rawget
- proxy.rawget = function(t, k)
- -- 獲得真實的metatable
- local mt = old_env.debug.getmetatable(t)
- -- 如果是安全table則使用代理table
- if mt and mt[SAFE_TABLE_FLAG] then
- t = mt.__index
- end
- -- 呼叫原始rawget
- return old_env.rawget(t, k)
- end
- -- 實現安全版的rawlen
- proxy.rawlen = function(v)
- -- 獲得真實的metatable
- local mt = old_env.debug.getmetatable(v)
- -- 如果是安全table則使用代理table
- if mt and mt[SAFE_TABLE_FLAG] then
- v = mt.__index
- end
- -- 呼叫原始rawlen
- return old_env.rawlen(v)
- end
- -- 實現安全版的rawset
- proxy.rawset = function(t, k, v)
- -- 獲得真實的metatable
- local mt = old_env.debug.getmetatable(t)
- -- 如果是安全table則使用代理table
- if mt and mt[SAFE_TABLE_FLAG] then
- t = mt.__index
- end
- -- 呼叫原始rawset
- return old_env.rawset(t, k, v)
- end
- -- 這裡可以自定義一些自己的內容
- -- 指令碼檔案裝載列表
- local loaded_proxy
- proxy.LOADED, loaded_proxy = CreateSafeTable()
- -- 匯入指令碼檔案
- proxy.import = function(s)
- -- 如果已經被匯入則返回true
- if LOADED[s] ~= nil then
- return true
- end
- -- 裝載檔案
- local f, msg = old_env.loadfile(s)
- -- 如果裝載失敗,輸出錯誤
- if not f then
- old_env.io.stderr:write(msg)
- return false
- end
- -- 否則執行該指令碼
- local r, msg = pcall(f)
- -- 如果執行過程中出錯,輸出錯誤
- if not r then
- old_env.io.stderr:write(msg)
- return false
- end
- -- 記錄檔名到裝載列表
- loaded_proxy[s] = f
- -- 成功
- return true
- end
- -- 由於外界(這裡指的是main.lua)環境已經初始化過環境了,沒辦法在safe.lua裡直接更改(我沒找到辦法)
- -- 因此這裡返回新環境給main.lua,main.lua需要在裝載完該檔案後把自己的環境設為該新環境
- -- 對於C這一步是不需要的,本身main.lua做作的一切可以都在C裡完成
- do return _ENV end
入口指令碼main.lua:
- -- 開啟全域性保護,並且更新自己的環境(見safe.lua末尾的說明)
- _ENV = dofile("safe.lua")
- -- 裝載其他指令碼
- import("test.lua")
- -- 輸出已裝載指令碼
- for k, v in pairs(LOADED) do
- print("["..k.."] = "..tostring(v))
- end
- -- 嘗試重複裝載指令碼
- import("test.lua")
測試指令碼test.lua:
- -- 嘗試定義全域性變數
- x = 1
- print(x)
- -- 嘗試修改已有全域性變數
- print = nil
- print(print)
- -- 嘗試修改安全table
- math.x = 0
- print(math.x)
- math.sin = nil
- print(math.sin)
命令列裡敲入lua main.lua,執行結果將為:
- cannot modify global enviroment!
- nil
- cannot modify global enviroment!
- function: 6D793C3C
- cannot modify safe table!
- nil
- cannot modify safe table!
- function: 6D796C34
- [test.lua] = function: 003E8310
可以看出所有寫操作都沒有成功,並且test.lua只加載了一次,在LOADED中有其記錄。