mission_script_lua_sandbox.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. #include <algorithm>
  2. #include <array>
  3. #include <cctype>
  4. #include <cstddef>
  5. #include <string_view>
  6. #include "mission_script_lua_bap_internal.h"
  7. #include "mission_script_lua_catalog_internal.h"
  8. #include "mission_script_lua_event_internal.h"
  9. #include "mission_script_lua_internal.h"
  10. #include "mission_script_lua_manifest_internal.h"
  11. #include "mission_script_lua_names.h"
  12. #include "mission_script_lua_types.h"
  13. #include "mission_script_lua_world_internal.h"
  14. #include "mission_script_vm_internal.h"
  15. namespace sunrise::server::activity::mission::lua_vm::detail {
  16. namespace {
  17. /** Refuses collection-sensitive metamethods and installs a hidden copy the caller cannot mutate. */
  18. [[nodiscard]] int safe_setmetatable(lua_State* state) {
  19. luaL_checktype(state, 1, LUA_TTABLE);
  20. if (lua_isnoneornil(state, 2)) {
  21. lua_pushvalue(state, 1);
  22. lua_pushnil(state);
  23. lua_setmetatable(state, -2);
  24. return 1;
  25. }
  26. luaL_checktype(state, 2, LUA_TTABLE);
  27. // A metatable a script installs may not run code or weaken a reference at collection.
  28. constexpr std::array<const char*, 3> refused{"__gc", "__mode", "__close"};
  29. for (const char* const key : refused) {
  30. lua_pushstring(state, key);
  31. lua_rawget(state, 2);
  32. const bool present = !lua_isnil(state, -1);
  33. lua_pop(state, 1);
  34. if (present) {
  35. return luaL_error(state, "collection-sensitive metamethod is unavailable");
  36. }
  37. }
  38. // Apply an unreachable copy. The caller may keep and mutate its source table through rawset,
  39. // but it can never add collection-sensitive entries to the installed metatable afterward.
  40. lua_newtable(state);
  41. const int installed = lua_gettop(state);
  42. lua_pushnil(state);
  43. while (lua_next(state, 2) != 0) {
  44. lua_pushvalue(state, -2);
  45. lua_pushvalue(state, -2);
  46. lua_rawset(state, installed);
  47. lua_pop(state, 1);
  48. }
  49. lua_pushliteral(state, "__metatable");
  50. lua_pushboolean(state, 0);
  51. lua_rawset(state, installed);
  52. lua_pushvalue(state, 1);
  53. lua_pushvalue(state, installed);
  54. lua_setmetatable(state, -2);
  55. return 1;
  56. }
  57. /** Preserves base pcall semantics while making the instruction-budget error uncatchable. */
  58. [[nodiscard]] int safe_pcall(lua_State* state) {
  59. const int arguments = lua_gettop(state) - 1;
  60. luaL_checktype(state, 1, LUA_TFUNCTION);
  61. const int status = lua_pcall(state, arguments, LUA_MULTRET, 0);
  62. CallFrame& frame = active_frame(state);
  63. if (status == LUA_ERRMEM || status == LUA_ERRERR) {
  64. frame.memoryExceeded = true;
  65. }
  66. if (frame.instructionExceeded || frame.memoryExceeded || frame.intentAllocationFailed) {
  67. if (status == LUA_OK) {
  68. lua_pushliteral(state, "instruction_budget");
  69. }
  70. return lua_error(state);
  71. }
  72. lua_pushboolean(state, status == LUA_OK ? 1 : 0);
  73. lua_insert(state, 1);
  74. return lua_gettop(state);
  75. }
  76. void remove_global(lua_State* state, const char* name) {
  77. lua_pushnil(state);
  78. lua_setglobal(state, name);
  79. }
  80. /** Allows only generated dotted module names below the one configured SDK directory. */
  81. [[nodiscard]] int safe_require(lua_State* state) {
  82. std::size_t length = 0;
  83. const char* const name = luaL_checklstring(state, 1, &length);
  84. if (length == 0 || name[0] == '.' || name[length - 1] == '.') {
  85. return luaL_error(state, "generated SDK module name is invalid");
  86. }
  87. bool previousDot = false;
  88. for (std::size_t index = 0; index < length; ++index) {
  89. const unsigned char byte = static_cast<unsigned char>(name[index]);
  90. const bool dot = byte == '.';
  91. if ((!dot && byte != '_' && std::isalnum(byte) == 0) || (dot && previousDot)) {
  92. return luaL_error(state, "generated SDK module name is invalid");
  93. }
  94. previousDot = dot;
  95. }
  96. lua_settop(state, 1);
  97. lua_pushvalue(state, lua_upvalueindex(1));
  98. lua_insert(state, 1);
  99. lua_call(state, 1, LUA_MULTRET);
  100. return lua_gettop(state);
  101. }
  102. /** Opens only the Lua-file package searcher and hides package mutation from authored code. */
  103. void configure_generated_require(lua_State* state, const ProgramIdentity& identity) {
  104. if (identity.sdkLuaSearchPath[0] == '\0') {
  105. return;
  106. }
  107. luaL_requiref(state, LUA_LOADLIBNAME, luaopen_package, 1);
  108. lua_pushstring(state, identity.sdkLuaSearchPath.data());
  109. lua_setfield(state, -2, "path");
  110. lua_pushliteral(state, "");
  111. lua_setfield(state, -2, "cpath");
  112. lua_pushnil(state);
  113. lua_setfield(state, -2, "loadlib");
  114. lua_getfield(state, -1, "searchers");
  115. if (lua_istable(state, -1)) {
  116. lua_pushnil(state);
  117. lua_rawseti(state, -2, 3);
  118. lua_pushnil(state);
  119. lua_rawseti(state, -2, 4);
  120. }
  121. lua_pop(state, 2);
  122. lua_getglobal(state, "require");
  123. lua_pushcclosure(state, &safe_require, 1);
  124. lua_setglobal(state, "require");
  125. remove_global(state, LUA_LOADLIBNAME);
  126. }
  127. /** Opens the allowed libraries, removes unsafe globals, and registers every handle metatable. */
  128. [[nodiscard]] int setup_sandbox(lua_State* state) {
  129. Impl* const impl = impl_from_state(state);
  130. luaL_requiref(state, "_G", luaopen_base, 1);
  131. lua_pop(state, 1);
  132. luaL_requiref(state, LUA_STRLIBNAME, luaopen_string, 1);
  133. lua_pop(state, 1);
  134. luaL_requiref(state, LUA_TABLIBNAME, luaopen_table, 1);
  135. lua_pop(state, 1);
  136. luaL_requiref(state, LUA_MATHLIBNAME, luaopen_math, 1);
  137. lua_pop(state, 1);
  138. configure_generated_require(state, impl->identity);
  139. // Globals a sandboxed program may not reach: host IO, collection control, and any
  140. // iteration or protected call whose cost the instruction budget cannot charge.
  141. constexpr std::array<const char*, 13> removedGlobals{
  142. "collectgarbage",
  143. "dofile",
  144. "getmetatable",
  145. "load",
  146. "loadfile",
  147. "next",
  148. "pairs",
  149. "pcall",
  150. "print",
  151. "rawequal",
  152. "rawlen",
  153. "warn",
  154. "xpcall",
  155. };
  156. for (const char* const name : removedGlobals) {
  157. remove_global(state, name);
  158. }
  159. lua_pushcfunction(state, &safe_setmetatable);
  160. lua_setglobal(state, "setmetatable");
  161. lua_pushcfunction(state, &safe_pcall);
  162. lua_setglobal(state, "pcall");
  163. lua_getglobal(state, LUA_STRLIBNAME);
  164. // String functions whose pattern matching has no instruction bound.
  165. constexpr std::array<const char*, 5> removedStringFunctions{
  166. "dump", "find", "match", "gmatch", "gsub"};
  167. for (const char* const name : removedStringFunctions) {
  168. lua_pushnil(state);
  169. lua_setfield(state, -2, name);
  170. }
  171. lua_pop(state, 1);
  172. lua_getglobal(state, LUA_MATHLIBNAME);
  173. lua_pushnil(state);
  174. lua_setfield(state, -2, "random");
  175. lua_pushnil(state);
  176. lua_setfield(state, -2, "randomseed");
  177. lua_pop(state, 1);
  178. // One call per domain. A domain owns its own metatable list, so adding a type never edits
  179. // this function.
  180. register_context_metatables(state);
  181. register_definition_metatables(state);
  182. register_collection_metatables(state);
  183. register_state_metatables(state);
  184. register_lifetime_metatables(state);
  185. register_event_metatables(state);
  186. register_squad_metatables(state);
  187. register_scene_metatables(state);
  188. register_slot_metatables(state);
  189. register_actor_sequence_metatables(state);
  190. register_peer_metatables(state);
  191. register_bap_metatables(state);
  192. register_value_metatables(state);
  193. register_enum_metatables(state);
  194. register_key_metatables(state);
  195. register_bounded_metatables(state);
  196. catalog_api::register_metatables(state);
  197. manifest_api::register_metatables(state);
  198. world_api::register_metatables(state);
  199. return 0;
  200. }
  201. void push_context(lua_State* state) {
  202. push_handle(state, kContextMetatable, ContextHandle{});
  203. }
  204. void push_state(lua_State* state) {
  205. push_handle(state, kStateMetatable, StateHandle{});
  206. }
  207. void push_event(lua_State* state, const host::Event& source) {
  208. push_handle(state, event_metatable(source.kind), EventHandle{source});
  209. }
  210. } // namespace
  211. /** Copies one sanitized diagnostic and never allocates. */
  212. void set_error(Impl& impl, std::string_view error) noexcept {
  213. impl.lastError = {};
  214. const std::size_t length = (std::min)(error.size(), impl.lastError.size() - 1);
  215. for (std::size_t index = 0; index < length; ++index) {
  216. const char value = error[index];
  217. // The log line quotes this text, so a double quote here would end the field early.
  218. const bool blank = value == '\r' || value == '\n' || value == '\t';
  219. impl.lastError[index] = blank ? ' ' : value == '"' ? '\'' : value;
  220. }
  221. }
  222. /** Builds the sandbox from additions while setup itself remains protected. */
  223. bool initialize_sandbox(Impl& impl) noexcept {
  224. attach_impl(impl.state, impl);
  225. lua_pushcfunction(impl.state, &setup_sandbox);
  226. const int result = lua_pcall(impl.state, 0, 0, 0);
  227. if (result == LUA_OK) {
  228. return true;
  229. }
  230. const char* const error =
  231. lua_type(impl.state, -1) == LUA_TSTRING ? lua_tostring(impl.state, -1) : nullptr;
  232. set_error(impl,
  233. error == nullptr ? "sandbox initialization failed with a non-string error" : error);
  234. lua_settop(impl.state, 0);
  235. return false;
  236. }
  237. /** Executes one already-selected entry; all argument allocation stays under lua_pcall. */
  238. int protected_callback(lua_State* state) {
  239. Impl* const impl = impl_from_state(state);
  240. CallFrame& frame = active_frame(state);
  241. const int reference = frame.handler == Handler::event && frame.event != nullptr
  242. ? event_reference(*impl, frame.event->kind)
  243. : handler_reference(*impl, frame.handler);
  244. if (reference == LUA_NOREF) {
  245. lua_pushboolean(state, 0);
  246. return 1;
  247. }
  248. lua_rawgeti(state, LUA_REGISTRYINDEX, reference);
  249. if (!lua_isfunction(state, -1)) {
  250. return luaL_error(state, "mission entry is no longer a function");
  251. }
  252. push_context(state);
  253. push_state(state);
  254. int arguments = 2;
  255. if (frame.handler == Handler::event) {
  256. if (frame.event == nullptr) {
  257. return luaL_error(state, "mission event is missing");
  258. }
  259. push_event(state, *frame.event);
  260. arguments = 3;
  261. }
  262. lua_call(state, arguments, 0);
  263. lua_pushboolean(state, 1);
  264. return 1;
  265. }
  266. /** Closes Lua before resetting the allocator that owns its objects. */
  267. void destroy_state(Impl& impl) noexcept {
  268. if (impl.state != nullptr) {
  269. lua_close(impl.state);
  270. impl.arenaBytesAfterClose = impl.arena.used;
  271. }
  272. impl.identity = {};
  273. impl.definitions = {};
  274. std::vector<state::activity::mission::DeviceRequestReport>{}.swap(impl.deviceRequests);
  275. std::vector<state::activity::mission::SquadPopulation>{}.swap(impl.squadPopulations);
  276. impl.deviceRequestGeneration = 0;
  277. impl.ghostLevels = {};
  278. impl.ghostLevelCount = 0;
  279. std::vector<Intent>{}.swap(impl.outbox);
  280. impl.variables = {};
  281. impl.timers = {};
  282. impl.peers = {};
  283. impl.peerCount = 0;
  284. impl.outboxRead = 0;
  285. impl.variableCount = 0;
  286. impl.timerCount = 0;
  287. impl.nextTimerSequence = state::activity::mission::kFirstTimerSequence;
  288. impl.stateRevision = 0;
  289. impl.callbacks = 0;
  290. impl.committedCallbacks = 0;
  291. impl.refusedCallbacks = 0;
  292. impl.collections = 0;
  293. impl.phase = 0;
  294. impl.initialStateRegion = -1;
  295. impl.initialStateSpawnSet = 0;
  296. impl.lastError = {};
  297. impl.state = nullptr;
  298. impl.frame = nullptr;
  299. impl.programReference = LUA_NOREF;
  300. impl.startReference = LUA_NOREF;
  301. impl.eventReferences.fill(LUA_NOREF);
  302. impl.loadReference = LUA_NOREF;
  303. impl.hasInitialState = false;
  304. impl.active = false;
  305. impl.faulted = false;
  306. arena_release(impl.arena);
  307. }
  308. } // namespace sunrise::server::activity::mission::lua_vm::detail