diff --git a/BlockLua.dll b/BlockLua.dll index a11bb18..3546521 100644 Binary files a/BlockLua.dll and b/BlockLua.dll differ diff --git a/readme.md b/readme.md index e78f471..f54d2d9 100644 --- a/readme.md +++ b/readme.md @@ -14,10 +14,10 @@ Lua scripting for Blockland ### From TorqueScript `'print('hello world')` - Execute Lua in the console by prepending a `'` (single quote) `luaeval("code");` - Execute Lua code -`luacall("funcName", %args...);` - Call a Lua global function -`luaexec("fileName");` - Execute a Lua file. Path rules are the same as executing .cs files. -`luaget("varName");` - Read a Lua global variable -`luaset("varName", %value);` - Write a Lua global variable +`luacall("funcName", %args...);` - Call a Lua function (supports indexing tables and object methods) +`luaexec("fileName");` - Execute a Lua file. Path rules are the same as when executing .cs files, relative paths are allowed. +`luaget("varName");` - Read a Lua global variable (supports indexing tables) +`luaset("varName", %value);` - Write a Lua global variable (supports indexing tables) ### From Lua `bl.eval('code')` - Eval TorqueScript code @@ -35,7 +35,7 @@ Lua scripting for Blockland `object.key = value` - Associate Lua data with a Torque object `object:method(args)` - Call a Torque object method `object[index]` - Access a member of a Torque set or group -`for childIndex, child in object:members() do` - Iterate objects within of a Torque set or group. Indices start at 0 like in Torque. +`for child in object:members() do` - Iterate objects within of a Torque set or group. Indices start at 0 like in Torque. `bl.isObject(object, objectID, or 'objectName')` - Check if an object exists `object:exists()` - Check if an object exists @@ -49,14 +49,14 @@ Lua scripting for Blockland `for object in bl.radiusSearch(vector{centerX,y,z}, radius, 'objtype'/{'objtypes',...}) do` - Find all objects of the specified type(s) whose bounding box overlaps with the specified sphere. See the Types section for a list of valid object types. ### Server-Client Communication -`bl.addServerCmd('commandName', function(client, args...) yourCode end)` - Register a /command on the server -`bl.addClientCmd('commandName', function(args...) yourCode end)` - Register a client command on the client +`bl.addServerCmd('commandName', function(client, args...) ... end)` - Register a /command on the server +`bl.addClientCmd('commandName', function(args...) ... end)` - Register a client command on the client `bl.commandToServer('commandName', args...)` - Execute a server command as a client `bl.commandToClient('commandName', args...)` - As the server, execute a client command on a specific client `bl.commandToAll('commandName', args...)` - As the server, execute a client command on all clients ### Packages/Hooks -`bl.hook('packageName', 'functionName', 'before'/'after', function(args) yourCode end)` - Hook a Torque function with a Lua function. +`bl.hook('packageName', 'functionName', 'before'/'after', function(args) ... end)` - Hook a Torque function with a Lua function. `args` is an array containing the arguments provided to the function. If the hook is `before`, these can be modified before being passed to the parent function. If `args._return` is set to anything other than nil by a `before` hook, the parent function will not be called, and the function will simply return that value. Also in this case, any `after` hook will not be executed. In an `after` hook, `args._return` is set to the value returned by the parent function, and can be modified. @@ -195,7 +195,8 @@ All Lua code is sandboxed, and file access is confined to the default directorie BlockLua also has access to any C libraries installed in the `modules/lualib` folder, so be careful throwing things in there. ### Unsafe Mode BlockLua can be built in Unsafe Mode by specifying the `-DBLLUA_UNSAFE` compiler flag. This removes the sandboxing of Lua code, allowing it to access any file and use any library, including ffi. -Please do not publish add-ons that require unsafe mode. +A more limited option is `-DBLLUA_ALLOWFFI`, which allows the use of the `ffi` library. This can still be exploited to grant all the same access as full unsafe mode. +Please do not publish add-ons that require either of these. ### List of Object Types `'all'` - Any object @@ -210,6 +211,6 @@ Other types: `'static'`, `'environment'`, `'terrain'`, `'water'`, `'trigger'`, ` ## Compiling With any *32-bit* variant of GCC installed (such as MinGW or MSYS2), run the following command in the repo directory: -`g++ src/bllua4.cpp -o BlockLua.dll -m32 -shared -static-libgcc -Isrc -Iinc/tsfuncs -Iinc/lua -lpsapi -L. -llua5.1 src/bllua` - +`g++ src/bllua4.cpp -o BlockLua.dll -m32 -shared -static-libgcc -Isrc -Iinc/tsfuncs -Iinc/lua -lpsapi -L. -llua5.1` + LuaJIT (lua5.1.dll) can be obtained from https://luajit.org/ diff --git a/src/bllua4.cpp b/src/bllua4.cpp index 5e1d94b..3d184b1 100644 --- a/src/bllua4.cpp +++ b/src/bllua4.cpp @@ -1,4 +1,4 @@ -// BlockLua (bllua4): Simple Lua interface for TorqueScript +// BlockLua (bllua4): Advanced Lua interface for TorqueScript // Includes @@ -57,22 +57,26 @@ bool init() { // Set up Lua environment BLL_LOAD_LUA(gL, bll_fileLuaEnv); + #ifdef BLLUA_ALLOWFFI + lua_pushboolean(gL, true); + lua_setglobal(gL, "_bllua_allowffi"); + #endif #ifndef BLLUA_UNSAFE BLL_LOAD_LUA(gL, bll_fileLuaEnvSafe); #endif - // Expose Lua API to TS - BlAddFunction(NULL, NULL, "_bllua_luacall", bll_ts_luacall, "LuaCall(name, ...) - Call Lua function and return result", 2, 20); - BlEval(bll_fileTsEnv); - - // Load utilities + // Load utilities in Lua BLL_LOAD_LUA(gL, bll_fileLuaStd); BLL_LOAD_LUA(gL, bll_fileLuaVector); BLL_LOAD_LUA(gL, bll_fileLuaMatrix); BLL_LOAD_LUA(gL, bll_fileLuaLibts); - BlEval(bll_fileTsLibts); BLL_LOAD_LUA(gL, bll_fileLuaLibbl); BLL_LOAD_LUA(gL, bll_fileLuaLibblTypes); + + // Expose Lua API to TS + BlAddFunction(NULL, NULL, "_bllua_luacall", bll_ts_luacall, "LuaCall(name, ...) - Call Lua function and return result", 2, 20); + BlEval(bll_fileTsEnv); + BlEval(bll_fileTsLibts); BlEval(bll_fileTsLibblSupport); BlEval(bll_fileLoadaddons); @@ -85,8 +89,7 @@ bool init() { bool deinit() { BlPrintf("BlockLua: Unloading"); - BlEval("deactivatePackage(_bllua_main);"); - BlEval("$_bllua_active = 0;"); + BlEval("$_bllua_active=0;deactivatePackage(_bllua_main);"); bll_LuaEval(gL, "for _,f in pairs(_bllua_on_unload) do f() end"); lua_close(gL); diff --git a/src/lua-env-safe.lua b/src/lua-env-safe.lua index adb5275..63c61a9 100644 --- a/src/lua-env-safe.lua +++ b/src/lua-env-safe.lua @@ -12,6 +12,7 @@ local old_require = require local old_os = os local old_debug = debug local old_package = package +local old_allowffi = _bllua_allowffi -- Remove all global variables except a whitelist local ok_names = tmap { @@ -37,13 +38,10 @@ end -- Sanitize file paths to point only to allowed files within the game directory -- List of allowed directories for reading/writing +-- modules/lualib is also allowed as read-only local allowed_dirs = tmap { 'add-ons', 'base', 'config', 'saves', 'screenshots', 'shaders' } --- List of allowed directories for reading only -local allowed_dirs_readonly = tmap { - 'lualib' -} -- List of disallowed file extensions - basically executable file extensions -- Note that even without this protection, exploiting would still require somehow -- getting a file within the allowed directories to autorun, @@ -79,14 +77,15 @@ local function safe_path(fn, readonly) end -- allow only whitelisted dirs local dir = fn:match('^([^/]+)/') - if (not dir) or ( - (not allowed_dirs[dir:lower()]) and - ((not readonly) or (not allowed_dirs_readonly[dir:lower()])) ) then - return nil, 'filename is in disallowed directory '..(dir or 'nil') + if not (dir and ( + allowed_dirs[dir:lower()] or + ( readonly and fn:find('^modules/lualib/') ) )) + then + return nil, 'File is in disallowed directory '..(dir or 'nil') end - -- disallow blacklisted extensions or no extension + -- disallow blacklisted extensions local ext = fn:match('%.([^/%.]+)$') - if (not ext) or (disallowed_exts[ext:lower()]) then + if ext and disallowed_exts[ext:lower()] then return nil, 'Filename \''..fn..'\' has disallowed extension \''.. (ext or '')..'\'' end @@ -117,6 +116,7 @@ local disallowed_packages = tmap { 'ffi', 'debug', 'package', 'io', 'os', '_bllua_ts', } +if old_allowffi then disallowed_packages['ffi'] = nil end function _bllua_requiresecure(name) if name:find('[^a-zA-Z0-9_%-%.]') or name:find('%.%.') or name:find('^%.') or name:find('%.$') then diff --git a/src/lua-env.lua b/src/lua-env.lua index e16f7c2..d129e88 100644 --- a/src/lua-env.lua +++ b/src/lua-env.lua @@ -37,5 +37,9 @@ function _bllua_on_error(err) return table.concat(tracelines, '\n') end +-- overridden in lua-env-safe.lua (executed if not BLLUA_UNSAFE) +_bllua_io_open = io.open +_bllua_requiresecure = require + print = _bllua_ts.echo print(' Executed bllua-env.lua') diff --git a/src/util/libbl.lua b/src/util/libbl.lua index 52195df..878522a 100644 --- a/src/util/libbl.lua +++ b/src/util/libbl.lua @@ -107,13 +107,14 @@ end -- Type conversion from TS to Lua local fromTsForceTypes = { - ['boolean'] = tsBool, - ['object'] = function(val) toTsObject(val) end, -- wrap because toTsObject not defined yet + ['boolean'] = function(val) return tsBool(val) end, + ['object'] = function(val) return toTsObject(val) end, -- wrap because toTsObject not defined yet ['string'] = function(val) return val end, } local function forceValFromTs(val, typ) - return fromTsForceTypes[typ](val) or - error('valFromTs: invalid force type '..typ, 4) + local func = fromTsForceTypes[typ] + if not func then error('valFromTs: invalid force type \''..typ..'\'', 4) end + return func(val) end local function vectorFromTs(val) local xS,yS,zS = val:match('^(%-?[0-9%.e]+) (%-?[0-9%.e]+) (%-?[0-9%.e]+)$') @@ -154,7 +155,8 @@ local function multinumericFromTs(val) end end bl._forceType = bl._forceType or {} -local function valFromTs(val, name, name2) -- todo: ensure name and name2 are already lowercase +-- todo: ensure name and name2 are already lowercase +local function valFromTs(val, name, name2) if type(val)~='string' then error('valFromTs: expected string, got '..type(val), 3) end if name then @@ -362,17 +364,19 @@ local tsObjectMeta = { tsIsFunctionNs(rawget(t,'_tsNamespace'), name) or tsIsFunctionNs(rawget(t,'_tsName'), name) then - return function(t, ...) - local args = {...} - local argsS = arglistToTs(args) - return valFromTs( - _bllua_ts.callobj(rawget(t,'_tsObjectId'), name, unpack(argsS)), - rawget(t,'_tsName') and rawget(t,'_tsName')..'::'..name, - rawget(t,'_tsNamespace')..'::'..name) + return function(t2, ...) + if t2==nil or type(t2)~='table' or not t2._tsObjectId then + error('ts object method: be sure to use :func() not .func()', 2) end + local argsS = arglistToTs({...}) + local res = + _bllua_ts.callobj(t2._tsObjectId, name, unpack(argsS)) + return valFromTs(res, + t2._tsName and t2._tsName..'::'..name, + t2._tsNamespace..'::'..name) end else - return valFromTs( - _bllua_ts.getfield(rawget(t,'_tsObjectId'), name), + local res = _bllua_ts.getfield(rawget(t,'_tsObjectId'), name) + return valFromTs(res, rawget(t,'_tsName') and rawget(t,'_tsName')..'.'..name, rawget(t,'_tsNamespace')..'.'..name) end @@ -437,7 +441,8 @@ local tsObjectMeta = { local obj = toTsObject(_bllua_ts.callobj(t._tsObjectId, 'getObject', tostring(idx))) idx = idx+1 - return idx-1, obj + --return idx-1, obj + return obj else return nil end @@ -559,9 +564,9 @@ local function tsNamespacedCallTfname(name) end local function tsCallGen(name) return function(...) - local args = {...} - local argsS = arglistToTs(args) - return valFromTs(_bllua_ts.call(name, unpack(argsS)), name) + local argsS = arglistToTs({...}) + local res = _bllua_ts.call(name, unpack(argsS)) + return valFromTs(res, name) end end @@ -587,14 +592,16 @@ local tsMeta = { if not rest:find('::') and tsIsFunctionNs(ns, rest) then return tsCallGen(tsNamespacedCallTfname(name)) else - return valFromTs(_bllua_ts.getvar(name), name) + local res = _bllua_ts.getvar(name) + return valFromTs(res, name) end elseif tsIsFunction(name) then return tsCallGen(name) elseif tsIsObject(name) then return toTsObject(name) else - return valFromTs(_bllua_ts.getvar(name), name) + local res = _bllua_ts.getvar(name) + return valFromTs(res, name) end end end, @@ -613,10 +620,12 @@ function bl.call(func, ...) return _bllua_ts.call(func, unpack(argsS)) end function bl.eval(code) - return valFromTs(_bllua_ts.eval(code)) + local res = _bllua_ts.eval(code) + return valFromTs(res) end function bl.exec(file) - return valFromTs(_bllua_ts.call('exec', file)) + local res = _bllua_ts.call('exec', file) + return valFromTs(res) end function bl.array(name, ...) local rest = {...} @@ -645,7 +654,6 @@ end -- Lua calling from TS local luaLookup luaLookup = function(tbl, name, set, val) - print('lookup', tbl, name, set, val) if name:find('%.') then local first, rest = name:match('^([^%.:]+)%.(.+)$') if not isValidFuncName(first) then @@ -661,6 +669,8 @@ luaLookup = function(tbl, name, set, val) error('luacall: cannot have : or . after :', 3) end if not isValidFuncName(first) then error('luacall: invalid name \''..tostring(first)..'\'', 3) end + if not isValidFuncName(rest) then + error('luacall: invalid method name \''..tostring(first)..'\'', 3) end if not tbl[first] then error('luacall: no object named \''..rest..'\'', 3) end if not tbl[first][rest] then @@ -681,7 +691,7 @@ function _bllua_call(fname, ...) local args = arglistFromTs(fname:lower(), {...}) -- todo: separate lua from ts func names? local func = luaLookup(_G, fname) if not func then - error('luacall: no global in lua named \''..name..'\'', 2) end + error('luacall: no global in lua named \''..fname..'\'', 2) end local res = func(unpack(args)) return valToTs(res) end diff --git a/src/util/libts-lua.lua b/src/util/libts-lua.lua index 56f4a9d..149d2b9 100644 --- a/src/util/libts-lua.lua +++ b/src/util/libts-lua.lua @@ -89,11 +89,12 @@ local allowed_zip_dirs = tflip{ local function io_open_absolute(fn, mode) -- if file exists, use original mode local res, err = _bllua_io_open(fn, mode) - if res then return res end + if res then return res + elseif err and not err:find('No such file or directory$') then return nil, err end -- otherwise, if TS sees file but Lua doesn't, it must be in a zip, so use TS reader local dir = fn:match('^[^/]+') - if not allowed_zip_dirs[dir:lower()] then return nil, 'File is not in one of the allowed directories' end + if not allowed_zip_dirs[dir:lower()] then return nil, 'Zip is not in one of the allowed directories' end local exist = _bllua_ts.call('isFile', fn) == '1' if not exist then return nil, err end diff --git a/src/util/std.lua b/src/util/std.lua index ca79eea..53e9e84 100644 --- a/src/util/std.lua +++ b/src/util/std.lua @@ -179,7 +179,7 @@ valueToString = function(v, tabLevel, seen) return tostring(v) else --error('table.tostring: table contains a '..t..' value, cannot serialize') - return 'nil --[[ cannot serialize '..tostring(v)..' ]]' + return 'nil --[[ '..tostring(v)..' ]]' end end function table.tostring(t)