Something went wrong. Try again.
Monorepo for PackTools - tooling for Minecraft Modpacks!
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341#include "runtime.hpp"
#include "Luau/Compiler.h"#include "bindings/bindings.hpp"#include "header.hpp"#include "lua.h"#include "lualib.h"#include "stdlib.hpp"#include "stdlib_guards.hpp"
#include <array>#include <string_view>#include <utility>
// Errors unwind as C++ exceptions, so destructors run across a failing script (SPEC.md section 4).static_assert(LUA_USE_LONGJMP == 0, "packluau needs Luau built in C++ exception mode");
namespace packluau {
namespace {
/// Base-library globals a script must not reach (SPEC.md section 5.1). `os` carries the clocks,/// `print` writes to the process's stdout, and the fenv functions break the per-thread sandbox.constexpr std::array<const char *, 6> removed_globals = {"os", "debug", "print", "loadstring", "getfenv", "setfenv"};
std::string_view view(pl_str text) noexcept { if (text.len == 0) { return {}; } // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): UTF-8 bytes as chars. return {reinterpret_cast<const char *>(text.ptr), text.len};}
Failure failure(pl_status status, std::string message) { return Failure{.status = status, .message = std::move(message), .chunk = {}, .line = 0};}
/// Reads the line out of Luau's "<chunk>:<line>: <message>" prefix, or 0 without one.std::uint32_t line_of(std::string_view message, std::string_view chunk) noexcept { if (chunk.empty() || !message.starts_with(chunk) || message.size() <= chunk.size() || message.at(chunk.size()) != ':') { return 0; } const std::string_view rest = message.substr(chunk.size() + 1); std::uint32_t line = 0; std::size_t digits = 0; for (; digits < rest.size() && rest.at(digits) >= '0' && rest.at(digits) <= '9'; ++digits) { if (line > 100'000'000) { return 0; } line = (line * 10) + static_cast<std::uint32_t>(rest.at(digits) - '0'); } if (digits == 0 || digits == rest.size() || rest.at(digits) != ':') { return 0; } return line;}
/// The message a failed thread left on its stack, whatever type the error value has.std::string error_message(lua_State *thread) { std::size_t length = 0; if (const char *text = lua_tolstring(thread, -1, &length); text != nullptr) { return {text, length}; } return std::string("(error object is a ") + luaL_typename(thread, -1) + " value)";}
/// Runs `action` when the scope ends, however it ends.template <typename Action> class ScopeExit { public: explicit ScopeExit(Action action) noexcept : action_(std::move(action)) {} ~ScopeExit() { action_(); } ScopeExit(const ScopeExit &) = delete; ScopeExit &operator=(const ScopeExit &) = delete; ScopeExit(ScopeExit &&) = delete; ScopeExit &operator=(ScopeExit &&) = delete;
private: Action action_;};
} // namespace
void Runtime::StateCloser::operator()(lua_State *state) const noexcept { lua_close(state);}
Runtime::Runtime(Key /*key*/, const pl_host &host) : host_(host), allocator_(host) {}
Runtime::~Runtime() = default;
std::expected<std::unique_ptr<Runtime>, Failure> Runtime::create(const pl_host &host) { auto runtime = std::make_unique<Runtime>(Key{}, host); lua_State *state = lua_newstate(&Allocator::allocate, &runtime->allocator_); if (state == nullptr) { return std::unexpected(failure(PL_BUDGET, "out of memory creating the Luau state")); } runtime->state_.reset(state); lua_Callbacks *callbacks = lua_callbacks(state); callbacks->userdata = runtime.get(); callbacks->interrupt = &Runtime::on_interrupt; runtime->install_globals(); return runtime;}
void Runtime::install_globals() { lua_State *state = state_.get(); luaL_openlibs(state); for (const char *name : removed_globals) { lua_pushnil(state); lua_setglobal(state, name); } // Unseeded randomness; scripts get a seeded generator instead (SPEC.md section 7.8). lua_getglobal(state, "math"); lua_pushnil(state); lua_setfield(state, -2, "random"); lua_pushnil(state); lua_setfield(state, -2, "randomseed"); lua_pop(state, 1); install_stdlib_guards(state); bindings::install(state); luaL_sandbox(state);}
Runtime &Runtime::from(lua_State *state) noexcept { return *static_cast<Runtime *>(lua_callbacks(state)->userdata);}
RunContext *Runtime::active(lua_State *state) noexcept { return from(state).active_;}
RunContext &context(lua_State *state) { RunContext *run = Runtime::active(state); if (run == nullptr) { luaL_error(state, "pw is only available while a script runs"); } return *run;}
RunContext &context(lua_State *state, Capability capability) { RunContext &run = context(state); if (!run.header.allows(capability)) { run.stop = Stop::refused; run.refusal = "this script's --!pw header does not declare " + std::string(name_of(capability)); luaL_error(state, "%s", run.refusal.c_str()); } return run;}
/// Counts ticks and stops the run when the budget, the memory cap or the host's cancel byte says/// so. The stop is sticky: a script that catches the error is stopped again at its next safepoint,/// and a `lua_break` (where the thread can be suspended) cannot be caught at all.void Runtime::on_interrupt(lua_State *state, int gc) { if (gc >= 0) { return; } const Runtime &runtime = from(state); RunContext *active = runtime.active_; if (active == nullptr) { return; } if (active->stop == Stop::none) { if (runtime.host_.cancel != nullptr && *runtime.host_.cancel != 0) { active->stop = Stop::canceled; } else if (runtime.allocator_.exceeded() || active->ticks == 0) { active->stop = Stop::budget; } else { --active->ticks; return; } } if (lua_isyieldable(state) != 0) { lua_break(state); return; } luaL_error(state, "%s", active->stop == Stop::canceled ? "canceled" : active->stop == Stop::refused || active->stop == Stop::failed ? active->refusal.c_str() : "budget exhausted");}
std::expected<RunOutput, Failure> Runtime::run(const pl_run_request &request) { if (request.tier != PL_TIER_USER && request.tier != PL_TIER_STDLIB) { return std::unexpected(failure(PL_INVALID, "unknown tier")); } if ((request.flags & ~PL_RUN_EXPERIMENTAL) != 0) { return std::unexpected(failure(PL_INVALID, "unknown run flags")); } if (request.cpu_budget == 0 || request.memory_budget == 0) { return std::unexpected(failure(PL_INVALID, "a run needs a CPU and a memory budget")); } if (request.arg_count > 0 && request.args == nullptr) { return std::unexpected(failure(PL_INVALID, "arg_count is set but args is null")); } const std::string chunk(view(request.chunk)); if (chunk.empty() || !is_utf8(chunk)) { return std::unexpected(failure(PL_INVALID, "the chunk name must be non-empty UTF-8")); }
// The stdlib tier runs an embedded module's main(...) with the capabilities the module // declares, through a bootstrap that requires it, so the module loads as any require does. std::string bootstrap; std::string_view source = view(request.source); if (request.tier == PL_TIER_STDLIB) { const StdlibModule *module = chunk.starts_with("@pw/") ? find_stdlib(std::string_view(chunk).substr(4)) : nullptr; if (module == nullptr || !source.empty()) { return std::unexpected( failure(PL_INVALID, "a stdlib run names an embedded module (\"@pw/<name>\") and no source")); } bootstrap = std::string(module->header) + "local m = require(\"" + chunk + "\")\nassert(type(m) == \"table\" and type(m.main) == \"function\", \"" + chunk + " has no main\")\nreturn m.main(...)\n"; source = bootstrap; } const auto parsed = parse_header(source); if (!parsed) { return std::unexpected(Failure{.status = PL_INVALID, .message = chunk + ":" + std::to_string(parsed.error().line) + ": " + parsed.error().message, .chunk = chunk, .line = parsed.error().line}); } const Header header = *parsed; if (header.experimental() && (request.flags & PL_RUN_EXPERIMENTAL) == 0) { return std::unexpected(Failure{.status = PL_REFUSED, .message = chunk + ": this script is experimental, and experimental " "scripts are not turned on", .chunk = chunk, .line = 0}); }
Luau::CompileOptions options; options.optimizationLevel = 1; options.debugLevel = 1; const std::string bytecode = Luau::compile(std::string(source), options);
lua_State *state = state_.get(); const int base = lua_gettop(state); const ScopeExit restore([state, base] { lua_settop(state, base); }); lua_State *thread = lua_newthread(state); luaL_sandboxthread(thread); // "=" makes Luau print the name as given, without quoting it as a source string. const std::string chunk_name = "=" + chunk; if (luau_load(thread, chunk_name.c_str(), bytecode.data(), bytecode.size(), 0) != 0) { std::string message = error_message(thread); const std::uint32_t line = line_of(message, chunk); return std::unexpected(Failure{.status = PL_SCRIPT_ERROR, .message = std::move(message), .chunk = chunk, .line = line}); } if (request.arg_count > static_cast<std::size_t>(LUAI_MAXCSTACK) || lua_checkstack(thread, static_cast<int>(request.arg_count)) == 0) { return std::unexpected(failure(PL_INVALID, "too many arguments")); } for (std::size_t i = 0; i < request.arg_count; ++i) { // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic): a C array. const std::string_view argument = view(request.args[i]); lua_pushlstring(thread, argument.empty() ? "" : argument.data(), argument.size()); } RunContext run; run.host = &host_; run.ticks = request.cpu_budget; run.outside_budget = request.memory_budget; run.header = header; run.user = request.tier == PL_TIER_USER; run.generation = ++generation_; active_ = &run; allocator_.limit_growth(static_cast<std::size_t>(request.memory_budget)); const ScopeExit reset([this, state, &run] { active_ = nullptr; allocator_.unlimit(); if (run.pack_ref != LUA_NOREF) { lua_unref(state, run.pack_ref); } if (run.module_cache_ref != LUA_NOREF) { lua_unref(state, run.module_cache_ref); } }); const int status = lua_resume(thread, nullptr, static_cast<int>(request.arg_count)); return finish(thread, status, run, chunk);}
std::expected<RunOutput, Failure> Runtime::finish(lua_State *thread, int status, const RunContext &run, const std::string &chunk) { if (run.stop == Stop::refused) { return std::unexpected( Failure{.status = PL_REFUSED, .message = run.refusal, .chunk = chunk, .line = 0}); } if (run.stop == Stop::failed) { return std::unexpected(Failure{.status = PL_FAILED, .message = run.refusal, .chunk = chunk, .line = 0, .kind = run.fail_kind}); } if (run.stop == Stop::canceled) { return std::unexpected(failure(PL_CANCELED, "the run was canceled")); } if (run.stop == Stop::budget || allocator_.exceeded() || status == LUA_ERRMEM) { return std::unexpected(failure(PL_BUDGET, allocator_.exceeded() || status == LUA_ERRMEM ? "the script ran out of its memory budget" : "the script ran out of its CPU budget")); } if (status == LUA_OK) { // The script's first result is its plan; no result, or nil, means it only reports. std::string plan; if (lua_gettop(thread) > 0 && !lua_isnil(thread, 1)) { auto document = bindings::plan_document(thread, 1); if (!document) { return std::unexpected(Failure{.status = PL_SCRIPT_ERROR, .message = chunk + ": " + document.error(), .chunk = chunk, .line = 0}); } plan = std::move(*document); } return RunOutput{.plan = std::move(plan), .report = run.report.to_json()}; } if (status == LUA_YIELD || status == LUA_BREAK) { return std::unexpected(Failure{.status = PL_SCRIPT_ERROR, .message = "a script cannot yield at its top level", .chunk = chunk, .line = 0}); } std::string message = error_message(thread); const std::uint32_t line = line_of(message, chunk); return std::unexpected(Failure{ .status = PL_SCRIPT_ERROR, .message = std::move(message), .chunk = chunk, .line = line});}
} // namespace packluau