diff --git a/bin/elb-linux-amd64 b/bin/elb-linux-amd64 index 6e88975..cafbdd1 100755 Binary files a/bin/elb-linux-amd64 and b/bin/elb-linux-amd64 differ diff --git a/bin/elc-linux-amd64 b/bin/elc-linux-amd64 index 375aeb6..97e0f40 100755 Binary files a/bin/elc-linux-amd64 and b/bin/elc-linux-amd64 differ diff --git a/runtime/el_runtime.c b/runtime/el_runtime.c index df83e95..9697867 100644 --- a/runtime/el_runtime.c +++ b/runtime/el_runtime.c @@ -42,6 +42,7 @@ #include #include #include +#include /* getrusage — memory guard */ #ifdef HAVE_CURL #include #endif @@ -2241,6 +2242,43 @@ el_val_t url_decode(el_val_t sv) { return el_wrap_str(out); } +/* ── html_raw ──────────────────────────────────────────────────────────────── + * Identity passthrough for raw HTML template interpolation. + * El's {raw(expr)} compiles to html_raw(expr) — the value is output as-is + * without any escaping. The caller is responsible for safety. + */ +el_val_t html_raw(el_val_t s) { + return s; +} + +/* ── html_escape ───────────────────────────────────────────────────────────── + * Escape < > " ' & for safe HTML text interpolation. + * El's {expr} in HTML templates compiles to html_escape(expr). + */ +el_val_t html_escape(el_val_t sv) { + const char* src = EL_CSTR(sv); + if (!src) return EL_STR(""); + size_t len = strlen(src); + /* Worst case: every byte → 6 chars (") */ + char* out = (char*)malloc(len * 6 + 1); + if (!out) return sv; + el_arena_track(out); + char* p = out; + for (size_t i = 0; i < len; i++) { + unsigned char c = (unsigned char)src[i]; + switch (c) { + case '&': memcpy(p, "&", 5); p += 5; break; + case '<': memcpy(p, "<", 4); p += 4; break; + case '>': memcpy(p, ">", 4); p += 4; break; + case '"': memcpy(p, """, 6); p += 6; break; + case '\'': memcpy(p, "'", 5); p += 5; break; + default: *p++ = (char)c; break; + } + } + *p = '\0'; + return el_wrap_str(out); +} + /* ── HTML allowlist sanitizer ──────────────────────────────────────────────── * el_html_sanitize(input, allowlist_json) * @@ -5674,6 +5712,50 @@ el_val_t getpid_now(void) { return (el_val_t)getpid(); } +/* el_mem_check — self-terminating memory guard for long-running compiler runs. + * + * Call this periodically (e.g. after each function compiled) to detect runaway + * memory growth before the OS OOM-killer fires. Reads the limit from the env + * var ELC_MAX_MEM_MB (default 512 MB). If resident set size exceeds the limit, + * prints a diagnostic to stderr and exits with code 1 so the caller (elb or a + * CI script) can handle the failure gracefully instead of having the whole + * machine go down. + * + * Platform notes: + * macOS — ru_maxrss is in bytes. + * Linux — ru_maxrss is in kilobytes. + * We normalise to MB before comparing. + * + * Returns 0 always (the only non-return path is the exit() branch). + */ +el_val_t el_mem_check(void) { + /* Read limit from env; default 512 MB. */ + long limit_mb = 512; + const char *env_val = getenv("ELC_MAX_MEM_MB"); + if (env_val && *env_val) { + long v = atol(env_val); + if (v > 0) limit_mb = v; + } + + struct rusage ru; + if (getrusage(RUSAGE_SELF, &ru) != 0) return 0; /* can't read — skip check */ + + long rss_mb; +#if defined(__APPLE__) || defined(__MACH__) + /* macOS: ru_maxrss is bytes */ + rss_mb = (long)(ru.ru_maxrss / (1024L * 1024L)); +#else + /* Linux: ru_maxrss is kilobytes */ + rss_mb = (long)(ru.ru_maxrss / 1024L); +#endif + + if (rss_mb >= limit_mb) { + fprintf(stderr, "elc: memory limit exceeded (%ldMB), aborting\n", limit_mb); + exit(1); + } + return 0; +} + /* ── args() — command-line argument access ────────────────────────────────── * Compiled El programs call args() to get a list of CLI arguments. * Call el_runtime_init_args(argc, argv) at the start of C main() to populate. diff --git a/runtime/el_runtime.h b/runtime/el_runtime.h index c0529ef..2f9583f 100644 --- a/runtime/el_runtime.h +++ b/runtime/el_runtime.h @@ -227,6 +227,8 @@ el_val_t url_decode(el_val_t s); /* '+' → space, %XX → byte */ * {"p":[],"a":["href","title"],"strong":[],...} * where each value is the array of attribute names allowed for that tag. */ el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json); +el_val_t html_raw(el_val_t s); +el_val_t html_escape(el_val_t s); /* ── Filesystem ──────────────────────────────────────────────────────────── */ @@ -531,6 +533,12 @@ el_val_t parse_int(el_val_t s, el_val_t default_val); el_val_t exit_program(el_val_t code); el_val_t getpid_now(void); +/* Self-terminating memory guard. Reads ELC_MAX_MEM_MB (default 512) and + * exits with code 1 if resident memory exceeds the limit. Call periodically + * during long compilation loops (e.g. after each function is compiled). + * Returns 0 when memory is within bounds. */ +el_val_t el_mem_check(void); + /* ── CGI identity ───────────────────────────────────────────────────────────── * Called at the start of main() in CGI programs (those with a `cgi {}` block). * Records the program's DHARMA identity before any other code executes. */