// SPDX-FileCopyrightText: © 2022—2026 Vladimir Zorin // SPDX-License-Identifier: LicenseRef-OWL-1.0-or-later // Licensed under OWL v1.0+. See LICENSE. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef __NR_pidfd_open #define __NR_pidfd_open 434 #endif #ifndef P_PIDFD #define P_PIDFD 3 #endif #include "litls.h" #include "tls13.h" /* ── constants ─────────────────────────────────────────────────────── */ #define LEV_LOOP_MT "lev.loop" #define LEV_UDP_MT "lev.udp" #define LEV_TCP_MT "lev.tcp" #define LEV_SUBPROCESS_MT "lev.subprocess" #define LEV_TLS_IDENTITY_MT "lev.tls_identity" #define LEV_MAX_EVENTS 64 #define LEV_INIT_FD_CAP 256 #define LEV_INIT_TIMER 32 #define LEV_INIT_READY 64 #define LEV_MAX_SIGNALS 32 #define LEV_TCP_RECV_BUF 16384 #define LEV_DEFAULT_MAXLEN 4096 #define LEV_DEFAULT_BACKLOG 128 #define RETURN_ERR(L) \ do { \ lua_pushnil(L); \ lua_pushstring(L, strerror(errno)); \ return 2; \ } while (0) #define RETURN_CUSTOM_ERR(L, msg) \ do { \ lua_pushnil(L); \ lua_pushstring(L, msg); \ return 2; \ } while (0) /* ── timer min-heap ────────────────────────────────────────────────── */ typedef struct { double deadline; uint32_t id; int co_ref; /* LUA_NOREF if cancelled */ int fd; /* fd this timer is a timeout for, -1 for standalone (sleep) timers */ } lev_timer_t; /* ── fd wait entry ─────────────────────────────────────────────────── */ typedef struct { int co_ref; /* waiting coroutine, LUA_NOREF if none */ uint32_t timer_id; /* associated timeout timer, 0 if none */ } lev_fd_wait_t; /* ── ready queue entry ─────────────────────────────────────────────── */ typedef struct { int co_ref; int nargs; } lev_ready_t; /* ── main loop state ───────────────────────────────────────────────── */ typedef struct { int epfd; lev_fd_wait_t *fd_waits; int fd_waits_cap; lev_timer_t *timers; int timer_count; int timer_cap; uint32_t next_timer_id; lev_ready_t *ready; int ready_count; int ready_cap; int signal_fd; sigset_t orig_sigmask; int sig_handler_refs[LEV_MAX_SIGNALS]; int on_complete_ref; int active_coros; int registered_fds; int running; lua_State *L; } lev_loop_t; /* ── UDP socket userdata ───────────────────────────────────────────── */ typedef struct { int fd; } lev_udp_t; /* ── TCP socket userdata ───────────────────────────────────────────── */ typedef struct { int fd; litls_tls13_ctx *tls; /* LITLS client/server, NULL until starttls */ litls_x509_trust_anchor *tls_anchors; /* LITLS client: trust anchors (owned, freed on close) */ int tls_mode; /* 0=none, 1=client, 2=server */ litls_server_identity *srv_default_id; /* LITLS server: default identity */ litls_server_identity *srv_sni_ids; /* LITLS server: SNI identities */ int srv_sni_count; int srv_ids_owned; /* 1=allocated per-connection (free on close), 0=cached (don't free) */ litls_server_identity *client_identity; /* mTLS client cert+key (owned, freed on close) */ } lev_tcp_t; /* ── subprocess userdata ───────────────────────────────────────────── */ typedef struct { pid_t pid; int pidfd; /* -1 if closed */ int stdin_fd; /* parent write end, -1 if closed */ int stdout_fd; /* parent read end, -1 if closed */ int stderr_fd; /* parent read end, -1 if closed */ int exited; int exit_code; int exit_signal; /* >0 if killed by signal */ } lev_subprocess_t; /* ── global loop pointer (one active loop at a time) ───────────────── */ static lev_loop_t *g_loop = NULL; /* ── monotonic clock ───────────────────────────────────────────────── */ static double monotonic_now(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; } static int l_now(lua_State *L) { lua_pushnumber(L, monotonic_now()); return 1; } /* ── timer heap operations ─────────────────────────────────────────── */ static void heap_swap(lev_timer_t *a, lev_timer_t *b) { lev_timer_t tmp = *a; *a = *b; *b = tmp; } static void heap_sift_up(lev_timer_t *h, int i) { while (i > 0) { int parent = (i - 1) / 2; if (h[parent].deadline <= h[i].deadline) break; heap_swap(&h[parent], &h[i]); i = parent; } } static void heap_sift_down(lev_timer_t *h, int n, int i) { while (1) { int smallest = i; int left = 2 * i + 1; int right = 2 * i + 2; if (left < n && h[left].deadline < h[smallest].deadline) smallest = left; if (right < n && h[right].deadline < h[smallest].deadline) smallest = right; if (smallest == i) break; heap_swap(&h[i], &h[smallest]); i = smallest; } } static int heap_push(lev_loop_t *loop, lev_timer_t timer) { if (loop->timer_count >= loop->timer_cap) { int new_cap = loop->timer_cap * 2; lev_timer_t *new_timers = (lev_timer_t *)realloc(loop->timers, (size_t)new_cap * sizeof(lev_timer_t)); if (!new_timers) return -1; loop->timers = new_timers; loop->timer_cap = new_cap; } loop->timers[loop->timer_count] = timer; heap_sift_up(loop->timers, loop->timer_count); loop->timer_count++; return 0; } static lev_timer_t heap_pop(lev_loop_t *loop) { lev_timer_t top = loop->timers[0]; loop->timer_count--; if (loop->timer_count > 0) { loop->timers[0] = loop->timers[loop->timer_count]; heap_sift_down(loop->timers, loop->timer_count, 0); } return top; } static void cancel_timer_by_id(lev_loop_t *loop, uint32_t id) { for (int i = 0; i < loop->timer_count; i++) { if (loop->timers[i].id == id) { loop->timers[i].co_ref = LUA_NOREF; return; } } } /* ── fd wait registry ──────────────────────────────────────────────── */ static int ensure_fd_cap(lev_loop_t *loop, int fd) { if (fd < loop->fd_waits_cap) return 0; int new_cap = loop->fd_waits_cap; while (new_cap <= fd) { if (new_cap > INT_MAX / 2) return -1; new_cap *= 2; } lev_fd_wait_t *new_waits = (lev_fd_wait_t *)realloc(loop->fd_waits, (size_t)new_cap * sizeof(lev_fd_wait_t)); if (!new_waits) return -1; for (int i = loop->fd_waits_cap; i < new_cap; i++) { new_waits[i].co_ref = LUA_NOREF; new_waits[i].timer_id = 0; } loop->fd_waits = new_waits; loop->fd_waits_cap = new_cap; return 0; } /* ── ready queue ───────────────────────────────────────────────────── */ static int ready_push(lev_loop_t *loop, int co_ref, int nargs) { if (loop->ready_count >= loop->ready_cap) { int new_cap = loop->ready_cap * 2; lev_ready_t *new_ready = (lev_ready_t *)realloc(loop->ready, (size_t)new_cap * sizeof(lev_ready_t)); if (!new_ready) return -1; loop->ready = new_ready; loop->ready_cap = new_cap; } loop->ready[loop->ready_count].co_ref = co_ref; loop->ready[loop->ready_count].nargs = nargs; loop->ready_count++; return 0; } /* ── coroutine resume + completion callback ────────────────────────── */ static void call_on_complete(lev_loop_t *loop, lua_State *co, int ok, int nresults) { lua_State *L = loop->L; loop->active_coros--; if (loop->on_complete_ref == LUA_NOREF) return; /* callback + thread + ok + results, plus pcall slack */ if (!lua_checkstack(L, nresults + 4)) { fprintf(stderr, "lev: on_complete skipped (stack overflow)\n"); return; } /* Push the on_complete callback */ lua_rawgeti(L, LUA_REGISTRYINDEX, loop->on_complete_ref); /* Push the coroutine thread */ lua_pushthread(co); lua_xmove(co, L, 1); /* Push ok boolean */ lua_pushboolean(L, ok); /* Transfer results/error from co to L */ if (nresults > 0) { /* Move the top nresults from co's stack to L */ lua_xmove(co, L, nresults); } /* Call: on_complete(co, ok, results...) */ int pcall_status = lua_pcall(L, 2 + nresults, 0, 0); if (pcall_status != 0) { fprintf(stderr, "lev: on_complete error: %s\n", lua_tostring(L, -1)); lua_pop(L, 1); } } static void resume_coroutine(lev_loop_t *loop, int co_ref, int nargs) { lua_State *L = loop->L; /* Fetch the coroutine and keep it on L's stack as a GC anchor for the whole resume. LuaJIT does not root a coroutine merely because it is being resumed, and we drop its registry reference just below while co_to_task holds it only weakly. Without this anchor, a GC cycle triggered by allocation *inside* the coroutine (e.g. parsing a large document) can collect the running coroutine and free its stack — a use-after-free that crashes in the next stack-growth/return. */ lua_rawgeti(L, LUA_REGISTRYINDEX, co_ref); lua_State *co = lua_tothread(L, -1); /* Release the registry ref — the coroutine is either going to re-register itself (if it yields again) or finish. The on-stack anchor above keeps it reachable until this resume returns. */ luaL_unref(L, LUA_REGISTRYINDEX, co_ref); /* Defensive: never resume a dead coroutine. Resumable states are LUA_YIELD, or status 0 with the function still on the stack (not yet started). Anything else means a stale ready-queue entry (e.g. a double enqueue) — resuming would corrupt the Lua stack. Its completion was already accounted, so skip without touching counters. */ int co_status = lua_status(co); if (co_status != LUA_YIELD && !(co_status == 0 && lua_gettop(co) > 0)) { fprintf(stderr, "lev: BUG: skipping resume of dead coroutine\n"); lua_pop(L, 1); /* drop the GC anchor */ return; } int status = lua_resume(co, nargs); if (status == 0) { /* Coroutine finished successfully */ int nresults = lua_gettop(co); call_on_complete(loop, co, 1, nresults); } else if (status == LUA_YIELD) { /* Coroutine yielded — it should have re-registered itself */ } else { /* Coroutine errored: only the error value on top of co's stack is meaningful; any locals still on the stack are incidental. Pass just that one value so on_complete's first vararg is the error. */ call_on_complete(loop, co, 0, 1); } lua_pop(L, 1); /* drop the GC anchor */ } /* ── signal dispatch ───────────────────────────────────────────────── */ static void dispatch_signals(lev_loop_t *loop) { struct signalfd_siginfo ssi; while (1) { ssize_t n = read(loop->signal_fd, &ssi, sizeof(ssi)); if (n != sizeof(ssi)) break; int signo = (int)ssi.ssi_signo; if (signo >= 0 && signo < LEV_MAX_SIGNALS && loop->sig_handler_refs[signo] != LUA_NOREF) { lua_State *L = loop->L; lua_rawgeti(L, LUA_REGISTRYINDEX, loop->sig_handler_refs[signo]); lua_pushinteger(L, signo); int pcall_status = lua_pcall(L, 1, 0, 0); if (pcall_status != 0) { fprintf(stderr, "lev: signal handler error: %s\n", lua_tostring(L, -1)); lua_pop(L, 1); } } } } /* ── loop lifecycle ────────────────────────────────────────────────── */ static lev_loop_t *check_loop(lua_State *L, int idx) { return (lev_loop_t *)luaL_checkudata(L, idx, LEV_LOOP_MT); } static int l_loop_new(lua_State *L) { int epfd = epoll_create1(EPOLL_CLOEXEC); if (epfd < 0) RETURN_ERR(L); lev_loop_t *loop = (lev_loop_t *)lua_newuserdata(L, sizeof(lev_loop_t)); memset(loop, 0, sizeof(lev_loop_t)); loop->epfd = epfd; loop->fd_waits = (lev_fd_wait_t *)calloc((size_t)LEV_INIT_FD_CAP, sizeof(lev_fd_wait_t)); if (!loop->fd_waits) { close(epfd); RETURN_CUSTOM_ERR(L, "memory allocation failed"); } loop->fd_waits_cap = LEV_INIT_FD_CAP; for (int i = 0; i < LEV_INIT_FD_CAP; i++) { loop->fd_waits[i].co_ref = LUA_NOREF; loop->fd_waits[i].timer_id = 0; } loop->timers = (lev_timer_t *)malloc((size_t)LEV_INIT_TIMER * sizeof(lev_timer_t)); if (!loop->timers) { free(loop->fd_waits); close(epfd); RETURN_CUSTOM_ERR(L, "memory allocation failed"); } loop->timer_cap = LEV_INIT_TIMER; loop->timer_count = 0; loop->next_timer_id = 1; loop->ready = (lev_ready_t *)malloc((size_t)LEV_INIT_READY * sizeof(lev_ready_t)); if (!loop->ready) { free(loop->timers); free(loop->fd_waits); close(epfd); RETURN_CUSTOM_ERR(L, "memory allocation failed"); } loop->ready_cap = LEV_INIT_READY; loop->ready_count = 0; loop->signal_fd = -1; sigemptyset(&loop->orig_sigmask); for (int i = 0; i < LEV_MAX_SIGNALS; i++) loop->sig_handler_refs[i] = LUA_NOREF; loop->on_complete_ref = LUA_NOREF; loop->active_coros = 0; loop->running = 0; loop->L = NULL; luaL_getmetatable(L, LEV_LOOP_MT); lua_setmetatable(L, -2); return 1; } /* Abort the loop with nil + an error message (deadlock, epoll failure). Pending waiter refs are released later by loop_destroy/loop_cleanup. */ static int loop_exit_err(lev_loop_t *loop, lua_State *L, const char *msg) { if (loop->on_complete_ref != LUA_NOREF) { luaL_unref(L, LUA_REGISTRYINDEX, loop->on_complete_ref); loop->on_complete_ref = LUA_NOREF; } g_loop = NULL; loop->running = 0; loop->L = NULL; lua_pushnil(L); lua_pushstring(L, msg); return 2; } static int l_loop_run(lua_State *L) { lev_loop_t *loop = check_loop(L, 1); if (g_loop) return luaL_error(L, "lev: another loop is already running"); /* arg 2: on_complete callback */ luaL_checktype(L, 2, LUA_TFUNCTION); lua_pushvalue(L, 2); loop->on_complete_ref = luaL_ref(L, LUA_REGISTRYINDEX); loop->L = L; loop->running = 1; g_loop = loop; struct epoll_event events[LEV_MAX_EVENTS]; while (loop->running && loop->active_coros > 0) { /* Compute timeout from timer heap */ int timeout_ms = -1; if (loop->ready_count > 0) { timeout_ms = 0; } else if (loop->timer_count > 0) { double now = monotonic_now(); double diff = loop->timers[0].deadline - now; if (diff <= 0) timeout_ms = 0; else { if (diff > (double)INT_MAX / 1000.0) diff = (double)INT_MAX / 1000.0; timeout_ms = (int)(diff * 1000.0); if (timeout_ms < 1) timeout_ms = 1; } } /* Deadlock detection: all coroutines blocked with no pending I/O, timers, or signal handlers to wake them */ if (timeout_ms == -1 && loop->registered_fds == 0 && loop->active_coros > 0 && loop->signal_fd < 0) return loop_exit_err(loop, L, "deadlock: all coroutines blocked with no pending I/O or timers"); int n = epoll_wait(loop->epfd, events, LEV_MAX_EVENTS, timeout_ms); if (n < 0) { if (errno == EINTR) continue; char msg[128]; snprintf(msg, sizeof(msg), "epoll_wait: %s", strerror(errno)); return loop_exit_err(loop, L, msg); } /* 1. Process I/O events */ for (int i = 0; i < n; i++) { int fd = events[i].data.fd; if (fd == loop->signal_fd) { dispatch_signals(loop); continue; } if (fd >= loop->fd_waits_cap) continue; lev_fd_wait_t *w = &loop->fd_waits[fd]; if (w->co_ref == LUA_NOREF) continue; /* Cancel associated timeout timer */ if (w->timer_id > 0) { cancel_timer_by_id(loop, w->timer_id); w->timer_id = 0; } /* Get coroutine and push true as resume arg */ int co_ref = w->co_ref; w->co_ref = LUA_NOREF; if (epoll_ctl(loop->epfd, EPOLL_CTL_DEL, fd, NULL) == 0) loop->registered_fds--; /* Push true onto the coroutine's stack before resuming */ lua_rawgeti(L, LUA_REGISTRYINDEX, co_ref); lua_State *co = lua_tothread(L, -1); lua_pop(L, 1); /* Use ready queue to avoid reentrant resume issues */ if (!lua_checkstack(co, 1) || ready_push(loop, co_ref, 1) < 0) { fprintf(stderr, "lev: waiter enqueue failed (OOM), coroutine lost\n"); luaL_unref(L, LUA_REGISTRYINDEX, co_ref); loop->active_coros--; continue; } lua_pushboolean(co, 1); } /* 2. Fire expired timers */ double now = monotonic_now(); while (loop->timer_count > 0 && loop->timers[0].deadline <= now) { lev_timer_t top = heap_pop(loop); if (top.co_ref == LUA_NOREF) continue; /* lazy cancel */ /* Timeout timer for an fd wait — deregister the fd. A live co_ref guarantees the wait entry still references this timer (completion paths lazily cancel the timer first). */ int is_fd_timeout = 0; if (top.fd >= 0 && top.fd < loop->fd_waits_cap && loop->fd_waits[top.fd].timer_id == top.id && loop->fd_waits[top.fd].co_ref != LUA_NOREF) { loop->fd_waits[top.fd].co_ref = LUA_NOREF; loop->fd_waits[top.fd].timer_id = 0; if (epoll_ctl(loop->epfd, EPOLL_CTL_DEL, top.fd, NULL) == 0) loop->registered_fds--; is_fd_timeout = 1; } lua_rawgeti(L, LUA_REGISTRYINDEX, top.co_ref); lua_State *co = lua_tothread(L, -1); lua_pop(L, 1); if (!lua_checkstack(co, 2) || ready_push(loop, top.co_ref, is_fd_timeout ? 2 : 1) < 0) { fprintf(stderr, "lev: waiter enqueue failed (OOM), coroutine lost\n"); luaL_unref(L, LUA_REGISTRYINDEX, top.co_ref); loop->active_coros--; continue; } if (is_fd_timeout) { /* fd wait timed out: resume with nil, "timeout" */ lua_pushnil(co); lua_pushstring(co, "timeout"); } else { /* Sleep timer expired: resume with true */ lua_pushboolean(co, 1); } } /* 3. Drain ready queue (snapshot count) */ int ready_n = loop->ready_count; for (int i = 0; i < ready_n; i++) { lev_ready_t r = loop->ready[i]; resume_coroutine(loop, r.co_ref, r.nargs); } if (ready_n > 0) { int remaining = loop->ready_count - ready_n; if (remaining > 0) memmove(loop->ready, loop->ready + ready_n, (size_t)remaining * sizeof(lev_ready_t)); loop->ready_count = remaining; } } /* Clean up */ if (loop->on_complete_ref != LUA_NOREF) { luaL_unref(L, LUA_REGISTRYINDEX, loop->on_complete_ref); loop->on_complete_ref = LUA_NOREF; } g_loop = NULL; loop->running = 0; loop->L = NULL; return 0; } static int l_loop_stop(lua_State *L) { lev_loop_t *loop = check_loop(L, 1); loop->running = 0; return 0; } static int l_loop_stats(lua_State *L) { lev_loop_t *loop = check_loop(L, 1); lua_newtable(L); lua_pushinteger(L, loop->active_coros); lua_setfield(L, -2, "active_coros"); lua_pushinteger(L, loop->registered_fds); lua_setfield(L, -2, "registered_fds"); lua_pushinteger(L, loop->timer_count); lua_setfield(L, -2, "timer_count"); lua_pushinteger(L, loop->ready_count); lua_setfield(L, -2, "ready_count"); return 1; } static void loop_cleanup(lev_loop_t *loop, lua_State *L) { /* Unref any pending coroutines in fd_waits */ for (int i = 0; i < loop->fd_waits_cap; i++) { if (loop->fd_waits[i].co_ref != LUA_NOREF) { luaL_unref(L, LUA_REGISTRYINDEX, loop->fd_waits[i].co_ref); loop->fd_waits[i].co_ref = LUA_NOREF; } if (loop->fd_waits[i].timer_id > 0) { cancel_timer_by_id(loop, loop->fd_waits[i].timer_id); loop->fd_waits[i].timer_id = 0; } } /* Unref any pending coroutines in timers */ for (int i = 0; i < loop->timer_count; i++) { if (loop->timers[i].co_ref != LUA_NOREF) { luaL_unref(L, LUA_REGISTRYINDEX, loop->timers[i].co_ref); loop->timers[i].co_ref = LUA_NOREF; } } /* Unref any pending coroutines in ready queue */ for (int i = 0; i < loop->ready_count; i++) { luaL_unref(L, LUA_REGISTRYINDEX, loop->ready[i].co_ref); } loop->ready_count = 0; /* Unref signal handlers */ for (int i = 0; i < LEV_MAX_SIGNALS; i++) { if (loop->sig_handler_refs[i] != LUA_NOREF) { luaL_unref(L, LUA_REGISTRYINDEX, loop->sig_handler_refs[i]); loop->sig_handler_refs[i] = LUA_NOREF; } } /* Unref on_complete */ if (loop->on_complete_ref != LUA_NOREF) { luaL_unref(L, LUA_REGISTRYINDEX, loop->on_complete_ref); loop->on_complete_ref = LUA_NOREF; } /* Restore signal mask and close signalfd */ if (loop->signal_fd >= 0) { sigprocmask(SIG_SETMASK, &loop->orig_sigmask, NULL); close(loop->signal_fd); loop->signal_fd = -1; } /* Close epoll fd */ if (loop->epfd >= 0) { close(loop->epfd); loop->epfd = -1; } free(loop->fd_waits); loop->fd_waits = NULL; free(loop->timers); loop->timers = NULL; free(loop->ready); loop->ready = NULL; } static int l_loop_destroy(lua_State *L) { lev_loop_t *loop = check_loop(L, 1); if (loop->epfd >= 0) loop_cleanup(loop, L); return 0; } static int l_loop_gc(lua_State *L) { return l_loop_destroy(L); } /* ── spawn: add coroutine to ready queue ───────────────────────────── */ static int l_spawn(lua_State *L) { lev_loop_t *loop = check_loop(L, 1); luaL_checktype(L, 2, LUA_TTHREAD); /* Ref the coroutine */ lua_pushvalue(L, 2); int co_ref = luaL_ref(L, LUA_REGISTRYINDEX); if (ready_push(loop, co_ref, 0) < 0) { luaL_unref(L, LUA_REGISTRYINDEX, co_ref); return luaL_error(L, "lev: memory allocation failed"); } loop->active_coros++; return 0; } /* ── yield_to_loop: pure lua_yield ─────────────────────────────────── */ static int l_yield_to_loop(lua_State *L) { (void)L; return lua_yield(L, 0); } /* ── ready_enqueue: add coroutine to ready queue with pre-pushed args */ static int l_ready_enqueue(lua_State *L) { lev_loop_t *loop = check_loop(L, 1); luaL_checktype(L, 2, LUA_TTHREAD); int nargs = luaL_optint(L, 3, 0); lua_pushvalue(L, 2); int co_ref = luaL_ref(L, LUA_REGISTRYINDEX); if (ready_push(loop, co_ref, nargs) < 0) { luaL_unref(L, LUA_REGISTRYINDEX, co_ref); return luaL_error(L, "lev: memory allocation failed"); } return 0; } /* ── wait_readable / wait_writable ─────────────────────────────────── */ static int l_wait_fd(lua_State *L, uint32_t epoll_events) { if (!g_loop) return luaL_error(L, "lev: no event loop running"); lev_loop_t *loop = g_loop; int fd = luaL_checkinteger(L, 1); double timeout = luaL_optnumber(L, 2, -1.0); if (fd < 0) return luaL_error(L, "lev: invalid fd"); if (ensure_fd_cap(loop, fd) < 0) return luaL_error(L, "lev: memory allocation failed"); if (loop->fd_waits[fd].co_ref != LUA_NOREF) return luaL_error(L, "lev: fd %d already has a waiter", fd); /* Register this coroutine */ lua_pushthread(L); int co_ref = luaL_ref(L, LUA_REGISTRYINDEX); loop->fd_waits[fd].co_ref = co_ref; /* Register with epoll */ struct epoll_event ev; ev.events = epoll_events; ev.data.fd = fd; if (epoll_ctl(loop->epfd, EPOLL_CTL_ADD, fd, &ev) < 0) { loop->fd_waits[fd].co_ref = LUA_NOREF; luaL_unref(L, LUA_REGISTRYINDEX, co_ref); RETURN_ERR(L); } loop->registered_fds++; /* Optional timeout */ if (timeout > 0) { lev_timer_t t; t.deadline = monotonic_now() + timeout; t.id = loop->next_timer_id++; if (loop->next_timer_id == 0) loop->next_timer_id = 1; t.co_ref = co_ref; t.fd = fd; loop->fd_waits[fd].timer_id = t.id; if (heap_push(loop, t) < 0) { epoll_ctl(loop->epfd, EPOLL_CTL_DEL, fd, NULL); loop->fd_waits[fd].co_ref = LUA_NOREF; loop->fd_waits[fd].timer_id = 0; luaL_unref(L, LUA_REGISTRYINDEX, co_ref); return luaL_error(L, "lev: memory allocation failed"); } } /* Yield — will be resumed by the main loop */ return lua_yield(L, 0); } static int l_wait_readable(lua_State *L) { return l_wait_fd(L, EPOLLIN); } static int l_wait_writable(lua_State *L) { return l_wait_fd(L, EPOLLOUT); } /* ── fd_deregister ─────────────────────────────────────────────────── */ static int l_fd_deregister(lua_State *L) { if (!g_loop) return 0; lev_loop_t *loop = g_loop; int fd = luaL_checkinteger(L, 1); if (fd < 0 || fd >= loop->fd_waits_cap) return 0; if (loop->fd_waits[fd].co_ref != LUA_NOREF) { if (epoll_ctl(loop->epfd, EPOLL_CTL_DEL, fd, NULL) == 0) loop->registered_fds--; int co_ref = loop->fd_waits[fd].co_ref; loop->fd_waits[fd].co_ref = LUA_NOREF; /* Another coroutine is blocked waiting on this fd (deregister is called from close() paths). Wake it with nil, "closed" via the ready queue — same mechanism as cancel_wait — instead of abandoning it suspended forever. */ lua_rawgeti(L, LUA_REGISTRYINDEX, co_ref); lua_State *co = lua_tothread(L, -1); lua_pop(L, 1); if (co == NULL || co == L) { /* Defensive: a running coroutine cannot hold a wait entry */ luaL_unref(L, LUA_REGISTRYINDEX, co_ref); } else if (lua_checkstack(co, 2) && ready_push(loop, co_ref, 2) == 0) { lua_pushnil(co); lua_pushstring(co, "closed"); } else { fprintf(stderr, "lev: waiter enqueue failed (OOM), coroutine lost\n"); luaL_unref(L, LUA_REGISTRYINDEX, co_ref); loop->active_coros--; } } if (loop->fd_waits[fd].timer_id > 0) { cancel_timer_by_id(loop, loop->fd_waits[fd].timer_id); loop->fd_waits[fd].timer_id = 0; } return 0; } /* ── read_fd / set_nonblocking ─────────────────────────────────────── */ /* Non-blocking read from an arbitrary fd (e.g. stdin). Returns data on success, nil,"EAGAIN" if it would block, nil,"closed" on EOF, or nil,strerror on error. The fd must already be in non-blocking mode (see l_set_nonblocking). */ static int l_read_fd(lua_State *L) { int fd = luaL_checkinteger(L, 1); int maxlen = luaL_optint(L, 2, LEV_DEFAULT_MAXLEN); if (fd < 0) RETURN_CUSTOM_ERR(L, "closed"); if (maxlen <= 0) maxlen = LEV_DEFAULT_MAXLEN; if (maxlen > LEV_TCP_RECV_BUF) maxlen = LEV_TCP_RECV_BUF; char buf[LEV_TCP_RECV_BUF]; ssize_t n = read(fd, buf, (size_t)maxlen); if (n < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { lua_pushnil(L); lua_pushstring(L, "EAGAIN"); return 2; } RETURN_ERR(L); } if (n == 0) { lua_pushnil(L); lua_pushstring(L, "closed"); return 2; } lua_pushlstring(L, buf, (size_t)n); return 1; } /* Toggle O_NONBLOCK on an arbitrary fd. Returns the previous non-blocking state (boolean) so callers can restore it, or nil,strerror on error. */ static int l_set_nonblocking(lua_State *L) { int fd = luaL_checkinteger(L, 1); int enable = lua_toboolean(L, 2); if (fd < 0) RETURN_CUSTOM_ERR(L, "invalid fd"); int flags = fcntl(fd, F_GETFL, 0); if (flags < 0) RETURN_ERR(L); int was_nonblocking = (flags & O_NONBLOCK) ? 1 : 0; int new_flags = enable ? (flags | O_NONBLOCK) : (flags & ~O_NONBLOCK); if (new_flags != flags) { if (fcntl(fd, F_SETFL, new_flags) < 0) RETURN_ERR(L); } lua_pushboolean(L, was_nonblocking); return 1; } /* ── timer_sleep ───────────────────────────────────────────────────── */ static int l_timer_sleep(lua_State *L) { if (!g_loop) return luaL_error(L, "lev: no event loop running"); lev_loop_t *loop = g_loop; double seconds = luaL_checknumber(L, 1); if (seconds != seconds) /* NaN check */ return luaL_error(L, "lev: sleep seconds must be a valid number"); if (seconds < 0) seconds = 0; lua_pushthread(L); int co_ref = luaL_ref(L, LUA_REGISTRYINDEX); lev_timer_t t; t.deadline = monotonic_now() + seconds; t.id = loop->next_timer_id++; if (loop->next_timer_id == 0) loop->next_timer_id = 1; t.co_ref = co_ref; t.fd = -1; if (heap_push(loop, t) < 0) { luaL_unref(L, LUA_REGISTRYINDEX, co_ref); return luaL_error(L, "lev: memory allocation failed"); } /* Yield — main loop resumes with true on normal expiry, or nil,"cancelled" if cancel_wait is called */ return lua_yield(L, 0); } /* ── timer_register ────────────────────────────────────────────────── */ /* Like timer_sleep but returns the timer_id instead of yielding. Lets Lua register the timer with a cancel token before yielding. */ static int l_timer_register(lua_State *L) { if (!g_loop) return luaL_error(L, "lev: no event loop running"); lev_loop_t *loop = g_loop; double seconds = luaL_checknumber(L, 1); if (seconds != seconds) /* NaN check */ return luaL_error(L, "lev: sleep seconds must be a valid number"); if (seconds < 0) seconds = 0; lua_pushthread(L); int co_ref = luaL_ref(L, LUA_REGISTRYINDEX); lev_timer_t t; t.deadline = monotonic_now() + seconds; t.id = loop->next_timer_id++; if (loop->next_timer_id == 0) loop->next_timer_id = 1; t.co_ref = co_ref; t.fd = -1; if (heap_push(loop, t) < 0) { luaL_unref(L, LUA_REGISTRYINDEX, co_ref); return luaL_error(L, "lev: memory allocation failed"); } lua_pushinteger(L, t.id); return 1; } /* ── cancel_wait ───────────────────────────────────────────────────── */ static int ref_is_thread(lua_State *L, int ref, lua_State *co) { lua_rawgeti(L, LUA_REGISTRYINDEX, ref); lua_State *t = lua_tothread(L, -1); lua_pop(L, 1); return t == co; } static int l_cancel_wait(lua_State *L) { if (!g_loop) return 0; lev_loop_t *loop = g_loop; luaL_checktype(L, 1, LUA_TTHREAD); lua_State *co = lua_tothread(L, 1); int fd = luaL_optint(L, 2, -1); uint32_t timer_id = (uint32_t)luaL_optinteger(L, 3, 0); /* Resume the coroutine only if an active wait that belongs to it is actually deregistered here. If the wait already completed (an fd event or timer expiry queued the resume earlier in this same loop iteration), this must be a no-op: pushing args and enqueueing a second resume would corrupt the coroutine's stack and double-resume it. The thread-identity check also guards against an fd that was closed and reused by another waiter within the same iteration. */ int found = 0; /* Deregister fd if provided */ if (fd >= 0 && fd < loop->fd_waits_cap) { if (loop->fd_waits[fd].co_ref != LUA_NOREF && ref_is_thread(L, loop->fd_waits[fd].co_ref, co)) { luaL_unref(L, LUA_REGISTRYINDEX, loop->fd_waits[fd].co_ref); loop->fd_waits[fd].co_ref = LUA_NOREF; if (epoll_ctl(loop->epfd, EPOLL_CTL_DEL, fd, NULL) == 0) loop->registered_fds--; if (loop->fd_waits[fd].timer_id > 0) { cancel_timer_by_id(loop, loop->fd_waits[fd].timer_id); loop->fd_waits[fd].timer_id = 0; } found = 1; } } /* Cancel standalone timer if provided */ if (timer_id > 0) { for (int i = 0; i < loop->timer_count; i++) { if (loop->timers[i].id == timer_id && loop->timers[i].co_ref != LUA_NOREF && ref_is_thread(L, loop->timers[i].co_ref, co)) { luaL_unref(L, LUA_REGISTRYINDEX, loop->timers[i].co_ref); loop->timers[i].co_ref = LUA_NOREF; found = 1; break; } } } if (!found) return 0; /* Push nil, "cancelled" onto the coroutine's stack and enqueue */ if (!lua_checkstack(co, 2)) return luaL_error(L, "lev: memory allocation failed"); lua_pushnil(co); lua_pushstring(co, "cancelled"); /* Ref the coroutine for ready queue */ lua_pushvalue(L, 1); int co_ref = luaL_ref(L, LUA_REGISTRYINDEX); if (ready_push(loop, co_ref, 2) < 0) { luaL_unref(L, LUA_REGISTRYINDEX, co_ref); return luaL_error(L, "lev: memory allocation failed"); } return 0; } /* ── signal handling ───────────────────────────────────────────────── */ static int l_signal_setup(lua_State *L) { if (!g_loop) return luaL_error(L, "lev: no event loop running"); lev_loop_t *loop = g_loop; int signum = luaL_checkinteger(L, 1); luaL_checktype(L, 2, LUA_TFUNCTION); if (signum < 1 || signum >= LEV_MAX_SIGNALS) return luaL_error(L, "lev: signal number out of range"); /* Store handler ref */ if (loop->sig_handler_refs[signum] != LUA_NOREF) luaL_unref(L, LUA_REGISTRYINDEX, loop->sig_handler_refs[signum]); lua_pushvalue(L, 2); loop->sig_handler_refs[signum] = luaL_ref(L, LUA_REGISTRYINDEX); /* Build the signal mask with all registered signals */ sigset_t mask; sigemptyset(&mask); for (int i = 0; i < LEV_MAX_SIGNALS; i++) { if (loop->sig_handler_refs[i] != LUA_NOREF) sigaddset(&mask, i); } /* Create or update signalfd */ if (loop->signal_fd >= 0) { /* Update existing signalfd — don't overwrite orig_sigmask */ sigprocmask(SIG_BLOCK, &mask, NULL); if (signalfd(loop->signal_fd, &mask, 0) < 0) RETURN_ERR(L); } else { /* First signal setup — save original mask */ sigprocmask(SIG_BLOCK, &mask, &loop->orig_sigmask); loop->signal_fd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC); if (loop->signal_fd < 0) { sigprocmask(SIG_SETMASK, &loop->orig_sigmask, NULL); RETURN_ERR(L); } /* Register with epoll */ struct epoll_event ev; ev.events = EPOLLIN; ev.data.fd = loop->signal_fd; if (epoll_ctl(loop->epfd, EPOLL_CTL_ADD, loop->signal_fd, &ev) < 0) { close(loop->signal_fd); loop->signal_fd = -1; sigprocmask(SIG_SETMASK, &loop->orig_sigmask, NULL); RETURN_ERR(L); } } lua_pushboolean(L, 1); return 1; } /* ── UDP socket ────────────────────────────────────────────────────── */ static lev_udp_t *check_udp(lua_State *L, int idx) { return (lev_udp_t *)luaL_checkudata(L, idx, LEV_UDP_MT); } static int l_udp_new(lua_State *L) { int fd = socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); if (fd < 0) RETURN_ERR(L); lev_udp_t *u = (lev_udp_t *)lua_newuserdata(L, sizeof(lev_udp_t)); u->fd = fd; luaL_getmetatable(L, LEV_UDP_MT); lua_setmetatable(L, -2); return 1; } static int l_udp_bind(lua_State *L) { lev_udp_t *u = check_udp(L, 1); const char *addr = luaL_checkstring(L, 2); int port = luaL_checkinteger(L, 3); if (u->fd < 0) RETURN_CUSTOM_ERR(L, "socket is closed"); struct sockaddr_in sa; memset(&sa, 0, sizeof(sa)); sa.sin_family = AF_INET; sa.sin_port = htons((uint16_t)port); if (inet_pton(AF_INET, addr, &sa.sin_addr) != 1) RETURN_CUSTOM_ERR(L, "invalid address"); if (bind(u->fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) RETURN_ERR(L); lua_pushboolean(L, 1); return 1; } static int l_udp_sendto(lua_State *L) { lev_udp_t *u = check_udp(L, 1); size_t len; const char *data = luaL_checklstring(L, 2, &len); const char *addr = luaL_checkstring(L, 3); int port = luaL_checkinteger(L, 4); if (u->fd < 0) RETURN_CUSTOM_ERR(L, "socket is closed"); struct sockaddr_in sa; memset(&sa, 0, sizeof(sa)); sa.sin_family = AF_INET; sa.sin_port = htons((uint16_t)port); if (inet_pton(AF_INET, addr, &sa.sin_addr) != 1) RETURN_CUSTOM_ERR(L, "invalid address"); ssize_t sent = sendto(u->fd, data, len, 0, (struct sockaddr *)&sa, sizeof(sa)); if (sent < 0) RETURN_ERR(L); lua_pushinteger(L, (lua_Integer)sent); return 1; } static int l_udp_recvfrom(lua_State *L) { lev_udp_t *u = check_udp(L, 1); int maxlen = luaL_optint(L, 2, LEV_DEFAULT_MAXLEN); if (u->fd < 0) RETURN_CUSTOM_ERR(L, "socket is closed"); if (maxlen <= 0) maxlen = LEV_DEFAULT_MAXLEN; if (maxlen > LEV_TCP_RECV_BUF) maxlen = LEV_TCP_RECV_BUF; char buf[LEV_TCP_RECV_BUF]; struct sockaddr_in sa; socklen_t sa_len = sizeof(sa); memset(&sa, 0, sizeof(sa)); ssize_t n = recvfrom(u->fd, buf, (size_t)maxlen, 0, (struct sockaddr *)&sa, &sa_len); if (n < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { lua_pushnil(L); lua_pushstring(L, "EAGAIN"); return 2; } RETURN_ERR(L); } lua_pushlstring(L, buf, (size_t)n); /* Convert source address to string */ char addr_str[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &sa.sin_addr, addr_str, sizeof(addr_str)); lua_pushstring(L, addr_str); lua_pushinteger(L, ntohs(sa.sin_port)); return 3; } static int l_udp_setsockopt(lua_State *L) { lev_udp_t *u = check_udp(L, 1); const char *name = luaL_checkstring(L, 2); int val = luaL_checkinteger(L, 3); if (u->fd < 0) RETURN_CUSTOM_ERR(L, "socket is closed"); int optname; int level = SOL_SOCKET; if (strcmp(name, "reuseaddr") == 0) optname = SO_REUSEADDR; else if (strcmp(name, "rcvbuf") == 0) optname = SO_RCVBUF; else if (strcmp(name, "sndbuf") == 0) optname = SO_SNDBUF; else if (strcmp(name, "reuseport") == 0) optname = SO_REUSEPORT; else RETURN_CUSTOM_ERR(L, "unknown socket option"); if (setsockopt(u->fd, level, optname, &val, sizeof(val)) < 0) RETURN_ERR(L); lua_pushboolean(L, 1); return 1; } static int l_udp_close(lua_State *L) { lev_udp_t *u = (lev_udp_t *)luaL_checkudata(L, 1, LEV_UDP_MT); if (u->fd >= 0) { close(u->fd); u->fd = -1; } return 0; } static int l_udp_fd(lua_State *L) { lev_udp_t *u = check_udp(L, 1); lua_pushinteger(L, u->fd); return 1; } static int l_udp_gc(lua_State *L) { return l_udp_close(L); } /* ── TCP socket ────────────────────────────────────────────────────── */ static lev_tcp_t *check_tcp(lua_State *L, int idx) { return (lev_tcp_t *)luaL_checkudata(L, idx, LEV_TCP_MT); } static lev_tcp_t *tcp_push_ud(lua_State *L, int fd) { lev_tcp_t *u = (lev_tcp_t *)lua_newuserdata(L, sizeof(lev_tcp_t)); u->fd = fd; u->tls = NULL; u->tls_anchors = NULL; u->tls_mode = 0; u->srv_default_id = NULL; u->srv_sni_ids = NULL; u->srv_sni_count = 0; u->srv_ids_owned = 0; u->client_identity = NULL; luaL_getmetatable(L, LEV_TCP_MT); lua_setmetatable(L, -2); return u; } static int l_tcp_new(lua_State *L) { int fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); if (fd < 0) RETURN_ERR(L); tcp_push_ud(L, fd); return 1; } /* ── AF_UNIX stream sockets ──────────────────────────────────────────── Reuse lev_tcp_t and the lev.tcp metatable: send/recv/shutdown/close/fd/ setsockopt/starttls are address-family agnostic and work unchanged on a connected unix stream fd. Only socket creation, connect, bind, and accept are family-specific and live here. */ /* Build a sockaddr_un from a Lua path string. A leading '@' selects the Linux abstract namespace (sun_path[0] = '\0', no filesystem entry). On error, returns -1 and points *err at a static message. */ static int unix_addr_init(struct sockaddr_un *sa, const char *path, socklen_t *len, const char **err) { memset(sa, 0, sizeof(*sa)); sa->sun_family = AF_UNIX; size_t plen = strlen(path); if (plen == 0) { *err = "empty path"; return -1; } if (path[0] == '@') { /* abstract namespace: leading NUL, then the name after '@' */ size_t nlen = plen - 1; if (nlen + 1 > sizeof(sa->sun_path)) { *err = "path too long"; return -1; } sa->sun_path[0] = '\0'; memcpy(sa->sun_path + 1, path + 1, nlen); *len = (socklen_t)(offsetof(struct sockaddr_un, sun_path) + 1 + nlen); } else { if (plen + 1 > sizeof(sa->sun_path)) { *err = "path too long"; return -1; } memcpy(sa->sun_path, path, plen + 1); *len = (socklen_t)sizeof(struct sockaddr_un); } return 0; } static int l_unix_new(lua_State *L) { int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); if (fd < 0) RETURN_ERR(L); tcp_push_ud(L, fd); return 1; } static int l_tcp_connect_unix(lua_State *L) { lev_tcp_t *u = check_tcp(L, 1); const char *path = luaL_checkstring(L, 2); if (u->fd < 0) RETURN_CUSTOM_ERR(L, "socket is closed"); struct sockaddr_un sa; socklen_t sa_len; const char *err = NULL; if (unix_addr_init(&sa, path, &sa_len, &err) != 0) RETURN_CUSTOM_ERR(L, err); int ret = connect(u->fd, (struct sockaddr *)&sa, sa_len); if (ret == 0) { lua_pushboolean(L, 1); return 1; } if (errno == EINPROGRESS) { lua_pushnil(L); lua_pushstring(L, "EINPROGRESS"); return 2; } RETURN_ERR(L); } static int l_tcp_bind_unix(lua_State *L) { lev_tcp_t *u = check_tcp(L, 1); const char *path = luaL_checkstring(L, 2); if (u->fd < 0) RETURN_CUSTOM_ERR(L, "socket is closed"); struct sockaddr_un sa; socklen_t sa_len; const char *err = NULL; if (unix_addr_init(&sa, path, &sa_len, &err) != 0) RETURN_CUSTOM_ERR(L, err); if (bind(u->fd, (struct sockaddr *)&sa, sa_len) < 0) RETURN_ERR(L); lua_pushboolean(L, 1); return 1; } static int l_tcp_accept_unix(lua_State *L) { lev_tcp_t *u = check_tcp(L, 1); if (u->fd < 0) RETURN_CUSTOM_ERR(L, "socket is closed"); struct sockaddr_un sa; socklen_t sa_len = sizeof(sa); memset(&sa, 0, sizeof(sa)); int client_fd = accept4(u->fd, (struct sockaddr *)&sa, &sa_len, SOCK_NONBLOCK | SOCK_CLOEXEC); if (client_fd < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { lua_pushnil(L); lua_pushstring(L, "EAGAIN"); return 2; } RETURN_ERR(L); } /* A connecting unix client is typically unbound, so there is no meaningful peer address to report — return just the client. */ tcp_push_ud(L, client_fd); return 1; } static int l_tcp_connect(lua_State *L) { lev_tcp_t *u = check_tcp(L, 1); const char *addr = luaL_checkstring(L, 2); int port = luaL_checkinteger(L, 3); if (u->fd < 0) RETURN_CUSTOM_ERR(L, "socket is closed"); struct sockaddr_in sa; memset(&sa, 0, sizeof(sa)); sa.sin_family = AF_INET; sa.sin_port = htons((uint16_t)port); if (inet_pton(AF_INET, addr, &sa.sin_addr) != 1) RETURN_CUSTOM_ERR(L, "invalid address"); int ret = connect(u->fd, (struct sockaddr *)&sa, sizeof(sa)); if (ret == 0) { lua_pushboolean(L, 1); return 1; } if (errno == EINPROGRESS) { lua_pushnil(L); lua_pushstring(L, "EINPROGRESS"); return 2; } RETURN_ERR(L); } static int l_tcp_connect_finish(lua_State *L) { lev_tcp_t *u = check_tcp(L, 1); if (u->fd < 0) RETURN_CUSTOM_ERR(L, "socket is closed"); int err = 0; socklen_t sz = sizeof(err); if (getsockopt(u->fd, SOL_SOCKET, SO_ERROR, &err, &sz) < 0) RETURN_ERR(L); if (err != 0) { lua_pushnil(L); lua_pushstring(L, strerror(err)); return 2; } lua_pushboolean(L, 1); return 1; } static int l_tcp_bind(lua_State *L) { lev_tcp_t *u = check_tcp(L, 1); const char *addr = luaL_checkstring(L, 2); int port = luaL_checkinteger(L, 3); if (u->fd < 0) RETURN_CUSTOM_ERR(L, "socket is closed"); struct sockaddr_in sa; memset(&sa, 0, sizeof(sa)); sa.sin_family = AF_INET; sa.sin_port = htons((uint16_t)port); if (inet_pton(AF_INET, addr, &sa.sin_addr) != 1) RETURN_CUSTOM_ERR(L, "invalid address"); if (bind(u->fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) RETURN_ERR(L); lua_pushboolean(L, 1); return 1; } static int l_tcp_listen(lua_State *L) { lev_tcp_t *u = check_tcp(L, 1); int backlog = luaL_optint(L, 2, LEV_DEFAULT_BACKLOG); if (u->fd < 0) RETURN_CUSTOM_ERR(L, "socket is closed"); if (listen(u->fd, backlog) < 0) RETURN_ERR(L); lua_pushboolean(L, 1); return 1; } static int l_tcp_accept(lua_State *L) { lev_tcp_t *u = check_tcp(L, 1); if (u->fd < 0) RETURN_CUSTOM_ERR(L, "socket is closed"); struct sockaddr_in sa; socklen_t sa_len = sizeof(sa); memset(&sa, 0, sizeof(sa)); int client_fd = accept4(u->fd, (struct sockaddr *)&sa, &sa_len, SOCK_NONBLOCK | SOCK_CLOEXEC); if (client_fd < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { lua_pushnil(L); lua_pushstring(L, "EAGAIN"); return 2; } RETURN_ERR(L); } tcp_push_ud(L, client_fd); char addr_str[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &sa.sin_addr, addr_str, sizeof(addr_str)); lua_pushstring(L, addr_str); lua_pushinteger(L, ntohs(sa.sin_port)); return 3; } static int l_tcp_send(lua_State *L) { lev_tcp_t *u = check_tcp(L, 1); size_t len; const char *data = luaL_checklstring(L, 2, &len); if (u->fd < 0) RETURN_CUSTOM_ERR(L, "socket is closed"); if (u->tls) { /* LITLS client/server path */ int ret = litls_tls13_write(u->tls, (const uint8_t *)data, len); if (ret < 0) { lua_pushnil(L); lua_pushstring(L, u->tls->error); return 2; } /* Flush last buffered record */ litls_io_want w = litls_record_flush(u->tls); if (w == LITLS_ERROR) { lua_pushnil(L); lua_pushstring(L, u->tls->error); return 2; } if (w == LITLS_WANT_WRITE) { if (ret > 0) { lua_pushinteger(L, ret); return 1; } lua_pushnil(L); lua_pushstring(L, "want_write"); return 2; } lua_pushinteger(L, ret); return 1; } ssize_t sent = send(u->fd, data, len, MSG_NOSIGNAL); if (sent < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { lua_pushnil(L); lua_pushstring(L, "EAGAIN"); return 2; } RETURN_ERR(L); } lua_pushinteger(L, (lua_Integer)sent); return 1; } /* Flush any TLS records still buffered in the write path. litls_tls13_write reports bytes consumed into wbuf, not bytes on the wire, so send() must drain this before reporting success — otherwise the tail of a response can be silently dropped at close (litls_tls13_shutdown flushes best-effort only). Returns "done" (nothing pending / all flushed, including the non-TLS case), "want_write", or nil, err. */ static int l_tcp_tls_flush(lua_State *L) { lev_tcp_t *u = check_tcp(L, 1); if (u->fd < 0) RETURN_CUSTOM_ERR(L, "socket is closed"); if (u->tls) { litls_io_want w = litls_record_flush(u->tls); if (w == LITLS_ERROR) { lua_pushnil(L); lua_pushstring(L, u->tls->error); return 2; } if (w == LITLS_WANT_WRITE) { lua_pushstring(L, "want_write"); return 1; } } lua_pushstring(L, "done"); return 1; } /* Drain buffered TLS application data into buf. Returns litls_tls13_read's result: >0 bytes read, 0 need-more-data, <0 error/closed. Loops while complete records remain in the read buffer and progress is being made. */ static int tls_drain_read(litls_tls13_ctx *tls, char *buf, size_t maxlen) { int ret; size_t prev_rpos; do { prev_rpos = tls->rbuf.pos; ret = litls_tls13_read(tls, (uint8_t *)buf, maxlen); } while (ret == 0 && tls->rbuf.len > tls->rbuf.pos && tls->rbuf.pos != prev_rpos); return ret; } static int l_tcp_recv(lua_State *L) { lev_tcp_t *u = check_tcp(L, 1); int maxlen = luaL_optint(L, 2, LEV_DEFAULT_MAXLEN); if (u->fd < 0) RETURN_CUSTOM_ERR(L, "socket is closed"); if (maxlen <= 0) maxlen = LEV_DEFAULT_MAXLEN; if (maxlen > LEV_TCP_RECV_BUF) maxlen = LEV_TCP_RECV_BUF; char tbuf[LEV_TCP_RECV_BUF]; if (u->tls) { /* LITLS client/server path */ /* Fill read buffer from socket */ litls_io_want w = litls_record_fill(u->tls); if (w == LITLS_ERROR) { lua_pushnil(L); lua_pushstring(L, u->tls->error); return 2; } if (w == LITLS_CLOSED) { /* Try to drain any complete records still in the buffer */ int ret = tls_drain_read(u->tls, tbuf, (size_t)maxlen); if (ret > 0) { lua_pushlstring(L, tbuf, (size_t)ret); return 1; } /* No more complete data — connection is closed */ lua_pushnil(L); lua_pushstring(L, "closed"); return 2; } int ret = tls_drain_read(u->tls, tbuf, (size_t)maxlen); if (ret > 0) { lua_pushlstring(L, tbuf, (size_t)ret); return 1; } if (ret == 0) { /* Need more data */ lua_pushnil(L); lua_pushstring(L, "want_read"); return 2; } if (ret == -2) { /* Peer closed */ lua_pushnil(L); lua_pushstring(L, "closed"); return 2; } /* Error */ lua_pushnil(L); lua_pushstring(L, u->tls->error); return 2; } ssize_t n = recv(u->fd, tbuf, (size_t)maxlen, 0); if (n < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { lua_pushnil(L); lua_pushstring(L, "EAGAIN"); return 2; } RETURN_ERR(L); } if (n == 0) { lua_pushnil(L); lua_pushstring(L, "closed"); return 2; } lua_pushlstring(L, tbuf, (size_t)n); return 1; } static int l_tcp_setsockopt(lua_State *L) { lev_tcp_t *u = check_tcp(L, 1); const char *name = luaL_checkstring(L, 2); int val = luaL_checkinteger(L, 3); if (u->fd < 0) RETURN_CUSTOM_ERR(L, "socket is closed"); int optname; int level = SOL_SOCKET; if (strcmp(name, "reuseaddr") == 0) optname = SO_REUSEADDR; else if (strcmp(name, "reuseport") == 0) optname = SO_REUSEPORT; else if (strcmp(name, "rcvbuf") == 0) optname = SO_RCVBUF; else if (strcmp(name, "sndbuf") == 0) optname = SO_SNDBUF; else if (strcmp(name, "keepalive") == 0) optname = SO_KEEPALIVE; else if (strcmp(name, "nodelay") == 0) { level = IPPROTO_TCP; optname = TCP_NODELAY; } else RETURN_CUSTOM_ERR(L, "unknown socket option"); if (setsockopt(u->fd, level, optname, &val, sizeof(val)) < 0) RETURN_ERR(L); lua_pushboolean(L, 1); return 1; } static int l_tcp_getpeername(lua_State *L) { lev_tcp_t *u = check_tcp(L, 1); if (u->fd < 0) RETURN_CUSTOM_ERR(L, "socket is closed"); struct sockaddr_in sa; socklen_t sa_len = sizeof(sa); memset(&sa, 0, sizeof(sa)); if (getpeername(u->fd, (struct sockaddr *)&sa, &sa_len) < 0) RETURN_ERR(L); char addr_str[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &sa.sin_addr, addr_str, sizeof(addr_str)); lua_pushstring(L, addr_str); lua_pushinteger(L, ntohs(sa.sin_port)); return 2; } static int l_tcp_shutdown(lua_State *L) { lev_tcp_t *u = check_tcp(L, 1); const char *how = luaL_checkstring(L, 2); if (u->fd < 0) RETURN_CUSTOM_ERR(L, "socket is closed"); int flag; if (strcmp(how, "r") == 0) flag = SHUT_RD; else if (strcmp(how, "w") == 0) flag = SHUT_WR; else if (strcmp(how, "rw") == 0) flag = SHUT_RDWR; else RETURN_CUSTOM_ERR(L, "invalid shutdown mode (use 'r', 'w', or 'rw')"); if (shutdown(u->fd, flag) < 0) RETURN_ERR(L); lua_pushboolean(L, 1); return 1; } static int l_tcp_close(lua_State *L) { lev_tcp_t *u = (lev_tcp_t *)luaL_checkudata(L, 1, LEV_TCP_MT); if (u->tls) { litls_tls13_shutdown(u->tls); free(u->tls); u->tls = NULL; } if (u->tls_anchors) { free(u->tls_anchors); u->tls_anchors = NULL; } /* Identity structs hold private keys — wipe before freeing */ if (u->client_identity) { litls_secure_zero(u->client_identity, sizeof(litls_server_identity)); free(u->client_identity); u->client_identity = NULL; } if (u->srv_ids_owned) { if (u->srv_default_id) { litls_secure_zero(u->srv_default_id, sizeof(litls_server_identity)); free(u->srv_default_id); } if (u->srv_sni_ids) { litls_secure_zero(u->srv_sni_ids, (size_t)u->srv_sni_count * sizeof(litls_server_identity)); free(u->srv_sni_ids); } } u->srv_default_id = NULL; u->srv_sni_ids = NULL; u->srv_sni_count = 0; u->srv_ids_owned = 0; u->tls_mode = 0; if (u->fd >= 0) { close(u->fd); u->fd = -1; } return 0; } static int l_tcp_fd(lua_State *L) { lev_tcp_t *u = check_tcp(L, 1); lua_pushinteger(L, u->fd); return 1; } static int l_tcp_gc(lua_State *L) { return l_tcp_close(L); } /* ── TLS identity parsing ──────────────────────────────────────────── */ /* Helper: parse a PEM cert chain into a server identity struct. * Returns 0 on success, -1 on error (pushes error string on L). */ static int parse_identity_from_files(lua_State *L, const char *cert_path, const char *key_path, const char *hostname, litls_server_identity *id) { memset(id, 0, sizeof(*id)); /* Parse private key */ FILE *kf = fopen(key_path, "rb"); if (!kf) { lua_pushnil(L); lua_pushstring(L, "failed to open private key file"); return -1; } fseek(kf, 0, SEEK_END); long key_file_len = ftell(kf); fseek(kf, 0, SEEK_SET); uint8_t *key_pem = (uint8_t *)malloc((size_t)key_file_len); if (!key_pem || (long)fread(key_pem, 1, (size_t)key_file_len, kf) != key_file_len) { if (key_pem) litls_secure_zero(key_pem, (size_t)key_file_len); free(key_pem); fclose(kf); lua_pushnil(L); lua_pushstring(L, "failed to read private key file"); return -1; } fclose(kf); if (litls_parse_private_key_pem(key_pem, (size_t)key_file_len, &id->key_type, id->private_key, &id->private_key_len, id->public_key, &id->public_key_len) != 0) { /* Detect RSA keys to give a helpful error message */ int is_rsa = 0; for (long i = 0; i + 15 < key_file_len; i++) { if (memcmp(key_pem + i, "RSA PRIVATE KEY", 15) == 0) { is_rsa = 1; break; } if (memcmp(key_pem + i, "-----END", 8) == 0) break; } litls_secure_zero(key_pem, (size_t)key_file_len); free(key_pem); lua_pushnil(L); if (is_rsa) lua_pushstring(L, "unsupported key type: RSA (only ECDSA P-256 and Ed25519 are supported)"); else lua_pushstring(L, "failed to parse private key (only ECDSA P-256 and Ed25519 are supported)"); return -1; } litls_secure_zero(key_pem, (size_t)key_file_len); free(key_pem); /* Parse certificate chain */ FILE *cf = fopen(cert_path, "rb"); if (!cf) { lua_pushnil(L); lua_pushstring(L, "failed to open certificate file"); return -1; } fseek(cf, 0, SEEK_END); long cert_file_len = ftell(cf); fseek(cf, 0, SEEK_SET); uint8_t *cert_pem = (uint8_t *)malloc((size_t)cert_file_len); if (!cert_pem || (long)fread(cert_pem, 1, (size_t)cert_file_len, cf) != cert_file_len) { free(cert_pem); fclose(cf); lua_pushnil(L); lua_pushstring(L, "failed to read certificate file"); return -1; } fclose(cf); id->cert_count = 0; size_t chain_offset = 0; const uint8_t *pem_ptr = cert_pem; size_t pem_remaining = (size_t)cert_file_len; while (pem_remaining > 0 && id->cert_count < LITLS_X509_MAX_CHAIN) { const uint8_t *found = NULL; for (size_t i = 0; i + 11 < pem_remaining; i++) { if (memcmp(pem_ptr + i, "-----BEGIN ", 11) == 0) { found = pem_ptr + i; break; } } if (!found) break; size_t block_offset = (size_t)(found - pem_ptr); size_t block_remaining = pem_remaining - block_offset; uint8_t der_tmp[8192]; size_t der_len = sizeof(der_tmp); if (litls_pem_decode(found, block_remaining, der_tmp, &der_len) != 0) break; if (chain_offset + der_len > LITLS_MAX_CERT_CHAIN_DER) break; id->cert_der_offsets[id->cert_count] = chain_offset; id->cert_der_lens[id->cert_count] = der_len; memcpy(id->cert_chain_der + chain_offset, der_tmp, der_len); chain_offset += der_len; id->cert_count++; const uint8_t *end_found = NULL; for (size_t i = block_offset; i + 9 < pem_remaining; i++) { if (memcmp(pem_ptr + i, "-----END ", 9) == 0) { size_t j = i; while (j < pem_remaining && pem_ptr[j] != '\n') j++; if (j < pem_remaining) j++; end_found = pem_ptr + j; break; } } if (!end_found) break; size_t consumed = (size_t)(end_found - pem_ptr); pem_ptr += consumed; pem_remaining -= consumed; } free(cert_pem); if (id->cert_count == 0) { lua_pushnil(L); lua_pushstring(L, "no certificates found in PEM file"); return -1; } if (hostname) { size_t hn_len = strlen(hostname); if (hn_len >= sizeof(id->hostname)) hn_len = sizeof(id->hostname) - 1; memcpy(id->hostname, hostname, hn_len); id->hostname[hn_len] = '\0'; } return 0; } /* lev.core.tls_parse_identity(cert_path, key_path [, hostname]) -> identity userdata */ static int l_tls_parse_identity(lua_State *L) { const char *cert_path = luaL_checkstring(L, 1); const char *key_path = luaL_checkstring(L, 2); const char *hostname = lua_isstring(L, 3) ? lua_tostring(L, 3) : NULL; litls_server_identity *id = (litls_server_identity *)lua_newuserdata(L, sizeof(litls_server_identity)); luaL_getmetatable(L, LEV_TLS_IDENTITY_MT); lua_setmetatable(L, -2); if (parse_identity_from_files(L, cert_path, key_path, hostname, id) != 0) { /* parse_identity_from_files pushed nil + error string */ /* Remove the userdata that's below them on the stack */ lua_remove(L, -3); return 2; } return 1; } /* ── TLS (starttls) ────────────────────────────────────────────────── */ static int l_tcp_starttls_init(lua_State *L) { lev_tcp_t *u = check_tcp(L, 1); luaL_checktype(L, 2, LUA_TTABLE); if (u->fd < 0) RETURN_CUSTOM_ERR(L, "socket is closed"); if (u->tls) RETURN_CUSTOM_ERR(L, "TLS already initialized"); /* Read mode */ lua_getfield(L, 2, "mode"); const char *mode_str = luaL_optstring(L, -1, "client"); int is_server = (strcmp(mode_str, "server") == 0); lua_pop(L, 1); /* Read verify flag */ lua_getfield(L, 2, "verify"); int verify; if (lua_isnil(L, -1)) verify = is_server ? 0 : 1; /* default: client verifies, server doesn't */ else verify = lua_toboolean(L, -1); lua_pop(L, 1); if (!is_server) { /* Client: use LITLS TLS 1.3 implementation */ lua_getfield(L, 2, "server_name"); const char *sni = lua_tostring(L, -1); lua_pop(L, 1); litls_tls13_ctx *tls = (litls_tls13_ctx *)malloc(sizeof(litls_tls13_ctx)); if (!tls) RETURN_CUSTOM_ERR(L, "memory allocation failed for TLS context"); litls_x509_trust_anchor *anchors = NULL; int anchor_count = 0; if (verify) { lua_getfield(L, 2, "cafile"); const char *cafile = lua_tostring(L, -1); lua_pop(L, 1); if (cafile) { FILE *f = fopen(cafile, "rb"); if (!f) { free(tls); RETURN_CUSTOM_ERR(L, "failed to open CA file"); } fseek(f, 0, SEEK_END); long ca_len = ftell(f); fseek(f, 0, SEEK_SET); uint8_t *ca_pem = (uint8_t *)malloc((size_t)ca_len); if (!ca_pem || (long)fread(ca_pem, 1, (size_t)ca_len, f) != ca_len) { free(ca_pem); fclose(f); free(tls); RETURN_CUSTOM_ERR(L, "failed to read CA file"); } fclose(f); anchors = (litls_x509_trust_anchor *)calloc(LITLS_X509_MAX_ANCHORS, sizeof(litls_x509_trust_anchor)); if (!anchors) { free(ca_pem); free(tls); RETURN_CUSTOM_ERR(L, "memory allocation failed for trust anchors"); } anchor_count = litls_x509_load_anchors(ca_pem, (size_t)ca_len, anchors, LITLS_X509_MAX_ANCHORS); free(ca_pem); if (anchor_count <= 0) { free(anchors); free(tls); RETURN_CUSTOM_ERR(L, "no trust anchors loaded"); } } } /* Client certificate for mTLS */ litls_server_identity *client_id = NULL; lua_getfield(L, 2, "cert"); const char *client_cert = lua_tostring(L, -1); lua_pop(L, 1); lua_getfield(L, 2, "key"); const char *client_key = lua_tostring(L, -1); lua_pop(L, 1); if (client_cert && client_key) { client_id = (litls_server_identity *)calloc(1, sizeof(litls_server_identity)); if (!client_id) { if (anchors) free(anchors); free(tls); RETURN_CUSTOM_ERR(L, "memory allocation failed for client identity"); } if (parse_identity_from_files(L, client_cert, client_key, NULL, client_id) != 0) { free(client_id); if (anchors) free(anchors); free(tls); return 2; /* parse_identity_from_files pushed nil + error */ } } if (litls_tls13_client_init(tls, u->fd, verify, sni, anchors, anchor_count, client_id) != 0) { if (client_id) free(client_id); if (anchors) free(anchors); char err[300]; snprintf(err, sizeof(err), "TLS init failed: %s", tls->error); free(tls); RETURN_CUSTOM_ERR(L, err); } u->tls = tls; u->tls_anchors = anchors; u->client_identity = client_id; u->tls_mode = 1; lua_pushboolean(L, 1); return 1; } /* Server path: use LITLS */ { litls_server_identity *default_id = NULL; litls_server_identity *sni_ids = NULL; int sni_count = 0; int ids_owned = 0; /* Check for pre-parsed cached identity userdata */ lua_getfield(L, 2, "identity"); if (lua_isuserdata(L, -1)) { /* Cached identity path — copy from Lua userdata into malloc'd memory * (can't use Lua userdata pointer directly because close() will free it) */ litls_server_identity *cached = (litls_server_identity *)luaL_checkudata(L, -1, LEV_TLS_IDENTITY_MT); lua_pop(L, 1); default_id = (litls_server_identity *)malloc(sizeof(litls_server_identity)); if (!default_id) RETURN_CUSTOM_ERR(L, "memory allocation failed for server identity"); memcpy(default_id, cached, sizeof(litls_server_identity)); ids_owned = 1; /* Check for cached SNI hosts (table of hostname -> identity userdata) */ lua_getfield(L, 2, "hosts"); if (lua_istable(L, -1)) { /* Count entries */ int host_count = 0; lua_pushnil(L); while (lua_next(L, -2) != 0) { host_count++; lua_pop(L, 1); } if (host_count > 0) { sni_ids = (litls_server_identity *)calloc((size_t)host_count, sizeof(litls_server_identity)); if (!sni_ids) { free(default_id); lua_pop(L, 1); RETURN_CUSTOM_ERR(L, "memory allocation failed for SNI hosts"); } lua_pushnil(L); while (lua_next(L, -2) != 0) { const char *hostname = lua_tostring(L, -2); if (!hostname) { free(default_id); free(sni_ids); lua_pop(L, 2); RETURN_CUSTOM_ERR(L, "server TLS hosts keys must be hostnames"); } litls_server_identity *cached_id = (litls_server_identity *)luaL_testudata(L, -1, LEV_TLS_IDENTITY_MT); if (!cached_id) { free(default_id); free(sni_ids); lua_pop(L, 2); RETURN_CUSTOM_ERR(L, "server TLS hosts values must be TLS identities"); } memcpy(&sni_ids[sni_count], cached_id, sizeof(litls_server_identity)); sni_count++; lua_pop(L, 1); } } } lua_pop(L, 1); /* pop hosts table (or nil) */ } else { lua_pop(L, 1); /* pop nil identity */ /* File-based path: cert/key PEM paths in the config, parsed per connection */ lua_getfield(L, 2, "cert"); const char *cert_path = lua_tostring(L, -1); lua_pop(L, 1); lua_getfield(L, 2, "key"); const char *key_path = lua_tostring(L, -1); lua_pop(L, 1); if (!cert_path || !key_path) RETURN_CUSTOM_ERR(L, "cert and key paths required for server TLS"); default_id = (litls_server_identity *)calloc(1, sizeof(litls_server_identity)); if (!default_id) RETURN_CUSTOM_ERR(L, "memory allocation failed for server identity"); if (parse_identity_from_files(L, cert_path, key_path, NULL, default_id) != 0) { free(default_id); /* parse_identity_from_files already pushed nil + error */ return 2; } ids_owned = 1; /* Parse optional SNI hosts from file paths */ lua_getfield(L, 2, "hosts"); if (lua_istable(L, -1)) { int host_count = 0; lua_pushnil(L); while (lua_next(L, -2) != 0) { host_count++; lua_pop(L, 1); } if (host_count > 0) { sni_ids = (litls_server_identity *)calloc((size_t)host_count, sizeof(litls_server_identity)); if (!sni_ids) { free(default_id); lua_pop(L, 1); RETURN_CUSTOM_ERR(L, "memory allocation failed for SNI hosts"); } lua_pushnil(L); while (lua_next(L, -2) != 0) { const char *hostname = lua_tostring(L, -2); if (!hostname) { free(default_id); free(sni_ids); lua_pop(L, 2); RETURN_CUSTOM_ERR(L, "server TLS hosts keys must be hostnames"); } if (!lua_istable(L, -1)) { free(default_id); free(sni_ids); lua_pop(L, 2); RETURN_CUSTOM_ERR(L, "server TLS hosts values must be tables with cert and key"); } lua_getfield(L, -1, "cert"); const char *h_cert = lua_tostring(L, -1); lua_pop(L, 1); lua_getfield(L, -1, "key"); const char *h_key = lua_tostring(L, -1); lua_pop(L, 1); if (!h_cert || !h_key) { free(default_id); free(sni_ids); lua_pop(L, 2); RETURN_CUSTOM_ERR(L, "server TLS host entry missing cert or key"); } litls_server_identity *sid = &sni_ids[sni_count]; if (parse_identity_from_files(L, h_cert, h_key, hostname, sid) != 0) { free(default_id); free(sni_ids); lua_remove(L, -3); /* remove host config table, keep nil + error */ return 2; } sni_count++; lua_pop(L, 1); } } } lua_pop(L, 1); /* pop hosts table (or nil) */ } /* Create LITLS TLS context */ litls_tls13_ctx *tls = (litls_tls13_ctx *)malloc(sizeof(litls_tls13_ctx)); if (!tls) { if (ids_owned) { free(default_id); if (sni_ids) free(sni_ids); } RETURN_CUSTOM_ERR(L, "memory allocation failed for TLS context"); } if (litls_tls13_server_init(tls, u->fd, default_id, sni_ids, sni_count) != 0) { char err[300]; snprintf(err, sizeof(err), "TLS server init failed: %s", tls->error); free(tls); if (ids_owned) { free(default_id); if (sni_ids) free(sni_ids); } RETURN_CUSTOM_ERR(L, err); } u->tls = tls; u->tls_mode = 2; u->srv_default_id = default_id; u->srv_sni_ids = sni_ids; u->srv_sni_count = sni_count; u->srv_ids_owned = ids_owned; lua_pushboolean(L, 1); return 1; } } static int l_tcp_starttls_step(lua_State *L) { lev_tcp_t *u = check_tcp(L, 1); if (u->tls) { /* LITLS client/server path */ litls_io_want fill_rc = litls_record_fill(u->tls); if (fill_rc == LITLS_CLOSED && u->tls->rbuf.len == u->tls->rbuf.pos) { lua_pushnil(L); lua_pushstring(L, "closed"); return 2; } if (fill_rc == LITLS_ERROR) { lua_pushnil(L); lua_pushstring(L, u->tls->error); return 2; } litls_io_want w; size_t prev_pos; size_t prev_hs; do { prev_pos = u->tls->rbuf.pos; prev_hs = u->tls->hs_buf_len; w = u->tls->is_server ? litls_tls13_server_handshake_step(u->tls) : litls_tls13_handshake_step(u->tls); } while (w == LITLS_WANT_READ && (u->tls->rbuf.len > u->tls->rbuf.pos || u->tls->hs_buf_len > 0) && (u->tls->rbuf.pos != prev_pos || u->tls->hs_buf_len != prev_hs)); if (u->tls->wbuf.len > u->tls->wbuf.pos) litls_record_flush(u->tls); switch (w) { case LITLS_DONE: lua_pushstring(L, "done"); return 1; case LITLS_WANT_READ: if (fill_rc == LITLS_CLOSED) { lua_pushnil(L); lua_pushstring(L, "closed"); return 2; } lua_pushstring(L, "want_read"); return 1; case LITLS_WANT_WRITE: lua_pushstring(L, "want_write"); return 1; case LITLS_CLOSED: lua_pushnil(L); lua_pushstring(L, "closed"); return 2; default: lua_pushnil(L); lua_pushstring(L, u->tls->error); return 2; } } RETURN_CUSTOM_ERR(L, "TLS not initialized"); } /* ── subprocess ────────────────────────────────────────────────────── */ static lev_subprocess_t *check_subprocess(lua_State *L, int idx) { return (lev_subprocess_t *)luaL_checkudata(L, idx, LEV_SUBPROCESS_MT); } static int l_subprocess_spawn(lua_State *L) { const char *path = luaL_checkstring(L, 1); luaL_checktype(L, 2, LUA_TTABLE); /* Build argv array from table */ int argc = (int)lua_objlen(L, 2); if (argc < 1) RETURN_CUSTOM_ERR(L, "argv must have at least one element"); char **argv = (char **)malloc(sizeof(char *) * ((size_t)argc + 1)); if (!argv) RETURN_CUSTOM_ERR(L, "memory allocation failed"); /* Entries must be actual strings: lua_tostring on a number returns a pointer anchored only by the popped stack slot (GC hazard), and a NULL from a non-string would silently truncate the argument list. */ for (int i = 1; i <= argc; i++) { lua_rawgeti(L, 2, i); if (lua_type(L, -1) != LUA_TSTRING) { lua_pop(L, 1); free(argv); RETURN_CUSTOM_ERR(L, "argv elements must be strings"); } argv[i - 1] = (char *)lua_tostring(L, -1); lua_pop(L, 1); } argv[argc] = NULL; /* Parse opts */ int stderr_to_stdout = 0; const char *cwd = NULL; int has_env = 0; char **envp = NULL; int envc = 0; if (lua_istable(L, 3)) { lua_getfield(L, 3, "stderr_to_stdout"); stderr_to_stdout = lua_toboolean(L, -1); lua_pop(L, 1); lua_getfield(L, 3, "cwd"); cwd = lua_tostring(L, -1); lua_pop(L, 1); lua_getfield(L, 3, "env"); if (lua_istable(L, -1)) { has_env = 1; envc = (int)lua_objlen(L, -1); envp = (char **)malloc(sizeof(char *) * ((size_t)envc + 1)); if (!envp) { free(argv); lua_pop(L, 1); RETURN_CUSTOM_ERR(L, "memory allocation failed"); } for (int i = 1; i <= envc; i++) { lua_rawgeti(L, -1, i); if (lua_type(L, -1) != LUA_TSTRING) { lua_pop(L, 2); free(argv); free(envp); RETURN_CUSTOM_ERR(L, "env elements must be strings"); } envp[i - 1] = (char *)lua_tostring(L, -1); lua_pop(L, 1); } envp[envc] = NULL; } lua_pop(L, 1); } /* Create error-reporting pipe (self-pipe trick for exec failure) */ int err_pipe[2]; if (pipe2(err_pipe, O_CLOEXEC) < 0) { free(argv); free(envp); RETURN_ERR(L); } /* Create stdin pipe */ int stdin_pipe[2]; if (pipe2(stdin_pipe, O_CLOEXEC) < 0) { close(err_pipe[0]); close(err_pipe[1]); free(argv); free(envp); RETURN_ERR(L); } /* Create stdout pipe */ int stdout_pipe[2]; if (pipe2(stdout_pipe, O_CLOEXEC) < 0) { close(err_pipe[0]); close(err_pipe[1]); close(stdin_pipe[0]); close(stdin_pipe[1]); free(argv); free(envp); RETURN_ERR(L); } /* Create stderr pipe */ int stderr_pipe[2]; if (pipe2(stderr_pipe, O_CLOEXEC) < 0) { close(err_pipe[0]); close(err_pipe[1]); close(stdin_pipe[0]); close(stdin_pipe[1]); close(stdout_pipe[0]); close(stdout_pipe[1]); free(argv); free(envp); RETURN_ERR(L); } pid_t pid = fork(); if (pid < 0) { int saved_errno = errno; close(err_pipe[0]); close(err_pipe[1]); close(stdin_pipe[0]); close(stdin_pipe[1]); close(stdout_pipe[0]); close(stdout_pipe[1]); close(stderr_pipe[0]); close(stderr_pipe[1]); free(argv); free(envp); errno = saved_errno; RETURN_ERR(L); } if (pid == 0) { /* ── Child process ── */ /* Close parent ends */ close(err_pipe[0]); close(stdin_pipe[1]); close(stdout_pipe[0]); close(stderr_pipe[0]); /* Redirect stdin/stdout/stderr */ dup2(stdin_pipe[0], STDIN_FILENO); dup2(stdout_pipe[1], STDOUT_FILENO); if (stderr_to_stdout) dup2(stdout_pipe[1], STDERR_FILENO); else dup2(stderr_pipe[1], STDERR_FILENO); /* Close original child-end fds (now duplicated to 0/1/2) */ close(stdin_pipe[0]); close(stdout_pipe[1]); close(stderr_pipe[1]); /* Change directory if requested */ if (cwd && chdir(cwd) < 0) { int err = errno; (void)write(err_pipe[1], &err, sizeof(err)); _exit(127); } /* Execute */ if (has_env) execve(path, argv, envp); else execvp(path, argv); /* If we get here, exec failed */ { int err = errno; (void)write(err_pipe[1], &err, sizeof(err)); _exit(127); } } /* ── Parent process ── */ /* Close child ends */ close(err_pipe[1]); close(stdin_pipe[0]); close(stdout_pipe[1]); close(stderr_pipe[1]); free(argv); free(envp); /* Check if exec failed via error pipe */ int child_errno; ssize_t nr; do { nr = read(err_pipe[0], &child_errno, sizeof(child_errno)); } while (nr < 0 && errno == EINTR); close(err_pipe[0]); if (nr > 0) { /* Exec failed — clean up */ close(stdin_pipe[1]); close(stdout_pipe[0]); close(stderr_pipe[0]); waitpid(pid, NULL, 0); lua_pushnil(L); lua_pushstring(L, strerror(child_errno)); return 2; } /* Set non-blocking on parent pipe ends */ if (fcntl(stdin_pipe[1], F_SETFL, fcntl(stdin_pipe[1], F_GETFL) | O_NONBLOCK) < 0 || fcntl(stdout_pipe[0], F_SETFL, fcntl(stdout_pipe[0], F_GETFL) | O_NONBLOCK) < 0 || fcntl(stderr_pipe[0], F_SETFL, fcntl(stderr_pipe[0], F_GETFL) | O_NONBLOCK) < 0) { int saved_errno = errno; close(stdin_pipe[1]); close(stdout_pipe[0]); close(stderr_pipe[0]); kill(pid, SIGKILL); waitpid(pid, NULL, 0); errno = saved_errno; RETURN_ERR(L); } /* Open pidfd */ int pidfd = (int)syscall(__NR_pidfd_open, pid, 0); if (pidfd < 0) { int saved_errno = errno; close(stdin_pipe[1]); close(stdout_pipe[0]); close(stderr_pipe[0]); kill(pid, SIGKILL); waitpid(pid, NULL, 0); errno = saved_errno; RETURN_ERR(L); } /* Create userdata */ lev_subprocess_t *proc = (lev_subprocess_t *)lua_newuserdata(L, sizeof(lev_subprocess_t)); proc->pid = pid; proc->pidfd = pidfd; proc->stdin_fd = stdin_pipe[1]; proc->stdout_fd = stdout_pipe[0]; proc->stderr_fd = stderr_pipe[0]; proc->exited = 0; proc->exit_code = -1; proc->exit_signal = 0; luaL_getmetatable(L, LEV_SUBPROCESS_MT); lua_setmetatable(L, -2); return 1; } static int subprocess_read_fd(lua_State *L, int fd) { int maxlen = luaL_optint(L, 2, LEV_DEFAULT_MAXLEN); if (fd < 0) RETURN_CUSTOM_ERR(L, "closed"); if (maxlen <= 0) maxlen = LEV_DEFAULT_MAXLEN; if (maxlen > LEV_TCP_RECV_BUF) maxlen = LEV_TCP_RECV_BUF; char buf[LEV_TCP_RECV_BUF]; ssize_t n = read(fd, buf, (size_t)maxlen); if (n < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { lua_pushnil(L); lua_pushstring(L, "EAGAIN"); return 2; } RETURN_ERR(L); } if (n == 0) { lua_pushnil(L); lua_pushstring(L, "closed"); return 2; } lua_pushlstring(L, buf, (size_t)n); return 1; } static int l_subprocess_read(lua_State *L) { lev_subprocess_t *proc = check_subprocess(L, 1); return subprocess_read_fd(L, proc->stdout_fd); } static int l_subprocess_read_stderr(lua_State *L) { lev_subprocess_t *proc = check_subprocess(L, 1); return subprocess_read_fd(L, proc->stderr_fd); } static int l_subprocess_write(lua_State *L) { lev_subprocess_t *proc = check_subprocess(L, 1); size_t len; const char *data = luaL_checklstring(L, 2, &len); if (proc->stdin_fd < 0) RETURN_CUSTOM_ERR(L, "closed"); ssize_t n = write(proc->stdin_fd, data, len); if (n < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { lua_pushnil(L); lua_pushstring(L, "EAGAIN"); return 2; } if (errno == EPIPE) { lua_pushnil(L); lua_pushstring(L, "closed"); return 2; } RETURN_ERR(L); } lua_pushinteger(L, (lua_Integer)n); return 1; } static int l_subprocess_close_stdin(lua_State *L) { lev_subprocess_t *proc = check_subprocess(L, 1); if (proc->stdin_fd >= 0) { close(proc->stdin_fd); proc->stdin_fd = -1; } return 0; } static int l_subprocess_kill(lua_State *L) { lev_subprocess_t *proc = check_subprocess(L, 1); int sig = luaL_optint(L, 2, SIGTERM); if (proc->exited) { lua_pushboolean(L, 1); return 1; } if (kill(proc->pid, sig) < 0) RETURN_ERR(L); lua_pushboolean(L, 1); return 1; } static int l_subprocess_waitid_pidfd(lua_State *L) { lev_subprocess_t *proc = check_subprocess(L, 1); if (proc->exited) { lua_pushinteger(L, proc->exit_code); lua_pushinteger(L, proc->exit_signal); return 2; } if (proc->pidfd < 0) RETURN_CUSTOM_ERR(L, "closed"); siginfo_t info; memset(&info, 0, sizeof(info)); int ret = waitid(P_PIDFD, (id_t)proc->pidfd, &info, WEXITED | WNOHANG); if (ret < 0) RETURN_ERR(L); if (info.si_pid == 0) { /* Child hasn't exited yet */ lua_pushnil(L); lua_pushstring(L, "EAGAIN"); return 2; } proc->exited = 1; if (info.si_code == CLD_EXITED) { proc->exit_code = info.si_status; proc->exit_signal = 0; } else { proc->exit_code = -1; proc->exit_signal = info.si_status; } lua_pushinteger(L, proc->exit_code); lua_pushinteger(L, proc->exit_signal); return 2; } static int l_subprocess_pid(lua_State *L) { lev_subprocess_t *proc = check_subprocess(L, 1); lua_pushinteger(L, proc->pid); return 1; } static int l_subprocess_pidfd_method(lua_State *L) { lev_subprocess_t *proc = check_subprocess(L, 1); lua_pushinteger(L, proc->pidfd); return 1; } static int l_subprocess_stdin_fd(lua_State *L) { lev_subprocess_t *proc = check_subprocess(L, 1); lua_pushinteger(L, proc->stdin_fd); return 1; } static int l_subprocess_stdout_fd(lua_State *L) { lev_subprocess_t *proc = check_subprocess(L, 1); lua_pushinteger(L, proc->stdout_fd); return 1; } static int l_subprocess_stderr_fd(lua_State *L) { lev_subprocess_t *proc = check_subprocess(L, 1); lua_pushinteger(L, proc->stderr_fd); return 1; } static int l_subprocess_close(lua_State *L) { lev_subprocess_t *proc = (lev_subprocess_t *)luaL_checkudata(L, 1, LEV_SUBPROCESS_MT); if (proc->stdin_fd >= 0) { close(proc->stdin_fd); proc->stdin_fd = -1; } if (proc->stdout_fd >= 0) { close(proc->stdout_fd); proc->stdout_fd = -1; } if (proc->stderr_fd >= 0) { close(proc->stderr_fd); proc->stderr_fd = -1; } if (proc->pidfd >= 0) { close(proc->pidfd); proc->pidfd = -1; } /* Reap zombie if not yet exited. Record the result so a later kill() cannot signal a recycled PID. If the child is still running it stays unreaped — callers should wait() (or kill() + wait()) before close. */ if (!proc->exited && proc->pid > 0) { int status; if (waitpid(proc->pid, &status, WNOHANG) == proc->pid) { proc->exited = 1; if (WIFEXITED(status)) { proc->exit_code = WEXITSTATUS(status); proc->exit_signal = 0; } else if (WIFSIGNALED(status)) { proc->exit_code = -1; proc->exit_signal = WTERMSIG(status); } } } return 0; } static int l_subprocess_gc(lua_State *L) { return l_subprocess_close(L); } /* ── module registration ───────────────────────────────────────────── */ static const luaL_Reg tcp_methods[] = { {"connect", l_tcp_connect }, {"connect_finish", l_tcp_connect_finish}, {"connect_unix", l_tcp_connect_unix }, {"bind", l_tcp_bind }, {"bind_unix", l_tcp_bind_unix }, {"listen", l_tcp_listen }, {"accept", l_tcp_accept }, {"accept_unix", l_tcp_accept_unix }, {"send", l_tcp_send }, {"recv", l_tcp_recv }, {"tls_flush", l_tcp_tls_flush }, {"setsockopt", l_tcp_setsockopt }, {"getpeername", l_tcp_getpeername }, {"shutdown", l_tcp_shutdown }, {"close", l_tcp_close }, {"fd", l_tcp_fd }, {"starttls_init", l_tcp_starttls_init }, {"starttls_step", l_tcp_starttls_step }, {NULL, NULL } }; static const luaL_Reg subprocess_methods[] = { {"read", l_subprocess_read }, {"read_stderr", l_subprocess_read_stderr }, {"write", l_subprocess_write }, {"close_stdin", l_subprocess_close_stdin }, {"kill", l_subprocess_kill }, {"waitid_pidfd", l_subprocess_waitid_pidfd}, {"pid", l_subprocess_pid }, {"pidfd", l_subprocess_pidfd_method}, {"stdin_fd", l_subprocess_stdin_fd }, {"stdout_fd", l_subprocess_stdout_fd }, {"stderr_fd", l_subprocess_stderr_fd }, {"close", l_subprocess_close }, {NULL, NULL } }; static const luaL_Reg udp_methods[] = { {"bind", l_udp_bind }, {"sendto", l_udp_sendto }, {"recvfrom", l_udp_recvfrom }, {"setsockopt", l_udp_setsockopt}, {"close", l_udp_close }, {"fd", l_udp_fd }, {NULL, NULL } }; /* The identity userdata holds private-key material — wipe it on collection */ static int l_tls_identity_gc(lua_State *L) { litls_server_identity *id = (litls_server_identity *)luaL_checkudata(L, 1, LEV_TLS_IDENTITY_MT); litls_secure_zero(id, sizeof(*id)); return 0; } static const luaL_Reg funcs[] = { {"loop_new", l_loop_new }, {"loop_run", l_loop_run }, {"loop_stop", l_loop_stop }, {"loop_stats", l_loop_stats }, {"loop_destroy", l_loop_destroy }, {"spawn", l_spawn }, {"yield_to_loop", l_yield_to_loop }, {"ready_enqueue", l_ready_enqueue }, {"wait_readable", l_wait_readable }, {"wait_writable", l_wait_writable }, {"fd_deregister", l_fd_deregister }, {"read_fd", l_read_fd }, {"set_nonblocking", l_set_nonblocking }, {"timer_sleep", l_timer_sleep }, {"timer_register", l_timer_register }, {"cancel_wait", l_cancel_wait }, {"signal_setup", l_signal_setup }, {"now", l_now }, {"udp_new", l_udp_new }, {"tcp_new", l_tcp_new }, {"unix_new", l_unix_new }, {"subprocess_spawn", l_subprocess_spawn }, {"tls_parse_identity", l_tls_parse_identity}, {NULL, NULL } }; int luaopen_lev_core(lua_State *L) { /* Loop metatable — accessed via module functions (core.loop_run(loop)), so only __gc is needed */ luaL_newmetatable(L, LEV_LOOP_MT); lua_pushcfunction(L, l_loop_gc); lua_setfield(L, -2, "__gc"); lua_pop(L, 1); /* UDP metatable */ luaL_newmetatable(L, LEV_UDP_MT); lua_newtable(L); luaL_register(L, NULL, udp_methods); lua_setfield(L, -2, "__index"); lua_pushcfunction(L, l_udp_gc); lua_setfield(L, -2, "__gc"); lua_pop(L, 1); /* TCP metatable */ luaL_newmetatable(L, LEV_TCP_MT); lua_newtable(L); luaL_register(L, NULL, tcp_methods); lua_setfield(L, -2, "__index"); lua_pushcfunction(L, l_tcp_gc); lua_setfield(L, -2, "__gc"); lua_pop(L, 1); /* Subprocess metatable */ luaL_newmetatable(L, LEV_SUBPROCESS_MT); lua_newtable(L); luaL_register(L, NULL, subprocess_methods); lua_setfield(L, -2, "__index"); lua_pushcfunction(L, l_subprocess_gc); lua_setfield(L, -2, "__gc"); lua_pop(L, 1); /* TLS identity metatable — __gc wipes key material; Lua owns the memory */ luaL_newmetatable(L, LEV_TLS_IDENTITY_MT); lua_pushcfunction(L, l_tls_identity_gc); lua_setfield(L, -2, "__gc"); lua_pop(L, 1); /* Module table */ luaL_newlib(L, funcs); return 1; }