diff --git a/.luacheckrc b/.luacheckrc index ae6aa728ee..54cf9e3a2a 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -41,7 +41,13 @@ stds.menu_common = { }, } -files["builtin/client/register.lua"] = { +files["builtin/client/init.lua"] = { + globals = { + debug = {fields={"getinfo"}}, + } +} + +files["builtin/sscsm_client/init.lua"] = { globals = { debug = {fields={"getinfo"}}, } diff --git a/builtin/client/init.lua b/builtin/client/init.lua index ee0f267db7..769fbe56cb 100644 --- a/builtin/client/init.lua +++ b/builtin/client/init.lua @@ -13,3 +13,6 @@ dofile(commonpath .. "information_formspecs.lua") dofile(clientpath .. "chatcommands.lua") dofile(clientpath .. "misc.lua") assert(loadfile(commonpath .. "item_s.lua"))({}) -- Just for push/read node functions + +-- unset, as promised in initializeSecurityClient() +debug.getinfo = nil diff --git a/builtin/common/register.lua b/builtin/common/register.lua index cbeac7c64f..9ad8a16fb6 100644 --- a/builtin/common/register.lua +++ b/builtin/common/register.lua @@ -1,4 +1,5 @@ local builtin_shared = ... +local debug_getinfo = debug.getinfo do local default = {mod = "??", name = "??"} @@ -56,7 +57,7 @@ function builtin_shared.make_registration() core.callback_origins[func] = { -- may be nil or return nil mod = core.get_current_modname and core.get_current_modname() or "??", - name = debug.getinfo(1, "n").name or "??" + name = debug_getinfo(1, "n").name or "??" } end return t, registerfunc @@ -69,7 +70,7 @@ function builtin_shared.make_registration_reverse() core.callback_origins[func] = { -- may be nil or return nil mod = core.get_current_modname and core.get_current_modname() or "??", - name = debug.getinfo(1, "n").name or "??" + name = debug_getinfo(1, "n").name or "??" } end return t, registerfunc diff --git a/builtin/common/strict.lua b/builtin/common/strict.lua index b3c4ccce46..c2c673aa6f 100644 --- a/builtin/common/strict.lua +++ b/builtin/common/strict.lua @@ -1,4 +1,4 @@ -local getinfo, rawget, rawset = debug.getinfo, rawget, rawset +local debug_getinfo, rawget, rawset = debug.getinfo, rawget, rawset function core.global_exists(name) if type(name) ~= "string" then @@ -18,7 +18,7 @@ function meta:__newindex(name, value) if declared[name] then return end - local info = getinfo(2, "Sl") + local info = debug_getinfo(2, "Sl") if info ~= nil then local desc = ("%s:%d"):format(info.short_src, info.currentline) local warn_key = ("%s\0%d\0%s"):format(info.source, info.currentline, name) @@ -36,7 +36,7 @@ function meta:__index(name) if declared[name] then return end - local info = getinfo(2, "Sl") + local info = debug_getinfo(2, "Sl") if info == nil then return end diff --git a/builtin/game/register.lua b/builtin/game/register.lua index b832ccc6db..cc2f5fe980 100644 --- a/builtin/game/register.lua +++ b/builtin/game/register.lua @@ -1,5 +1,6 @@ local builtin_shared = ... local S = core.get_translator("__builtin") +local debug_getinfo = debug.getinfo -- -- Make raw registration functions inaccessible to anyone except this file @@ -548,7 +549,7 @@ function core.registered_on_player_hpchange(player, hp_change, reason) local func = core.registered_on_player_hpchanges.modifiers[i] hp_change, last = func(player, hp_change, reason) if type(hp_change) ~= "number" then - local debuginfo = debug.getinfo(func) + local debuginfo = debug_getinfo(func) error("The register_on_hp_changes function has to return a number at " .. debuginfo.short_src .. " line " .. debuginfo.linedefined) end @@ -570,7 +571,7 @@ function core.register_on_player_hpchange(func, modifier) end core.callback_origins[func] = { mod = core.get_current_modname() or "??", - name = debug.getinfo(1, "n").name or "??" + name = debug_getinfo(1, "n").name or "??" } end diff --git a/builtin/init.lua b/builtin/init.lua index 59d1558fca..c0ba5f4001 100644 --- a/builtin/init.lua +++ b/builtin/init.lua @@ -76,6 +76,10 @@ elseif INIT == "async_game" then dofile(asyncpath .. "game.lua") elseif INIT == "client" then dofile(scriptdir .. "client" .. DIR_DELIM .. "init.lua") +elseif INIT == "sscsm" and core.get_current_modname() == "*client_builtin*" then + dofile(scriptdir .. "sscsm_client" .. DIR_DELIM .. "init.lua") +elseif INIT == "sscsm" and core.get_current_modname() == "*server_builtin*" then + dofile(scriptdir .. "sscsm_server" .. DIR_DELIM .. "init.lua") elseif INIT == "emerge" then dofile(scriptdir .. "emerge" .. DIR_DELIM .. "init.lua") elseif INIT == "pause_menu" then diff --git a/builtin/profiler/instrumentation.lua b/builtin/profiler/instrumentation.lua index 2b34295daf..39947cc7ef 100644 --- a/builtin/profiler/instrumentation.lua +++ b/builtin/profiler/instrumentation.lua @@ -5,6 +5,7 @@ local format, ipairs, type = string.format, ipairs, type local core, get_current_modname = core, core.get_current_modname local profiler, sampler = ... +local debug_getinfo = debug.getinfo local instrument_builtin = core.settings:get_bool("instrument.builtin", false) @@ -67,7 +68,7 @@ local worldmods_path = regex_escape(core.get_worldpath()) local user_path = regex_escape(core.get_user_path()) local builtin_path = regex_escape(core.get_builtin_path()) local function generate_source_location(def) - local info = debug.getinfo(def.func) + local info = debug_getinfo(def.func) local modpath = regex_escape(core.get_modpath(def.mod) or "") local source = info.source if modpath ~= "" then diff --git a/builtin/settingtypes.txt b/builtin/settingtypes.txt index aad629900a..f1af101a2c 100644 --- a/builtin/settingtypes.txt +++ b/builtin/settingtypes.txt @@ -1883,6 +1883,10 @@ mgvalleys_np_dungeons (Dungeon noise) noise_params_3d 0.9, 0.5, (500, 500, 500), # This support is experimental and API can change. enable_client_modding (Client modding) [client] bool false +# Where to enable server-sent client-side modding (SSCSM). +# Warning: Experimental. +enable_sscsm (Enable SSCSM) [client] enum nowhere nowhere,singleplayer,localhost,lan,everywhere + # Replaces the default main menu with a custom one. main_menu_script (Main menu script) [client] string diff --git a/builtin/sscsm_client/init.lua b/builtin/sscsm_client/init.lua new file mode 100644 index 0000000000..2bfc58c759 --- /dev/null +++ b/builtin/sscsm_client/init.lua @@ -0,0 +1,25 @@ +local scriptpath = core.get_builtin_path() +local commonpath = scriptpath .. "common" .. DIR_DELIM +local mypath = scriptpath .. "sscsm_client".. DIR_DELIM + +-- Shared between builtin files, but +-- not exposed to outer context +local builtin_shared = {} + +-- placeholders +-- FIXME: send actual content defs to sscsm env +function core.get_content_id(name) + return tonumber(name) +end +function core.get_name_from_content_id(id) + return tostring(id) +end + +assert(loadfile(commonpath .. "item_s.lua"))(builtin_shared) +assert(loadfile(commonpath .. "register.lua"))(builtin_shared) +assert(loadfile(mypath .. "register.lua"))(builtin_shared) + +dofile(commonpath .. "after.lua") + +-- unset, as promised in initializeSecuritySSCSM() +debug.getinfo = nil diff --git a/builtin/sscsm_client/register.lua b/builtin/sscsm_client/register.lua new file mode 100644 index 0000000000..b224053bbb --- /dev/null +++ b/builtin/sscsm_client/register.lua @@ -0,0 +1,5 @@ +local builtin_shared = ... + +local make_registration = builtin_shared.make_registration + +core.registered_globalsteps, core.register_globalstep = make_registration() diff --git a/builtin/sscsm_server/init.lua b/builtin/sscsm_server/init.lua new file mode 100644 index 0000000000..e69de29bb2 diff --git a/doc/sscsm_api.md b/doc/sscsm_api.md new file mode 100644 index 0000000000..a79de7a1f9 --- /dev/null +++ b/doc/sscsm_api.md @@ -0,0 +1,213 @@ +# Server-sent client-side modding (SSCSM) API reference + +**Warning:** SSCSM is very experimental. The API will break. Always start your +mod with a version check (using `core.get_version()`). + +In SSCSM, the server sends scripts to the client, which it executes +client-side (in a sandbox, see also `sscsm_security.md`). +As modder, you can add these scripts to your server-side mod, and tell the engine +to send them. + +Please refer to `lua_api.md` for server-side modding. +(And refer to `client_lua_api.md` for client-provided client-side modding (CPCSM).) + + + +## Loading mods + +### Paths + +SSCSM uses a virtual file system (just a dictionary of virtual paths (strings)) +to file contents (strings). + +Each mod's files have paths of the form `modname:foo/bla.lua`. +Please don't rely on this, use `core.get_modpath()` instead. + +The virtual file paths within a mod are meant to mimic the filepaths on the +server, for example `/common/foo.lua` gets sent as `modname:common/foo.lua`. + +The engine loads `modname:init.lua` for all mods, in server mod dependency order. + +There is client and server builtin (modnames are `*client_builtin*` and +`*server_builtin*`). The server builtin is sent from the server, like any other +SSCSM, and the client builtin is located on the client. + + +### Mod sending API + +Currently, you can not add any mods. There's only a small hardcoded preview script +in C++ which is loaded when you set `enable_sscsm` to `singleplayer`. + + + +## API + +Unless noted otherwise, these work the same as in the server modding API. + +### Global callbacks + +* `core.register_globalstep(function(dtime))` + + +### SSCSM-specific API + +* `core.get_node_or_nil(pos)` +* `core.get_content_id(name)` +* `core.get_name_from_content_id(id)` + + +### Util API + +* `core.log([level,] text)` +* `core.get_us_time()` + * Limited in precision. +* `core.parse_json(str[, nullvalue])` +* `core.write_json(data[, styled])` +* `core.is_yes(arg)` +* `core.compress(data, method, ...)` +* `core.decompress(data, method, ...)` +* `core.encode_base64(string)` +* `core.decode_base64(string)` +* `core.get_version()` +* `core.sha1(string, raw)` +* `core.sha256(string, raw)` +* `core.colorspec_to_colorstring(colorspec)` +* `core.colorspec_to_bytes(colorspec)` +* `core.colorspec_to_table(colorspec)` +* `core.time_to_day_night_ratio(time_of_day)` +* `core.get_last_run_mod()` +* `core.set_last_run_mod(modname)` +* `core.urlencode(value)` + + +### Other + +* `core.get_current_modname()` +* `core.get_modpath(modname)` + + +### Builtin helpers + +* `math.*` additions + +* `vector.*` + +* `core.global_exists(name)` + +* `core.serialize(value)` +* `core.deserialize(str, safe)` + +* `dump2(obj, name, dumped)` +* `dump(obj, dumped)` +* `string.*` additions +* `table.*` additions +* `core.formspec_escape(text)` +* `core.hypertext_escape(text)` +* `core.wrap_text(str, limit, as_table)` +* `core.explode_table_event(evt)` +* `core.explode_textlist_event(evt)` +* `core.explode_scrollbar_event(evt)` +* `core.rgba(r, g, b, a)` +* `core.pos_to_string(pos, decimal_places)` +* `core.string_to_pos(value)` +* `core.string_to_area(value, relative_to)` +* `core.get_color_escape_sequence(color)` +* `core.get_background_escape_sequence(color)` +* `core.colorize(color, message)` +* `core.strip_foreground_colors(str)` +* `core.strip_background_colors(str)` +* `core.strip_colors(str)` +* `core.translate(textdomain, str, ...)` +* `core.translate_n(textdomain, str, str_plural, n, ...)` +* `core.get_translator(textdomain)` +* `core.pointed_thing_to_face_pos(placer, pointed_thing)` +* `core.string_to_privs(str, delim)` +* `core.privs_to_string(privs, delim)` +* `core.is_nan(number)` +* `core.parse_relative_number(arg, relative_to)` +* `core.parse_coordinates(x, y, z, relative_to)` + +* `core.inventorycube(img1, img2, img3)` +* `core.dir_to_facedir(dir, is6d)` +* `core.facedir_to_dir(facedir)` +* `core.dir_to_fourdir(dir)` +* `core.fourdir_to_dir(fourdir)` +* `core.dir_to_wallmounted(dir)` +* `core.wallmounted_to_dir(wallmounted)` +* `core.dir_to_yaw(dir)` +* `core.yaw_to_dir(yaw)` +* `core.is_colored_paramtype(ptype)` +* `core.strip_param2_color(param2, paramtype2)` + +* `core.after(time, func, ...)` + + +### Lua standard library + +* `assert` +* `collectgarbage` +* `error` +* `getfenv` +* `ipairs` +* `next` +* `pairs` +* `pcall` +* `print` +* `rawequal` +* `rawget` +* `rawset` +* `select` +* `setfenv` +* `getmetatable` +* `setmetatable` +* `tonumber` +* `tostring` +* `type` +* `unpack` +* `_VERSION` +* `xpcall` +* `dofile` + * Overwritten. +* `load` + * Overwritten. +* `loadfile` + * Overwritten. +* `loadstring` + * Overwritten. +* `coroutine.*` +* `table.*` +* `math.*` +* `string.*` + * except `string.dump` +* `os.difftime` +* `os.time` +* `os.clock` + * Reduced precision. +* `debug.traceback` + + +### LuaJIT `jit` library + +* `jit.arch` +* `jit.flush` +* `jit.off` +* `jit.on` +* `jit.opt` +* `jit.os` +* `jit.status` +* `jit.version` +* `jit.version_num` + + +### Bit library + +* `bit.*` + + +### API only for client builtin + +* `core.get_builtin_path()` + * Returns path, depending on which builtin currently loads, or `nil`. +* `debug.getinfo(...)` +* `INIT` + * Is `"sscsm"`. diff --git a/doc/sscsm_security.md b/doc/sscsm_security.md new file mode 100644 index 0000000000..acebad00d2 --- /dev/null +++ b/doc/sscsm_security.md @@ -0,0 +1,76 @@ +# SSCSM security + + +## Threat model + +* SSCSM scripts come from the server (potential malicious actor). We are the client. +* Authenticity of server is not given (our networking is not secure). So we have + to expect anyone who can send us UDP packets to the appropriate IP address to be + able to act on behalf of the server. +* The server may not tamper with, or get access to information of, anything besides + the stuff explicitly made accessible via the modding API (i.e. gameplay relevant + stuff, like map, node definitions, ...). + In particular, this excludes for (non-exhaustive) example files, file paths, + and settings. +* DOS is not an issue (as it is already easily possible to DOS a client). +* We already have an API via network packets (see `networkprotocol.h`). + This acts as upper bound: Every SSCSM API function could instead be a network + packet endpoint. There are no efforts to make SSCSM more secure than this. + + +## Non-binary `enable_sscsm` setting + +The `enable_sscsm` setting does not just allow en-/disabling SSCSM, it also allows +limiting on what sort of servers to enable SSCSM. Options are `nowhere`, `singleplayer`, +`localhost` (or singleplayer), `lan` (or lower), and everywhere. +On options `localhost` and lower, we know that (anyone who acts on the behalf of) +the server runs on the same machine, and the risk of it being malicious is pretty +much zero. + +Until sufficient security measures are in place, users are disallowed to set this +setting to anything higher than `localhost`. + + +## Lua sandbox + +* We execute only Lua scripts, in a Lua sandbox. +* See also `initializeSecuritySSCSM()`. +* We do not trust the Lua implementation to not have bugs. => Additional process + isolation layer as fallback. + + +## Process isolation + +* Not yet implemented. +* Separate SSCSM process. +* Sandboxing: + * Linux: Uses SECCOMP. + * ... (FIXME: write down stuff when you implement) + + +## Limit where we call into SSCSM + +* Even if the Lua sandbox and/or the process isolation are bug-free, the main + process client code can still be vulnerable. Consider this example: + * Client has an inventorylist A. + * User moves an item. + * SSCSM gets called (callback when item is moved). + * SSCSM can do anything now. It decides to delete A, then returns. + * Client still has reference to A on stack, tries to access it. + * => Use-after-free. +* To avoid these sort of issues, we only give control-flow to SSCSM in few special + places. + In particular, this includes packet handlers, and the client's `step()` function. +* In these places, the client already does not assume anything about the current + state (e.g. that an inventory exists). +* This makes sure that SSCSM API calls can also just happen in these places. + In packet handlers, the server can already cause arbitrary network API "calls" + to happen. Hence, new SSCSM API calls here do not lead to new vulnerabilities + that a network API would not cause as well. + + +## No precise clocks + +To mitigate time-based side-channel attacks, all available clock API functions +(`os.clock()` and `core.get_us_time()`) only have a precision of +`SSCSM_CLOCK_RESOLUTION_US` (20) us. diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index ac98d4bd88..60f756b428 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -75,6 +75,7 @@ set(client_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/texturesource.cpp ${CMAKE_CURRENT_SOURCE_DIR}/imagesource.cpp ${CMAKE_CURRENT_SOURCE_DIR}/wieldmesh.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/mod_vfs.cpp ${CMAKE_CURRENT_SOURCE_DIR}/shadows/dynamicshadows.cpp ${CMAKE_CURRENT_SOURCE_DIR}/shadows/dynamicshadowsrender.cpp ${CMAKE_CURRENT_SOURCE_DIR}/shadows/shadowsshadercallbacks.cpp diff --git a/src/client/client.cpp b/src/client/client.cpp index b1dfa59931..e610fe3741 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -10,6 +10,7 @@ #include #include "client.h" #include "client/fontengine.h" +#include "client/mod_vfs.h" #include "network/clientopcodes.h" #include "network/connection.h" #include "network/networkpacket.h" @@ -54,6 +55,8 @@ #include "content/mod_configuration.h" #include "mapnode.h" #include "item_visuals_manager.h" +#include "script/sscsm/sscsm_controller.h" +#include "script/sscsm/sscsm_events.h" extern gui::IGUIEnvironment* guienv; @@ -136,6 +139,62 @@ Client::Client( m_cache_save_interval = g_settings->getU16("server_map_save_interval"); m_mesh_grid = { g_settings->getU16("client_mesh_chunk") }; + + m_sscsm_controller = SSCSMController::create(); + + { + auto event1 = std::make_unique(); + + ModVFS tmp_mod_vfs; + // FIXME: only read files that are relevant to sscsm, and compute sha2 digests + tmp_mod_vfs.scanModIntoMemory("*client_builtin*", getBuiltinLuaPath()); + + for (auto &p : tmp_mod_vfs.m_vfs) { + event1->files.emplace_back(p.first, std::move(p.second)); + } + + m_sscsm_controller->runEvent(this, std::move(event1)); + + // load client builtin immediately + auto event2 = std::make_unique(); + event2->mods.emplace_back("*client_builtin*", "*client_builtin*:init.lua"); + m_sscsm_controller->runEvent(this, std::move(event2)); + } + + { + //FIXME: network packets + //FIXME: check that *client_builtin* is not overridden + + std::string enable_sscsm = g_settings->get("enable_sscsm"); + if (enable_sscsm == "singleplayer") { //FIXME: enum + auto event1 = std::make_unique(); + + // some simple test code + event1->files.emplace_back("sscsm_test0:init.lua", + R"=+=( +print("sscsm_test0: loading") + +--print(dump(_G)) +--print(debug.traceback()) + +do + local pos = vector.zero() + local function print_nodes() + print(string.format("node at %s: %s", pos, dump(core.get_node_or_nil(pos)))) + pos = pos:offset(1, 0, 0) + core.after(1, print_nodes) + end + core.after(0, print_nodes) +end + )=+="); + + m_sscsm_controller->runEvent(this, std::move(event1)); + + auto event2 = std::make_unique(); + event2->mods.emplace_back("sscsm_test0", "sscsm_test0:init.lua"); + m_sscsm_controller->runEvent(this, std::move(event2)); + } + } } void Client::migrateModStorage() @@ -183,12 +242,14 @@ void Client::loadMods() return; } + m_mod_vfs = std::make_unique(); + m_script = new ClientScripting(this); m_env.setScript(m_script); m_script->setEnv(&m_env); // Load builtin - scanModIntoMemory(BUILTIN_MOD_NAME, getBuiltinLuaPath()); + m_mod_vfs->scanModIntoMemory(BUILTIN_MOD_NAME, getBuiltinLuaPath()); m_script->loadModFromMemory(BUILTIN_MOD_NAME); m_script->checkSetByBuiltin(); @@ -227,7 +288,7 @@ void Client::loadMods() // Load "mod" scripts for (const ModSpec &mod : m_mods) { mod.checkAndLog(); - scanModIntoMemory(mod.name, mod.path); + m_mod_vfs->scanModIntoMemory(mod.name, mod.path); } // Run them @@ -249,35 +310,6 @@ void Client::loadMods() m_script->on_minimap_ready(m_minimap.get()); } -void Client::scanModSubfolder(const std::string &mod_name, const std::string &mod_path, - std::string mod_subpath) -{ - std::string full_path = mod_path + DIR_DELIM + mod_subpath; - std::vector mod = fs::GetDirListing(full_path); - for (const fs::DirListNode &j : mod) { - if (j.name[0] == '.') - continue; - - if (j.dir) { - scanModSubfolder(mod_name, mod_path, mod_subpath + j.name + DIR_DELIM); - continue; - } - std::replace(mod_subpath.begin(), mod_subpath.end(), DIR_DELIM_CHAR, '/'); - - std::string real_path = full_path + j.name; - std::string vfs_path = mod_name + ":" + mod_subpath + j.name; - infostream << "Client::scanModSubfolder(): Loading \"" << real_path - << "\" as \"" << vfs_path << "\"." << std::endl; - - std::string contents; - if (!fs::ReadFile(real_path, contents, true)) { - continue; - } - - m_mod_vfs.emplace(vfs_path, contents); - } -} - const std::string &Client::getBuiltinLuaPath() { static const std::string builtin_dir = porting::path_share + DIR_DELIM + "builtin"; @@ -523,6 +555,12 @@ void Client::step(float dtime) */ LocalPlayer *player = m_env.getLocalPlayer(); + { + auto event = std::make_unique(); + event->dtime = dtime; + m_sscsm_controller->runEvent(this, std::move(event)); + } + // Step environment (also handles player controls) m_env.step(dtime); m_sound->step(dtime); @@ -2034,23 +2072,6 @@ scene::IAnimatedMesh* Client::getMesh(const std::string &filename, bool cache) return mesh; } -const std::string* Client::getModFile(std::string filename) -{ - // strip dir delimiter from beginning of path - auto pos = filename.find_first_of(':'); - if (pos == std::string::npos) - return nullptr; - pos++; - auto pos2 = filename.find_first_not_of('/', pos); - if (pos2 > pos) - filename.erase(pos, pos2 - pos); - - StringMap::const_iterator it = m_mod_vfs.find(filename); - if (it == m_mod_vfs.end()) - return nullptr; - return &it->second; -} - /* * Mod channels */ diff --git a/src/client/client.h b/src/client/client.h index 4b36472f95..523418825b 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -45,6 +45,8 @@ class NodeDefManager; class ParticleManager; class RenderingEngine; class SingleMediaDownloader; +class ClientScripting; +class SSCSMController; struct ChatMessage; struct ClientDynamicInfo; struct ClientEvent; @@ -53,6 +55,7 @@ struct MapNode; struct PlayerControl; struct PointedThing; struct ItemVisualsManager; +struct ModVFS; namespace scene { class IAnimatedMesh; @@ -100,8 +103,6 @@ private: std::map m_packets; }; -class ClientScripting; - class Client : public con::PeerHandler, public InventoryManager, public IGameDef { public: @@ -127,14 +128,6 @@ public: ~Client(); DISABLE_CLASS_COPY(Client); - // Load local mods into memory - void scanModSubfolder(const std::string &mod_name, const std::string &mod_path, - std::string mod_subpath); - inline void scanModIntoMemory(const std::string &mod_name, const std::string &mod_path) - { - scanModSubfolder(mod_name, mod_path, ""); - } - /* request all threads managed by client to be stopped */ @@ -385,7 +378,7 @@ public: bool checkLocalPrivilege(const std::string &priv) { return checkPrivilege(priv); } virtual scene::IAnimatedMesh* getMesh(const std::string &filename, bool cache = false); - const std::string* getModFile(std::string filename); + ModVFS *getModVFS() { return m_mod_vfs.get(); } ModStorageDatabase *getModStorageDatabase() override { return m_mod_storage_database; } ItemVisualsManager *getItemVisualsManager() { return m_item_visuals_manager; } @@ -586,7 +579,10 @@ private: ModStorageDatabase *m_mod_storage_database = nullptr; float m_mod_storage_save_timer = 10.0f; std::vector m_mods; - StringMap m_mod_vfs; + std::unique_ptr m_mod_vfs; + + // SSCSM + std::unique_ptr m_sscsm_controller; bool m_shutdown = false; diff --git a/src/client/mod_vfs.cpp b/src/client/mod_vfs.cpp new file mode 100644 index 0000000000..391beb4ea6 --- /dev/null +++ b/src/client/mod_vfs.cpp @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2025 Luanti authors +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "mod_vfs.h" +#include "filesys.h" +#include "log.h" +#include + +void ModVFS::scanModSubfolder(const std::string &mod_name, const std::string &mod_path, + std::string mod_subpath) +{ + std::string full_path = mod_path + DIR_DELIM + mod_subpath; + std::vector mod = fs::GetDirListing(full_path); + for (const fs::DirListNode &j : mod) { + if (j.name[0] == '.') + continue; + + if (j.dir) { + scanModSubfolder(mod_name, mod_path, mod_subpath + j.name + DIR_DELIM); + continue; + } + std::replace(mod_subpath.begin(), mod_subpath.end(), DIR_DELIM_CHAR, '/'); + + std::string real_path = full_path + j.name; + std::string vfs_path = mod_name + ":" + mod_subpath + j.name; + infostream << "ModVFS::scanModSubfolder(): Loading \"" << real_path + << "\" as \"" << vfs_path << "\"." << std::endl; + + std::string contents; + if (!fs::ReadFile(real_path, contents)) { + errorstream << "ModVFS::scanModSubfolder(): Can't read file \"" + << real_path << "\"." << std::endl; + continue; + } + + m_vfs.emplace(vfs_path, contents); + } +} + +const std::string *ModVFS::getModFile(std::string filename) +{ + // strip dir delimiter from beginning of path + auto pos = filename.find_first_of(':'); + if (pos == std::string::npos) + return nullptr; + ++pos; + auto pos2 = filename.find_first_not_of('/', pos); + if (pos2 > pos) + filename.erase(pos, pos2 - pos); + + auto it = m_vfs.find(filename); + if (it == m_vfs.end()) + return nullptr; + return &it->second; +} diff --git a/src/client/mod_vfs.h b/src/client/mod_vfs.h new file mode 100644 index 0000000000..c192b22890 --- /dev/null +++ b/src/client/mod_vfs.h @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2025 Luanti authors +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include +#include + +struct ModVFS +{ + void scanModSubfolder(const std::string &mod_name, const std::string &mod_path, + std::string mod_subpath); + + inline void scanModIntoMemory(const std::string &mod_name, const std::string &mod_path) + { + scanModSubfolder(mod_name, mod_path, ""); + } + + const std::string *getModFile(std::string filename); + + std::unordered_map m_vfs; +}; diff --git a/src/constants.h b/src/constants.h index e97bd0507b..9f66b69f83 100644 --- a/src/constants.h +++ b/src/constants.h @@ -105,3 +105,7 @@ // The intent is to ensure that the rendering doesn't turn terribly blurry // when filtering is enabled. #define TEXTURE_FILTER_MIN_SIZE 192U + +// Resolution of clocks that SSCSM has access to, in us. +// Used as countermeasure against side-channel attacks. +#define SSCSM_CLOCK_RESOLUTION_US 20 diff --git a/src/defaultsettings.cpp b/src/defaultsettings.cpp index c27da7988e..c5bcc9fb09 100644 --- a/src/defaultsettings.cpp +++ b/src/defaultsettings.cpp @@ -121,6 +121,7 @@ void set_default_settings() settings->setDefault("curl_verify_cert", "true"); settings->setDefault("enable_remote_media_server", "true"); settings->setDefault("enable_client_modding", "false"); + settings->setDefault("enable_sscsm", "nowhere"); settings->setDefault("max_out_chat_queue_size", "20"); settings->setDefault("pause_on_lost_focus", "false"); settings->setDefault("enable_split_login_register", "true"); diff --git a/src/exceptions.h b/src/exceptions.h index 902d1b571e..b4d0c021ca 100644 --- a/src/exceptions.h +++ b/src/exceptions.h @@ -87,6 +87,11 @@ public: ModError(const std::string &s): BaseException(s) {} }; +class MisbehavedSSCSMException : public BaseException { +public: + MisbehavedSSCSMException(const std::string &s): BaseException(s) {} +}; + /* Some "old-style" interrupts: diff --git a/src/script/CMakeLists.txt b/src/script/CMakeLists.txt index b331049421..a45e9ffd4b 100644 --- a/src/script/CMakeLists.txt +++ b/src/script/CMakeLists.txt @@ -1,6 +1,7 @@ add_subdirectory(common) add_subdirectory(cpp_api) add_subdirectory(lua_api) +add_subdirectory(sscsm) # Used by server and client file(GLOB common_SCRIPT_HDRS "${CMAKE_CURRENT_SOURCE_DIR}/*.h") @@ -20,9 +21,11 @@ set(client_SCRIPT_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/scripting_mainmenu.cpp ${CMAKE_CURRENT_SOURCE_DIR}/scripting_client.cpp ${CMAKE_CURRENT_SOURCE_DIR}/scripting_pause_menu.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/scripting_sscsm.cpp ${client_SCRIPT_COMMON_SRCS} ${client_SCRIPT_CPP_API_SRCS} ${client_SCRIPT_LUA_API_SRCS} + ${client_SCRIPT_SSCSM_SRCS} PARENT_SCOPE) diff --git a/src/script/cpp_api/CMakeLists.txt b/src/script/cpp_api/CMakeLists.txt index 713dbe28b1..c256ce7537 100644 --- a/src/script/cpp_api/CMakeLists.txt +++ b/src/script/cpp_api/CMakeLists.txt @@ -22,5 +22,6 @@ set(client_SCRIPT_CPP_API_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/s_client_common.cpp ${CMAKE_CURRENT_SOURCE_DIR}/s_mainmenu.cpp ${CMAKE_CURRENT_SOURCE_DIR}/s_pause_menu.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/s_sscsm.cpp PARENT_SCOPE) diff --git a/src/script/cpp_api/s_base.cpp b/src/script/cpp_api/s_base.cpp index 6ed43a2583..a29f03a42f 100644 --- a/src/script/cpp_api/s_base.cpp +++ b/src/script/cpp_api/s_base.cpp @@ -16,6 +16,8 @@ #include "server.h" #if CHECK_CLIENT_BUILD() #include "client/client.h" +#include "client/mod_vfs.h" +#include "sscsm/sscsm_environment.h" #endif #if BUILD_WITH_TRACY @@ -74,7 +76,7 @@ ScriptApiBase::ScriptApiBase(ScriptingType type): lua_atpanic(m_luastack, &luaPanic); - if (m_type == ScriptingType::Client) + if (m_type == ScriptingType::Client || m_type == ScriptingType::SSCSM) clientOpenLibs(m_luastack); else luaL_openlibs(m_luastack); @@ -143,7 +145,8 @@ ScriptApiBase::ScriptApiBase(ScriptingType type): // Finally, put the table into the global environment: lua_setglobal(m_luastack, "core"); - if (m_type == ScriptingType::Client) + if (m_type == ScriptingType::Client + || m_type == ScriptingType::SSCSM) lua_pushstring(m_luastack, "/"); else lua_pushstring(m_luastack, DIR_DELIM); @@ -210,7 +213,8 @@ void ScriptApiBase::checkSetByBuiltin() if (getType() == ScriptingType::Server || (getType() == ScriptingType::Async && m_gamedef) || getType() == ScriptingType::Emerge || - getType() == ScriptingType::Client) { + getType() == ScriptingType::Client || + getType() == ScriptingType::SSCSM) { CHECK(CUSTOM_RIDX_READ_NODE, "read_node"); CHECK(CUSTOM_RIDX_PUSH_NODE, "push_node"); } @@ -265,16 +269,18 @@ void ScriptApiBase::loadScript(const std::string &script_path) } #if CHECK_CLIENT_BUILD() -void ScriptApiBase::loadModFromMemory(const std::string &mod_name) +void ScriptApiBase::loadModFromMemory(const std::string &mod_name, std::string init_path) { ModNameStorer mod_name_storer(getStack(), mod_name); - sanity_check(m_type == ScriptingType::Client); + sanity_check(m_type == ScriptingType::Client + || m_type == ScriptingType::SSCSM); - const std::string init_filename = mod_name + ":init.lua"; - const std::string chunk_name = "@" + init_filename; + if (init_path.empty()) + init_path = mod_name + ":init.lua"; + const std::string chunk_name = "@" + init_path; - const std::string *contents = getClient()->getModFile(init_filename); + const std::string *contents = getModVFS()->getModFile(init_path); if (!contents) throw ModError("Mod \"" + mod_name + "\" lacks init.lua"); @@ -540,8 +546,18 @@ Server* ScriptApiBase::getServer() } #if CHECK_CLIENT_BUILD() -Client* ScriptApiBase::getClient() +Client *ScriptApiBase::getClient() { return dynamic_cast(m_gamedef); } + +ModVFS *ScriptApiBase::getModVFS() +{ + if (m_type == ScriptingType::Client) + return getClient()->getModVFS(); + else if (m_type == ScriptingType::SSCSM) + return getSSCSMEnv()->getModVFS(); + else + return nullptr; +} #endif diff --git a/src/script/cpp_api/s_base.h b/src/script/cpp_api/s_base.h index b532e9cd99..105eb04b16 100644 --- a/src/script/cpp_api/s_base.h +++ b/src/script/cpp_api/s_base.h @@ -43,11 +43,12 @@ extern "C" { enum class ScriptingType: u8 { Async, // either mainmenu (client) or ingame (server) - Client, + Client, // CPCSM MainMenu, Server, Emerge, PauseMenu, + SSCSM, }; class Server; @@ -58,8 +59,10 @@ class EmergeThread; class IGameDef; class Environment; class GUIEngine; +class SSCSMEnvironment; class ServerActiveObject; struct PlayerHPChangeReason; +struct ModVFS; class ScriptApiBase : protected LuaHelper { public: @@ -77,7 +80,7 @@ public: void loadScript(const std::string &script_path); #if CHECK_CLIENT_BUILD() - void loadModFromMemory(const std::string &mod_name); + void loadModFromMemory(const std::string &mod_name, std::string init_path = ""); #endif void runCallbacksRaw(int nargs, @@ -90,9 +93,10 @@ public: ScriptingType getType() { return m_type; } IGameDef *getGameDef() { return m_gamedef; } - Server* getServer(); + Server *getServer(); #if CHECK_CLIENT_BUILD() - Client* getClient(); + Client *getClient(); + ModVFS *getModVFS(); #endif // IMPORTANT: These cannot be used for any security-related uses, they exist @@ -158,6 +162,9 @@ protected: #if CHECK_CLIENT_BUILD() GUIEngine* getGuiEngine() { return m_guiengine; } void setGuiEngine(GUIEngine* guiengine) { m_guiengine = guiengine; } + + SSCSMEnvironment *getSSCSMEnv() { return m_sscsm_environment; } + void setSSCSMEnv(SSCSMEnvironment *env) { m_sscsm_environment = env; } #endif EmergeThread* getEmergeThread() { return m_emerge; } @@ -178,14 +185,15 @@ protected: private: static int luaPanic(lua_State *L); - lua_State *m_luastack = nullptr; + lua_State *m_luastack = nullptr; - IGameDef *m_gamedef = nullptr; - Environment *m_environment = nullptr; + IGameDef *m_gamedef = nullptr; + Environment *m_environment = nullptr; #if CHECK_CLIENT_BUILD() - GUIEngine *m_guiengine = nullptr; + GUIEngine *m_guiengine = nullptr; + SSCSMEnvironment *m_sscsm_environment = nullptr; #endif - EmergeThread *m_emerge = nullptr; + EmergeThread *m_emerge = nullptr; - ScriptingType m_type; + ScriptingType m_type; }; diff --git a/src/script/cpp_api/s_security.cpp b/src/script/cpp_api/s_security.cpp index 834650fdc6..36f6b045b8 100644 --- a/src/script/cpp_api/s_security.cpp +++ b/src/script/cpp_api/s_security.cpp @@ -9,8 +9,10 @@ #include "server.h" #if CHECK_CLIENT_BUILD() #include "client/client.h" +#include "client/mod_vfs.h" #endif #include "settings.h" +#include "constants.h" #include #include @@ -378,6 +380,140 @@ void ScriptApiSecurity::initializeSecurityClient() setLuaEnv(L, thread); } +void ScriptApiSecurity::initializeSecuritySSCSM() +{ + static const char *whitelist[] = { + "assert", + "core", + "collectgarbage", + "DIR_DELIM", + "error", + "getfenv", + "ipairs", + "next", + "pairs", + "pcall", + "rawequal", + "rawget", + "rawset", + "select", + "setfenv", + "getmetatable", + "setmetatable", + "tonumber", + "tostring", + "type", + "unpack", + "_VERSION", + "xpcall", + // Completely safe libraries + "coroutine", + "table", + "math", + "bit", + }; + static const char *os_whitelist[] = { + "difftime", + "time" + }; + static const char *debug_whitelist[] = { + "getinfo", // used by client builtin and unset before mods load + "traceback" + }; + static const char *string_whitelist[] = { // all but string.dump + "byte", + "char", + "dump", + "find", + "format", + "gmatch", + "gsub", + "len", + "lower", + "match", + "rep", + "reverse", + "sub", + "upper" + }; +#if USE_LUAJIT + static const char *jit_whitelist[] = { + "arch", + "flush", + "off", + "on", + "opt", + "os", + "status", + "version", + "version_num", + }; +#endif + + m_secure = true; + + lua_State *L = getStack(); + int thread = getThread(L); + + // create an empty environment + createEmptyEnv(L); + + // Copy safe base functions + lua_getglobal(L, "_G"); + lua_getfield(L, -2, "_G"); + copy_safe(L, whitelist, sizeof(whitelist)); + + // And replace unsafe ones + SECURE_API(g, dofile); + SECURE_API(g, load); + SECURE_API(g, loadfile); + SECURE_API(g, loadstring); + SECURE_API(g, require); + lua_pop(L, 2); + + + + // Copy safe OS functions + lua_getglobal(L, "os"); + lua_newtable(L); + copy_safe(L, os_whitelist, sizeof(os_whitelist)); + + // And replace unsafe ones + SECURE_API(os, clock); + + lua_setfield(L, -3, "os"); + lua_pop(L, 1); // Pop old OS + + + // Copy safe debug functions + lua_getglobal(L, "debug"); + lua_newtable(L); + copy_safe(L, debug_whitelist, sizeof(debug_whitelist)); + lua_setfield(L, -3, "debug"); + lua_pop(L, 1); // Pop old debug + + + // Copy safe string functions + lua_getglobal(L, "string"); + lua_newtable(L); + copy_safe(L, string_whitelist, sizeof(string_whitelist)); + lua_setfield(L, -3, "string"); + lua_pop(L, 1); // Pop old string + + +#if USE_LUAJIT + // Copy safe jit functions, if they exist + lua_getglobal(L, "jit"); + lua_newtable(L); + copy_safe(L, jit_whitelist, sizeof(jit_whitelist)); + lua_setfield(L, -3, "jit"); + lua_pop(L, 1); // Pop old jit +#endif + + // Set the environment to the one we created earlier + setLuaEnv(L, thread); +} + #endif int ScriptApiSecurity::getThread(lua_State *L) @@ -775,10 +911,11 @@ int ScriptApiSecurity::sl_g_loadfile(lua_State *L) #if CHECK_CLIENT_BUILD() ScriptApiBase *script = ModApiBase::getScriptApiBase(L); - // Client implementation - if (script->getType() == ScriptingType::Client) { + // SSCSM & CPCSM implementation + if (script->getType() == ScriptingType::Client + || script->getType() == ScriptingType::SSCSM) { std::string path = readParam(L, 1); - const std::string *contents = script->getClient()->getModFile(path); + const std::string *contents = script->getModVFS()->getModFile(path); if (!contents) { std::string error_msg = "Couldn't find script called: " + path; lua_pushnil(L); @@ -963,3 +1100,12 @@ int ScriptApiSecurity::sl_os_setlocale(lua_State *L) lua_call(L, cat ? 2 : 1, 1); return 1; } + + +int ScriptApiSecurity::sl_os_clock(lua_State *L) +{ + auto t = clock(); + t = t - t % (SSCSM_CLOCK_RESOLUTION_US * CLOCKS_PER_SEC / 1'000'000); + lua_pushnumber(L, static_cast(t) / static_cast(CLOCKS_PER_SEC)); + return 1; +} diff --git a/src/script/cpp_api/s_security.h b/src/script/cpp_api/s_security.h index 1241b0b06d..d72612ffe1 100644 --- a/src/script/cpp_api/s_security.h +++ b/src/script/cpp_api/s_security.h @@ -31,8 +31,10 @@ public: void initializeSecurity(); #if CHECK_CLIENT_BUILD() void initializeSecurityClient(); + void initializeSecuritySSCSM(); #else - inline void initializeSecurityClient() { assert(0); } + void initializeSecurityClient() { assert(0); } + void initializeSecuritySSCSM() { assert(0); } #endif // Checks if the Lua state has been secured @@ -115,4 +117,5 @@ private: static int sl_os_rename(lua_State *L); static int sl_os_remove(lua_State *L); static int sl_os_setlocale(lua_State *L); + static int sl_os_clock(lua_State *L); }; diff --git a/src/script/cpp_api/s_sscsm.cpp b/src/script/cpp_api/s_sscsm.cpp new file mode 100644 index 0000000000..8abd0443b4 --- /dev/null +++ b/src/script/cpp_api/s_sscsm.cpp @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2025 Luanti authors +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "s_sscsm.h" + +#include "s_internal.h" +#include "script/sscsm/sscsm_environment.h" + +void ScriptApiSSCSM::load_mods(const std::vector> &mods) +{ + infostream << "Loading SSCSMs:" << std::endl; + for (const auto &m : mods) { + infostream << "Loading SSCSM " << m.first << std::endl; + loadModFromMemory(m.first, m.second); + } +} + +void ScriptApiSSCSM::environment_step(float dtime) +{ + SCRIPTAPI_PRECHECKHEADER + + // Get core.registered_globalsteps + lua_getglobal(L, "core"); + lua_getfield(L, -1, "registered_globalsteps"); + // Call callbacks + lua_pushnumber(L, dtime); + runCallbacks(1, RUN_CALLBACKS_MODE_FIRST); +} diff --git a/src/script/cpp_api/s_sscsm.h b/src/script/cpp_api/s_sscsm.h new file mode 100644 index 0000000000..2e16dd8386 --- /dev/null +++ b/src/script/cpp_api/s_sscsm.h @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2025 Luanti authors +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "cpp_api/s_base.h" + +class ScriptApiSSCSM : virtual public ScriptApiBase +{ +public: + void load_mods(const std::vector> &mods); + + void environment_step(float dtime); +}; diff --git a/src/script/lua_api/CMakeLists.txt b/src/script/lua_api/CMakeLists.txt index 5f0cd08163..a1be8df728 100644 --- a/src/script/lua_api/CMakeLists.txt +++ b/src/script/lua_api/CMakeLists.txt @@ -43,4 +43,5 @@ set(client_SCRIPT_LUA_API_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/l_particles_local.cpp ${CMAKE_CURRENT_SOURCE_DIR}/l_pause_menu.cpp ${CMAKE_CURRENT_SOURCE_DIR}/l_storage.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/l_sscsm.cpp PARENT_SCOPE) diff --git a/src/script/lua_api/l_base.cpp b/src/script/lua_api/l_base.cpp index c95acc7335..31908e59e5 100644 --- a/src/script/lua_api/l_base.cpp +++ b/src/script/lua_api/l_base.cpp @@ -58,6 +58,11 @@ GUIEngine *ModApiBase::getGuiEngine(lua_State *L) { return getScriptApiBase(L)->getGuiEngine(); } + +SSCSMEnvironment *ModApiBase::getSSCSMEnv(lua_State *L) +{ + return getScriptApiBase(L)->getSSCSMEnv(); +} #endif EmergeThread *ModApiBase::getEmergeThread(lua_State *L) diff --git a/src/script/lua_api/l_base.h b/src/script/lua_api/l_base.h index 138b4cf24d..0666a93474 100644 --- a/src/script/lua_api/l_base.h +++ b/src/script/lua_api/l_base.h @@ -23,6 +23,7 @@ class EmergeThread; class ScriptApiBase; class Server; class Environment; +class SSCSMEnvironment; class ServerInventoryManager; class ModApiBase : protected LuaHelper { @@ -33,6 +34,7 @@ public: #if CHECK_CLIENT_BUILD() static Client* getClient(lua_State *L); static GUIEngine* getGuiEngine(lua_State *L); + static SSCSMEnvironment *getSSCSMEnv(lua_State *L); #endif // !SERVER static EmergeThread* getEmergeThread(lua_State *L); diff --git a/src/script/lua_api/l_client.cpp b/src/script/lua_api/l_client.cpp index 2aef6d470f..a8b6f14807 100644 --- a/src/script/lua_api/l_client.cpp +++ b/src/script/lua_api/l_client.cpp @@ -59,7 +59,7 @@ int ModApiClient::l_get_current_modname(lua_State *L) int ModApiClient::l_get_modpath(lua_State *L) { std::string modname = readParam(L, 1); - // Client mods use a virtual filesystem, see Client::scanModSubfolder() + // Client mods use a virtual filesystem, see ModVFS::scanModSubfolder() std::string path = modname + ":"; lua_pushstring(L, path.c_str()); return 1; @@ -284,7 +284,20 @@ int ModApiClient::l_get_privilege_list(lua_State *L) // get_builtin_path() int ModApiClient::l_get_builtin_path(lua_State *L) { - lua_pushstring(L, BUILTIN_MOD_NAME ":"); + std::string modname; + if (getScriptApiBase(L)->getType() == ScriptingType::Client) { + modname = BUILTIN_MOD_NAME; + } else if (getScriptApiBase(L)->getType() == ScriptingType::SSCSM) { + // get_builtin_path() is only called in builtin, so this is fine + modname = ScriptApiBase::getCurrentModNameInsecure(L); + if (modname != "*client_builtin*" && modname != "*server_builtin*") + modname = ""; + } + + if (modname.empty()) + return 0; + + lua_pushstring(L, (modname + ":").c_str()); return 1; } @@ -322,3 +335,11 @@ void ModApiClient::Initialize(lua_State *L, int top) API_FCT(get_language); API_FCT(get_csm_restrictions); } + +void ModApiClient::InitializeSSCSM(lua_State *L, int top) +{ + API_FCT(get_current_modname); + API_FCT(get_modpath); + API_FCT(print); + API_FCT(get_builtin_path); +} diff --git a/src/script/lua_api/l_client.h b/src/script/lua_api/l_client.h index 777b128cc2..ec0ceca32b 100644 --- a/src/script/lua_api/l_client.h +++ b/src/script/lua_api/l_client.h @@ -71,4 +71,5 @@ private: public: static void Initialize(lua_State *L, int top); + static void InitializeSSCSM(lua_State *L, int top); }; diff --git a/src/script/lua_api/l_sscsm.cpp b/src/script/lua_api/l_sscsm.cpp new file mode 100644 index 0000000000..a7447283b3 --- /dev/null +++ b/src/script/lua_api/l_sscsm.cpp @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2025 Luanti authors +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "l_sscsm.h" + +#include "common/c_content.h" +#include "common/c_converter.h" +#include "l_internal.h" +#include "script/sscsm/sscsm_environment.h" +#include "script/sscsm/sscsm_requests.h" + +// get_node_or_nil(pos) +// pos = {x=num, y=num, z=num} +int ModApiSSCSM::l_get_node_or_nil(lua_State *L) +{ + // pos + v3s16 pos = read_v3s16(L, 1); + + // Do it + auto request = SSCSMRequestGetNode{}; + request.pos = pos; + auto answer = getSSCSMEnv(L)->doRequest(std::move(request)); + + if (answer.is_pos_ok) { + // Return node + pushnode(L, answer.node); + } else { + lua_pushnil(L); + } + return 1; +} + +void ModApiSSCSM::Initialize(lua_State *L, int top) +{ + API_FCT(get_node_or_nil); +} diff --git a/src/script/lua_api/l_sscsm.h b/src/script/lua_api/l_sscsm.h new file mode 100644 index 0000000000..09a70163e2 --- /dev/null +++ b/src/script/lua_api/l_sscsm.h @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2025 Luanti authors +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "lua_api/l_base.h" + +class ModApiSSCSM : public ModApiBase +{ +private: + // get_node_or_nil(pos) + static int l_get_node_or_nil(lua_State *L); + +public: + static void Initialize(lua_State *L, int top); +}; diff --git a/src/script/lua_api/l_util.cpp b/src/script/lua_api/l_util.cpp index f91f4ca6fa..06f1a65ff8 100644 --- a/src/script/lua_api/l_util.cpp +++ b/src/script/lua_api/l_util.cpp @@ -30,6 +30,7 @@ #include "util/png.h" #include "player.h" #include "daynightratio.h" +#include "constants.h" #include // only available in zstd 1.3.5+ @@ -82,6 +83,16 @@ int ModApiUtil::l_get_us_time(lua_State *L) return 1; } +// get_us_time() for SSCSM +int ModApiUtil::l_get_us_time_sscsm(lua_State *L) +{ + NO_MAP_LOCK_REQUIRED; + auto t = porting::getTimeUs(); + t = t - t % SSCSM_CLOCK_RESOLUTION_US; + lua_pushnumber(L, t); + return 1; +} + // Maximum depth of a JSON object: // Reading and writing should not overflow the Lua, C, or jsoncpp stacks. constexpr static u16 MAX_JSON_DEPTH = 1024; @@ -798,6 +809,37 @@ void ModApiUtil::InitializeClient(lua_State *L, int top) lua_setfield(L, top, "settings"); } +void ModApiUtil::InitializeSSCSM(lua_State *L, int top) +{ + API_FCT(log); + + registerFunction(L, "get_us_time", l_get_us_time_sscsm, top); + + API_FCT(parse_json); + API_FCT(write_json); + + API_FCT(is_yes); + + API_FCT(compress); + API_FCT(decompress); + + API_FCT(encode_base64); + API_FCT(decode_base64); + + API_FCT(get_version); + API_FCT(sha1); + API_FCT(sha256); + API_FCT(colorspec_to_colorstring); + API_FCT(colorspec_to_bytes); + API_FCT(colorspec_to_table); + API_FCT(time_to_day_night_ratio); + + API_FCT(get_last_run_mod); + API_FCT(set_last_run_mod); + + API_FCT(urlencode); +} + void ModApiUtil::InitializeAsync(lua_State *L, int top) { API_FCT(log); diff --git a/src/script/lua_api/l_util.h b/src/script/lua_api/l_util.h index 7185f369a0..90944b0de3 100644 --- a/src/script/lua_api/l_util.h +++ b/src/script/lua_api/l_util.h @@ -28,6 +28,9 @@ private: // get us precision time static int l_get_us_time(lua_State *L); + // get_us_time() for SSCSM. less precise + static int l_get_us_time_sscsm(lua_State *L); + // parse_json(str[, nullvalue]) static int l_parse_json(lua_State *L); @@ -134,4 +137,5 @@ public: static void Initialize(lua_State *L, int top); static void InitializeAsync(lua_State *L, int top); static void InitializeClient(lua_State *L, int top); + static void InitializeSSCSM(lua_State *L, int top); }; diff --git a/src/script/scripting_sscsm.cpp b/src/script/scripting_sscsm.cpp new file mode 100644 index 0000000000..27e5354742 --- /dev/null +++ b/src/script/scripting_sscsm.cpp @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2025 Luanti authors +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "scripting_sscsm.h" +#include "cpp_api/s_internal.h" +#include "lua_api/l_sscsm.h" +#include "lua_api/l_util.h" +#include "lua_api/l_client.h" + +SSCSMScripting::SSCSMScripting(SSCSMEnvironment *env) : + ScriptApiBase(ScriptingType::SSCSM) +{ + setSSCSMEnv(env); + + SCRIPTAPI_PRECHECKHEADER + + initializeSecuritySSCSM(); + + lua_getglobal(L, "core"); + int top = lua_gettop(L); + + // Initialize our lua_api modules + initializeModApi(L, top); + lua_pop(L, 1); + + // Push builtin initialization type + lua_pushstring(L, "sscsm"); + lua_setglobal(L, "INIT"); +} + +void SSCSMScripting::initializeModApi(lua_State *L, int top) +{ + ModApiUtil::InitializeSSCSM(L, top); + ModApiClient::InitializeSSCSM(L, top); + ModApiSSCSM::Initialize(L, top); +} diff --git a/src/script/scripting_sscsm.h b/src/script/scripting_sscsm.h new file mode 100644 index 0000000000..25a5a05799 --- /dev/null +++ b/src/script/scripting_sscsm.h @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2025 Luanti authors +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "cpp_api/s_base.h" +#include "cpp_api/s_sscsm.h" +#include "cpp_api/s_security.h" + +class SSCSMScripting : + virtual public ScriptApiBase, + public ScriptApiSSCSM, + public ScriptApiSecurity +{ +public: + SSCSMScripting(SSCSMEnvironment *env); + +protected: + bool checkPathInternal(const std::string &abs_path, bool write_required, + bool *write_allowed) { return false; }; + +private: + void initializeModApi(lua_State *L, int top); +}; diff --git a/src/script/sscsm/CMakeLists.txt b/src/script/sscsm/CMakeLists.txt new file mode 100644 index 0000000000..1508216de9 --- /dev/null +++ b/src/script/sscsm/CMakeLists.txt @@ -0,0 +1,7 @@ +file(GLOB client_SCRIPT_SSCSM_HDRS "${CMAKE_CURRENT_SOURCE_DIR}/*.h") + +set(client_SCRIPT_SSCSM_SRCS + ${client_SCRIPT_SSCSM_HDRS} + ${CMAKE_CURRENT_SOURCE_DIR}/sscsm_controller.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/sscsm_environment.cpp + PARENT_SCOPE) diff --git a/src/script/sscsm/sscsm_controller.cpp b/src/script/sscsm/sscsm_controller.cpp new file mode 100644 index 0000000000..6f41162880 --- /dev/null +++ b/src/script/sscsm/sscsm_controller.cpp @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2024 Luanti authors +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "sscsm_controller.h" +#include "sscsm_environment.h" +#include "sscsm_requests.h" +#include "sscsm_events.h" +#include "sscsm_stupid_channel.h" + +std::unique_ptr SSCSMController::create() +{ + auto channel = std::make_shared(); + auto thread = std::make_unique(channel); + thread->start(); + + // Wait for thread to finish initializing. + auto req0 = deserializeSSCSMRequest(channel->recvB()); + FATAL_ERROR_IF(!dynamic_cast(req0.get()), + "First request must be pollEvent."); + + return std::make_unique(std::move(thread), channel); +} + +SSCSMController::SSCSMController(std::unique_ptr thread, + std::shared_ptr channel) : + m_thread(std::move(thread)), m_channel(std::move(channel)) +{ +} + +SSCSMController::~SSCSMController() +{ + // send tear-down + auto answer = SSCSMRequestPollNextEvent::Answer{}; + answer.next_event = std::make_unique(); + m_channel->sendB(serializeSSCSMAnswer(std::move(answer))); + // wait for death + m_thread->stop(); + m_thread->wait(); +} + +SerializedSSCSMAnswer SSCSMController::handleRequest(Client *client, ISSCSMRequest *req) +{ + return req->exec(client); +} + +void SSCSMController::runEvent(Client *client, std::unique_ptr event) +{ + auto answer0 = SSCSMRequestPollNextEvent::Answer{}; + answer0.next_event = std::move(event); + auto answer = serializeSSCSMAnswer(std::move(answer0)); + + while (true) { + auto request = deserializeSSCSMRequest(m_channel->exchangeB(std::move(answer))); + + if (dynamic_cast(request.get()) != nullptr) { + break; + } + + answer = handleRequest(client, request.get()); + } +} diff --git a/src/script/sscsm/sscsm_controller.h b/src/script/sscsm/sscsm_controller.h new file mode 100644 index 0000000000..e96e4fea90 --- /dev/null +++ b/src/script/sscsm/sscsm_controller.h @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2024 Luanti authors +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include +#include "irrlichttypes.h" +#include "sscsm_irequest.h" +#include "sscsm_ievent.h" +#include "util/basic_macros.h" + +class SSCSMEnvironment; +class StupidChannel; + +/** + * The purpose of this class is to: + * * Be the RAII owner of the SSCSM process. + * * Send events to SSCSM process, and process requests. (`runEvent`) + * * Hide details (e.g. that it is a separate process, or that it has to do IPC calls). + * + * See also SSCSMEnvironment for other side. + */ +class SSCSMController +{ + std::unique_ptr m_thread; + std::shared_ptr m_channel; + + SerializedSSCSMAnswer handleRequest(Client *client, ISSCSMRequest *req); + +public: + static std::unique_ptr create(); + + SSCSMController(std::unique_ptr thread, + std::shared_ptr channel); + + ~SSCSMController(); + + DISABLE_CLASS_COPY(SSCSMController); + + // Handles requests until the next event is polled + void runEvent(Client *client, std::unique_ptr event); +}; diff --git a/src/script/sscsm/sscsm_environment.cpp b/src/script/sscsm/sscsm_environment.cpp new file mode 100644 index 0000000000..8224ffdbb0 --- /dev/null +++ b/src/script/sscsm/sscsm_environment.cpp @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2024 Luanti authors +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "sscsm_environment.h" +#include "sscsm_requests.h" +#include "sscsm_events.h" +#include "sscsm_stupid_channel.h" +#include "client/mod_vfs.h" + + +SSCSMEnvironment::SSCSMEnvironment(std::shared_ptr channel) : + Thread("SSCSMEnvironment-thread"), + m_channel(std::move(channel)), + m_script(std::make_unique(this)), + m_vfs(std::make_unique()) +{ +} + +SSCSMEnvironment::~SSCSMEnvironment() = default; + +void *SSCSMEnvironment::run() +{ + while (true) { + auto next_event = [&]{ + auto request = SSCSMRequestPollNextEvent{}; + auto answer = doRequest(std::move(request)); + return std::move(answer.next_event); + }(); + + if (dynamic_cast(next_event.get())) { + break; + } + + try { + next_event->exec(this); + } catch (LuaError &e) { + setFatalError(std::string("Lua error: ") + e.what()); + } catch (ModError &e) { + setFatalError(std::string("Mod error: ") + e.what()); + } + } + + return nullptr; +} + +SerializedSSCSMAnswer SSCSMEnvironment::exchange(SerializedSSCSMRequest req) +{ + return m_channel->exchangeA(std::move(req)); +} + +void SSCSMEnvironment::updateVFSFiles(std::vector> &&files) +{ + for (auto &&p : files) { + m_vfs->m_vfs.emplace(std::move(p.first), std::move(p.second)); + } +} + +std::optional SSCSMEnvironment::readVFSFile(const std::string &path) +{ + auto it = m_vfs->m_vfs.find(path); + if (it == m_vfs->m_vfs.end()) + return std::nullopt; + else + return it->second; +} + +void SSCSMEnvironment::setFatalError(const std::string &reason) +{ + auto request = SSCSMRequestSetFatalError{}; + request.reason = reason; + doRequest(std::move(request)); +} diff --git a/src/script/sscsm/sscsm_environment.h b/src/script/sscsm/sscsm_environment.h new file mode 100644 index 0000000000..79313dc1dc --- /dev/null +++ b/src/script/sscsm/sscsm_environment.h @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2024 Luanti authors +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include +#include +#include +#include +#include "client/client.h" +#include "threading/thread.h" +#include "sscsm_controller.h" +#include "sscsm_irequest.h" +#include "../scripting_sscsm.h" + +/** The thread that runs SSCSM code. + * + * Meant to be replaced by a sandboxed process. + * + * RAII-owns and abstracts away resources to communicate to the main process / thread. + * + * See also SSCSMController for other side. + */ +class SSCSMEnvironment : public Thread +{ + std::shared_ptr m_channel; + std::unique_ptr m_script; + // the virtual file system. + // paths look like this: + // *client_builtin*:subdir/foo.lua + // *server_builtin*:subdir/foo.lua + // modname:subdir/foo.lua + std::unique_ptr m_vfs; + + void *run() override; + + SerializedSSCSMAnswer exchange(SerializedSSCSMRequest req); + +public: + SSCSMEnvironment(std::shared_ptr channel); + ~SSCSMEnvironment() override; + + SSCSMScripting *getScript() { return m_script.get(); } + + ModVFS *getModVFS() { return m_vfs.get(); } + void updateVFSFiles(std::vector> &&files); + std::optional readVFSFile(const std::string &path); + + void setFatalError(const std::string &reason); + + template + typename RQ::Answer doRequest(RQ &&rq) + { + return deserializeSSCSMAnswer( + exchange(serializeSSCSMRequest(std::forward(rq))) + ); + } +}; diff --git a/src/script/sscsm/sscsm_events.h b/src/script/sscsm/sscsm_events.h new file mode 100644 index 0000000000..c1ad578c6a --- /dev/null +++ b/src/script/sscsm/sscsm_events.h @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: 2024 Luanti authors +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "sscsm_ievent.h" +#include "debug.h" +#include "irrlichttypes.h" +#include "sscsm_environment.h" + +struct SSCSMEventTearDown : public ISSCSMEvent +{ + void exec(SSCSMEnvironment *env) override + { + FATAL_ERROR("SSCSMEventTearDown needs to be handled by SSCSMEnvironment::run()"); + } +}; + +struct SSCSMEventUpdateVFSFiles : public ISSCSMEvent +{ + // pairs are virtual path and file content + std::vector> files; + + void exec(SSCSMEnvironment *env) override + { + env->updateVFSFiles(std::move(files)); + } +}; + +struct SSCSMEventLoadMods : public ISSCSMEvent +{ + // modnames and paths to init.lua file, in load order + std::vector> mods; + + void exec(SSCSMEnvironment *env) override + { + env->getScript()->load_mods(mods); + } +}; + +struct SSCSMEventOnStep : public ISSCSMEvent +{ + f32 dtime; + + void exec(SSCSMEnvironment *env) override + { + env->getScript()->environment_step(dtime); + } +}; + diff --git a/src/script/sscsm/sscsm_ievent.h b/src/script/sscsm/sscsm_ievent.h new file mode 100644 index 0000000000..8742a3302d --- /dev/null +++ b/src/script/sscsm/sscsm_ievent.h @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2024 Luanti authors +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include +#include + +class SSCSMEnvironment; + +// Event triggered from the main env for the SSCSM env. +struct ISSCSMEvent +{ + virtual ~ISSCSMEvent() = default; + + // Note: No return value (difference to ISSCSMRequest). These are not callbacks + // that you can run at arbitrary locations, because the untrusted code could + // then clobber your local variables. + virtual void exec(SSCSMEnvironment *cntrl) = 0; +}; + +// FIXME: actually serialize, and replace this with a string +using SerializedSSCSMEvent = std::unique_ptr; + +template +inline SerializedSSCSMEvent serializeSSCSMEvent(const T &event) +{ + static_assert(std::is_base_of_v); + + return std::make_unique(event); +} + +inline std::unique_ptr deserializeSSCSMEvent(SerializedSSCSMEvent event_serialized) +{ + // The actual deserialization will have to use a type tag, and then choose + // the appropriate deserializer. + return event_serialized; +} diff --git a/src/script/sscsm/sscsm_irequest.h b/src/script/sscsm/sscsm_irequest.h new file mode 100644 index 0000000000..b6104e4f91 --- /dev/null +++ b/src/script/sscsm/sscsm_irequest.h @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2024 Luanti authors +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "exceptions.h" +#include +#include + +class SSCSMController; +class Client; + +struct ISSCSMAnswer +{ + virtual ~ISSCSMAnswer() = default; +}; + +// FIXME: actually serialize, and replace this with a string +using SerializedSSCSMAnswer = std::unique_ptr; + +// Request made by the sscsm env to the main env. +struct ISSCSMRequest +{ + virtual ~ISSCSMRequest() = default; + + virtual SerializedSSCSMAnswer exec(Client *client) = 0; +}; + +// FIXME: actually serialize, and replace this with a string +using SerializedSSCSMRequest = std::unique_ptr; + +template +inline SerializedSSCSMRequest serializeSSCSMRequest(const T &request) +{ + static_assert(std::is_base_of_v); + + return std::make_unique(request); +} + +template +inline T deserializeSSCSMAnswer(SerializedSSCSMAnswer answer_serialized) +{ + static_assert(std::is_base_of_v); + + // dynamic cast in place of actual deserialization + auto ptr = dynamic_cast(answer_serialized.get()); + if (!ptr) { + throw SerializationError("deserializeSSCSMAnswer failed"); + } + return std::move(*ptr); +} + +template +inline SerializedSSCSMAnswer serializeSSCSMAnswer(T &&answer) +{ + static_assert(std::is_base_of_v); + + return std::make_unique(std::move(answer)); +} + +inline std::unique_ptr deserializeSSCSMRequest(SerializedSSCSMRequest request_serialized) +{ + // The actual deserialization will have to use a type tag, and then choose + // the appropriate deserializer. + return request_serialized; +} diff --git a/src/script/sscsm/sscsm_requests.h b/src/script/sscsm/sscsm_requests.h new file mode 100644 index 0000000000..539ddd6e1f --- /dev/null +++ b/src/script/sscsm/sscsm_requests.h @@ -0,0 +1,107 @@ +// SPDX-FileCopyrightText: 2024 Luanti authors +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "sscsm_irequest.h" +#include "sscsm_ievent.h" +#include "mapnode.h" +#include "map.h" +#include "client/client.h" +#include "log_internal.h" + +// Poll the next event (e.g. on_globalstep) +struct SSCSMRequestPollNextEvent : public ISSCSMRequest +{ + struct Answer : public ISSCSMAnswer + { + std::unique_ptr next_event; + }; + + SerializedSSCSMAnswer exec(Client *client) override + { + FATAL_ERROR("SSCSMRequestPollNextEvent needs to be handled by SSCSMControler::runEvent()"); + } +}; + +// Some error occured in the SSCSM env +struct SSCSMRequestSetFatalError : public ISSCSMRequest +{ + struct Answer : public ISSCSMAnswer + { + }; + + std::string reason; + + SerializedSSCSMAnswer exec(Client *client) override + { + client->setFatalError("[SSCSM] " + reason); + + return serializeSSCSMAnswer(Answer{}); + } +}; + +// print(text) +// FIXME: override global loggers to use this in sscsm process +struct SSCSMRequestPrint : public ISSCSMRequest +{ + struct Answer : public ISSCSMAnswer + { + }; + + std::string text; + + SerializedSSCSMAnswer exec(Client *client) override + { + rawstream << text << std::endl; + + return serializeSSCSMAnswer(Answer{}); + } +}; + +// core.log(level, text) +// FIXME: override global loggers to use this in sscsm process +struct SSCSMRequestLog : public ISSCSMRequest +{ + struct Answer : public ISSCSMAnswer + { + }; + + std::string text; + LogLevel level; + + SerializedSSCSMAnswer exec(Client *client) override + { + if (level >= LL_MAX) { + throw MisbehavedSSCSMException("Tried to log at non-existent level."); + } else { + g_logger.log(level, text); + } + + return serializeSSCSMAnswer(Answer{}); + } +}; + +// core.get_node(pos) +struct SSCSMRequestGetNode : public ISSCSMRequest +{ + struct Answer : public ISSCSMAnswer + { + MapNode node; + bool is_pos_ok; + }; + + v3s16 pos; + + SerializedSSCSMAnswer exec(Client *client) override + { + bool is_pos_ok = false; + MapNode node = client->getEnv().getMap().getNode(pos, &is_pos_ok); + + Answer answer{}; + answer.node = node; + answer.is_pos_ok = is_pos_ok; + return serializeSSCSMAnswer(std::move(answer)); + } +}; diff --git a/src/script/sscsm/sscsm_stupid_channel.h b/src/script/sscsm/sscsm_stupid_channel.h new file mode 100644 index 0000000000..cb82d3ca12 --- /dev/null +++ b/src/script/sscsm/sscsm_stupid_channel.h @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2024 Luanti authors +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include +#include +#include +#include "sscsm_irequest.h" + +// FIXME: replace this with an ipc channel +class StupidChannel +{ + std::mutex m_mutex; + std::condition_variable m_condvar; + SerializedSSCSMRequest m_request; + SerializedSSCSMAnswer m_answer; + +public: + void sendA(SerializedSSCSMRequest request) + { + { + auto lock = std::lock_guard(m_mutex); + + m_request = std::move(request); + } + + m_condvar.notify_one(); + } + + SerializedSSCSMAnswer recvA() + { + auto lock = std::unique_lock(m_mutex); + + while (!m_answer) { + m_condvar.wait(lock); + } + + auto answer = std::move(m_answer); + m_answer = nullptr; + + return answer; + } + + SerializedSSCSMAnswer exchangeA(SerializedSSCSMRequest request) + { + sendA(std::move(request)); + + return recvA(); + } + + void sendB(SerializedSSCSMAnswer answer) + { + { + auto lock = std::lock_guard(m_mutex); + + m_answer = std::move(answer); + } + + m_condvar.notify_one(); + } + + SerializedSSCSMRequest recvB() + { + auto lock = std::unique_lock(m_mutex); + + while (!m_request) { + m_condvar.wait(lock); + } + + auto request = std::move(m_request); + m_request = nullptr; + + return request; + } + + SerializedSSCSMRequest exchangeB(SerializedSSCSMAnswer answer) + { + sendB(std::move(answer)); + + return recvB(); + } +};