Create SSCSM skeleton and scripting (#15818)

Contents:

* Adds a SSCSM controller and environment skeleton
* SSCSMs run in a separate thread, not yet in a separate process.
  API calls will later happen as IPC calls, this is already part of the software architecture.
* Adds a scripting env for SSCSM
  * It's not yet populated with luanti API functions 
  * Should include only safe functions
  * Clock precision (`core.get_us_time()` and `os.clock()`) is limited to 20 us.
    20 us was the value, firefox used as first response to the spectre attacks.
    now it's 100 us or 5 us, depending on whether it's "cross-origin isolated".
    we only have one origin, so choosing 20 us is probably fine, I guess
    see also:
    https://www.mozilla.org/en-US/security/advisories/mfsa2018-01/
    https://developer.mozilla.org/en-US/docs/Web/API/Performance/now#security_requirements
  * other clocks:
    * os.time() and os.date() only have seconds precision, AFAIK.
    * dtime is only given once per step, so it's not useful
    * there might be other ways to build clocks (if we get async envs for sscsm,
      with a busy loop, for example)
  * `tostring` is *not* overridden.
    `tostring({})` and `string.format("%s", {})` give you pointers.
    (see lj_strfmt_obj)
    this is not very critical, but attacks could be made harder if we change this.
    the effort of overwriting is not worth it I think right now
* Includes a `sscsm_security.md`, read it
* Adds a non-binary setting `enable_sscsm`
* Maybe some more tiny stuff. See PR and diffs.

---------

Co-authored-by: Jude Melton-Houghton <jwmhjwmh@gmail.com>
This commit is contained in:
DS
2026-01-27 15:52:21 +01:00
committed by GitHub
parent 2c4a5c5963
commit ab3994b03b
45 changed files with 1557 additions and 86 deletions
+6
View File
@@ -47,6 +47,12 @@ files["builtin/client/init.lua"] = {
}
}
files["builtin/sscsm_client/init.lua"] = {
globals = {
debug = {fields={"getinfo"}},
}
}
files["builtin/common/math.lua"] = {
globals = {
"math",
+4
View File
@@ -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
+4
View File
@@ -1893,6 +1893,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
+25
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
local builtin_shared = ...
local make_registration = builtin_shared.make_registration
core.registered_globalsteps, core.register_globalstep = make_registration()
View File
+216
View File
@@ -0,0 +1,216 @@
# 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 (i.e. at least check if `core.get_version().proto_max`
is (less or) equal to (any of) the tested version(s)).
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 `<modpath>/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.
Functions that take or return paths always use virtual paths.
### 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: Loading bytecode is prohibited (like in SSM).
* `load`
* As above.
* `loadfile`
* As above.
* `loadstring`
* As above.
* `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"`.
+77
View File
@@ -0,0 +1,77 @@
# SSCSM security
## Threat model
* SSCSM scripts come from the server (potential malicious actor). We are the client.
* Authenticity of server is not given (Luanti's 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).
(It's also low risk (uninteresting target, and no catastrophic impact).)
* There already is 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.
+1
View File
@@ -77,6 +77,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
+72 -49
View File
@@ -42,13 +42,18 @@
#include "util/string.h"
#include "version.h"
// Modding
// CPCSM
#include "content/mod_configuration.h"
#include "content/mods.h"
#include "modchannels.h"
#include "script/common/c_types.h" // LuaError
#include "script/scripting_client.h"
// SSCSM
#include "client/mod_vfs.h"
#include "script/sscsm/sscsm_controller.h"
#include "script/sscsm/sscsm_events.h"
// Network
#include "network/clientopcodes.h"
#include "network/connection.h"
@@ -150,6 +155,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<SSCSMEventUpdateVFSFiles>();
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<SSCSMEventLoadMods>();
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<SSCSMEventUpdateVFSFiles>();
// 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<SSCSMEventLoadMods>();
event2->mods.emplace_back("sscsm_test0", "sscsm_test0:init.lua");
m_sscsm_controller->runEvent(this, std::move(event2));
}
}
}
void Client::migrateModStorage()
@@ -197,12 +258,14 @@ void Client::loadMods()
return;
}
m_mod_vfs = std::make_unique<ModVFS>();
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();
@@ -241,7 +304,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
@@ -263,35 +326,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<fs::DirListNode> 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";
@@ -537,6 +571,12 @@ void Client::step(float dtime)
*/
LocalPlayer *player = m_env.getLocalPlayer();
{
auto event = std::make_unique<SSCSMEventOnStep>();
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);
@@ -2015,23 +2055,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
*/
+8 -12
View File
@@ -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<u16, u32> 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
*/
@@ -383,7 +376,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; }
@@ -585,7 +578,10 @@ private:
ModStorageDatabase *m_mod_storage_database = nullptr;
float m_mod_storage_save_timer = 10.0f;
std::vector<ModSpec> m_mods;
StringMap m_mod_vfs;
std::unique_ptr<ModVFS> m_mod_vfs;
// SSCSM
std::unique_ptr<SSCSMController> m_sscsm_controller;
bool m_shutdown = false;
+56
View File
@@ -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 <algorithm>
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<fs::DirListNode> 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;
}
+23
View File
@@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2025 Luanti authors
//
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include <string>
#include <unordered_map>
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<std::string, std::string> m_vfs;
};
+4
View File
@@ -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
+1
View File
@@ -122,6 +122,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");
+5
View File
@@ -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:
+3
View File
@@ -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)
+1
View File
@@ -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)
+25 -9
View File
@@ -14,6 +14,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
@@ -71,7 +73,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<Client *>(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
+18 -10
View File
@@ -42,11 +42,12 @@ extern "C" {
enum class ScriptingType: u8 {
Async, // either mainmenu (client) or ingame (server)
Client,
Client, // CPCSM
MainMenu,
Server,
Emerge,
PauseMenu,
SSCSM,
};
class Server;
@@ -57,8 +58,10 @@ class EmergeThread;
class IGameDef;
class Environment;
class GUIEngine;
class SSCSMEnvironment;
class ServerActiveObject;
struct PlayerHPChangeReason;
struct ModVFS;
class ScriptApiBase : protected LuaHelper {
public:
@@ -76,7 +79,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,
@@ -89,9 +92,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
@@ -157,6 +161,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; }
@@ -177,14 +184,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;
};
+148 -3
View File
@@ -8,9 +8,11 @@
#include "server.h"
#if CHECK_CLIENT_BUILD()
#include "client/client.h"
#include "client/mod_vfs.h"
#endif
#include "content/mods.h" // ModSpec
#include "settings.h"
#include "constants.h"
#include <cerrno>
#include <string>
@@ -381,6 +383,139 @@ void ScriptApiSecurity::initializeSecurityClient()
lua_pop(L, 1); // Pop old debug
#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);
}
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",
"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");
@@ -791,10 +926,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<std::string>(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);
@@ -981,6 +1117,15 @@ int ScriptApiSecurity::sl_os_setlocale(lua_State *L)
}
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<lua_Number>(t) / static_cast<lua_Number>(CLOCKS_PER_SEC));
return 1;
}
int ScriptApiSecurity::sl_debug_getinfo(lua_State *L)
{
// signature: [thread,] function [, what]
+6 -1
View File
@@ -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
@@ -116,6 +118,9 @@ private:
static int sl_os_remove(lua_State *L);
static int sl_os_setlocale(lua_State *L);
// reduced precision (for SSCSM)
static int sl_os_clock(lua_State *L);
static int sl_debug_getinfo(lua_State *L);
};
+29
View File
@@ -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<std::pair<std::string, std::string>> &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);
}
+15
View File
@@ -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<std::pair<std::string, std::string>> &mods);
void environment_step(float dtime);
};
+1
View File
@@ -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)
+5
View File
@@ -57,6 +57,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)
+2
View File
@@ -22,6 +22,7 @@ class EmergeThread;
class ScriptApiBase;
class Server;
class Environment;
class SSCSMEnvironment;
class ServerInventoryManager;
class ModApiBase : protected LuaHelper {
@@ -32,6 +33,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);
+23 -2
View File
@@ -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<std::string>(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);
}
+1
View File
@@ -69,4 +69,5 @@ private:
public:
static void Initialize(lua_State *L, int top);
static void InitializeSSCSM(lua_State *L, int top);
};
+37
View File
@@ -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);
}
+17
View File
@@ -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);
};
+42
View File
@@ -28,6 +28,7 @@
#include "util/png.h"
#include "player.h"
#include "daynightratio.h"
#include "constants.h"
#include <cstdio>
// only available in zstd 1.3.5+
@@ -80,6 +81,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;
@@ -813,6 +824,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);
+4
View File
@@ -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);
@@ -137,4 +140,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);
};
+37
View File
@@ -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);
}
+25
View File
@@ -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);
};
+7
View File
@@ -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)
+65
View File
@@ -0,0 +1,65 @@
// 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> SSCSMController::create()
{
auto channel = std::make_shared<StupidChannel>();
auto thread = std::make_unique<SSCSMEnvironment>(channel);
thread->start();
// Wait for thread to finish initializing.
auto req0 = deserializeSSCSMRequest(channel->recvB());
FATAL_ERROR_IF(!dynamic_cast<SSCSMRequestPollNextEvent *>(req0.get()),
"First request must be pollEvent.");
return std::make_unique<SSCSMController>(std::move(thread), channel);
}
SSCSMController::SSCSMController(std::unique_ptr<SSCSMEnvironment> thread,
std::shared_ptr<StupidChannel> 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<SSCSMEventTearDown>();
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<ISSCSMEvent> 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)));
// SSCSMRequestPollNextEvent means `event` is finished and we need to
// answer with the next event (that will be passed in a subsequent runEvent()
// call)
if (dynamic_cast<SSCSMRequestPollNextEvent *>(request.get()) != nullptr) {
break;
}
answer = handleRequest(client, request.get());
}
}
+43
View File
@@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2024 Luanti authors
//
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include <memory>
#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<SSCSMEnvironment> m_thread;
std::shared_ptr<StupidChannel> m_channel;
SerializedSSCSMAnswer handleRequest(Client *client, ISSCSMRequest *req);
public:
static std::unique_ptr<SSCSMController> create();
SSCSMController(std::unique_ptr<SSCSMEnvironment> thread,
std::shared_ptr<StupidChannel> channel);
~SSCSMController();
DISABLE_CLASS_COPY(SSCSMController);
// Handles requests until the next event is polled
void runEvent(Client *client, std::unique_ptr<ISSCSMEvent> event);
};
+74
View File
@@ -0,0 +1,74 @@
// 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"
#include "common/c_types.h" // LuaError
SSCSMEnvironment::SSCSMEnvironment(std::shared_ptr<StupidChannel> channel) :
Thread("SSCSMEnvironment-thread"),
m_channel(std::move(channel)),
m_script(std::make_unique<SSCSMScripting>(this)),
m_vfs(std::make_unique<ModVFS>())
{
}
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<SSCSMEventTearDown *>(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<std::pair<std::string, std::string>> &&files)
{
for (auto &&p : files) {
m_vfs->m_vfs.emplace(std::move(p.first), std::move(p.second));
}
}
std::optional<std::string_view> 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));
}
+59
View File
@@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: 2024 Luanti authors
//
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#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<StupidChannel> m_channel;
std::unique_ptr<SSCSMScripting> 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<ModVFS> m_vfs;
void *run() override;
SerializedSSCSMAnswer exchange(SerializedSSCSMRequest req);
public:
SSCSMEnvironment(std::shared_ptr<StupidChannel> channel);
~SSCSMEnvironment() override;
SSCSMScripting *getScript() { return m_script.get(); }
ModVFS *getModVFS() { return m_vfs.get(); }
void updateVFSFiles(std::vector<std::pair<std::string, std::string>> &&files);
std::optional<std::string_view> readVFSFile(const std::string &path);
void setFatalError(const std::string &reason);
template <typename RQ>
typename RQ::Answer doRequest(RQ &&rq)
{
return deserializeSSCSMAnswer<typename RQ::Answer>(
exchange(serializeSSCSMRequest(std::forward<RQ>(rq)))
);
}
};
+51
View File
@@ -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<std::pair<std::string, std::string>> 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<std::pair<std::string, std::string>> 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);
}
};
+39
View File
@@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: 2024 Luanti authors
//
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include <memory>
#include <type_traits>
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<ISSCSMEvent>;
template <typename T>
inline SerializedSSCSMEvent serializeSSCSMEvent(const T &event)
{
static_assert(std::is_base_of_v<ISSCSMEvent, T>);
return std::make_unique<T>(event);
}
inline std::unique_ptr<ISSCSMEvent> deserializeSSCSMEvent(SerializedSSCSMEvent event_serialized)
{
// The actual deserialization will have to use a type tag, and then choose
// the appropriate deserializer.
return event_serialized;
}
+82
View File
@@ -0,0 +1,82 @@
// SPDX-FileCopyrightText: 2024 Luanti authors
//
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include "exceptions.h"
#include <memory>
#include <type_traits>
class SSCSMController;
class Client;
// FIXME: remove once we have actual serialization
// this is just here so we can instead put them into unique_ptr
struct ISSCSMAnswer
{
virtual ~ISSCSMAnswer() = default;
};
// FIXME: actually serialize, and replace this with a std::vector<u8>.
// also update function argument declarations, to take
// `const SerializedSSCSMAnswer &` or whatever
// (not polymorphic. the receiving side will know the answer type that is in here)
using SerializedSSCSMAnswer = std::unique_ptr<ISSCSMAnswer>;
// Request made by the sscsm env to the main env.
struct ISSCSMRequest
{
virtual ~ISSCSMRequest() = default;
virtual SerializedSSCSMAnswer exec(Client *client) = 0;
};
// FIXME: as above, actually serialize
// (polymorphic. this can be any ISSCSMRequest. ==> needs type tag)
using SerializedSSCSMRequest = std::unique_ptr<ISSCSMRequest>;
template <typename T>
inline SerializedSSCSMRequest serializeSSCSMRequest(const T &request)
{
static_assert(std::is_base_of_v<ISSCSMRequest, T>);
// FIXME: this will need to use a type tag for T
return std::make_unique<T>(request);
}
template <typename T>
inline T deserializeSSCSMAnswer(SerializedSSCSMAnswer answer_serialized)
{
static_assert(std::is_base_of_v<ISSCSMAnswer, T>);
// FIXME: should look something like this:
// return sscsm::Serializer<T>{}.deserialize(answer_serialized);
// (note: answer_serialized does not need a type tag)
// dynamic cast in place of actual deserialization
auto ptr = dynamic_cast<T *>(answer_serialized.get());
if (!ptr) {
throw SerializationError("deserializeSSCSMAnswer failed");
}
return std::move(*ptr);
}
template <typename T>
inline SerializedSSCSMAnswer serializeSSCSMAnswer(T &&answer)
{
static_assert(std::is_base_of_v<ISSCSMAnswer, T>);
// FIXME: should look something like this:
// return sscsm::Serializer<T>{}.serialize(request);
return std::make_unique<T>(std::move(answer));
}
inline std::unique_ptr<ISSCSMRequest> deserializeSSCSMRequest(SerializedSSCSMRequest request_serialized)
{
// FIXME: The actual deserialization will have to use a type tag, and then
// choose the appropriate deserializer.
return request_serialized;
}
+107
View File
@@ -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 final : public ISSCSMRequest
{
struct Answer final : public ISSCSMAnswer
{
std::unique_ptr<ISSCSMEvent> 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 final : public ISSCSMRequest
{
struct Answer final : 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 final : public ISSCSMRequest
{
struct Answer final : 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 final : public ISSCSMRequest
{
struct Answer final : 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 final : public ISSCSMRequest
{
struct Answer final : 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));
}
};
+84
View File
@@ -0,0 +1,84 @@
// SPDX-FileCopyrightText: 2024 Luanti authors
//
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include <memory>
#include <mutex>
#include <condition_variable>
#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();
}
};