Template
feat(ext): embed sandboxed Lua runtime with C API
This commit is contained in:
+729
@@ -0,0 +1,729 @@
|
|||||||
|
/*
|
||||||
|
* src/ext/lua.c - embedded sandboxed Lua runtime (plan todo 19).
|
||||||
|
*
|
||||||
|
* Initializes the vendored Lua 5.4 as the stupidtools extension runtime
|
||||||
|
* with a SANDBOXED environment. See src/ext/lua.h for the exact
|
||||||
|
* sandbox contract (what is opened, blocked, removed) and the error
|
||||||
|
* contract of st_lua_run().
|
||||||
|
*
|
||||||
|
* LINKING / THE -lm TRAP (important for the build wiring):
|
||||||
|
* The unit-test harness compiles every test file under tests/unit/
|
||||||
|
* with NO extra libraries -- in particular no -lm. Stock Lua needs
|
||||||
|
* libm in TWO
|
||||||
|
* places: (1) lmathlib.c (sin/cos/pow/log/...), and (2) the CORE
|
||||||
|
* itself: lvm.c calls floor(), ltable.c frexp()+fabs(), lstrlib.c
|
||||||
|
* floor()+frexp(), lobject.c and lcode.c ldexp()+fabs(), and the
|
||||||
|
* arithmetic dispatcher luaO_arith() calls pow() and fmod(). On this
|
||||||
|
* host `floor` does NOT resolve without -lm (verified: gcc 16.2.1 /
|
||||||
|
* glibc 2.44 -- libm is a separate library).
|
||||||
|
*
|
||||||
|
* Two consequences:
|
||||||
|
* a) lmathlib.c and linit.c (which references luaopen_math) are NOT
|
||||||
|
* part of the link list; the curated `math` table below is
|
||||||
|
* written by hand with libm-free code, so extensions still get
|
||||||
|
* math.abs etc. (the plan's test requires math.abs(-3) to work).
|
||||||
|
* b) The core's remaining libm references are satisfied by the
|
||||||
|
* portable IEEE-754 binary64 shims at the top of this file
|
||||||
|
* (fabs/floor/frexp/ldexp/fmod/pow). They are correct for the
|
||||||
|
* common paths (the C23-annex-F binary64 assumption holds on
|
||||||
|
* gcc/clang x86-64/aarch64); pow's exotic sign edge cases are
|
||||||
|
* approximate. The main binary (todo 21+) may link the real
|
||||||
|
* libm later; these shims stay harmless because Lua's undefined
|
||||||
|
* references then simply resolve against libm instead... in
|
||||||
|
* fact they will NOT: our definitions take precedence. When the
|
||||||
|
* real libm becomes available, delete this block and add -lm.
|
||||||
|
*
|
||||||
|
* Include paths: the vendored headers are pulled in via path relative
|
||||||
|
* to THIS file (../../thirdparty/lua/...), because the unit-test
|
||||||
|
* harness only adds -I thirdparty/munit and -I src -- it never adds a
|
||||||
|
* Lua include dir. The file-relative form works under any invocation
|
||||||
|
* that compiles this file at its real location.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _POSIX_C_SOURCE
|
||||||
|
#define _POSIX_C_SOURCE 200809L /* strdup (POSIX.1-2008) */
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "../../thirdparty/lua/lauxlib.h"
|
||||||
|
#include "../../thirdparty/lua/lua.h"
|
||||||
|
#include "../../thirdparty/lua/lualib.h"
|
||||||
|
|
||||||
|
#include "lua.h"
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* libm-free math shims */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/*
|
||||||
|
* All six are exactly the symbols the vendored Lua core references from
|
||||||
|
* libm (see the -lm note above). fabs uses the compiler builtin (a
|
||||||
|
* single ANDPS, no call); floor/frexp/ldexp/fmod/pow are hand-written.
|
||||||
|
* Bit-level code assumes IEEE 754 binary64 doubles (8-byte), which C23
|
||||||
|
* annex F plus gcc/clang guarantee on the supported targets.
|
||||||
|
*/
|
||||||
|
|
||||||
|
double fabs(double x);
|
||||||
|
double floor(double x);
|
||||||
|
double frexp(double x, int *e);
|
||||||
|
double ldexp(double x, int n);
|
||||||
|
double fmod(double x, double y);
|
||||||
|
double pow(double x, double y);
|
||||||
|
|
||||||
|
double
|
||||||
|
fabs(double x)
|
||||||
|
{
|
||||||
|
return __builtin_fabs(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Truncation toward zero, safe for the full double range (casting an
|
||||||
|
* out-of-range double to long long is UB, hence the guard). */
|
||||||
|
static double
|
||||||
|
l_shim_trunc(double x)
|
||||||
|
{
|
||||||
|
static const double maxint = 9223372036854775808.0; /* 2^63 */
|
||||||
|
|
||||||
|
if (x > -maxint && x < maxint) {
|
||||||
|
return (double)(long long)x;
|
||||||
|
}
|
||||||
|
return x; /* NaN, +/-inf, or a magnitude already integral */
|
||||||
|
}
|
||||||
|
|
||||||
|
double
|
||||||
|
floor(double x)
|
||||||
|
{
|
||||||
|
static const double minint = -9223372036854775808.0;
|
||||||
|
static const double maxint = 9223372036854775808.0;
|
||||||
|
|
||||||
|
if (x == 0.0) return x; /* preserves the sign of zero */
|
||||||
|
if (x > minint && x < maxint) {
|
||||||
|
double t = (double)(long long)x;
|
||||||
|
return x < t ? t - 1.0 : t;
|
||||||
|
}
|
||||||
|
return x; /* NaN, +/-inf, or out of range (already integral) */
|
||||||
|
}
|
||||||
|
|
||||||
|
double
|
||||||
|
frexp(double x, int *e)
|
||||||
|
{
|
||||||
|
union {
|
||||||
|
double d;
|
||||||
|
unsigned long long u;
|
||||||
|
} v;
|
||||||
|
unsigned long long expfield;
|
||||||
|
|
||||||
|
v.d = x;
|
||||||
|
expfield = (v.u >> 52) & 0x7FFULL;
|
||||||
|
if (expfield == 0x7FFULL) { /* inf or NaN: value returned unchanged */
|
||||||
|
*e = 0;
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
if (expfield == 0) { /* zero or subnormal */
|
||||||
|
if ((v.u & 0x000FFFFFFFFFFFFFULL) == 0ULL) { /* zero */
|
||||||
|
*e = 0;
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
/* Normalize a subnormal by scaling it up 2^64 (the largest
|
||||||
|
* subnormal is ~2^-1022, so the result is normal), then fix
|
||||||
|
* the exponent. */
|
||||||
|
x = frexp(x * 0x1p64, e);
|
||||||
|
*e -= 64;
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
/* Normal case: keep the fraction, force the exponent field to 1022
|
||||||
|
* so the result lands in [0.5, 1), and report the difference. */
|
||||||
|
*e = (int)expfield - 1022;
|
||||||
|
v.u = (v.u & 0x800FFFFFFFFFFFFFULL) | 0x3FE0000000000000ULL;
|
||||||
|
return v.d;
|
||||||
|
}
|
||||||
|
|
||||||
|
double
|
||||||
|
ldexp(double x, int n)
|
||||||
|
{
|
||||||
|
union {
|
||||||
|
double d;
|
||||||
|
unsigned long long u;
|
||||||
|
} v;
|
||||||
|
|
||||||
|
if (n > 1023) {
|
||||||
|
x *= 0x1p1023;
|
||||||
|
n -= 1023;
|
||||||
|
if (n > 1023) {
|
||||||
|
x *= 0x1p1023;
|
||||||
|
n -= 1023;
|
||||||
|
if (n > 1023) n = 1023;
|
||||||
|
}
|
||||||
|
} else if (n < -1022) {
|
||||||
|
/* pre-scale so the final multiply stays clear of the subnormal
|
||||||
|
* range and rounds once (avoids double rounding) */
|
||||||
|
x *= 0x1p-1022 * 0x1p53;
|
||||||
|
n += 1022 - 53;
|
||||||
|
if (n < -1022) {
|
||||||
|
x *= 0x1p-1022 * 0x1p53;
|
||||||
|
n += 1022 - 53;
|
||||||
|
if (n < -1022) n = -1022;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v.u = (unsigned long long)(0x3FF + n) << 52;
|
||||||
|
return x * v.d;
|
||||||
|
}
|
||||||
|
|
||||||
|
double
|
||||||
|
fmod(double x, double y)
|
||||||
|
{
|
||||||
|
double r;
|
||||||
|
double ax, ay;
|
||||||
|
int ex, ey;
|
||||||
|
int neg;
|
||||||
|
|
||||||
|
if (y == 0.0 || x != x || y != y) return __builtin_nan("");
|
||||||
|
if (x == 0.0) return x; /* keeps the sign */
|
||||||
|
if (fabs(x) == __builtin_inf()) return __builtin_nan("");
|
||||||
|
if (fabs(y) == __builtin_inf()) return x; /* fmod(x, inf) = x */
|
||||||
|
|
||||||
|
neg = x < 0.0;
|
||||||
|
ax = fabs(x);
|
||||||
|
ay = fabs(y);
|
||||||
|
frexp(ax, &ex);
|
||||||
|
frexp(ay, &ey);
|
||||||
|
/* Binary long division: subtract the largest multiple of ay that
|
||||||
|
* fits, then halve. Bounded by the exponent range (~2100 steps). */
|
||||||
|
r = ax;
|
||||||
|
while (ex >= ey && r != 0.0) {
|
||||||
|
double s = ldexp(ay, ex - ey);
|
||||||
|
if (r >= s) r -= s;
|
||||||
|
ex--;
|
||||||
|
}
|
||||||
|
return neg ? -r : r;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* e^x via the reduction x = k*ln(2) + r, |r| <= ln(2)/2, then a Taylor
|
||||||
|
* series (20 terms: the truncation error is ~4e-30, far below double
|
||||||
|
* precision). */
|
||||||
|
static double
|
||||||
|
l_shim_exp(double x)
|
||||||
|
{
|
||||||
|
static const double ln2 = 0.69314718055994530942;
|
||||||
|
static const double invln2 = 1.44269504088896340736;
|
||||||
|
long long k;
|
||||||
|
double r, t, y;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
if (x > 709.7827128933839731) return __builtin_inf(); /* overflow */
|
||||||
|
if (x < -745.1332191019411085) return 0.0; /* underflow */
|
||||||
|
if (x != x) return x; /* NaN */
|
||||||
|
|
||||||
|
k = (long long)(x * invln2 + (x >= 0.0 ? 0.5 : -0.5));
|
||||||
|
r = x - (double)k * ln2;
|
||||||
|
y = 1.0;
|
||||||
|
t = 1.0;
|
||||||
|
for (i = 1; i <= 20; i++) {
|
||||||
|
t = t * (r / (double)i);
|
||||||
|
y += t;
|
||||||
|
}
|
||||||
|
return ldexp(y, (int)k);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ln(x) via x = m * 2^e (m in [0.5, 1)), then the atanh series around
|
||||||
|
* m = 2/3: log(m) = log(2/3) + 2*atanh(t), t = (m - 2/3)/(m + 2/3),
|
||||||
|
* |t| <= 0.2. 12 terms leave ~1e-18, i.e. a few ulps. */
|
||||||
|
static double
|
||||||
|
l_shim_log(double x)
|
||||||
|
{
|
||||||
|
static const double ln2 = 0.69314718055994530942;
|
||||||
|
static const double log32 = -0.40546510810816438098; /* ln(2/3) */
|
||||||
|
double m, t, p, term, sum;
|
||||||
|
int e;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
if (x < 0.0 || x != x) return __builtin_nan("");
|
||||||
|
if (x == 0.0) return -__builtin_inf();
|
||||||
|
if (x == __builtin_inf()) return x;
|
||||||
|
|
||||||
|
m = frexp(x, &e);
|
||||||
|
t = (m - 0.66666666666666663) / (m + 0.66666666666666663);
|
||||||
|
p = t * t;
|
||||||
|
term = t;
|
||||||
|
sum = t;
|
||||||
|
for (i = 1; i <= 12; i++) {
|
||||||
|
term *= p;
|
||||||
|
sum += term / (double)(2 * i + 1);
|
||||||
|
}
|
||||||
|
return (double)e * ln2 + log32 + 2.0 * sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
double
|
||||||
|
pow(double x, double y)
|
||||||
|
{
|
||||||
|
if (y == 0.0) return 1.0; /* even pow(NaN, 0) == 1 */
|
||||||
|
if (x == 1.0) return 1.0;
|
||||||
|
if ((x == -1.0) &&
|
||||||
|
(y == __builtin_inf() || y == -__builtin_inf())) return 1.0;
|
||||||
|
if (x != x || y != y) return __builtin_nan("");
|
||||||
|
|
||||||
|
/* Integral exponent within the exactly-representable range: exact
|
||||||
|
* repeated squaring (this also covers negative bases). */
|
||||||
|
if (y == l_shim_trunc(y) && y > -9007199254740992.0 &&
|
||||||
|
y < 9007199254740992.0) {
|
||||||
|
long long n = (long long)y;
|
||||||
|
unsigned long long m = n < 0 ? (unsigned long long)(-n)
|
||||||
|
: (unsigned long long)n;
|
||||||
|
double r = 1.0;
|
||||||
|
double b = x;
|
||||||
|
while (m != 0ULL) {
|
||||||
|
if ((m & 1ULL) != 0ULL) r *= b;
|
||||||
|
b *= b;
|
||||||
|
m >>= 1;
|
||||||
|
}
|
||||||
|
return n < 0 ? 1.0 / r : r;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x == __builtin_inf()) return y > 0.0 ? x : 0.0;
|
||||||
|
if (x == -__builtin_inf()) return y > 0.0 ? __builtin_inf() : 0.0;
|
||||||
|
if (x == 0.0) return y > 0.0 ? 0.0 : __builtin_inf();
|
||||||
|
if (y == __builtin_inf()) return x > 1.0 ? __builtin_inf() : 0.0;
|
||||||
|
if (y == -__builtin_inf()) return x < 1.0 ? __builtin_inf() : 0.0;
|
||||||
|
if (x < 0.0) return __builtin_nan(""); /* negative base, non-int y */
|
||||||
|
return l_shim_exp(y * l_shim_log(x));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* runtime state */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
struct st_lua_reg {
|
||||||
|
struct st_lua_reg *next;
|
||||||
|
char *name; /* owned copy */
|
||||||
|
void *probe_spec; /* checks only; always NULL for languages */
|
||||||
|
};
|
||||||
|
|
||||||
|
struct st_lua_rt {
|
||||||
|
lua_State *L;
|
||||||
|
struct st_lua_reg *checks; /* insertion-ordered linked lists */
|
||||||
|
struct st_lua_reg *languages;
|
||||||
|
size_t check_count;
|
||||||
|
size_t language_count;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Appends (name, spec) at the TAIL of *head (insertion order, matching
|
||||||
|
* the ABI registries' contract) unless it is a duplicate. Returns 0 on
|
||||||
|
* success, -1 on duplicate or allocation failure. */
|
||||||
|
static int
|
||||||
|
reg_append(struct st_lua_reg **head, size_t *count, const char *name,
|
||||||
|
void *spec)
|
||||||
|
{
|
||||||
|
struct st_lua_reg *n, *p;
|
||||||
|
|
||||||
|
for (p = *head; p != NULL; p = p->next) {
|
||||||
|
if (strcmp(p->name, name) == 0) return -1; /* duplicate */
|
||||||
|
}
|
||||||
|
n = malloc(sizeof *n);
|
||||||
|
if (n == NULL) return -1;
|
||||||
|
n->name = strdup(name);
|
||||||
|
if (n->name == NULL) {
|
||||||
|
free(n);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
n->probe_spec = spec;
|
||||||
|
n->next = NULL;
|
||||||
|
if (*head == NULL) {
|
||||||
|
*head = n;
|
||||||
|
} else {
|
||||||
|
for (p = *head; p->next != NULL; p = p->next) {
|
||||||
|
}
|
||||||
|
p->next = n;
|
||||||
|
}
|
||||||
|
*count += 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
regs_free(struct st_lua_reg *head)
|
||||||
|
{
|
||||||
|
struct st_lua_reg *p = head;
|
||||||
|
while (p != NULL) {
|
||||||
|
struct st_lua_reg *next = p->next;
|
||||||
|
free(p->name);
|
||||||
|
free(p);
|
||||||
|
p = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const struct st_lua_reg *
|
||||||
|
reg_at(const struct st_lua_reg *head, size_t i)
|
||||||
|
{
|
||||||
|
const struct st_lua_reg *p = head;
|
||||||
|
while (p != NULL && i > 0) {
|
||||||
|
p = p->next;
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* sandboxed globals */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
/* The one string sandbox violations contain; st_lua_run() classifies
|
||||||
|
* errors by finding it in the message. Keep in sync with l_os_blocked(). */
|
||||||
|
#define SANDBOX_PREFIX "sandbox blocked: "
|
||||||
|
|
||||||
|
/* os.execute / os.exit / os.remove / os.rename all land here: a stub
|
||||||
|
* that raises a clear sandbox error no matter how it is called (direct,
|
||||||
|
* pcall'd, aliased, ...). */
|
||||||
|
static int
|
||||||
|
l_os_blocked(lua_State *L)
|
||||||
|
{
|
||||||
|
const char *name = lua_tostring(L, lua_upvalueindex(1));
|
||||||
|
return luaL_error(L, SANDBOX_PREFIX "os.%s is not available to "
|
||||||
|
"extension code",
|
||||||
|
name != NULL ? name : "?");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The ONLY os table extensions see: the four stubs above, nothing else
|
||||||
|
* (no clock/date/getenv -- extension code needs none of them in v1 and
|
||||||
|
* a smaller surface is a better sandbox). The stock loslib is not even
|
||||||
|
* linked. */
|
||||||
|
static void
|
||||||
|
l_os_open_sandboxed(lua_State *L)
|
||||||
|
{
|
||||||
|
static const char *const blocked[] = { "execute", "exit", "remove",
|
||||||
|
"rename", NULL };
|
||||||
|
int i;
|
||||||
|
|
||||||
|
lua_newtable(L);
|
||||||
|
for (i = 0; blocked[i] != NULL; i++) {
|
||||||
|
lua_pushstring(L, blocked[i]); /* upvalue: the function's name */
|
||||||
|
lua_pushcclosure(L, l_os_blocked, 1);
|
||||||
|
lua_setfield(L, -2, blocked[i]);
|
||||||
|
}
|
||||||
|
lua_setglobal(L, "os");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Curated, libm-FREE math library (stock lmathlib would pull sin/cos/
|
||||||
|
* pow/log/... from libm; see the header note). abs/max/min/tointeger/
|
||||||
|
* type/maxinteger/mininteger/pi/huge cover what extension code needs
|
||||||
|
* in v1; floor/ceil/sqrt etc. arrive when the main binary links real
|
||||||
|
* libm (todo 21+). */
|
||||||
|
static int
|
||||||
|
l_math_abs(lua_State *L)
|
||||||
|
{
|
||||||
|
if (lua_isinteger(L, 1)) {
|
||||||
|
lua_Integer n = lua_tointeger(L, 1);
|
||||||
|
lua_pushinteger(L, n < 0 ? (lua_Integer)(0u - (lua_Unsigned)n) : n);
|
||||||
|
} else {
|
||||||
|
lua_pushnumber(L, fabs(luaL_checknumber(L, 1)));
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
l_math_max(lua_State *L)
|
||||||
|
{
|
||||||
|
int n = lua_gettop(L);
|
||||||
|
int i;
|
||||||
|
lua_Number m;
|
||||||
|
|
||||||
|
if (n == 0) return luaL_error(L, "math.max: expected at least one argument");
|
||||||
|
m = luaL_checknumber(L, 1);
|
||||||
|
for (i = 2; i <= n; i++) {
|
||||||
|
lua_Number v = luaL_checknumber(L, i);
|
||||||
|
if (v > m) m = v;
|
||||||
|
}
|
||||||
|
lua_pushnumber(L, m);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
l_math_min(lua_State *L)
|
||||||
|
{
|
||||||
|
int n = lua_gettop(L);
|
||||||
|
int i;
|
||||||
|
lua_Number m;
|
||||||
|
|
||||||
|
if (n == 0) return luaL_error(L, "math.min: expected at least one argument");
|
||||||
|
m = luaL_checknumber(L, 1);
|
||||||
|
for (i = 2; i <= n; i++) {
|
||||||
|
lua_Number v = luaL_checknumber(L, i);
|
||||||
|
if (v < m) m = v;
|
||||||
|
}
|
||||||
|
lua_pushnumber(L, m);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mirrors stock math.tointeger: converts a value that has an exact
|
||||||
|
* integer representation (this exercises the floor() shim through
|
||||||
|
* luaV_flttointeger). */
|
||||||
|
static int
|
||||||
|
l_math_tointeger(lua_State *L)
|
||||||
|
{
|
||||||
|
int valid;
|
||||||
|
lua_Integer n = lua_tointegerx(L, 1, &valid);
|
||||||
|
|
||||||
|
if (valid) {
|
||||||
|
lua_pushinteger(L, n);
|
||||||
|
} else {
|
||||||
|
luaL_checkany(L, 1);
|
||||||
|
lua_pushnil(L); /* not convertible */
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
l_math_type(lua_State *L)
|
||||||
|
{
|
||||||
|
if (lua_type(L, 1) == LUA_TNUMBER) {
|
||||||
|
if (lua_isinteger(L, 1)) {
|
||||||
|
lua_pushliteral(L, "integer");
|
||||||
|
} else {
|
||||||
|
lua_pushliteral(L, "float");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
luaL_checkany(L, 1);
|
||||||
|
lua_pushnil(L);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
l_math_open(lua_State *L)
|
||||||
|
{
|
||||||
|
static const luaL_Reg funcs[] = {
|
||||||
|
{ "abs", l_math_abs },
|
||||||
|
{ "max", l_math_max },
|
||||||
|
{ "min", l_math_min },
|
||||||
|
{ "tointeger", l_math_tointeger },
|
||||||
|
{ "type", l_math_type },
|
||||||
|
{ NULL, NULL },
|
||||||
|
};
|
||||||
|
|
||||||
|
luaL_newlib(L, funcs);
|
||||||
|
lua_pushinteger(L, LUA_MAXINTEGER);
|
||||||
|
lua_setfield(L, -2, "maxinteger");
|
||||||
|
lua_pushinteger(L, LUA_MININTEGER);
|
||||||
|
lua_setfield(L, -2, "mininteger");
|
||||||
|
lua_pushnumber(L, 3.14159265358979323846);
|
||||||
|
lua_setfield(L, -2, "pi");
|
||||||
|
lua_pushnumber(L, __builtin_inf());
|
||||||
|
lua_setfield(L, -2, "huge");
|
||||||
|
lua_setglobal(L, "math");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* registration API (Lua side) */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
/* The `st` table: st.register_check(name) / st.register_language(name).
|
||||||
|
* Both write into the runtime's own registries through the same C API
|
||||||
|
* todo 21 will enumerate when bridging into a struct st_ext_ctx. The
|
||||||
|
* rt travels as a light-userdata upvalue -- there is no global state. */
|
||||||
|
|
||||||
|
static int
|
||||||
|
l_st_register_check(lua_State *L)
|
||||||
|
{
|
||||||
|
struct st_lua_rt *rt = lua_touserdata(L, lua_upvalueindex(1));
|
||||||
|
size_t len;
|
||||||
|
const char *name;
|
||||||
|
|
||||||
|
if (lua_type(L, 1) != LUA_TSTRING) {
|
||||||
|
return luaL_typeerror(L, 1, "string");
|
||||||
|
}
|
||||||
|
name = lua_tostring(L, 1);
|
||||||
|
len = lua_rawlen(L, 1);
|
||||||
|
if (name == NULL || strlen(name) != len) {
|
||||||
|
return luaL_error(L, "check name must not contain a NUL byte");
|
||||||
|
}
|
||||||
|
if (st_lua_register_check(rt, name, NULL) != 0) {
|
||||||
|
return luaL_error(L, "check '%s' is already registered", name);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
l_st_register_language(lua_State *L)
|
||||||
|
{
|
||||||
|
struct st_lua_rt *rt = lua_touserdata(L, lua_upvalueindex(1));
|
||||||
|
size_t len;
|
||||||
|
const char *name;
|
||||||
|
|
||||||
|
if (lua_type(L, 1) != LUA_TSTRING) {
|
||||||
|
return luaL_typeerror(L, 1, "string");
|
||||||
|
}
|
||||||
|
name = lua_tostring(L, 1);
|
||||||
|
len = lua_rawlen(L, 1);
|
||||||
|
if (name == NULL || strlen(name) != len) {
|
||||||
|
return luaL_error(L, "language name must not contain a NUL byte");
|
||||||
|
}
|
||||||
|
if (st_lua_register_language(rt, name) != 0) {
|
||||||
|
return luaL_error(L, "language '%s' is already registered", name);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
l_st_open(lua_State *L, struct st_lua_rt *rt)
|
||||||
|
{
|
||||||
|
lua_newtable(L);
|
||||||
|
lua_pushlightuserdata(L, rt);
|
||||||
|
lua_pushcclosure(L, l_st_register_check, 1);
|
||||||
|
lua_setfield(L, -2, "register_check");
|
||||||
|
lua_pushlightuserdata(L, rt);
|
||||||
|
lua_pushcclosure(L, l_st_register_language, 1);
|
||||||
|
lua_setfield(L, -2, "register_language");
|
||||||
|
lua_setglobal(L, "st");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* runtime lifecycle + sandbox assembly */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
struct st_lua_rt *
|
||||||
|
st_lua_rt_new(void)
|
||||||
|
{
|
||||||
|
struct st_lua_rt *rt = calloc(1, sizeof *rt);
|
||||||
|
lua_State *L;
|
||||||
|
|
||||||
|
if (rt == NULL) return NULL;
|
||||||
|
L = luaL_newstate();
|
||||||
|
if (L == NULL) {
|
||||||
|
free(rt);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
rt->L = L;
|
||||||
|
|
||||||
|
/* Selective library set -- deliberately NOT luaL_openlibs():
|
||||||
|
* base / table / string are the only stock libraries loaded;
|
||||||
|
* math and os are the hand-built sandboxed ones below;
|
||||||
|
* package/io/debug/coroutine are never loaded (and their .c
|
||||||
|
* files are not even linked -- see the -lm note at the top);
|
||||||
|
* lmathlib/linit are excluded from the link for the same reason. */
|
||||||
|
luaL_requiref(L, "base", luaopen_base, 1);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
luaL_requiref(L, "table", luaopen_table, 1);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
luaL_requiref(L, "string", luaopen_string, 1);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
|
||||||
|
/* Remove the base-library globals that reach files/modules. (`load`
|
||||||
|
* is kept: with io/os/package gone it can only compile strings, and
|
||||||
|
* that is how extension loading works in todo 20/21.) */
|
||||||
|
lua_pushnil(L);
|
||||||
|
lua_setglobal(L, "dofile");
|
||||||
|
lua_pushnil(L);
|
||||||
|
lua_setglobal(L, "loadfile");
|
||||||
|
lua_pushnil(L);
|
||||||
|
lua_setglobal(L, "require"); /* defensive: 5.4 defines it in loadlib */
|
||||||
|
|
||||||
|
l_os_open_sandboxed(L);
|
||||||
|
l_math_open(L);
|
||||||
|
l_st_open(L, rt);
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
st_lua_rt_free(struct st_lua_rt *rt)
|
||||||
|
{
|
||||||
|
if (rt == NULL) return;
|
||||||
|
if (rt->L != NULL) lua_close(rt->L);
|
||||||
|
regs_free(rt->checks);
|
||||||
|
regs_free(rt->languages);
|
||||||
|
free(rt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* registration API (C side) + todo-21 enumeration bridge */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
int
|
||||||
|
st_lua_register_check(struct st_lua_rt *rt, const char *name,
|
||||||
|
void *probe_spec_or_null)
|
||||||
|
{
|
||||||
|
if (rt == NULL || name == NULL || name[0] == '\0') return -1;
|
||||||
|
return reg_append(&rt->checks, &rt->check_count, name, probe_spec_or_null);
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
st_lua_register_language(struct st_lua_rt *rt, const char *name)
|
||||||
|
{
|
||||||
|
if (rt == NULL || name == NULL || name[0] == '\0') return -1;
|
||||||
|
return reg_append(&rt->languages, &rt->language_count, name, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t
|
||||||
|
st_lua_check_count(const struct st_lua_rt *rt)
|
||||||
|
{
|
||||||
|
return rt != NULL ? rt->check_count : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *
|
||||||
|
st_lua_check_name(const struct st_lua_rt *rt, size_t i)
|
||||||
|
{
|
||||||
|
const struct st_lua_reg *r = rt != NULL ? reg_at(rt->checks, i) : NULL;
|
||||||
|
return r != NULL ? r->name : NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void *
|
||||||
|
st_lua_check_probe_spec(const struct st_lua_rt *rt, size_t i)
|
||||||
|
{
|
||||||
|
const struct st_lua_reg *r = rt != NULL ? reg_at(rt->checks, i) : NULL;
|
||||||
|
return r != NULL ? r->probe_spec : NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t
|
||||||
|
st_lua_language_count(const struct st_lua_rt *rt)
|
||||||
|
{
|
||||||
|
return rt != NULL ? rt->language_count : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *
|
||||||
|
st_lua_language_name(const struct st_lua_rt *rt, size_t i)
|
||||||
|
{
|
||||||
|
const struct st_lua_reg *r = rt != NULL ? reg_at(rt->languages, i) : NULL;
|
||||||
|
return r != NULL ? r->name : NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* execution */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
/* Takes the error object Lua left on the top of the stack, classifies
|
||||||
|
* it (sandbox violation vs. everything else), builds the st_error and
|
||||||
|
* pops the object. */
|
||||||
|
static struct st_error *
|
||||||
|
lua_error_to_st_error(lua_State *L)
|
||||||
|
{
|
||||||
|
const char *msg = lua_tostring(L, -1);
|
||||||
|
struct st_error *err;
|
||||||
|
|
||||||
|
if (msg == NULL) msg = "(non-string Lua error)";
|
||||||
|
/* luaL_error prepends "chunkname:line: " to the message, so the
|
||||||
|
* sandbox marker is matched as a substring, not a prefix. */
|
||||||
|
if (strstr(msg, SANDBOX_PREFIX) != NULL) {
|
||||||
|
err = st_error_kdl_schema(msg);
|
||||||
|
} else {
|
||||||
|
err = st_error_internal(msg);
|
||||||
|
}
|
||||||
|
lua_pop(L, 1);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct st_error *
|
||||||
|
st_lua_run(struct st_lua_rt *rt, const char *chunk, const char *chunkname)
|
||||||
|
{
|
||||||
|
lua_State *L;
|
||||||
|
int status;
|
||||||
|
|
||||||
|
if (rt == NULL) return st_error_internal("st_lua_run: NULL runtime");
|
||||||
|
if (chunk == NULL) return st_error_internal("st_lua_run: NULL chunk");
|
||||||
|
L = rt->L;
|
||||||
|
|
||||||
|
status = luaL_loadbuffer(L, chunk, strlen(chunk),
|
||||||
|
chunkname != NULL ? chunkname : "=(extension)");
|
||||||
|
if (status == LUA_OK) {
|
||||||
|
status = lua_pcall(L, 0, 0, 0);
|
||||||
|
}
|
||||||
|
if (status == LUA_OK) return NULL;
|
||||||
|
return lua_error_to_st_error(L);
|
||||||
|
}
|
||||||
+100
@@ -0,0 +1,100 @@
|
|||||||
|
#ifndef ST_EXT_LUA_H
|
||||||
|
#define ST_EXT_LUA_H
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Embedded sandboxed Lua runtime for stupidtools extensions (plan todo
|
||||||
|
* 19). Binds the vendored Lua 5.4 (thirdparty/lua/) as the extension
|
||||||
|
* language; the extension ABI (src/ext/abi.h, todo 13) is bridged in
|
||||||
|
* todo 21 -- THIS todo only ships the runtime, the sandbox, and the
|
||||||
|
* registration hooks.
|
||||||
|
*
|
||||||
|
* Sandbox contract (what extension code may and may not do):
|
||||||
|
*
|
||||||
|
* OPENED libraries (the ONLY ones linked and loaded):
|
||||||
|
* base -- minus the globals below
|
||||||
|
* table, string
|
||||||
|
* math -- a curated, libm-FREE subset written here: abs, max,
|
||||||
|
* min, tointeger, type, maxinteger, mininteger, pi,
|
||||||
|
* huge. The stock lmathlib.c is NOT linked: it pulls
|
||||||
|
* sin/cos/pow/... from libm and the unit-test harness
|
||||||
|
* links NO extra libraries (-lm is unavailable).
|
||||||
|
* os -- a hand-built table containing ONLY the stubs below.
|
||||||
|
*
|
||||||
|
* BLOCKED (exist but raise "sandbox blocked: ..." errors):
|
||||||
|
* os.execute, os.exit, os.remove, os.rename
|
||||||
|
*
|
||||||
|
* REMOVED (globals are nil; their C libraries are not even linked):
|
||||||
|
* dofile, loadfile, require, package, io, debug, coroutine
|
||||||
|
*
|
||||||
|
* KEPT with the rationale that they cannot reach the filesystem or a
|
||||||
|
* process without io/os/package (all gone): `load` (string chunks
|
||||||
|
* only), collectgarbage, print, warn, the string/table libraries.
|
||||||
|
*
|
||||||
|
* REGISTRATION API (the only way extensions talk to stupidtools):
|
||||||
|
* st.register_check("name")
|
||||||
|
* st.register_language("name")
|
||||||
|
* These record into the RUNTIME'S OWN registries (see below); the
|
||||||
|
* flush into a struct st_ext_ctx is todo 21's bridge.
|
||||||
|
*
|
||||||
|
* Error contract of st_lua_run():
|
||||||
|
* - success -> NULL;
|
||||||
|
* - sandbox violation -> st_error, category ST_ERR_KDL_SCHEMA, whose
|
||||||
|
* message contains "sandbox blocked: " (the extension violated
|
||||||
|
* its contract -- the closest existing category; error.h is frozen
|
||||||
|
* to this todo);
|
||||||
|
* - any other Lua failure (syntax error, runtime error, OOM) ->
|
||||||
|
* st_error, category ST_ERR_INTERNAL, carrying Lua's message
|
||||||
|
* (chunkname is propagated into syntax-error messages).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#include "error.h"
|
||||||
|
|
||||||
|
struct st_lua_rt; /* opaque */
|
||||||
|
|
||||||
|
/* Fresh runtime with the sandboxed environment installed. NULL only on
|
||||||
|
* allocation failure. */
|
||||||
|
struct st_lua_rt *st_lua_rt_new(void);
|
||||||
|
|
||||||
|
/* NULL is a no-op. */
|
||||||
|
void st_lua_rt_free(struct st_lua_rt *rt);
|
||||||
|
|
||||||
|
/* ---------------- registration API (C side) ---------------- */
|
||||||
|
|
||||||
|
/* Record a check registration in the runtime's own registry. `name`
|
||||||
|
* must be a non-empty NUL-terminated string; `probe_spec_or_null` is an
|
||||||
|
* opaque pointer kept verbatim (todo 21 passes it through to the ABI's
|
||||||
|
* st_ext_register_check). Returns 0 on success, -1 on bad arguments,
|
||||||
|
* duplicate name, or allocation failure. Mirrored from Lua by
|
||||||
|
* st.register_check(name). */
|
||||||
|
int st_lua_register_check(struct st_lua_rt *rt, const char *name,
|
||||||
|
void *probe_spec_or_null);
|
||||||
|
|
||||||
|
/* Same, for languages (mirrored by st.register_language(name)). */
|
||||||
|
int st_lua_register_language(struct st_lua_rt *rt, const char *name);
|
||||||
|
|
||||||
|
/* ---------------- enumeration (the todo 21 bridge) ----------------
|
||||||
|
*
|
||||||
|
* Everything the runtime has recorded, in registration order. Todo 21
|
||||||
|
* walks these and calls st_ext_register_check / st_ext_register_language
|
||||||
|
* on a struct st_ext_ctx -- no state lives anywhere else. */
|
||||||
|
|
||||||
|
size_t st_lua_check_count(const struct st_lua_rt *rt);
|
||||||
|
const char *st_lua_check_name(const struct st_lua_rt *rt, size_t i);
|
||||||
|
void *st_lua_check_probe_spec(const struct st_lua_rt *rt, size_t i);
|
||||||
|
|
||||||
|
size_t st_lua_language_count(const struct st_lua_rt *rt);
|
||||||
|
const char *st_lua_language_name(const struct st_lua_rt *rt, size_t i);
|
||||||
|
|
||||||
|
/* ---------------- execution ---------------- */
|
||||||
|
|
||||||
|
/* Compiles `chunk` (NUL-terminated; must not be NULL) and runs it in
|
||||||
|
* the sandbox. `chunkname` may be NULL and only feeds diagnostics.
|
||||||
|
* Returns NULL on success; otherwise see the error contract above (the
|
||||||
|
* caller owns the returned st_error). The runtime stays usable after an
|
||||||
|
* error. */
|
||||||
|
struct st_error *st_lua_run(struct st_lua_rt *rt, const char *chunk,
|
||||||
|
const char *chunkname);
|
||||||
|
|
||||||
|
#endif /* ST_EXT_LUA_H */
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
/* LINK: ../../src/ext/lua.c ../../src/error.c ../../src/span.c ../../thirdparty/lua/lapi.c ../../thirdparty/lua/lauxlib.c ../../thirdparty/lua/lbaselib.c ../../thirdparty/lua/lcode.c ../../thirdparty/lua/lctype.c ../../thirdparty/lua/ldebug.c ../../thirdparty/lua/ldo.c ../../thirdparty/lua/ldump.c ../../thirdparty/lua/lfunc.c ../../thirdparty/lua/lgc.c ../../thirdparty/lua/llex.c ../../thirdparty/lua/lmem.c ../../thirdparty/lua/lobject.c ../../thirdparty/lua/lopcodes.c ../../thirdparty/lua/lparser.c ../../thirdparty/lua/lstate.c ../../thirdparty/lua/lstring.c ../../thirdparty/lua/ltable.c ../../thirdparty/lua/ltm.c ../../thirdparty/lua/lundump.c ../../thirdparty/lua/lvm.c ../../thirdparty/lua/lzio.c ../../thirdparty/lua/lstrlib.c ../../thirdparty/lua/ltablib.c */
|
||||||
|
/* tests/unit/test_lua.c
|
||||||
|
*
|
||||||
|
* Unit tests for the embedded sandboxed Lua runtime (src/ext/lua.h,
|
||||||
|
* plan todo 19).
|
||||||
|
*
|
||||||
|
* The magic LINK comment on line 1 is REQUIRED by tests/run.sh: it lists
|
||||||
|
* the extra .c sources to compile into this test binary (paths relative
|
||||||
|
* to tests/unit/, space-separated). munit.c and the include dirs are
|
||||||
|
* added automatically by the harness.
|
||||||
|
*
|
||||||
|
* LINK LIST POLICY (the -lm trap):
|
||||||
|
* The harness compiles with NO extra libraries -- in particular NO
|
||||||
|
* -lm. Stock Lua needs libm in two ways: (1) lmathlib.c (sin/cos/pow/
|
||||||
|
* log/...) and (2) the CORE itself (floor in lvm.c, frexp/fabs in
|
||||||
|
* ltable.c, ldexp in lobject.c/lcode.c, fmod and pow via luaO_arith).
|
||||||
|
* Therefore this test links:
|
||||||
|
* - the 23 core .c files + lauxlib + lbaselib + lstrlib + ltablib;
|
||||||
|
* - NOT lmathlib, linit (which references luaopen_math), loadlib
|
||||||
|
* (package/require), liolib (io), loslib (we build our own
|
||||||
|
* sandboxed os table), ldblib (debug), lcorolib, lutf8lib;
|
||||||
|
* - NOT the lua.c / luac.c standalone mains.
|
||||||
|
* The remaining core libm references are satisfied by portable
|
||||||
|
* libm-free shims (fabs/floor/frexp/ldexp/fmod/pow) defined in
|
||||||
|
* src/ext/lua.c itself -- the binary links with zero libm code.
|
||||||
|
*/
|
||||||
|
#include "munit.h"
|
||||||
|
|
||||||
|
#include "ext/lua.h"
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* ---------- shared chunks ---------- */
|
||||||
|
|
||||||
|
/* Registers one check and one language from Lua; every registration in
|
||||||
|
* this file must be visible through the C enumeration API afterwards. */
|
||||||
|
static const char reg_chunk[] =
|
||||||
|
"st.register_check(\"magic\")\n"
|
||||||
|
"st.register_language(\"fortran\")\n";
|
||||||
|
|
||||||
|
/* Every dangerous entry point must raise a "sandbox blocked:" error even
|
||||||
|
* when wrapped in pcall, and the module-loading / file-loading globals
|
||||||
|
* must not exist at all. */
|
||||||
|
static const char audit_chunk[] =
|
||||||
|
"local function must_block(fn)\n"
|
||||||
|
" local ok, err = pcall(fn)\n"
|
||||||
|
" assert(ok == false, \"expected blocked call to succeed?\")\n"
|
||||||
|
" assert(err:match(\"sandbox blocked\"), \"expected sandbox error, got: \" .. tostring(err))\n"
|
||||||
|
"end\n"
|
||||||
|
"must_block(function() os.execute(\"id\") end)\n"
|
||||||
|
"must_block(function() os.exit(0) end)\n"
|
||||||
|
"must_block(function() os.remove(\"/etc/passwd\") end)\n"
|
||||||
|
"must_block(function() os.rename(\"a\", \"b\") end)\n"
|
||||||
|
"assert(io == nil, \"io must not exist\")\n"
|
||||||
|
"assert(package == nil, \"package must not exist\")\n"
|
||||||
|
"assert(require == nil, \"require must not exist\")\n"
|
||||||
|
"assert(dofile == nil, \"dofile must not exist\")\n"
|
||||||
|
"assert(loadfile == nil, \"loadfile must not exist\")\n";
|
||||||
|
|
||||||
|
/* The curated math library (libm-free subset) must work. */
|
||||||
|
static const char math_chunk[] =
|
||||||
|
"assert(math.abs(-3) == 3)\n"
|
||||||
|
"assert(math.type(3) == \"integer\")\n"
|
||||||
|
"assert(math.type(1.5) == \"float\")\n"
|
||||||
|
"assert(math.tointeger(3.0) == 3)\n"
|
||||||
|
"assert(math.tointeger(1.5) == nil)\n"
|
||||||
|
"assert(math.max(1, 5, 3) == 5)\n"
|
||||||
|
"assert(math.min(2, -1) == -1)\n"
|
||||||
|
"assert(math.pi > 3.14 and math.pi < 3.15)\n"
|
||||||
|
"assert(math.huge > 1e300)\n";
|
||||||
|
|
||||||
|
/* Operator smoke test for the libm-free shims inside src/ext/lua.c:
|
||||||
|
* ^ -> pow % -> fmod 0x1p3 -> ldexp (compile-time folding)
|
||||||
|
* float table keys -> frexp math.tointeger -> floor
|
||||||
|
*/
|
||||||
|
static const char shim_chunk[] =
|
||||||
|
"assert(2.5 ^ 2 == 6.25)\n"
|
||||||
|
"assert(2.5 % 1.0 == 0.5)\n"
|
||||||
|
"assert(0x1p3 == 8.0)\n"
|
||||||
|
"assert(1e300 > 1e100)\n"
|
||||||
|
"local t = { [1.5] = \"x\" }\n"
|
||||||
|
"assert(t[1.5] == \"x\")\n"
|
||||||
|
"assert(string.format(\"%.1f\", 1.5) == \"1.5\")\n";
|
||||||
|
|
||||||
|
/* ---------- (a) registrations land in the runtime's own registry ---------- */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_registration_visible(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
struct st_lua_rt *rt;
|
||||||
|
struct st_error *err;
|
||||||
|
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
|
||||||
|
rt = st_lua_rt_new();
|
||||||
|
munit_assert_not_null(rt);
|
||||||
|
|
||||||
|
err = st_lua_run(rt, reg_chunk, "=regtest");
|
||||||
|
munit_assert_null(err);
|
||||||
|
|
||||||
|
munit_assert_size(st_lua_check_count(rt), ==, 1);
|
||||||
|
munit_assert_string_equal(st_lua_check_name(rt, 0), "magic");
|
||||||
|
munit_assert_null(st_lua_check_probe_spec(rt, 0));
|
||||||
|
munit_assert_size(st_lua_language_count(rt), ==, 1);
|
||||||
|
munit_assert_string_equal(st_lua_language_name(rt, 0), "fortran");
|
||||||
|
|
||||||
|
/* C-side registration writes into the same registry (insertion
|
||||||
|
* order, so it is index 1). */
|
||||||
|
munit_assert_int(st_lua_register_check(rt, "via-c", NULL), ==, 0);
|
||||||
|
munit_assert_size(st_lua_check_count(rt), ==, 2);
|
||||||
|
munit_assert_string_equal(st_lua_check_name(rt, 0), "magic");
|
||||||
|
munit_assert_string_equal(st_lua_check_name(rt, 1), "via-c");
|
||||||
|
|
||||||
|
st_lua_rt_free(rt);
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- (b) sandbox: os.execute & friends are blocked ---------- */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_sandbox_blocks_exec(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
struct st_lua_rt *rt;
|
||||||
|
struct st_error *err;
|
||||||
|
const char *msg;
|
||||||
|
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
|
||||||
|
rt = st_lua_rt_new();
|
||||||
|
munit_assert_not_null(rt);
|
||||||
|
|
||||||
|
/* direct call: error carries the actual block reason */
|
||||||
|
err = st_lua_run(rt, "os.execute(\"id\")", "=evil");
|
||||||
|
munit_assert_not_null(err);
|
||||||
|
munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_SCHEMA);
|
||||||
|
msg = st_error_message(err);
|
||||||
|
munit_assert_not_null(msg);
|
||||||
|
munit_assert_not_null(strstr(msg, "sandbox blocked"));
|
||||||
|
munit_assert_not_null(strstr(msg, "os.execute"));
|
||||||
|
st_error_free(err);
|
||||||
|
|
||||||
|
/* pcall must not be an escape hatch: the call still fails, and the
|
||||||
|
* captured error still names the sandbox block. */
|
||||||
|
err = st_lua_run(rt,
|
||||||
|
"local ok, e = pcall(os.execute, \"id\")\n"
|
||||||
|
"assert(ok == false)\n"
|
||||||
|
"assert(e:match(\"sandbox blocked\"))\n",
|
||||||
|
"=evil-pcall");
|
||||||
|
munit_assert_null(err);
|
||||||
|
|
||||||
|
/* the rest of the dangerous surface, audited from inside the sandbox */
|
||||||
|
err = st_lua_run(rt, audit_chunk, "=audit");
|
||||||
|
munit_assert_null(err);
|
||||||
|
|
||||||
|
st_lua_rt_free(rt);
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- (c) syntax errors yield a clean st_error ---------- */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_syntax_error_clean(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
struct st_lua_rt *rt;
|
||||||
|
struct st_error *err;
|
||||||
|
const char *msg;
|
||||||
|
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
|
||||||
|
rt = st_lua_rt_new();
|
||||||
|
munit_assert_not_null(rt);
|
||||||
|
|
||||||
|
err = st_lua_run(rt, "local x =", "broken.lua");
|
||||||
|
munit_assert_not_null(err);
|
||||||
|
munit_assert_int(st_error_category_of(err), ==, ST_ERR_INTERNAL);
|
||||||
|
msg = st_error_message(err);
|
||||||
|
munit_assert_not_null(msg);
|
||||||
|
munit_assert_size(strlen(msg), >, 0);
|
||||||
|
munit_assert_not_null(strstr(msg, "broken.lua")); /* chunkname propagated */
|
||||||
|
st_error_free(err);
|
||||||
|
|
||||||
|
/* the runtime stays usable after an error */
|
||||||
|
err = st_lua_run(rt, "local y = 1", "=ok");
|
||||||
|
munit_assert_null(err);
|
||||||
|
|
||||||
|
st_lua_rt_free(rt);
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- (d) math is allowed (curated, libm-free subset) ---------- */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_math_allowed(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
struct st_lua_rt *rt;
|
||||||
|
struct st_error *err;
|
||||||
|
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
|
||||||
|
rt = st_lua_rt_new();
|
||||||
|
munit_assert_not_null(rt);
|
||||||
|
|
||||||
|
err = st_lua_run(rt, math_chunk, "=math");
|
||||||
|
munit_assert_null(err);
|
||||||
|
|
||||||
|
st_lua_rt_free(rt);
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- operator smoke for the libm-free shims ---------- */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_shims_through_operators(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
struct st_lua_rt *rt;
|
||||||
|
struct st_error *err;
|
||||||
|
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
|
||||||
|
rt = st_lua_rt_new();
|
||||||
|
munit_assert_not_null(rt);
|
||||||
|
|
||||||
|
err = st_lua_run(rt, shim_chunk, "=shims");
|
||||||
|
munit_assert_null(err);
|
||||||
|
|
||||||
|
st_lua_rt_free(rt);
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- defensive API behavior: NULL args, duplicates ---------- */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_null_and_dup_defensive(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
struct st_lua_rt *rt;
|
||||||
|
struct st_error *err;
|
||||||
|
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
|
||||||
|
rt = st_lua_rt_new();
|
||||||
|
munit_assert_not_null(rt);
|
||||||
|
|
||||||
|
err = st_lua_run(rt, NULL, NULL);
|
||||||
|
munit_assert_not_null(err);
|
||||||
|
st_error_free(err);
|
||||||
|
|
||||||
|
munit_assert_int(st_lua_register_check(rt, NULL, NULL), ==, -1);
|
||||||
|
munit_assert_int(st_lua_register_language(rt, NULL), ==, -1);
|
||||||
|
munit_assert_int(st_lua_register_check(rt, "", NULL), ==, -1);
|
||||||
|
|
||||||
|
/* duplicates are rejected C-side and Lua-side */
|
||||||
|
munit_assert_int(st_lua_register_check(rt, "dup", NULL), ==, 0);
|
||||||
|
munit_assert_int(st_lua_register_check(rt, "dup", NULL), ==, -1);
|
||||||
|
err = st_lua_run(rt, "st.register_check(\"dup\")", "=dup");
|
||||||
|
munit_assert_not_null(err);
|
||||||
|
st_error_free(err);
|
||||||
|
|
||||||
|
/* freeing NULL is a no-op */
|
||||||
|
st_lua_rt_free(NULL);
|
||||||
|
st_lua_rt_free(rt);
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static MunitTest tests[] = {
|
||||||
|
{ "/lua/registration-visible", test_registration_visible, NULL, NULL,
|
||||||
|
MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ "/lua/sandbox-blocks-exec", test_sandbox_blocks_exec, NULL, NULL,
|
||||||
|
MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ "/lua/syntax-error-clean", test_syntax_error_clean, NULL, NULL,
|
||||||
|
MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ "/lua/math-allowed", test_math_allowed, NULL, NULL,
|
||||||
|
MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ "/lua/shims-through-operators", test_shims_through_operators, NULL,
|
||||||
|
NULL, MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ "/lua/null-and-dup-defensive", test_null_and_dup_defensive, NULL, NULL,
|
||||||
|
MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
};
|
||||||
|
|
||||||
|
static const MunitSuite suite = {
|
||||||
|
"/lua", tests, NULL, 1, MUNIT_SUITE_OPTION_NONE,
|
||||||
|
};
|
||||||
|
|
||||||
|
int
|
||||||
|
main(int argc, char *argv[MUNIT_ARRAY_PARAM(argc + 1)])
|
||||||
|
{
|
||||||
|
return munit_suite_main(&suite, NULL, argc, argv);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user