Security
Security posture
Festina compiles to native executables and links a C runtime — the two places a memory-safety or supply-chain issue would actually bite. This page describes the current security posture: the attack surface a compiled program has, the properties the implementation maintains, and how they are verified. The full round-by-round audit and design record lives in claude.md and tests/CONTRACT.md; this page is the state of the product.
Reporting a vulnerability #
Open an issue on the repository describing the problem and, if possible, a minimal Festina program (or runtime input) that reproduces it. There is no dedicated security contact or embargoed-disclosure program; the project is young, and that process will grow if it needs to.
Attack surface of a compiled program #
A Festina program's external interfaces are exactly:
- The local filesystem — its SQLite database (
festina.sqlite, orDatabaseURL), and any path ablob/img/audis declared from orsaved to. Paths are ordinary text expressions; a program that lets untrusted input choose them has the same exposure as any native program doing the same. - The process environment —
environment.NAMEis a read-onlygetenv()wrapper with no parsing of the result. - The local X server and ALSA device, only for programs that actually use graphics or audio — and only those programs link the libraries at all (see Slim binaries below).
- The network, for a program that calls
openPort()— the one genuinely new external interface this language has, and the first that lets remote, untrusted input reach a compiled program at all (environment/the filesystem above are both local-attacker-only). A program that never callsopenPort()gets none of this:festina_runtime_http.c(the HTTP/WebSocket implementation) is linked only when it's actually used, the same per-feature splitting graphics/audio already get (see Slim binaries below). The reverse direction exists too:req.send()(zero arguments) makes a real outbound HTTP/HTTPS connection to whateverreq.urlnames — a program that builds that URL (or the body/headers sent with it) from untrusted input has the same SSRF/data-exfiltration exposure any other language's own outbound-HTTP client would; this runtime does no allowlisting of hosts on its own.
This is a real, structural change from every other builtin:
req.url/req.method/req.headers/a
WebSocket frame's own payload are the first values
in this language that originate entirely from an untrusted,
remote party by design, not merely something a local user
could feed a program that also happens to read stdin/argv.
Concretely:
- No TLS.
openPort()is plain HTTP/WebSocket, no certificate, no encryption — traffic is inspectable and modifiable by anything on the network path. A program handling anything sensitive needs a TLS-terminating reverse proxy in front of it, oropenSecurePort()(mbedTLS-backed); neither claims to be a hardened public-facing server on its own.req.headers/req.toText()/etc. are exactly as trustworthy as whatever sent them — this language does no authentication, does no input validation of its own, and never will (that's the PROGRAM'S job, using the ordinary building blocks —regex,.replace(), string comparison — every other kind of untrusted text already has). - Single-threaded, one request at a time (see api.md) — a slow or hung
on request/on messagehandler denies service to every OTHER connection for as long as it runs. This is a real, structural availability property of the design (not a bug to be fixed later), and matters most for a program whose handler does slow work (a largesqlite()query, a big JSON render) in the request path. Acallback-carrying outboundreq.send()and backgroundblobloading (.callback()on a text path expression) are both narrow, deliberate exceptions at the OS-process level — small background thread pools do the actual network/file I/O — but every piece of GENERATED FESTINA CODE, including either callback, still runs on that same single main thread; this bullet's own claim abouton request/on messagehandlers is unaffected by either, and no Festina-visible global/refcount ever becomes something a program has to reason about concurrently.openPort()may be combined with graphics — a program that does both blocks in the graphics event loop, which also services the open port, so a slowon mouseDown/on requesthandler denies service to EITHER side while it runs, not just its own; and Ctrl-C/SIGTERM on such a program skips the standalone server's own graceful-shutdown grace period entirely (see api.md's own HTTP Limitations) — a documented gap, not a silent one. - An 8MB per-connection buffer cap (request line + headers + body, or one WebSocket frame's payload) bounds a single connection's own memory use, but this runtime does not limit the NUMBER of concurrent connections at all — many simultaneous connections, each near the cap, could still exhaust memory. No rate limiting of any kind exists; a program facing genuinely hostile traffic needs that in front of it (a reverse proxy, a firewall), the same way any other minimal server implementation would. Keep-alive means a connection can legitimately stay open with no request in flight at all (waiting to be reused) — the cap above still applies to whatever a connection is actually buffering, and an idle keep-alive connection is closed automatically after ~15 seconds of nobody using it (see api.md's own Keep-alive section), so this doesn't add an unbounded-lifetime connection to the list above; it does mean a client that opens many connections and sends just enough on each to avoid the idle timeout can hold more connections open for longer than a plain one-request-then-close server would — the same reverse-proxy/firewall answer above still applies to a client doing that on purpose. WebSocket fragmentation reassembly uses a SEPARATE accumulator from the per-frame cap above (each wire frame is fully consumed out of the connection's own read buffer as soon as it's parsed, so that buffer's cap alone would never bound a message reassembled from many small frames) — explicitly capped at the same 8MB, closing the connection with WebSocket code 1009 ("Message Too Big") if a peer tries to exceed it, rather than growing without bound.
- The request parser (HTTP/1.1 headers, chunked-transfer-encoding bodies, WebSocket frames and fragmentation reassembly) is hand-written C parsing untrusted bytes — the single largest category of memory-unsafety risk this language has, audited and stress-tested (ASan + LeakSanitizer, including abrupt-disconnect and malformed-input cases) but, unlike SQLite/Cairo/libjpeg/libmpg123 elsewhere in this runtime, not a widely-deployed, independently-hardened third-party implementation. Treat it with the same caution any from-scratch network-facing parser deserves. A malformed request line closes the connection and frees its resources properly, the same as any other rejected connection.
Every other builtin's own external interface (filesystem,
environment, X11/ALSA) is unchanged: still
local-attacker-only, still covered by everything below.
Standing properties #
Verified properties of the implementation, with the reasoning that makes each structural rather than incidental:
- No SQL injection via identifiers. Table and column names are embedded into generated SQL (SQL cannot parameterize identifiers), but the lexer's
IDENTgrammar ([A-Za-z_][A-Za-z0-9_]*) cannot produce a quote, semicolon, or any other metacharacter — there is no Festina syntax that could smuggle a payload. Query values are always parameterized throughsqlite3_bind_*, including the byte buffers ofblob/img/audparameters. - No format-string vulnerabilities. Untrusted text flows into
printf-family calls throughout the runtime (log(),fail(), error messages), and every one passes it as a%sargument, never as the format string. - No hand-rolled deserialization. Query results are read through SQLite's typed column API; media decoding is delegated to Cairo, libjpeg, and libmpg123 rather than reimplemented.
- Regex is POSIX ERE from libc (
<regex.h>) — no bundled engine to maintain, and pattern compilation failures are clean runtime errors. - Database durability model: the database opens in WAL mode with
synchronous=NORMAL— transactions survive an application crash unconditionally; an OS crash or power loss can lose the most recent commits but can never corrupt the file. This is the standard application-embedded SQLite configuration. - Schema-sync SQL generation is overflow-checked.
festina_sync_tablebuilds SQL by accumulating into a fixed-size stack buffer; every accumulation step is bounds-checked (festina_check_sql_buffer), and an oversized schema fails loudly instead of overflowing that buffer. SIGPIPEis ignored for the lifetime of a process usingopenPort(). Writing to a connection the peer has already reset or closed early raisesSIGPIPE, whose default disposition terminates the whole process with no error message at all — ignoring it turns that into an ordinary, already-handled write failure (every write already checks its own return value,-1/errno == EPIPE, wherever it matters) instead of a one-line remote denial of service. Windows has no equivalent exposure in the first place: winsock2 has noSIGPIPEfor a broken socket at all —send()just returns an error — so this is a Linux/macOS-specific mitigation for a Linux/macOS-specific signal.
Memory safety #
The memory model is escape analysis plus reference counting,
with cycle collection by trial deletion for the types that
can form one, and free/delete as
explicit overrides — continuously exercised under
AddressSanitizer, LeakSanitizer and (for the audio thread
pool) ThreadSanitizer.
scripts/leak_stress.sh
runs eleven mixed churn programs (background
.callback() loading for
blob/img/aud, nested
.toStruct()/.toArr() JSON parsing,
ternary-branch ownership, amor arr[T]'s
amortized growth, and both enum representations,
among others) plus one isolation program per data type on
every test run, and a canary test proves the harness itself
can fail.
The knowingly accepted gaps, none of which is remotely triggerable and each of which is a leak or a documented manual contract, not corruption:
- Array indexing is not bounds-checked — a deliberate performance choice, documented with its rationale in api.md. An out-of-range index in Festina source can read or write out of bounds in the compiled program. This is the one place where a bug in your Festina code has C-like consequences; the index is never attacker-supplied unless your program makes it so.
freeis a decrement on every managed pointer type —struct,arr,map,blob,img,aud, andregexall carry the same refcount header, so a shared value survives afreethrough one binding and an alias never dangles. (Atextbinding always owns its buffer exclusively — copy-on-alias — so freeing it outright is equally safe.) Every runtime release is null-safe, so double-freethrough a binding is a no-op.- One row-array chain shape leaks (
rows()[0]on a call-result array of query rows) — see todo.md. A leak, never use-after-free; tests pin that distinction so an "optimization" cannot silently trade one for the other. Reference cycles are collected by trial deletion — a reachable cycle is provably restored intact, so the collector cannot be tricked into freeing live data by a cycle that is still held.
Slim binaries #
A compiled program links only what it uses. The runtime is
split into core / graphics (Cairo, X11, libjpeg) / audio
(ALSA, libmpg123) / http (plain POSIX sockets on
Linux/macOS, winsock2 on Windows; no third-party library on
any platform) / https (openSecurePort() only,
mbedTLS) translation units, and the compiler puts a feature's
object file and libraries on the link line only when the
program actually exercises it — a log('hello')
program links none of them. Fewer resident libraries is a
smaller patch surface for any deployment, independent of
whether a specific library has a known issue today.
Regression-tested via ldd on real compiled
binaries for all four graphics/audio combinations
(tests/test_codegen.py::TestSlimBinaries).