AegisLua/AegisVM

GitHub: AegisLua/AegisVM

AegisVM 是一个用 Luau 编写的全沙盒化解释器,用于在 Roblox 中安全执行动态 Luau 代码。

Stars: 2 | Forks: 1

 

# AegisVM AegisVM 是一个完整的、沙盒化的 Luau 解释器,完全用 Luau 编写。它能在运行时对 Luau 源代码进行词法分析、语法分析和求值 —— 无需 `loadstring`,无需 `getfenv`,无需字节码注入,无需外部执行器。所有部分都是手动实现的:一个手写的词法分析器、一个带有 Pratt 表达式解析的递归下降解析器,以及一个遍历抽象语法树的运行时。 它作为一组 ModuleScript 在 Roblox Studio 内运行,可用于安全地在任何游戏内执行不受信任或动态生成的 Luau 代码。 ## 架构 ``` Aegis.luau (public API - entry point) ├── Lexer.luau tokeniser, produces a flat token list ├── Parser.luau recursive-descent + Pratt parser, produces an AST ├── Runtime.luau AST evaluator / interpreter ├── Scope.luau lexical scope chain with upvalue semantics ├── StdLib.luau sandboxed standard library ├── Error.luau error types and control-flow signal sentinels └── LuaC.luau stack-based LuaC instruction interpreter ``` 源代码文本流经三个阶段: 1. **词法分析器** - 逐字节扫描原始源代码并输出词法单元 `{ type, value, line, col }`。处理长字符串、转义序列、所有 Luau 运算符以及复合赋值。 2. **语法分析器** - 从词法单元流构建抽象语法树。类型注解会被解析但随即丢弃;它们对运行时没有影响。 3. **运行时** - 遍历抽象语法树。`eval()` 处理表达式;`execStat()` 处理语句。控制流(`break`、`continue`、`return`、`goto`)通过 Lua 自身的 `error()`/`pcall()` 机制使用哨兵信号表进行传播。 ## 安装 ### 通过 Roblox 资产 ID ``` local Aegis = require(115970020351857) ``` 发布的资产包含一个 `MainModule` 包装器,直接返回 Aegis API。 ### 通过 Rojo 克隆仓库并使用 [Rojo](https://rojo.space) 同步到 Studio: ``` git clone https://github.com/AegisLua/AegisVM.git cd AegisVM rojo serve ``` `default.project.json` 将 `Aegis` ModuleScript 放置在 `ServerScriptService` 中。 ### 手动安装 将 `src/server/Aegis.luau` 和 `src/server/Aegis/` 文件夹复制到 Studio 中,作为名为 `Aegis` 的 ModuleScript,并将其子项放入其中。 ## 快速上手 ``` local Aegis = require(game:GetService("ServerScriptService").Aegis) -- Run code in a fresh, isolated sandbox local ok, err = Aegis.run([[ local function factorial(n) if n <= 1 then return 1 end return n * factorial(n - 1) end print(factorial(10)) -- 3628800 ]]) if not ok then warn("Error: " .. err) end ``` ## API 参考 ### `Aegis.run(源代码,可选的源名称?,可选的选项?)` 在一个全新的沙盒中编译并运行 `source`。成功时返回 `true, ...returnValues`,失败时返回 `false, errorMessage`。 ``` local ok, a, b = Aegis.run([[ return 1 + 1, "hello" ]]) -- ok = true, a = 2, b = "hello" ``` ### `Aegis.newSandbox(可选的选项?)` 创建一个可重用的沙盒对象。后续的 `runIn` 调用共享相同的全局环境。 ``` local sandbox = Aegis.newSandbox() Aegis.runIn(sandbox, "x = 100") Aegis.runIn(sandbox, "print(x)") -- 100 ``` ### `Aegis.runIn(沙盒,源代码,可选的源名称?)` 在现有沙盒中运行源代码。变量在调用之间持续存在。 ``` local sandbox = Aegis.newSandbox() Aegis.runIn(sandbox, [[ local total = 0 for i = 1, 10 do total += i end result = total ]]) Aegis.runIn(sandbox, "print(result)") -- 55 ``` ### `Aegis.compile(源代码,可选的源名称?)` 仅进行词法分析和语法分析;成功时返回 `(ast, nil)`,失败时返回 `(nil, errorMessage)`。对于语法检查而不执行非常有用。 ``` local ast, err = Aegis.compile("local x = ??") if not ast then warn(err) -- parse error with line/column end ``` ### `Aegis.execAST(沙盒,ast)` 在沙盒中执行预编译的抽象语法树。当你想一次编译多次运行时,可与 `Aegis.compile` 结合使用。 ``` local ast = assert(Aegis.compile("return math.pi * 2")) local sandbox1 = Aegis.newSandbox() local sandbox2 = Aegis.newSandbox() Aegis.execAST(sandbox1, ast) Aegis.execAST(sandbox2, ast) ``` ### `Aegis.LuaC.run(源代码,可选的源名称?,可选的选项?)` 在一个全新的沙盒中执行 LuaC 栈式虚拟机脚本。成功时返回 `true`,失败时返回 `false, errorMessage`。 ``` local ok, err = Aegis.LuaC.run([[ getglobal print pushstring Hello from LuaC pcall 1 0 0 emptystack ]]) ``` ### `Aegis.LuaC.runIn(沙盒,源代码,可选的源名称?)` 在现有沙盒中执行 LuaC 栈式虚拟机脚本。 ``` local sandbox = Aegis.newSandbox() Aegis.LuaC.runIn(sandbox, [[ getglobal game getfield -1 Players getfield -1 LocalPlayer getfield -1 Character getfield -1 Humanoid pushnumber 100 setfield -2 Health emptystack ]]) ``` LuaC 使用与 Lua C API 相同的栈索引约定:正索引从栈底开始计数;负索引从栈顶开始计数(`-1` = 栈顶)。每个 `getfield ` 从栈位置 `` 读取并推送结果,但不移除源值。`setfield ` 弹出栈顶值并将其赋给 `stack[idx][name]`,索引在弹出操作之前解析。`pcall <_>` 期望函数位于位置 `-(nargs+1)`,参数位于其上方;它移除函数和所有参数,然后精确推送 `` 个返回值。 附加指令:`getservice `(`game:GetService` 的简写),`loop ` / `loopend`(计数循环),`wait `(通过 `task.wait` 让出),`pushboolean <0|1>`,`pushnil`。 ### `sandbox.scope:defineGlobal(名称,值)` 将主机端的值注入沙盒全局作用域。 ``` local sandbox = Aegis.newSandbox() sandbox.scope:defineGlobal("MyAPI", { greet = function(name) return "Hello, " .. name end, }) Aegis.runIn(sandbox, [[ print(MyAPI.greet("world")) -- Hello, world ]]) ``` ## 沙盒选项 ``` local sandbox = Aegis.newSandbox({ maxCallDepth = 100, -- default 200; limits recursion depth noStdLib = false, -- set true to skip standard library population globals = { -- extra globals injected at construction time VERSION = "1.0.0", }, }) ``` ## 标准库 沙盒提供了一个安全的 Luau 标准库子集。所有环境操作函数均被排除。 | 库 | 包含内容 | |-------------|--------------------------------------------------------------------------| | 核心 | `print`, `warn`, `tostring`, `tonumber`, `type`, `typeof`, `error`, `assert`, `pcall`, `xpcall`, `select`, `ipairs`, `pairs`, `next`, `rawget`, `rawset`, `rawequal`, `rawlen`, `unpack`, `setmetatable`, `getmetatable`, `loadstring`, `getfenv`, `setfenv` | | `string` | 完整(`byte`, `char`, `find`, `format`, `gmatch`, `gsub`, `len`, `lower`, `match`, `rep`, `reverse`, `sub`, `upper`, `split`) | | `table` | 完整(`insert`, `remove`, `concat`, `sort`, `unpack`, `pack`, `move`, `find`, `create`, `clear`, `clone`) | | `math` | 完整,包括 Luau 扩展(`sign`, `clamp`, `round`) | | `bit32` | 完整 | | `utf8` | 完整 | | `coroutine` | 完整 | | `task` | `spawn`, `defer`, `delay`, `wait`, `cancel` | | `buffer` | 完整(Roblox buffer API) | | `os` | `time`, `clock`, `date`, `difftime` | | Roblox | `game`, `workspace`, `Enum`, `Instance`, `shared`, `newproxy`, 所有值类型构造器(`Vector3`, `CFrame`, `Color3`, `UDim2` 等) | **排除项:** `getfenv`, `setfenv`, `load`, `dofile`, `collectgarbage`, `debug.*`, `io.*`, 直接服务快捷方式(`Players`, `RunService` 等 - 请改用 `game:GetService()`)。 ## 语言特性 AegisVM 支持完整的 Luau 语法: - 所有表达式运算符,包括 `//`、`^`、`..`,以及通过 `bit32` 实现的位运算 - 复合赋值:`+=`, `-=`, `*=`, `/=`, `//=`, `%=`, `^=`, `..=`, `&=`, `|=`, `~=`, `<<=`, `>>=` - 多重赋值和多返回值 - 可变参数(`...`) - 具有正确词法上值语义的闭包 - 元表和所有元方法(`__index`, `__newindex`, `__add`, `__call`, `__tostring` 等) - 数值 `for` 循环、泛型 `for` 循环(`pairs`, `ipairs`, 自定义迭代器) - `while`、`repeat...until` 循环 - `if / elseif / else` 条件语句 - `break`、`continue`、`goto`、标签 - `do...end` 代码块 - 类型注解(解析后丢弃 - 对运行时无影响) - 方法语法(`obj:method()`) - 字符串方法调用(`("hello"):upper()`) - Roblox `Instance` 属性和方法访问 - `loadstring` - 通过 AegisVM 重新编译源代码并返回一个可调用函数 ## 示例 ### 闭包与上值 ``` Aegis.run([[ local function counter(start) local n = start return function() n += 1 return n end end local next = counter(0) print(next()) -- 1 print(next()) -- 2 print(next()) -- 3 ]]) ``` ### 元表 ``` Aegis.run([[ local Vector = {} Vector.__index = Vector function Vector.new(x, y) return setmetatable({ x = x, y = y }, Vector) end function Vector:__add(other) return Vector.new(self.x + other.x, self.y + other.y) end function Vector:__tostring() return string.format("(%g, %g)", self.x, self.y) end local a = Vector.new(1, 2) local b = Vector.new(3, 4) local c = a + b print(tostring(c)) -- (4, 6) ]]) ``` ### 注入 API 的共享沙盒 ``` local sandbox = Aegis.newSandbox() sandbox.scope:defineGlobal("DataStore", { save = function(key, value) print("Saving", key, "=", value) end, }) Aegis.runIn(sandbox, [[ DataStore.save("coins", 500) DataStore.save("level", 12) ]]) ``` ### 使用 `loadstring` ``` Aegis.run([[ local code = "return 2 ^ 10" local fn = loadstring(code) print(fn()) -- 1024 ]]) ``` ### 检查返回值 ``` local ok, sum, product = Aegis.run([[ local a, b = 6, 7 return a + b, a * b ]]) print(ok, sum, product) -- true 13 42 ``` ## 安全模型 AegisVM 从不调用: - `getfenv` / `setfenv` - 主机级别的 `loadstring` / `load` - Roblox 字节码或内部 API - 任何外部执行器或不安全的沙盒逃逸 访客代码完全在解释器的作用域链内运行。除非主机通过 `defineGlobal` 明确注入一个值,否则它无法到达主机环境。所有执行都是手动的抽象语法树遍历;没有即时编译路径,没有字节码编译,也没有本机代码执行。 ## 许可证 MIT ### AI 免责声明 本项目使用了人工智能(Claude Sonnet 4.6)来协助开发、设计决策和文档起草。 AI 辅助被用作生产力和工程辅助工具,用于以下任务: * 起草和完善文档 * 对解释器组件的结构改进建议 * 生成示例用法代码片段 * 协助调试逻辑和边界情况 * 帮助形式化架构说明 所有生成或建议的内容都经过人工审查、修改和整合。最终实现仍然是人工工程决策的结果,包括对词法分析器、语法分析器、运行时语义和沙盒模型的控制。 AI 没有被用作一个自主系统来生产未经检查的代码执行路径,或在没有审查的情况下做出安全关键决策。任何与沙盒化、环境隔离或执行安全相关的行为都在 Luau 和 Roblox Studio 约束的上下文中进行了明确验证。 本项目在运行时不依赖 AI。解释器本身是完全确定和独立的,在执行期间没有来自 AI 系统的外部模型调用、推理层或动态代码生成。 AI 工具仅在开发阶段严格使用,并非所交付的运行时、依赖项或执行管道的一部分。
标签:AegisVM, AST解释器, Luau语言, ModuleScripts, Roblox, Roblox脚本安全, 不可信代码处理, 动态代码执行, 安全执行, 沙箱技术, 游戏开发, 编程语言处理, 解释器, 词法分析, 语法解析, 运行时环境