Bicameral Engine
GPU-resident engine for a magic-research open-world game
Loading...
Searching...
No Matches
luau_sandbox.cpp
Go to the documentation of this file.
1// luau_sandbox.cpp — Luau の殻の実装(T-0020、13 §2・ADR-0030)。
2//
3// 組み立ての順番: lua_newstate(上限つきの確保)→ 許した標準ライブラリだけを開く → 見せない関数を消す・print を差し替える
4// → ホストの関数を登録 → luaL_sandbox(グローバルと標準ライブラリを読み取り専用に)。
5// 実行の順番: math の種を戻す → コンパイル(luau_compile)→ 自分用のスレッド(luaL_sandboxthread)で読み込み → lua_pcall → 戻り値を文字列に。
6// 上限: 安全点ごとに Interrupt が数え、超えたら error を投げる。確保は Allocate が数え、超えたら確保に失敗する(Luau がメモリの error にする)。
7// require(T-0138): RunOptions::modules がある Run だけ、そのスレッドのグローバルに置く。モジュールは自分用の環境(読むときは Run のグローバルへ
8// 抜ける表)で 1 回だけ走り、戻り値を Run の間だけキャッシュする。名前からファイルへの解決は呼び手(パッケージ)が決める。
9// Luau のヘッダは小さいので pch.h に入れず、ここだけで include する(Luau を知るのはこのファイルだけ)。
10#include "script/luau_sandbox.h"
11
12#include <algorithm>
13#include <cstdlib>
14#include <format>
15#include <string>
16#include <utility>
17
18#include "core/log.h"
19#include "lua.h"
20#include "luacode.h"
21#include "lualib.h"
22
23namespace bicameral::script {
24
25 namespace {
26
27 // --- 見せる標準ライブラリ(13 §2。os・debug は開かない。require は RunOptions::modules がある Run だけ。T-0138)---
28 struct LibraryEntry {
29 const char* name;
30 lua_CFunction open;
31 };
32
34 {.name = "", .open = luaopen_base},
35 {.name = LUA_COLIBNAME, .open = luaopen_coroutine},
36 {.name = LUA_TABLIBNAME, .open = luaopen_table},
37 {.name = LUA_STRLIBNAME, .open = luaopen_string},
38 {.name = LUA_MATHLIBNAME, .open = luaopen_math},
39 {.name = LUA_UTF8LIBNAME, .open = luaopen_utf8},
40 {.name = LUA_BITLIBNAME, .open = luaopen_bit32},
41 {.name = LUA_BUFFERLIBNAME, .open = luaopen_buffer},
42 {.name = LUA_VECLIBNAME, .open = luaopen_vector},
43 };
44
45 // base のうち見せないもの: メモリの量(実行の環境で変わりうる)・環境の書き換え・古い仕組み
46 constexpr const char* HIDDEN_GLOBALS[] = {"gcinfo", "getfenv", "setfenv", "newproxy"};
47
48 int OpenOneLibrary(lua_State* state) {
49 const auto* entry = static_cast<const LibraryEntry*>(lua_tolightuserdata(state, 1));
50 lua_pushcfunction(state, entry->open, entry->name);
51 lua_pushstring(state, entry->name);
52 lua_call(state, 1, 0);
53
54 return 0;
55 }
56
57 // 戻り値を、アドレスに依らない文字列にする(表・関数は型の名前だけ。決定性のため)
58 std::string ValueToText(lua_State* state, int index) {
59 switch (lua_type(state, index)) {
60 case LUA_TNIL: return "nil";
61 case LUA_TBOOLEAN: return lua_toboolean(state, index) ? "true" : "false";
62 case LUA_TNUMBER:
63 case LUA_TSTRING: {
64 lua_pushvalue(state, index); // lua_tolstring は数を文字列に書き換えるので、写しで変換する
65 size_t length = 0;
66 const char* text = lua_tolstring(state, -1, &length);
67 std::string result(text, length);
68 lua_pop(state, 1);
69
70 return result;
71 }
72 default: return std::string("<") + luaL_typename(state, index) + ">";
73 }
74 }
75
76 // --- Luau の値 → ScriptValue(表は中まで。鍵は数か文字列。関数などは持てない)---
77 constexpr size_t MAX_VALUE_DEPTH = 64;
78
79 std::expected<ScriptValue, std::string> ReadValue(lua_State* state, int index, std::vector<const void*>& path);
80
81 std::expected<ScriptValue, std::string> ReadKey(lua_State* state, int index) {
82 if (lua_type(state, index) == LUA_TSTRING) {
83 size_t length = 0;
84 const char* text = lua_tolstring(state, index, &length);
85
86 return ScriptValue::MakeString(std::string(text, length));
87 }
88
89 if (lua_type(state, index) == LUA_TNUMBER)
90 return ScriptValue::MakeNumber(lua_tonumber(state, index)); // NaN は Luau の表の鍵になれない
91
92 return std::unexpected(
93 std::format("表の鍵にできるのは数と文字列だけ({} があった)", luaL_typename(state, index)));
94 }
95
96 std::expected<ScriptValue, std::string> ReadTable(lua_State* state, int index, std::vector<const void*>& path) {
97 const void* identity = lua_topointer(state, index);
98 if (path.size() >= MAX_VALUE_DEPTH)
99 return std::unexpected(std::format("表が深すぎる({} 段まで)", MAX_VALUE_DEPTH));
100 if (std::ranges::find(path, identity) != path.end())
101 return std::unexpected("表が自分自身を含んでいる(循環)");
102 if (!lua_checkstack(state, 3))
103 return std::unexpected("Luau のスタックが足りない");
104
105 path.push_back(identity);
106 std::vector<ScriptField> fields;
107
108 // lua_next の順番はアドレスと挿入の履歴で決まるので、集めてから並べ直す(MakeTable)
109 lua_pushnil(state);
110 while (lua_next(state, index) != 0) {
111 auto key = ReadKey(state, -2);
112 auto value = key ? ReadValue(state, lua_gettop(state), path)
113 : std::expected<ScriptValue, std::string>{};
114 if (!key || !value) {
115 lua_pop(state, 2);
116 path.pop_back();
117
118 return std::unexpected(!key ? key.error() : value.error());
119 }
120
121 fields.push_back(ScriptField{.key = std::move(*key), .value = std::move(*value)});
122 lua_pop(state, 1);
123 }
124
125 path.pop_back();
126
127 return ScriptValue::MakeTable(std::move(fields));
128 }
129
130 std::expected<ScriptValue, std::string> ReadValue(lua_State* state, int index, std::vector<const void*>& path) {
131 switch (lua_type(state, index)) {
132 case LUA_TNIL: return ScriptValue{};
133 case LUA_TBOOLEAN: return ScriptValue::MakeBoolean(lua_toboolean(state, index) != 0);
134 case LUA_TSTRING: return ReadKey(state, index);
135 case LUA_TTABLE: return ReadTable(state, index, path);
136 case LUA_TNUMBER: {
137 const double number = lua_tonumber(state, index);
138 if (number != number)
139 return std::unexpected("NaN は表にできない(ビット列が機種で変わる)");
140
141 return ScriptValue::MakeNumber(number);
142 }
143 default:
144 return std::unexpected(
145 std::format("{} は表にできない(数・文字列・真偽・表だけ)", luaL_typename(state, index)));
146 }
147 }
148
149 // --- コンパイル(luau_compile は文法の誤りもバイトコードに入れて返し、luau_load が失敗する)---
150 using Bytecode = std::unique_ptr<char, decltype(&std::free)>;
151
152 Bytecode Compile(std::string_view source, size_t& bytecodeSize) {
153 lua_CompileOptions options{};
154 options.optimizationLevel = 1;
155 options.debugLevel = 1; // エラーに行番号を付ける
156
157 return Bytecode(luau_compile(source.data(), source.size(), &options, &bytecodeSize), &std::free);
158 }
159
160 ScriptErrorKind ClassifyError(int status, bool safepointExceeded, bool memoryExceeded) {
161 if (safepointExceeded)
163 if (memoryExceeded || status == LUA_ERRMEM)
165
167 }
168
169 } // namespace
170
171 // --- 作る・壊す ---
172
173 LuauSandbox::LuauSandbox(SandboxLimits limits) : m_limits(std::move(limits)) {}
174
176 if (m_state)
177 lua_close(m_state);
178 }
179
180 std::expected<std::unique_ptr<LuauSandbox>, std::string> LuauSandbox::Create(SandboxLimits limits) {
181 std::unique_ptr<LuauSandbox> sandbox(new LuauSandbox(std::move(limits)));
182
183 sandbox->m_state = lua_newstate(&LuauSandbox::Allocate, sandbox.get());
184 if (!sandbox->m_state)
185 return std::unexpected("Luau の状態を作れない(メモリの上限が小さすぎる?)");
186
187 lua_Callbacks* callbacks = lua_callbacks(sandbox->m_state);
188 callbacks->userdata = sandbox.get();
189 callbacks->interrupt = &LuauSandbox::Interrupt;
190
191 if (auto opened = sandbox->OpenLibraries(); !opened)
192 return std::unexpected(opened.error());
193
194 return sandbox;
195 }
196
197 std::expected<void, std::string> LuauSandbox::OpenLibraries() {
198 // 開く途中の error(メモリの上限)で落ちないよう、1 つずつ保護して呼ぶ
199 for (const LibraryEntry& entry : ALLOWED_LIBRARIES) {
200 lua_pushcfunction(m_state, &OpenOneLibrary, "OpenOneLibrary");
201 lua_pushlightuserdata(m_state, const_cast<LibraryEntry*>(&entry));
202 if (lua_pcall(m_state, 1, 0, 0) != LUA_OK) {
203 std::string message = lua_tostring(m_state, -1) ? lua_tostring(m_state, -1) : "?";
204 lua_pop(m_state, 1);
205
206 return std::unexpected(std::format("標準ライブラリ '{}' を開けない: {}", entry.name, message));
207 }
208 }
209
210 // --- 見せない関数を消し、print を差し替える ---
211 for (const char* name : HIDDEN_GLOBALS) {
212 lua_pushnil(m_state);
213 lua_setglobal(m_state, name);
214 }
215
216 lua_pushcfunction(m_state, &LuauSandbox::Print, "print");
217 lua_setglobal(m_state, "print");
218
219 return {};
220 }
221
222 // --- 組み立て ---
223
224 std::expected<void, ScriptError> LuauSandbox::RegisterFunction(const char* tableName, const char* name,
225 HostFunction function, void* context) {
226 if (m_sealed) {
227 std::string message = std::format("Seal の後に {}.{} を登録しようとした", tableName, name);
228
229 return std::unexpected(ScriptError{.kind = ScriptErrorKind::Usage, .message = std::move(message)});
230 }
231
232 // 表を探す(無ければ作ってグローバルに置く)
233 lua_getglobal(m_state, tableName);
234 if (!lua_istable(m_state, -1)) {
235 lua_pop(m_state, 1);
236 lua_newtable(m_state);
237 lua_pushvalue(m_state, -1);
238 lua_setglobal(m_state, tableName);
239 }
240
241 // context を上位値に持つ閉包として置く
242 lua_pushlightuserdata(m_state, context);
243 lua_pushcclosure(m_state, function, name, 1);
244 lua_setfield(m_state, -2, name);
245 lua_pop(m_state, 1);
246
247 return {};
248 }
249
251 if (m_sealed)
252 return;
253
254 luaL_sandbox(m_state);
255 m_sealed = true;
256 }
257
258 void* LuauSandbox::HostContext(lua_State* state) {
259 return lua_tolightuserdata(state, lua_upvalueindex(1));
260 }
261
262 // --- 実行 ---
263
265 // math.random は既定で時計とアドレスから種を作る(lmathlib.cpp)。実行ごとに決まった種へ戻す
266 lua_getglobal(m_state, LUA_MATHLIBNAME);
267 lua_getfield(m_state, -1, "randomseed");
268 lua_pushinteger(m_state, m_limits.randomSeed);
269 if (lua_pcall(m_state, 1, 0, 0) != LUA_OK)
270 lua_pop(m_state, 1); // 失敗しても種は決まった値のまま(メモリの上限くらいしか起きない)
271
272 lua_pop(m_state, 1); // math
273 }
274
275 std::expected<RunResult, ScriptError> LuauSandbox::Run(std::string_view chunkName, std::string_view source,
276 const RunOptions& options) {
277 if (!m_sealed)
278 return std::unexpected(ScriptError{.kind = ScriptErrorKind::Usage, .message = "Seal の前に Run した"});
279
280 // --- 実行ごとに戻すもの(決定性)。数を戻してから種を戻す(前の Run が上限を超えたままだと種の呼び出しが止められる)---
282 m_safepointExceeded = false;
283 m_memoryExceeded = false;
284 m_currentChunk = chunkName;
285 ReseedRandom();
287
288 size_t bytecodeSize = 0;
289 const Bytecode bytecode = Compile(source, bytecodeSize);
290 if (!bytecode)
291 return std::unexpected(
292 ScriptError{.kind = ScriptErrorKind::Memory, .message = "コンパイルの結果を確保できない"});
293
294 // --- 自分用のスレッドで読み込む(グローバルへの書き込みはこのスレッドの表に入り、ほかの Run と混ざらない)---
295 lua_State* thread = lua_newthread(m_state); // m_state のスタックに積まれ、終わるまで GC から守られる
296 luaL_sandboxthread(thread);
297 BeginModules(thread, options.modules);
298
299 auto result = RunThread(thread, chunkName, bytecode.get(), bytecodeSize, options);
300
301 EndModules();
302 lua_pop(m_state, 1); // スレッド
303
304 // 上限で止まった時のごみをすぐ片付ける(Luau は確保の失敗で GC を走らせないので、次の Run が巻き添えで止まらないように)
305 if (!result && result.error().kind == ScriptErrorKind::Memory)
306 lua_gc(m_state, LUA_GCCOLLECT, 0);
307
308 return result;
309 }
310
311 std::expected<RunResult, ScriptError> LuauSandbox::RunThread(lua_State* thread, std::string_view chunkName,
312 const char* bytecode, size_t bytecodeSize,
313 const RunOptions& options) {
314 const std::string luauChunkName = "=" + std::string(chunkName);
315 if (luau_load(thread, luauChunkName.c_str(), bytecode, bytecodeSize, 0) != LUA_OK)
316 return std::unexpected(ScriptError{.kind = ScriptErrorKind::Compile, .message = ValueToText(thread, -1)});
317
318 const int status = lua_pcall(thread, 0, LUA_MULTRET, 0);
319 if (status != LUA_OK) {
320 const ScriptErrorKind kind = ClassifyError(status, m_safepointExceeded, m_memoryExceeded);
321
322 return std::unexpected(ScriptError{.kind = kind, .message = ValueToText(thread, -1)});
323 }
324
325 return CollectResults(thread, chunkName, options);
326 }
327
328 std::expected<RunResult, ScriptError> LuauSandbox::CollectResults(lua_State* thread, std::string_view chunkName,
329 const RunOptions& options) {
330 RunResult result;
332
333 const int returnCount = lua_gettop(thread);
334 for (int index = 1; index <= returnCount; ++index) {
335 result.returns.push_back(ValueToText(thread, index));
336 if (!options.captureValues)
337 continue;
338
339 std::vector<const void*> path;
340 auto value = ReadValue(thread, index, path);
341 if (!value) {
342 std::string message = std::format("{}: 戻り値 {}: {}", chunkName, index, value.error());
343
344 return std::unexpected(ScriptError{.kind = ScriptErrorKind::Runtime, .message = std::move(message)});
345 }
346
347 result.values.push_back(std::move(*value));
348 }
349
350 return result;
351 }
352
353 // --- require(T-0138。RunOptions::modules がある Run の間だけ)---
354
355 void LuauSandbox::BeginModules(lua_State* thread, const ModuleResolver* modules) {
356 m_modules = modules;
357 m_moduleLoading.clear();
358 if (!modules)
359 return;
360
361 lua_newtable(m_state);
362 m_moduleCacheRef = lua_ref(m_state, -1);
363 lua_pop(m_state, 1);
364
365 // Run のスレッドのグローバル(書き込める自分用の表)にだけ置く。殻のグローバルには無いまま
366 lua_pushcfunction(thread, &LuauSandbox::Require, "require");
367 lua_setglobal(thread, "require");
368 }
369
371 if (m_moduleCacheRef != 0)
372 lua_unref(m_state, m_moduleCacheRef);
373
375 m_modules = nullptr;
376 m_moduleLoading.clear();
377 }
378
379 int LuauSandbox::Require(lua_State* state) {
380 const char* name = luaL_checkstring(state, 1);
381 auto* self = static_cast<LuauSandbox*>(lua_callbacks(state)->userdata);
382
383 // C++ のものを片付けてから error を投げる
384 bool failed = false;
385 {
386 auto loaded = self->LoadModule(state, name);
387 if (!loaded) {
388 lua_pushlstring(state, loaded.error().data(), loaded.error().size());
389 failed = true;
390 }
391 }
392
393 if (failed)
394 lua_error(state);
395
396 return 1;
397 }
398
399 std::expected<void, std::string> LuauSandbox::LoadModule(lua_State* state, std::string_view name) {
400 if (!m_modules)
401 return std::unexpected("require はパッケージの中だけで使える");
402
403 auto file = (*m_modules)(name);
404 if (!file)
405 return std::unexpected(std::format("require(\"{}\"): {}", name, file.error()));
406
407 // --- 読み込み済みならキャッシュから ---
408 lua_getref(state, m_moduleCacheRef); // [cache]
409 lua_getfield(state, -1, file->chunkName.c_str());
410 if (!lua_isnil(state, -1)) {
411 lua_remove(state, -2);
412
413 return {};
414 }
415
416 lua_pop(state, 1); // [cache]
417 if (std::ranges::find(m_moduleLoading, file->chunkName) != m_moduleLoading.end()) {
418 lua_pop(state, 1);
419 std::string chain;
420 for (const std::string& loading : m_moduleLoading)
421 chain += loading + " → ";
422
423 return std::unexpected(std::format("require の循環: {}{}", chain, file->chunkName));
424 }
425
426 return RunModule(state, file->chunkName, file->source);
427 }
428
429 std::expected<void, std::string> LuauSandbox::RunModule(lua_State* state, const std::string& chunkName,
430 std::string_view source) {
431 // スタック: [cache] → 成功なら [モジュールの戻り値]
432 size_t bytecodeSize = 0;
433 const Bytecode bytecode = Compile(source, bytecodeSize);
434 if (!bytecode) {
435 lua_pop(state, 1);
436
437 return std::unexpected("コンパイルの結果を確保できない");
438 }
439
440 // --- モジュール用の環境: 書き込みはここに入り、読むときは Run のグローバルへ抜ける ---
441 lua_newtable(state); // [cache, env]
442 lua_newtable(state); // [cache, env, meta]
443 lua_pushvalue(state, LUA_GLOBALSINDEX);
444 lua_setfield(state, -2, "__index");
445 lua_setreadonly(state, -1, true);
446 lua_setmetatable(state, -2);
447
448 const std::string luauChunkName = "=" + chunkName;
449 if (luau_load(state, luauChunkName.c_str(), bytecode.get(), bytecodeSize, lua_gettop(state)) != LUA_OK) {
450 std::string message = ValueToText(state, -1);
451 lua_pop(state, 3);
452
453 return std::unexpected(std::move(message));
454 }
455
456 lua_remove(state, -2); // [cache, function]
457
458 // --- 走らせる(失敗しても読み込み中の印を外せるよう pcall で)---
459 m_moduleLoading.push_back(chunkName);
460 const int status = lua_pcall(state, 0, 1, 0); // [cache, result | error]
461 m_moduleLoading.pop_back();
462
463 if (status != LUA_OK || lua_isnil(state, -1)) {
464 std::string message = status != LUA_OK ? ValueToText(state, -1)
465 : std::format("{}: モジュールは nil 以外の値を 1 つ返す", chunkName);
466 lua_pop(state, 2);
467
468 return std::unexpected(std::move(message));
469 }
470
471 lua_pushvalue(state, -1); // [cache, result, result]
472 lua_setfield(state, -3, chunkName.c_str()); // cache[chunkName] = result
473 lua_remove(state, -2); // [result]
474
475 return {};
476 }
477
478 // --- Luau から呼ばれるもの ---
479
480 void* LuauSandbox::Allocate(void* userData, void* pointer, size_t oldSize, size_t newSize) {
481 auto* self = static_cast<LuauSandbox*>(userData);
482 const size_t previousSize = pointer ? oldSize : 0;
483
484 if (newSize == 0) {
485 std::free(pointer);
486 self->m_memoryBytes -= previousSize;
487
488 return nullptr;
489 }
490
491 // 増えるときだけ上限を見る(減らす・同じ大きさは必ず通す)
492 const uint64_t nextBytes = self->m_memoryBytes - previousSize + newSize;
493 if (newSize > previousSize && nextBytes > self->m_limits.memoryLimitBytes) {
494 self->m_memoryExceeded = true;
495
496 return nullptr;
497 }
498
499 void* resized = std::realloc(pointer, newSize);
500 if (!resized)
501 return nullptr;
502
503 self->m_memoryBytes = nextBytes;
504
505 return resized;
506 }
507
508 void LuauSandbox::Interrupt(lua_State* state, int gc) {
509 if (gc >= 0)
510 return; // GC の段の通知(安全点ではない)
511
512 auto* self = static_cast<LuauSandbox*>(lua_callbacks(state)->userdata);
513 ++self->m_safepointCount;
514 if (self->m_safepointCount <= self->m_limits.safepointLimit)
515 return;
516
517 // 超えた後は安全点ごとに投げ続ける(pcall で捕まえても外側のループで止まる)
518 self->m_safepointExceeded = true;
519 const std::string message = std::format("安全点の上限({})を超えた", self->m_limits.safepointLimit);
520 luaL_error(state, "%s", message.c_str());
521 }
522
523 int LuauSandbox::Print(lua_State* state) {
524 auto* self = static_cast<LuauSandbox*>(lua_callbacks(state)->userdata);
525
526 std::string text;
527 const int argumentCount = lua_gettop(state);
528 for (int index = 1; index <= argumentCount; ++index) {
529 if (index > 1)
530 text += '\t';
531 text += ValueToText(state, index);
532 }
533
534 if (self->m_limits.printSink)
535 self->m_limits.printSink(self->m_currentChunk, text);
536 else
537 Log(Channel::Tool, Level::Info, "[luau {}] {}", self->m_currentChunk, text);
538
539 return 0;
540 }
541
542} // namespace bicameral::script
std::expected< RunResult, ScriptError > Run(std::string_view chunkName, std::string_view source, const RunOptions &options={})
void BeginModules(lua_State *thread, const ModuleResolver *modules)
std::expected< void, ScriptError > RegisterFunction(const char *tableName, const char *name, HostFunction function, void *context)
std::expected< RunResult, ScriptError > CollectResults(lua_State *thread, std::string_view chunkName, const RunOptions &options)
static void Interrupt(lua_State *state, int gc)
std::expected< void, std::string > OpenLibraries()
static int Require(lua_State *state)
std::expected< void, std::string > RunModule(lua_State *state, const std::string &chunkName, std::string_view source)
std::expected< void, std::string > LoadModule(lua_State *state, std::string_view name)
LuauSandbox(const LuauSandbox &)=delete
static std::expected< std::unique_ptr< LuauSandbox >, std::string > Create(SandboxLimits limits)
std::expected< RunResult, ScriptError > RunThread(lua_State *thread, std::string_view chunkName, const char *bytecode, size_t bytecodeSize, const RunOptions &options)
static void * Allocate(void *userData, void *pointer, size_t oldSize, size_t newSize)
const ModuleResolver * m_modules
std::vector< std::string > m_moduleLoading
static void * HostContext(lua_State *state)
static int Print(lua_State *state)
std::expected< ScriptValue, std::string > ReadTable(lua_State *state, int index, std::vector< const void * > &path)
std::string ValueToText(lua_State *state, int index)
std::expected< ScriptValue, std::string > ReadKey(lua_State *state, int index)
std::expected< ScriptValue, std::string > ReadValue(lua_State *state, int index, std::vector< const void * > &path)
ScriptErrorKind ClassifyError(int status, bool safepointExceeded, bool memoryExceeded)
std::unique_ptr< char, decltype(&std::free)> Bytecode
std::function< std::expected< ModuleFile, std::string >(std::string_view name)> ModuleResolver
int(*)(lua_State *state) HostFunction
void Log(Channel channel, Level level, FormatWithLocation< std::type_identity_t< Args >... > pattern, Args &&... args)
Definition log.h:140
const ModuleResolver * modules
std::vector< std::string > returns
std::vector< ScriptValue > values
static ScriptValue MakeString(std::string value)
static ScriptValue MakeTable(std::vector< ScriptField > fields)
static ScriptValue MakeNumber(double value)
static ScriptValue MakeBoolean(bool value)