2759 lines · 72.6 KB · C
RawDownload
// SPDX-License-Identifier: ISC#include <dirent.h>#include <errno.h>#include <stdbool.h>#include <stddef.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include <sys/queue.h>#include <sys/stat.h>#include <time.h> #include <lua.h>#include <lauxlib.h>#include <lualib.h> #include <cjson/cJSON.h> #include "clm/clm.h"#include "clm/cleanup.h"#include "clm/internal.h"#include "clm/lua_plugin.h"#include "clm/log.h"#include "lua_internal.h"#include "useful.h"#include "banned.h" /* Forward declarations for modules registered into each plugin state. */int clm_lua_json_open(lua_State *L);void clm_lua_push_json_value(lua_State *L, cJSON *obj);int clm_lua_http_open(lua_State *L, struct clm_agent *agent); #define CLM_LUA_MEM_LIMIT (8 * 1024 * 1024) /* 8 MiB per plugin */#define CLM_LUA_EXEC_TIMEOUT_MS 30000u     /* default: 30s CPU timeout */#define CLM_LUA_LOAD_TIMEOUT_MS 500u       /* plugin load must be quick */#define CLM_LUA_HOOK_INTERVAL 10000        /* check deadline every N instructions */#define CLM_LUA_HTTP_MAX_INFLIGHT 8        /* max concurrent HTTP requests */#define CLM_LUA_HTTP_MAX_PER_CALL 128      /* max total HTTP requests per tool call */#define CLM_LUA_JSON_DECODE_MAX (2 * 1024 * 1024) /* 2 MiB max input to json.decode */ /* Forward declaration. */struct lua_tool_user; /* Per-plugin resource budget and peak tracking. */struct clm_lua_budget {	/* Limits (set from env defaults). */	size_t http_max_inflight;	size_t http_max_per_call;	size_t json_decode_max; 	/* Current counters (reset per tool call). */	size_t http_inflight;	size_t http_total;	uint64_t call_start_ns;   /* wall-clock start of current call */ 	/* Peak counters (lifetime of plugin, never reset). */	size_t peak_mem;	size_t peak_http_inflight;	size_t peak_http_total;    /* peak per single tool call */	uint64_t peak_call_ns;     /* longest tool call duration */}; /* Per-plugin state. */struct clm_lua_plugin {	TAILQ_ENTRY(clm_lua_plugin) entry;	lua_State *L;	struct clm_agent *agent;	size_t mem_used;	size_t mem_limit;	char *path;	struct lua_tool_user **tool_users; /* tracked for teardown */	size_t tool_user_count;	size_t tool_user_cap;	uint64_t deadline_ns; /* wall-clock deadline for current execution */	struct clm_lua_budget budget;	struct clm_lua_pending_list pending;	uint64_t next_call_serial;	bool tearing_down; 	/* every live tool coroutine, grouped by clm_lua_call. */	struct clm_lua_coroutine_list coroutines;}; TAILQ_HEAD(clm_lua_plugin_list, clm_lua_plugin); /* Top-level environment holding all plugins. */struct clm_lua_env {	struct clm_agent *agent;	struct clm_lua_plugin_list plugins;	uint64_t exec_timeout_ms; /* global execution timeout */	/* Global budget defaults. */	size_t http_max_inflight;	size_t http_max_per_call;	size_t json_decode_max;	/* Per-tool config (parsed JSON object, keyed by tool/plugin name). */	cJSON *tool_config;}; intclm_lua_pending_add(struct clm_lua_plugin *plugin, lua_State *co,    struct clm_lua_pending *pending, clm_lua_pending_teardown_fn teardown){	struct clm_lua_coroutine *lco; 	if (plugin == NULL || co == NULL || pending == NULL || teardown == NULL)		return -EINVAL;	if (plugin->tearing_down)		return -ECANCELED;	lco = clm_lua_coroutine_find(co);	if (lco == NULL || lco->plugin != plugin || lco->call->completed)		return -ECANCELED;	pending->plugin = plugin;	pending->coroutine = lco;	pending->teardown = teardown;	TAILQ_INSERT_TAIL(&plugin->pending, pending, plugin_entry);	TAILQ_INSERT_TAIL(&lco->pendings, pending, coroutine_entry);	return 0;} struct clm_lua_plugin *clm_lua_pending_remove(struct clm_lua_pending *pending){	struct clm_lua_plugin *plugin;	struct clm_lua_coroutine *lco; 	if (pending == NULL || pending->plugin == NULL)		return NULL;	plugin = pending->plugin;	lco = pending->coroutine;	TAILQ_REMOVE(&plugin->pending, pending, plugin_entry);	if (lco != NULL)		TAILQ_REMOVE(&lco->pendings, pending, coroutine_entry);	pending->plugin = NULL;	pending->coroutine = NULL;	return plugin;} static voidclm_lua_pending_teardown_all(struct clm_lua_plugin *plugin){	struct clm_lua_pending *pending; 	plugin->tearing_down = true;	while ((pending = TAILQ_FIRST(&plugin->pending)) != NULL) {		(void)clm_lua_pending_remove(pending);		pending->teardown(pending);	}} /* Budget helpers called from lua_http.c (defined below). */void clm_lua_http_done(struct clm_lua_plugin *plugin);int clm_lua_http_start(struct clm_lua_plugin *plugin);void clm_lua_budget_report(struct clm_lua_plugin *plugin, const char *name);int clm_lua_resume_with_deadline(struct clm_lua_plugin *plugin,    lua_State *co, lua_State *from, int nargs, int *nresults,    uint64_t timeout_ms); /* ------------------------------------------------------------------ *//* Capped allocator                                                    *//* ------------------------------------------------------------------ */ static void *lua_capped_alloc(void *ud, void *ptr, size_t osize, size_t nsize){	struct clm_lua_plugin *p = ud; 	/* For a brand-new allocation (ptr == NULL), Lua 5.4 passes the	 * object's type tag in `osize`, not a real previous size -- e.g.	 * osize=8 for a coroutine, not "8 bytes previously allocated here".	 * Treating that as bytes-to-subtract silently corrupts mem_used on	 * every single new allocation (it ends up mem_used - tag too low).	 * Over thousands of allocations in a long-running session this	 * drifts mem_used further and further below the real heap size,	 * until the cap approves allocations the real system can no longer	 * satisfy -- realloc() then genuinely fails outside any protected	 * Lua call, which Lua has no way to recover from (its OOM error has	 * nowhere to unwind to, so the default panic handler aborts()).	 * Only ptr != NULL (an actual resize/free of a real prior block)	 * carries a real previous size worth accounting for. */	size_t real_osize = ptr != NULL ? osize : 0; 	if (nsize == 0) {		p->mem_used -= real_osize;		free(ptr);		return NULL;	}	if (p->mem_used - real_osize + nsize > p->mem_limit)		return NULL; /* refused; Lua raises OOM */	void *np = realloc(ptr, nsize);	if (np == NULL)		return NULL;	p->mem_used = p->mem_used - real_osize + nsize;	if (p->mem_used > p->budget.peak_mem)		p->budget.peak_mem = p->mem_used;	return np;} /* ------------------------------------------------------------------ *//* Invocation context userdata                                         *//* ------------------------------------------------------------------ */ #define CLM_LUA_CTX_META "clm_tool_ctx"#define CLM_LUA_TASK_META "clm_task" enum clm_lua_task_state {	CLM_LUA_TASK_PENDING,	CLM_LUA_TASK_DONE,	CLM_LUA_TASK_ERROR,	CLM_LUA_TASK_CANCELLED,}; struct clm_lua_wait; struct clm_lua_task {	struct clm_lua_call *call;	uint64_t call_serial;	struct clm_lua_coroutine *lco;	struct clm_lua_wait *waiter;	enum clm_lua_task_state state;	int self_ref;	int results_ref;	int nresults;	char *error;	bool error_observed;}; struct clm_lua_wait {	struct clm_lua_pending pending;	struct clm_lua_task **tasks;	size_t ntasks;	struct clm_agent *agent;	struct clm_timer *timer;	bool any;}; struct lua_tool_ctx {	struct clm_lua_call *call;}; static void lua_call_result(struct clm_lua_call *call, bool failed,    const char *text, struct clm_lua_coroutine *current); static struct lua_tool_ctx *check_ctx(lua_State *L, int idx){	return luaL_checkudata(L, idx, CLM_LUA_CTX_META);} static intlua_ctx_complete(lua_State *L){	struct lua_tool_ctx *ctx = check_ctx(L, 1);	struct clm_lua_coroutine *lco = clm_lua_coroutine_find(L);	const char *result; 	if (ctx->call->completed)		return luaL_error(L, "tool already completed");	if (lco == NULL || !lco->is_main)		return luaL_error(L, "ctx:complete may only be called from the main "		    "tool coroutine");	result = luaL_checkstring(L, 2);	if (ctx->call->active_coroutines > 1) {		lua_call_result(ctx->call, true,		    "tool completed with unawaited tasks", lco);		return 0;	}	if (ctx->call->unobserved_errors != 0) {		lua_call_result(ctx->call, true,		    ctx->call->child_error != NULL ? ctx->call->child_error :		    "task failed without its result being observed", lco);		return 0;	}	lua_call_result(ctx->call, false, result, lco);	return 0;} static intlua_ctx_fail(lua_State *L){	struct lua_tool_ctx *ctx = check_ctx(L, 1);	struct clm_lua_coroutine *lco = clm_lua_coroutine_find(L);	const char *msg; 	if (ctx->call->completed)		return luaL_error(L, "tool already completed");	if (lco == NULL || !lco->is_main)		return luaL_error(L, "ctx:fail may only be called from the main "		    "tool coroutine");	msg = luaL_checkstring(L, 2);	lua_call_result(ctx->call, true, msg, lco);	return 0;} static intlua_ctx_args_raw(lua_State *L){	struct lua_tool_ctx *ctx = check_ctx(L, 1);	const char *args = ctx->call->inv != NULL ?	    clm_tool_invocation_args(ctx->call->inv) : NULL; 	if (args == NULL)		lua_pushstring(L, "{}");	else		lua_pushstring(L, args);	return 1;} static intlua_ctx_log(lua_State *L){	struct lua_tool_ctx *ctx = check_ctx(L, 1);	const char *msg = luaL_checkstring(L, 2); 	(void)ctx;	clm_debug("lua plugin: %s", msg);	return 0;} static const luaL_Reg ctx_methods[] = {	{"complete", lua_ctx_complete},	{"fail", lua_ctx_fail},	{"args_raw", lua_ctx_args_raw},	{"log", lua_ctx_log},	{NULL, NULL},}; static voidregister_ctx_meta(lua_State *L){	luaL_newmetatable(L, CLM_LUA_CTX_META);	lua_pushvalue(L, -1);	lua_setfield(L, -2, "__index");	luaL_setfuncs(L, ctx_methods, 0);	lua_pop(L, 1);} static int lua_task_wait(lua_State *L);static int lua_task_result(lua_State *L);static int lua_task_cancel_method(lua_State *L); static struct clm_lua_task *check_task(lua_State *L, int idx){	return luaL_checkudata(L, idx, CLM_LUA_TASK_META);} static intlua_task_gc(lua_State *L){	struct clm_lua_task *task = check_task(L, 1); 	if (task->results_ref != LUA_NOREF) {		luaL_unref(L, LUA_REGISTRYINDEX, task->results_ref);		task->results_ref = LUA_NOREF;	}	free(task->error);	task->error = NULL;	return 0;} static const luaL_Reg task_methods[] = {	{"wait", lua_task_wait},	{"result", lua_task_result},	{"cancel", lua_task_cancel_method},	{NULL, NULL},}; static voidregister_task_meta(lua_State *L){	luaL_newmetatable(L, CLM_LUA_TASK_META);	lua_pushvalue(L, -1);	lua_setfield(L, -2, "__index");	luaL_setfuncs(L, task_methods, 0);	lua_pushcfunction(L, lua_task_gc);	lua_setfield(L, -2, "__gc");	lua_pushstring(L, "clm task");	lua_setfield(L, -2, "__name");	lua_pop(L, 1);} /* ------------------------------------------------------------------ *//* Tool invoke bridge                                                  *//* ------------------------------------------------------------------ */ /* * Stored per registered Lua tool: the plugin and the Lua registry reference * for the invoke function. */struct lua_tool_user {	struct clm_lua_plugin *plugin;	int invoke_ref; /* LUA_REGISTRYINDEX reference to the invoke fn */}; /* ------------------------------------------------------------------ *//* CPU timeout hook                                                    *//* ------------------------------------------------------------------ */ static uint64_tclock_ns(void){	struct timespec ts;	clock_gettime(CLOCK_MONOTONIC, &ts);	return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec;} /* * lua_sethook count callback. Fires every CLM_LUA_HOOK_INTERVAL bytecode * instructions. Checks if the plugin's wall-clock deadline has been exceeded * and raises an error if so, unwinding to lua_resume. */static voidlua_exec_hook(lua_State *L, lua_Debug *ar){	(void)ar;	/* Retrieve the plugin pointer from the registry. */	lua_getfield(L, LUA_REGISTRYINDEX, "_clm_plugin");	struct clm_lua_plugin *p = lua_touserdata(L, -1);	lua_pop(L, 1); 	if (p == NULL || p->deadline_ns == 0)		return;	if (clock_ns() > p->deadline_ns)		luaL_error(L, "plugin exceeded execution time budget");} /* * resume a tool coroutine with a fresh execution deadline. the deadline is * active only while lua bytecode runs, so time suspended on async work does * not consume the execution budget. */intclm_lua_resume_with_deadline(struct clm_lua_plugin *plugin, lua_State *co,    lua_State *from, int nargs, int *nresults, uint64_t timeout_ms){	int rc; 	if (timeout_ms == 0)		timeout_ms = CLM_LUA_EXEC_TIMEOUT_MS;	struct clm_lua_coroutine *lco = clm_lua_coroutine_find(co);	uint64_t previous_deadline = plugin->deadline_ns;	plugin->deadline_ns = clock_ns() + timeout_ms * 1000000ULL;	lua_sethook(co, lua_exec_hook, LUA_MASKCOUNT, CLM_LUA_HOOK_INTERVAL);	if (lco != NULL)		lco->running = true;	rc = lua_resume(co, from, nargs, nresults);	if (lco != NULL)		lco->running = false;	plugin->deadline_ns = previous_deadline;	lua_sethook(co, NULL, 0, 0);	return rc;} static void lua_task_cancel_internal(struct clm_lua_task *task,    const char *reason);static void lua_wait_task_done(struct clm_lua_task *task); /* register a coroutine while its thread object is on owner's stack. */struct clm_lua_coroutine *clm_lua_coroutine_register(struct clm_lua_plugin *plugin,    struct clm_lua_call *call, lua_State *owner, lua_State *co, bool is_main){	struct clm_lua_coroutine *lco; 	if (plugin == NULL || call == NULL || owner == NULL || co == NULL ||	    lua_tothread(owner, -1) != co)		return NULL;	lco = calloc(1, sizeof(*lco));	if (lco == NULL)		return NULL;	lco->plugin = plugin;	lco->call = call;	lco->co = co;	lco->main_L = plugin->L;	lco->agent = plugin->agent;	lco->is_main = is_main;	TAILQ_INIT(&lco->pendings); 	/* lua_newthread left the thread on owner; copy and root that value. */	lua_pushvalue(owner, -1);	lco->ref = luaL_ref(owner, LUA_REGISTRYINDEX);	TAILQ_INSERT_TAIL(&plugin->coroutines, lco, entry);	call->active_coroutines++;	return lco;} voidclm_lua_coroutine_unregister(struct clm_lua_coroutine *lco){	struct clm_lua_call *call; 	if (lco == NULL)		return;	while (!TAILQ_EMPTY(&lco->pendings)) {		struct clm_lua_pending *pending = TAILQ_FIRST(&lco->pendings);		(void)clm_lua_pending_remove(pending);		pending->teardown(pending);	}	if (lco->task != NULL) {		lua_task_cancel_internal(lco->task, "task cancelled");

Lines 1–500 of 2759.