Template
feat(ext): embed sandboxed Lua runtime with C API
This commit is contained in:
@@ -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