make :members() not return index, add -DBLLUA_ALLOWFFI, allow reading modules/lualib/, bug fixes

This commit is contained in:
Redo
2025-10-06 23:03:12 -05:00
parent 76c758a47b
commit 7232ede09d
8 changed files with 76 additions and 57 deletions

Binary file not shown.

View File

@@ -14,10 +14,10 @@ Lua scripting for Blockland
### From TorqueScript ### From TorqueScript
`'print('hello world')` - Execute Lua in the console by prepending a `'` (single quote) `'print('hello world')` - Execute Lua in the console by prepending a `'` (single quote)
`luaeval("code");` - Execute Lua code `luaeval("code");` - Execute Lua code
`luacall("funcName", %args...);` - Call a Lua global function `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 executing .cs files. `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 `luaget("varName");` - Read a Lua global variable (supports indexing tables)
`luaset("varName", %value);` - Write a Lua global variable `luaset("varName", %value);` - Write a Lua global variable (supports indexing tables)
### From Lua ### From Lua
`bl.eval('code')` - Eval TorqueScript code `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.key = value` - Associate Lua data with a Torque object
`object:method(args)` - Call a Torque object method `object:method(args)` - Call a Torque object method
`object[index]` - Access a member of a Torque set or group `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 `bl.isObject(object, objectID, or 'objectName')` - Check if an object exists
`object:exists()` - 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. `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 ### Server-Client Communication
`bl.addServerCmd('commandName', function(client, args...) yourCode end)` - Register a /command on the server `bl.addServerCmd('commandName', function(client, args...) ... end)` - Register a /command on the server
`bl.addClientCmd('commandName', function(args...) yourCode end)` - Register a client command on the client `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.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.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 `bl.commandToAll('commandName', args...)` - As the server, execute a client command on all clients
### Packages/Hooks ### 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. `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. 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. 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. BlockLua also has access to any C libraries installed in the `modules/lualib` folder, so be careful throwing things in there.
### Unsafe Mode ### 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. 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 ### List of Object Types
`'all'` - Any object `'all'` - Any object
@@ -210,6 +211,6 @@ Other types: `'static'`, `'environment'`, `'terrain'`, `'water'`, `'trigger'`, `
## Compiling ## Compiling
With any *32-bit* variant of GCC installed (such as MinGW or MSYS2), run the following command in the repo directory: 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/ LuaJIT (lua5.1.dll) can be obtained from https://luajit.org/

View File

@@ -1,4 +1,4 @@
// BlockLua (bllua4): Simple Lua interface for TorqueScript // BlockLua (bllua4): Advanced Lua interface for TorqueScript
// Includes // Includes
@@ -57,22 +57,26 @@ bool init() {
// Set up Lua environment // Set up Lua environment
BLL_LOAD_LUA(gL, bll_fileLuaEnv); BLL_LOAD_LUA(gL, bll_fileLuaEnv);
#ifdef BLLUA_ALLOWFFI
lua_pushboolean(gL, true);
lua_setglobal(gL, "_bllua_allowffi");
#endif
#ifndef BLLUA_UNSAFE #ifndef BLLUA_UNSAFE
BLL_LOAD_LUA(gL, bll_fileLuaEnvSafe); BLL_LOAD_LUA(gL, bll_fileLuaEnvSafe);
#endif #endif
// Expose Lua API to TS // Load utilities in Lua
BlAddFunction(NULL, NULL, "_bllua_luacall", bll_ts_luacall, "LuaCall(name, ...) - Call Lua function and return result", 2, 20);
BlEval(bll_fileTsEnv);
// Load utilities
BLL_LOAD_LUA(gL, bll_fileLuaStd); BLL_LOAD_LUA(gL, bll_fileLuaStd);
BLL_LOAD_LUA(gL, bll_fileLuaVector); BLL_LOAD_LUA(gL, bll_fileLuaVector);
BLL_LOAD_LUA(gL, bll_fileLuaMatrix); BLL_LOAD_LUA(gL, bll_fileLuaMatrix);
BLL_LOAD_LUA(gL, bll_fileLuaLibts); BLL_LOAD_LUA(gL, bll_fileLuaLibts);
BlEval(bll_fileTsLibts);
BLL_LOAD_LUA(gL, bll_fileLuaLibbl); BLL_LOAD_LUA(gL, bll_fileLuaLibbl);
BLL_LOAD_LUA(gL, bll_fileLuaLibblTypes); 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_fileTsLibblSupport);
BlEval(bll_fileLoadaddons); BlEval(bll_fileLoadaddons);
@@ -85,8 +89,7 @@ bool init() {
bool deinit() { bool deinit() {
BlPrintf("BlockLua: Unloading"); BlPrintf("BlockLua: Unloading");
BlEval("deactivatePackage(_bllua_main);"); BlEval("$_bllua_active=0;deactivatePackage(_bllua_main);");
BlEval("$_bllua_active = 0;");
bll_LuaEval(gL, "for _,f in pairs(_bllua_on_unload) do f() end"); bll_LuaEval(gL, "for _,f in pairs(_bllua_on_unload) do f() end");
lua_close(gL); lua_close(gL);

View File

@@ -12,6 +12,7 @@ local old_require = require
local old_os = os local old_os = os
local old_debug = debug local old_debug = debug
local old_package = package local old_package = package
local old_allowffi = _bllua_allowffi
-- Remove all global variables except a whitelist -- Remove all global variables except a whitelist
local ok_names = tmap { local ok_names = tmap {
@@ -37,13 +38,10 @@ end
-- Sanitize file paths to point only to allowed files within the game directory -- Sanitize file paths to point only to allowed files within the game directory
-- List of allowed directories for reading/writing -- List of allowed directories for reading/writing
-- modules/lualib is also allowed as read-only
local allowed_dirs = tmap { local allowed_dirs = tmap {
'add-ons', 'base', 'config', 'saves', 'screenshots', 'shaders' '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 -- List of disallowed file extensions - basically executable file extensions
-- Note that even without this protection, exploiting would still require somehow -- Note that even without this protection, exploiting would still require somehow
-- getting a file within the allowed directories to autorun, -- getting a file within the allowed directories to autorun,
@@ -79,14 +77,15 @@ local function safe_path(fn, readonly)
end end
-- allow only whitelisted dirs -- allow only whitelisted dirs
local dir = fn:match('^([^/]+)/') local dir = fn:match('^([^/]+)/')
if (not dir) or ( if not (dir and (
(not allowed_dirs[dir:lower()]) and allowed_dirs[dir:lower()] or
((not readonly) or (not allowed_dirs_readonly[dir:lower()])) ) then ( readonly and fn:find('^modules/lualib/') ) ))
return nil, 'filename is in disallowed directory '..(dir or 'nil') then
return nil, 'File is in disallowed directory '..(dir or 'nil')
end end
-- disallow blacklisted extensions or no extension -- disallow blacklisted extensions
local ext = fn:match('%.([^/%.]+)$') 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 \''.. return nil, 'Filename \''..fn..'\' has disallowed extension \''..
(ext or '')..'\'' (ext or '')..'\''
end end
@@ -117,6 +116,7 @@ local disallowed_packages = tmap {
'ffi', 'debug', 'package', 'io', 'os', 'ffi', 'debug', 'package', 'io', 'os',
'_bllua_ts', '_bllua_ts',
} }
if old_allowffi then disallowed_packages['ffi'] = nil end
function _bllua_requiresecure(name) function _bllua_requiresecure(name)
if name:find('[^a-zA-Z0-9_%-%.]') or name:find('%.%.') or if name:find('[^a-zA-Z0-9_%-%.]') or name:find('%.%.') or
name:find('^%.') or name:find('%.$') then name:find('^%.') or name:find('%.$') then

View File

@@ -37,5 +37,9 @@ function _bllua_on_error(err)
return table.concat(tracelines, '\n') return table.concat(tracelines, '\n')
end 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 = _bllua_ts.echo
print(' Executed bllua-env.lua') print(' Executed bllua-env.lua')

View File

@@ -107,13 +107,14 @@ end
-- Type conversion from TS to Lua -- Type conversion from TS to Lua
local fromTsForceTypes = { local fromTsForceTypes = {
['boolean'] = tsBool, ['boolean'] = function(val) return tsBool(val) end,
['object'] = function(val) toTsObject(val) end, -- wrap because toTsObject not defined yet ['object'] = function(val) return toTsObject(val) end, -- wrap because toTsObject not defined yet
['string'] = function(val) return val end, ['string'] = function(val) return val end,
} }
local function forceValFromTs(val, typ) local function forceValFromTs(val, typ)
return fromTsForceTypes[typ](val) or local func = fromTsForceTypes[typ]
error('valFromTs: invalid force type '..typ, 4) if not func then error('valFromTs: invalid force type \''..typ..'\'', 4) end
return func(val)
end end
local function vectorFromTs(val) local function vectorFromTs(val)
local xS,yS,zS = val:match('^(%-?[0-9%.e]+) (%-?[0-9%.e]+) (%-?[0-9%.e]+)$') 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
end end
bl._forceType = bl._forceType or {} 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 if type(val)~='string' then
error('valFromTs: expected string, got '..type(val), 3) end error('valFromTs: expected string, got '..type(val), 3) end
if name then if name then
@@ -362,17 +364,19 @@ local tsObjectMeta = {
tsIsFunctionNs(rawget(t,'_tsNamespace'), name) or tsIsFunctionNs(rawget(t,'_tsNamespace'), name) or
tsIsFunctionNs(rawget(t,'_tsName'), name) tsIsFunctionNs(rawget(t,'_tsName'), name)
then then
return function(t, ...) return function(t2, ...)
local args = {...} if t2==nil or type(t2)~='table' or not t2._tsObjectId then
local argsS = arglistToTs(args) error('ts object method: be sure to use :func() not .func()', 2) end
return valFromTs( local argsS = arglistToTs({...})
_bllua_ts.callobj(rawget(t,'_tsObjectId'), name, unpack(argsS)), local res =
rawget(t,'_tsName') and rawget(t,'_tsName')..'::'..name, _bllua_ts.callobj(t2._tsObjectId, name, unpack(argsS))
rawget(t,'_tsNamespace')..'::'..name) return valFromTs(res,
t2._tsName and t2._tsName..'::'..name,
t2._tsNamespace..'::'..name)
end end
else else
return valFromTs( local res = _bllua_ts.getfield(rawget(t,'_tsObjectId'), name)
_bllua_ts.getfield(rawget(t,'_tsObjectId'), name), return valFromTs(res,
rawget(t,'_tsName') and rawget(t,'_tsName')..'.'..name, rawget(t,'_tsName') and rawget(t,'_tsName')..'.'..name,
rawget(t,'_tsNamespace')..'.'..name) rawget(t,'_tsNamespace')..'.'..name)
end end
@@ -437,7 +441,8 @@ local tsObjectMeta = {
local obj = toTsObject(_bllua_ts.callobj(t._tsObjectId, local obj = toTsObject(_bllua_ts.callobj(t._tsObjectId,
'getObject', tostring(idx))) 'getObject', tostring(idx)))
idx = idx+1 idx = idx+1
return idx-1, obj --return idx-1, obj
return obj
else else
return nil return nil
end end
@@ -559,9 +564,9 @@ local function tsNamespacedCallTfname(name)
end end
local function tsCallGen(name) local function tsCallGen(name)
return function(...) return function(...)
local args = {...} local argsS = arglistToTs({...})
local argsS = arglistToTs(args) local res = _bllua_ts.call(name, unpack(argsS))
return valFromTs(_bllua_ts.call(name, unpack(argsS)), name) return valFromTs(res, name)
end end
end end
@@ -587,14 +592,16 @@ local tsMeta = {
if not rest:find('::') and tsIsFunctionNs(ns, rest) then if not rest:find('::') and tsIsFunctionNs(ns, rest) then
return tsCallGen(tsNamespacedCallTfname(name)) return tsCallGen(tsNamespacedCallTfname(name))
else else
return valFromTs(_bllua_ts.getvar(name), name) local res = _bllua_ts.getvar(name)
return valFromTs(res, name)
end end
elseif tsIsFunction(name) then elseif tsIsFunction(name) then
return tsCallGen(name) return tsCallGen(name)
elseif tsIsObject(name) then elseif tsIsObject(name) then
return toTsObject(name) return toTsObject(name)
else else
return valFromTs(_bllua_ts.getvar(name), name) local res = _bllua_ts.getvar(name)
return valFromTs(res, name)
end end
end end
end, end,
@@ -613,10 +620,12 @@ function bl.call(func, ...)
return _bllua_ts.call(func, unpack(argsS)) return _bllua_ts.call(func, unpack(argsS))
end end
function bl.eval(code) function bl.eval(code)
return valFromTs(_bllua_ts.eval(code)) local res = _bllua_ts.eval(code)
return valFromTs(res)
end end
function bl.exec(file) function bl.exec(file)
return valFromTs(_bllua_ts.call('exec', file)) local res = _bllua_ts.call('exec', file)
return valFromTs(res)
end end
function bl.array(name, ...) function bl.array(name, ...)
local rest = {...} local rest = {...}
@@ -645,7 +654,6 @@ end
-- Lua calling from TS -- Lua calling from TS
local luaLookup local luaLookup
luaLookup = function(tbl, name, set, val) luaLookup = function(tbl, name, set, val)
print('lookup', tbl, name, set, val)
if name:find('%.') then if name:find('%.') then
local first, rest = name:match('^([^%.:]+)%.(.+)$') local first, rest = name:match('^([^%.:]+)%.(.+)$')
if not isValidFuncName(first) then if not isValidFuncName(first) then
@@ -661,6 +669,8 @@ luaLookup = function(tbl, name, set, val)
error('luacall: cannot have : or . after :', 3) end error('luacall: cannot have : or . after :', 3) end
if not isValidFuncName(first) then if not isValidFuncName(first) then
error('luacall: invalid name \''..tostring(first)..'\'', 3) end 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 if not tbl[first] then
error('luacall: no object named \''..rest..'\'', 3) end error('luacall: no object named \''..rest..'\'', 3) end
if not tbl[first][rest] then 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 args = arglistFromTs(fname:lower(), {...}) -- todo: separate lua from ts func names?
local func = luaLookup(_G, fname) local func = luaLookup(_G, fname)
if not func then 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)) local res = func(unpack(args))
return valToTs(res) return valToTs(res)
end end

View File

@@ -89,11 +89,12 @@ local allowed_zip_dirs = tflip{
local function io_open_absolute(fn, mode) local function io_open_absolute(fn, mode)
-- if file exists, use original mode -- if file exists, use original mode
local res, err = _bllua_io_open(fn, 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 -- otherwise, if TS sees file but Lua doesn't, it must be in a zip, so use TS reader
local dir = fn:match('^[^/]+') 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' local exist = _bllua_ts.call('isFile', fn) == '1'
if not exist then return nil, err end if not exist then return nil, err end

View File

@@ -179,7 +179,7 @@ valueToString = function(v, tabLevel, seen)
return tostring(v) return tostring(v)
else else
--error('table.tostring: table contains a '..t..' value, cannot serialize') --error('table.tostring: table contains a '..t..' value, cannot serialize')
return 'nil --[[ cannot serialize '..tostring(v)..' ]]' return 'nil --[[ '..tostring(v)..' ]]'
end end
end end
function table.tostring(t) function table.tostring(t)