From 2b5229e73f5145fd8b1f4de4fe938b61d6d6ad44 Mon Sep 17 00:00:00 2001 From: sanjinpepic Date: Sun, 16 Aug 2026 20:03:52 +0200 Subject: [PATCH] Deny require("jit.util") in the mod sandbox DENIED_PREFIX blocked love.* and ffi.* submodule requires but had no entry for jit, so require("jit.util") walked straight through to the real module. jit.util is LuaJIT's own equivalent of the debug library this file already denies by name: funcbc, funck and the rest read the bytecode and constants of any function a chunk can reach, which is enough to recover upvalues -- the real _G, love, io -- that the sandbox exists to keep out of a mod's hands. Adding "jit" to DENIED_PREFIX blocks jit.* submodule requires the same way love.* and ffi.* already are, while leaving the bare jit global (env.jit, handed over directly for jit.on/off/flush) and a bare require("jit") untouched -- jit.util is not a field of that table without its own require, so neither route was ever a way to reach it. --- src/mods/Sandbox.lua | 10 ++++++++-- tests/modkit/cases/sandbox.lua | 7 +++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/mods/Sandbox.lua b/src/mods/Sandbox.lua index e41190ba..f52a07ce 100644 --- a/src/mods/Sandbox.lua +++ b/src/mods/Sandbox.lua @@ -28,8 +28,14 @@ local DENIED = { } -- Same idea one level up: love.filesystem is reachable by name, and --- love.thread starts a Lua state this sandbox has no say over. -local DENIED_PREFIX = { ["love"] = true, ["ffi"] = true } +-- love.thread starts a Lua state this sandbox has no say over. jit.util +-- is the LuaJIT-specific equal of the debug library above -- funcbc, +-- funck and friends read the bytecode and constants of any function a +-- chunk can reach, which is enough to walk back to upvalues (the real +-- _G, love, io) the rest of this file exists to keep out of reach. The +-- bare `jit` table stays -- env.jit above hands it over directly for +-- jit.on/off/flush -- so only the submodule require is denied. +local DENIED_PREFIX = { ["love"] = true, ["ffi"] = true, ["jit"] = true } -- The wire, which is what the network permission governs. local NETWORK = { socket = true, enet = true, http = true, https = true, diff --git a/tests/modkit/cases/sandbox.lua b/tests/modkit/cases/sandbox.lua index 27133e15..5f9c06c8 100644 --- a/tests/modkit/cases/sandbox.lua +++ b/tests/modkit/cases/sandbox.lua @@ -44,6 +44,11 @@ local PROBE = [[ out.requireDebug = attempt(require, "debug") out.requirePackage = attempt(require, "package") out.requireFfi = attempt(require, "ffi") + -- jit.util exposes bytecode/constant introspection over any function this + -- chunk can reach -- the same class of escape the debug library is denied + -- for -- so it must fail the same way require("debug") does rather than + -- walking straight through under the bare "jit" global's cover. + out.requireJitUtil = attempt(require, "jit.util") out.requireSocket = attempt(require, "socket") out.requireSemver = select(2, pcall(require, "src.mods.Semver")) @@ -180,6 +185,8 @@ T.eq(out.getfenv, nil, "getfenv is still absent, so a mod cannot read the real _ T.eq(out.debug, nil, "the debug library is still absent") T.check(out.loveThread ~= false, "love.thread is still refused: it opens a full Lua state") T.check(out.requireFfi ~= false, "require(\"ffi\") is still refused: it is arbitrary C") +T.check(out.requireJitUtil ~= false, + "require(\"jit.util\") is still refused: it is bytecode/constant introspection") T.check(out.requireDebug ~= false, "require(\"debug\") is still refused") T.check(out.requirePackage ~= false, "require(\"package\") is still refused") T.eq(out.popen, nil, "io.popen refuses rather than spawning a process")