2160 lines · 59.3 KB · C
RawDownload
// SPDX-License-Identifier: ISC#include <dirent.h>#include <errno.h>#include <setjmp.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); /* Invocation-thread guard, consulted by the http bindings (lua_http.c). */void clm_lua_mark_invocation_thread(lua_State *L, lua_State *co, int on);int clm_lua_is_invocation_thread(lua_State *L); /* Clears the per-invocation registry stash (_clm_co_ref/_clm_tool_name/ * _clm_inv, set in lua_tool_invoke below) once a coroutine is truly done. * Called from both here and lua_http.c's async completion callbacks. */void clm_lua_clear_invocation_registry(lua_State *L); #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 */ /* How far the heap may grow past what is live before the next cycle. Lua's * default of 200 means it doubles, which against an 8 MiB cap reaches the cap * on garbage alone once a plugin holds a few MiB. */#define CLM_LUA_GC_PAUSE 130#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 */}; TAILQ_HEAD(clm_lua_pending_list, clm_lua_pending); /* 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;	bool tearing_down;	/* Unprotected-call recovery: see lua_plugin_panic. */	jmp_buf panic_jmp;	bool panic_ready;	bool dead;}; 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,    struct clm_lua_pending *pending, clm_lua_pending_teardown_fn teardown){	if (plugin == NULL || pending == NULL || teardown == NULL)		return -EINVAL;	if (plugin->tearing_down)		return -ECANCELED;	pending->plugin = plugin;	pending->teardown = teardown;	TAILQ_INSERT_TAIL(&plugin->pending, pending, entry);	return 0;} struct clm_lua_plugin *clm_lua_pending_remove(struct clm_lua_pending *pending){	struct clm_lua_plugin *plugin; 	if (pending == NULL || pending->plugin == NULL)		return NULL;	plugin = pending->plugin;	TAILQ_REMOVE(&plugin->pending, pending, entry);	pending->plugin = 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;	}	/* Only growth is capped. A shrink already returns memory, and the	 * collector reallocates downward while it works, so refusing those	 * would make a plugin at its limit unable to get back under it. */	if (nsize > real_osize &&	    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;} /* ------------------------------------------------------------------ *//* Panic and warning handlers                                          *//* ------------------------------------------------------------------ */ /* Where an error raised outside any protected call lands; without this Lua * aborts the process. Callers arm panic_ready around bare C API calls and * recover through panic_jmp. Nothing unwound the state, so the plugin is * then dead and L must never be touched again, lua_close included. */static intlua_plugin_panic(lua_State *L){	struct clm_lua_plugin *p = NULL;	const char *msg = lua_tostring(L, -1); 	(void)lua_getallocf(L, (void **)&p);	clm_debug("lua panic [%s]: %s", p && p->path ? p->path : "?",	    msg ? msg : "(unknown)");	if (p != NULL && p->panic_ready) {		p->panic_ready = false;		p->dead = true;		longjmp(p->panic_jmp, 1);	}	return 0; /* returning from here aborts, as before */} /* Where a Lua warning goes; with no warn function Lua drops them. The one * that matters is an error inside a __gc handler: Lua reports a failing * finalizer through this path instead of raising it. */static voidlua_plugin_warn(void *ud, const char *msg, int tocont){	struct clm_lua_plugin *p = ud; 	(void)tocont;	clm_debug("lua warning [%s]: %s", p && p->path ? p->path : "?",	    msg ? msg : "");} /* ------------------------------------------------------------------ *//* Invocation context userdata                                         *//* ------------------------------------------------------------------ */ #define CLM_LUA_CTX_META "clm_tool_ctx" struct lua_tool_ctx {	struct clm_tool_invocation *inv;	lua_State *main_L; /* plugin's main state */	int co_ref;        /* registry ref keeping coroutine alive */	bool completed;}; 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);	const char *result; 	if (ctx->completed)		return luaL_error(L, "tool already completed");	result = luaL_checkstring(L, 2);	ctx->completed = true;	clm_tool_complete(ctx->inv, result);	return 0;} static intlua_ctx_fail(lua_State *L){	struct lua_tool_ctx *ctx = check_ctx(L, 1);	const char *msg; 	if (ctx->completed)		return luaL_error(L, "tool already completed");	msg = luaL_checkstring(L, 2);	ctx->completed = true;	clm_tool_fail(ctx->inv, msg);	return 0;} static intlua_ctx_args_raw(lua_State *L){	struct lua_tool_ctx *ctx = check_ctx(L, 1);	const char *args = clm_tool_invocation_args(ctx->inv); 	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);} /* ------------------------------------------------------------------ *//* 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;	plugin->deadline_ns = clock_ns() + timeout_ms * 1000000ULL;	lua_sethook(co, lua_exec_hook, LUA_MASKCOUNT, CLM_LUA_HOOK_INTERVAL);	rc = lua_resume(co, from, nargs, nresults);	plugin->deadline_ns = 0;	lua_sethook(co, NULL, 0, 0);	return rc;} /* * Mark (on != 0) or clear (on == 0) `co` as a live tool-invocation coroutine, * keyed by its thread pointer in the registry. clm's async HTTP model requires * that http.get/post run directly on this coroutine, so their lua_yield unwinds * to clm's C-level lua_resume; the http bindings consult this marker to reject * calls from a nested coroutine or from load time (the main thread), which * would otherwise drive completion tracking out of band. Keyed per-thread, so * concurrent invocations of one plugin do not alias one another. */voidclm_lua_mark_invocation_thread(lua_State *L, lua_State *co, int on){	lua_pushlightuserdata(L, co);	if (on)		lua_pushboolean(L, 1);	else		lua_pushnil(L);	lua_settable(L, LUA_REGISTRYINDEX);} /* True if the running state L is a live tool-invocation coroutine. */intclm_lua_is_invocation_thread(lua_State *L){	int ok;	lua_pushlightuserdata(L, L);	lua_gettable(L, LUA_REGISTRYINDEX);	ok = lua_toboolean(L, -1);	lua_pop(L, 1);	return ok;} /* * lua_tool_invoke stashes _clm_co_ref/_clm_tool_name/_clm_inv in the * (shared, coroutine-and-main-state-wide) registry so http.get/post can * retrieve them from an async callback. They were only ever being * overwritten by the next invocation, never explicitly cleared -- so * between calls the registry held a stale _clm_inv lightuserdata pointing * at an already-freed clm_tool_invocation, and kept the previous call's * tool-name string rooted for no reason. Call this once a coroutine is * truly finished (same terminal points as the co_ref unref). */voidclm_lua_clear_invocation_registry(lua_State *L){	lua_pushnil(L);	lua_setfield(L, LUA_REGISTRYINDEX, "_clm_co_ref");	lua_pushnil(L);	lua_setfield(L, LUA_REGISTRYINDEX, "_clm_tool_name");	lua_pushnil(L);	lua_setfield(L, LUA_REGISTRYINDEX, "_clm_inv");	/* Nudge the collector now rather than waiting for its own pacing to	 * notice -- an incremental step is cheap and reclaims the coroutine	 * (now genuinely unreferenced) and its stack contents promptly	 * instead of letting several turns' worth of garbage pile up under	 * the plugin's tight memory cap before the GC gets around to it. */	lua_gc(L, LUA_GCSTEP, 0);} /* * Log resource peaks for this plugin after a tool call completes. */voidclm_lua_budget_report(struct clm_lua_plugin *plugin, const char *tool_name){	struct clm_lua_budget *b = &plugin->budget;	uint64_t elapsed_ns = clock_ns() - b->call_start_ns; 	/* Update peaks before reporting. */	if (elapsed_ns > b->peak_call_ns)		b->peak_call_ns = elapsed_ns;	if (b->http_total > b->peak_http_total)		b->peak_http_total = b->http_total; 	size_t elapsed_ms = (size_t)(elapsed_ns / 1000000ULL);	size_t peak_ms = (size_t)(b->peak_call_ns / 1000000ULL); 	clm_debug("lua budget [%s]: call=%zums mem_peak=%zu/%zu "	          "http=%zu http_peak_inflight=%zu peak_call=%zums",	    tool_name, elapsed_ms, b->peak_mem, plugin->mem_limit,	    b->http_total, b->peak_http_inflight, peak_ms);} /* * Called by lua_http.c when an HTTP request completes (success or error). * Decrements the in-flight counter. plugin may be NULL (defensive). */voidclm_lua_http_done(struct clm_lua_plugin *plugin){	if (plugin == NULL)		return;	if (plugin->budget.http_inflight > 0)		plugin->budget.http_inflight--;} /* * Called by lua_http.c before starting an HTTP request. Returns 0 if allowed, * -1 if the budget is exceeded (caller should raise an error). */

Lines 1–500 of 2160.