186 lines
6.8 KiB
C
186 lines
6.8 KiB
C
// testSandbox.c -- per-context resource limits (calogContextOpenLimited). Verifies the allow-list
|
|
// (a script may call only permitted natives, engine-agnostically), the Lua memory cap (an
|
|
// over-budget allocation fails cleanly instead of exhausting the host), and the wall-clock budget
|
|
// on Lua and JavaScript (a runaway loop is retired). The honest coverage is documented on
|
|
// CalogLimitsT: memory/time apply to Lua + JS only; the allow-list applies to every engine.
|
|
|
|
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include "calog.h"
|
|
|
|
#include <stdatomic.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#define CHECK(cond, msg) checkImpl((cond), (msg), __LINE__)
|
|
|
|
#define PUMP_LIMIT 6000
|
|
|
|
static CalogT *calog = NULL;
|
|
static _Atomic bool reachedFlag = false;
|
|
static _Atomic bool forbiddenFlag = false;
|
|
static _Atomic int32_t reportValue = -1;
|
|
static _Atomic int32_t errorCount = 0;
|
|
static _Atomic bool doneFlag = false;
|
|
static int32_t testsRun = 0;
|
|
static int32_t testsFailed = 0;
|
|
|
|
static void checkImpl(bool condition, const char *message, int32_t line);
|
|
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
|
static int32_t nativeForbidden(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
|
static int32_t nativeReached(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
|
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
|
static void onError(uint64_t contextId, const char *message, void *userData);
|
|
static void resetFlags(void);
|
|
static void runLimited(const CalogEngineT *engine, const char *source, const CalogLimitsT *limits);
|
|
|
|
|
|
static void checkImpl(bool condition, const char *message, int32_t line) {
|
|
testsRun++;
|
|
if (!condition) {
|
|
testsFailed++;
|
|
printf("FAIL testSandbox.c:%d %s\n", line, message);
|
|
}
|
|
}
|
|
|
|
|
|
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
|
(void)args;
|
|
(void)argCount;
|
|
(void)userData;
|
|
atomic_store(&doneFlag, true);
|
|
calogValueNil(result);
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
static int32_t nativeForbidden(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
|
(void)args;
|
|
(void)argCount;
|
|
(void)userData;
|
|
atomic_store(&forbiddenFlag, true);
|
|
calogValueNil(result);
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
static int32_t nativeReached(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
|
(void)args;
|
|
(void)argCount;
|
|
(void)userData;
|
|
atomic_store(&reachedFlag, true);
|
|
calogValueNil(result);
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
|
(void)userData;
|
|
calogValueNil(result);
|
|
if (argCount == 1 && args[0].type == calogBoolE) {
|
|
atomic_store(&reportValue, args[0].as.b ? 1 : 0);
|
|
}
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
static void onError(uint64_t contextId, const char *message, void *userData) {
|
|
(void)contextId;
|
|
(void)message;
|
|
(void)userData;
|
|
atomic_fetch_add(&errorCount, 1);
|
|
atomic_store(&doneFlag, true);
|
|
}
|
|
|
|
|
|
static void resetFlags(void) {
|
|
atomic_store(&reachedFlag, false);
|
|
atomic_store(&forbiddenFlag, false);
|
|
atomic_store(&reportValue, -1);
|
|
atomic_store(&errorCount, 0);
|
|
atomic_store(&doneFlag, false);
|
|
}
|
|
|
|
|
|
// Open a limited context, run the script fire-and-forget, and pump until it signals done() or its
|
|
// error reaches the handler (or a bounded timeout).
|
|
static void runLimited(const CalogEngineT *engine, const char *source, const CalogLimitsT *limits) {
|
|
CalogContextT *ctx;
|
|
struct timespec ts = { 0, 500000 };
|
|
int32_t i;
|
|
|
|
ctx = calogContextOpenLimited(calog, engine, limits);
|
|
if (ctx == NULL) {
|
|
checkImpl(false, "context open failed", __LINE__);
|
|
return;
|
|
}
|
|
calogContextEval(ctx, source);
|
|
for (i = 0; i < PUMP_LIMIT; i++) {
|
|
calogPump(calog);
|
|
if (atomic_load(&doneFlag)) {
|
|
calogPump(calog);
|
|
break;
|
|
}
|
|
nanosleep(&ts, NULL);
|
|
}
|
|
calogContextClose(ctx);
|
|
}
|
|
|
|
|
|
int main(void) {
|
|
static const char *const allowList[] = { "reached", "report", "done", NULL };
|
|
CalogLimitsT allowLimits;
|
|
CalogLimitsT memLimits;
|
|
CalogLimitsT timeLimits;
|
|
|
|
calog = calogCreate();
|
|
if (calog == NULL) {
|
|
printf("calog create failed\n");
|
|
return 1;
|
|
}
|
|
calogSetErrorHandler(calog, onError, NULL);
|
|
calogRegister(calog, "reached", nativeReached, NULL);
|
|
calogRegister(calog, "forbidden", nativeForbidden, NULL);
|
|
calogRegister(calog, "report", nativeReport, NULL);
|
|
calogRegisterInline(calog, "done", nativeDone, NULL);
|
|
|
|
// 1. Allow-list: the script may call reached/report/done, but forbidden is denied (pcall catches
|
|
// the "not permitted" error), so its native body never runs.
|
|
memset(&allowLimits, 0, sizeof(allowLimits));
|
|
allowLimits.allowList = allowList;
|
|
resetFlags();
|
|
runLimited(&calogLuaEngine, "reached(); local ok = pcall(forbidden); report(ok); done()", &allowLimits);
|
|
CHECK(atomic_load(&reachedFlag), "allow-list: a permitted native runs");
|
|
CHECK(atomic_load(&reportValue) == 0, "allow-list: calling a forbidden native errors (pcall -> false)");
|
|
CHECK(!atomic_load(&forbiddenFlag), "allow-list: the forbidden native body never ran");
|
|
|
|
// 2. Memory cap (Lua): a 2 MiB context cannot allocate a 10 MiB string, so it errors before
|
|
// reached(); an unlimited context runs the same allocation fine.
|
|
memset(&memLimits, 0, sizeof(memLimits));
|
|
memLimits.memoryBytes = 2 * 1024 * 1024;
|
|
resetFlags();
|
|
runLimited(&calogLuaEngine, "local s = string.rep('x', 10 * 1024 * 1024); reached(); done()", &memLimits);
|
|
CHECK(atomic_load(&errorCount) >= 1, "memory cap: over-budget allocation errors");
|
|
CHECK(!atomic_load(&reachedFlag), "memory cap: the script did not continue past the failed allocation");
|
|
resetFlags();
|
|
runLimited(&calogLuaEngine, "local s = string.rep('x', 10 * 1024 * 1024); reached(); done()", NULL);
|
|
CHECK(atomic_load(&reachedFlag), "no memory cap: the same allocation succeeds");
|
|
|
|
// 3. Wall-clock budget: a runaway loop is retired (its error reaches the handler) within the
|
|
// bounded pump, on Lua and on JavaScript.
|
|
memset(&timeLimits, 0, sizeof(timeLimits));
|
|
timeLimits.wallClockMillis = 100;
|
|
resetFlags();
|
|
runLimited(&calogLuaEngine, "while true do end", &timeLimits);
|
|
CHECK(atomic_load(&errorCount) >= 1, "time budget (Lua): a runaway loop is retired");
|
|
resetFlags();
|
|
runLimited(&calogJsEngine, "while (true) {}", &timeLimits);
|
|
CHECK(atomic_load(&errorCount) >= 1, "time budget (JS): a runaway loop is retired");
|
|
|
|
calogDestroy(calog);
|
|
|
|
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
|
|
fflush(stdout);
|
|
return testsFailed == 0 ? 0 : 1;
|
|
}
|