Brief for an external security review
Status: maintained packet for a third-party reviewer (ROADMAP §7, still open). Scope, threat model, prior passes, and reproducing lanes below; the FFI review and SECURITY.md are pointed at, not summarised.
ROADMAP.md §7 asks for a third-party audit and has been open since the roadmap was written. It is the last 1.0 condition that no amount of internal work can close, and it stayed open partly because "get an audit" is not a task anyone can pick up — it needs a scope, a threat model, and an account of what has already been looked at. This is that packet. Hand it to a reviewer.
It deliberately does not summarise docs/internals/ffi-review-2026-08-14.md or SECURITY.md; it points at them and adds what a stranger needs that an insider would not think to write down.
1. What the thing is, in one paragraph
Tycho is a compiled language whose self-hosted compiler (compiler/, about 26k lines of Tycho) transpiles to C and hands the result to cc; bootstrap/tycho_compiler.c is the generated cold-start artifact, not a second implementation. There is no VM and no runtime interpreter. Programs get value semantics, arena allocation freed at scope exit, and an affine handle type whose destructor runs at scope exit. The standard library (corelib/, 89 packages) is written in Tycho except where it binds a system library, and those bindings are the subject below.
The security-relevant consequence of the design: a Tycho program's memory safety is whatever the emitted C has, plus whatever the hand-written C shims have. The first is machine-generated and sanitizer-tested; the second is hand-written and is where a reviewer should spend their time.
2. Threat model — where untrusted bytes actually enter
Ranked by how directly attacker-controlled data reaches hand-written C.
| entry point | package | reaches |
|---|---|---|
| an HTTP response body | core:http (libcurl) | a realloc accumulation loop in http_shim.c |
| a TLS stream | core:tls (OpenSSL) | tls_shim.c, 128 lines, client side only |
| a decompressed stream | core:compress (zlib) | an output-growth loop; reached from tycho-ar, core:zip, any gzip body |
| an archive | core:zip, tools/tycho-ar | le16/le32 offset arithmetic, path handling (zip-slip) |
| a PNG | core:image (libpng) | size arithmetic before a malloc |
| an HTTP request | core:httpd, server/ | request-line and header parsing, connection lifetime |
| a text document | core:json, csv, toml, markdown, utf8 | pure Tycho, but feeds everything above |
| a key or ciphertext | core:crypto (OpenSSL) | key import, AEAD, memory hygiene |
| a SQL parameter | core:sqlite | prepared statements, sqlite3_bind_text |
Two things a reviewer should know before reading any of it:
- The FFI boundary is unsafe by design.
extern fndeclarations are not checked against the C they name. A wrong signature is a silent ABI mismatch. There are 13corelib/*/*_shim.cfiles;wc -lthem plusruntime/tycho_rt.cfor the current size. core:osruns processes without a shell (posix_spawnpwith a real argv vector).osx_systemexists and is the documented shell API doing what it says. That distinction is load-bearing.
3. What has already been measured, and the scope of each claim
This is the part that saves a reviewer time, and every row understates rather than overstates. Each named lane is runnable — see §5.
| claim | how it is held | what it does NOT cover |
|---|---|---|
core:tls verifies chain AND hostname | make tls-verify — a real openssl s_server on loopback, three outcomes that must disagree | the tls shim's memory handling, error paths, session lifetime |
core:http verifies certificates | make http-verify | anything above the transport; the CA-override env vars are trusted input |
| no secret survives in memory the crypto shim released | sh scripts/crypto_hygiene.sh — interposes free, scans released blocks | memory the shim never owned (OpenSSL's own allocations, the runtime's FFI copy) |
| key-import hex decode is constant-time | same lane, under valgrind with the input marked undefined | cache-line and table-lookup side channels; everything else in the package |
| decompression cannot run away | a 1 GiB ceiling with its own error, proved to fire at a reduced ceiling | nothing else in zlib's surface |
| the emitted C traps out-of-bounds indexing | probes with the true length in the message | bytes/string slices CLAMP by design (FRICTION #5) |
| Windows undefined behaviour | make wine-ubsan — 361 fixtures, trap-mode UBSan | use-after-free and heap overflow: mingw-w64 here ships no ASan |
| text parsers survive mutation | scripts/fuzz_shims.sh | seed quality is the whole game — see §4 |
| markdown escapes text, scheme AND attributes | corelib/test/markdown, with the escaper defeated as a control | raw-HTML passthrough is not a feature, so it is untested rather than proven absent |
Nothing here is a substitute for §7 and this table is not a defence. It is a map of which stones have been turned over by someone who chose which stones to turn.
4. Why an outside reviewer is expected to find things
Not an argument from principle — from the record.
- The first structured pass over the shims (2026-08-14) found 5 issues in code that was green under every gate: an uncapped response body, a
size_twrap on ILP32, unbounded redirects with no scheme pin, an unbounded decompressor, and anSSL_readlength truncation. - A second pass the next day, choosing different targets, found 9 more — plaintext left in freed heap in both AEAD directions, a hex decode that timed where the bad digit was,
javascript:hrefs emitted live, a strict check placed downstream of a lenient parse so it never ran, and others. - A third pass (2026-08-15) is recorded in FRICTION #65 onward — the count is deliberately not written here, because it went stale inside the same session that produced it: this line said "10 more" and nine further entries landed after it.
grep -c '^### ' docs/internals/FRICTION.mdis the number, today. It includes a bound SQL parameter truncated at its first NUL and a password truncated the same way, both failing by *collision* — two distinct values becoming one, which is the mode that does not look like a bug.
Three passes, three different sets of targets, zero overlap between them. That is the shape of a surface that has not been reviewed enough, and the reason §7 is written as a hard condition rather than a nice-to-have.
The other half of the record is about instruments, and it is the more useful warning. Roughly a third of the time in those passes went into measurements that were themselves wrong *and looked like results*: a fuzzing corpus whose seeds died at the first byte while reporting 1950 clean mutants; a wine sweep where every invocation was command not found; a differential lane that scored 1197 clean answers while the defect it was built for sat untested in front of it, because the corpus was integers and the bug was in floats. Budget for the instrument being the defect. Every lane added since refuses to score anything until a deliberately wrong expectation has been shown to fail.
4b. There is a released artifact now
0.9.2 shipped 2026-11-12 with prebuilt Linux, Windows and macOS tarballs, so a reviewer does not have to build anything to start:
gh release download v0.9.2 -R StefanVonRanda/tycho
sha256sum -c tycho-v0.9.2-linux-x86_64.tar.gz.sha256
shasum -a 256 -c tycho-v0.9.2-darwin-arm64.tar.gz.sha256
The source at tag v0.9.2 is what those binaries were built from. Building from source is still the better basis for a review — the tarball tells you nothing about how it was made — but the artifact is there if you want to reproduce a finding against exactly what users get.
5. Reproducing anything here
No cloud CI, deliberately. Everything runs locally with a C compiler and make.
make # build ./tychoc from the self-hosted compiler
make test # 1039 fixtures under ASan/UBSan/LeakSanitizer
make corelib # every corelib package against its golden
make ci # the whole sweep, four lanes in parallel
Timings are given as ranges, and the ranges are wide on purpose. Measured across four green sweeps on one machine in one afternoon (2026-09-18): the whole of make ci between 422s and 1123s, make test between 82s and 144s, make corelib between 169s and 379s. Steps that were not touched between runs moved by 2x, so a single number here would be an invented precision — see FRICTION 91 for the measurement and why no figure in this tree should be read off one run. Expect single-digit minutes on a workstation; budget more on a shared or throttled box.
make ci runs under gcc or clang (both measured green, 2026-09-18). It needed gcc specifically until then, for three unrelated reasons that were all in the gates rather than the language — FRICTION 89, 90 and 92, each with the compiler measurement that closed it.
Security-specific lanes, each cheap and each with its controls documented in its own header:
make tls-verify make http-verify make math-diff
sh scripts/crypto_hygiene.sh sh scripts/fuzz_shims.sh
make wine-ubsan # ~13 min, Windows UB only
sh scripts/asan_self.sh # the COMPILER under ASan/UBSan over the whole corpus
CONTRIBUTING.md has a table mapping each part of the tree to the one lane that can redden for it. A package whose system dependency is absent is skipped, not failed, and the verdict line names it — read the last line, not the exit code.
6. Where we would start, if we were you
Offered as a starting point, not a scope — a reviewer who picks differently is the entire point of the exercise.
corelib/zip/andtools/tycho-ar. Archive parsing is offset arithmetic over attacker-controlled integers, and the zip-slip defence rests onpath.safe_join, which has a property test but no adversarial one. The fuzzing seeds for this were wrong twice before they reached the parser at all.core:httpdandserver/. Slowloris is bounded, not eliminated, and the residual is a design property of connection-per-worker. Request parsing survived every malformed shape thrown at it by one person.- The FFI signature surface itself. Not any single shim — the fact that nothing checks an
extern fnagainst the C it names. A systematic diff of declarations against headers is work nobody has done. Start with the interior-NUL class, and take the history as the warning: the rule is normative indocs/spec/14-ffi.md, a deliberate sweep for it ran on 2026-08-13, three packages carry explicit guards naming each other — and two packages that were never on that list both had the bug, one of them collapsing two passwords into one derived key (FRICTION #70, #75, #76, #77). Mechanically listing all 46 externs that take astringfound them in a single pass. Grep the declarations; do not reason about which ones matter. core:cryptobeyond the comparison.cx_ct_equalusesCRYPTO_memcmpand the key-import decode is constant-time; the rest of the package's side-channel surface is unexamined.- Windows use-after-free. Structurally uncovered: mingw-w64 here ships no ASan, so
make wine-ubsancatches undefined behaviour and nothing else.
7. What we want back
A list of findings with reproductions, and — more valuable — which classes you looked for and found nothing in, since that is what tells us where the map is genuinely blank rather than merely unvisited. Negative results with their controls are recorded here as first-class (see the markdown entry at the end of FRICTION.md), and yours would be too.
Report privately per SECURITY.md.