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.
This commit is contained in:
sanjinpepic
2026-08-16 20:03:52 +02:00
parent 881670db91
commit 2b5229e73f
2 changed files with 15 additions and 2 deletions
+8 -2
View File
@@ -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,
+7
View File
@@ -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")