TYCHO Search

Search documentation

    ↑ ↓ to move · Enter to open · Esc to close

    Defect hunt, 2026-09-25

    Status: closed 2026-09-29 — all 90 baseline defects fixed, none open (Windows-only validation excluded). Baseline b9df4aa, linux-x86_64, gcc 13.3.

    A session aimed at finding defects nobody had recorded, after the open list in FRICTION.md was closed on 2026-08-18. Host: linux-x86_64, Ubuntu 24.04, gcc 13.3, 4 cores. Baseline: b9df4aa. The findings describe that commit; remediation status below was reconciled on 2026-09-29. Every entry records a defect in that baseline, where it lived, and how it was reproduced; the repro programs themselves lived in a scratch directory that did not survive the session.

    Method: eleven finder agents, each on one surface (compiler CLI, generated-source scale, compiler parity, emitted C, runtime, shims, the FFI surface, the network stack, tycho-db, archive and text parsers, and a real 1000-line program). Each finding was then handed to a separate verifier told to refute it: re-run it, check it against FRICTION, the spec and the reference, minimise it, and locate the cause. "verified" below means it survived that; "unverified" means a finder reproduced it but the session ended before a verifier ran; "reproduced" means found and reproduced by hand during the session. These labels in the detailed findings describe baseline evidence; the summary table records current fix status.

    Baseline totals: 90 defects -- 3 reproduced, 25 unverified, 62 verified; by severity 2 critical, 18 high, 47 medium, 23 low. Current table status: 90 fixed, none partial, and none open (excluding unavailable Windows-only validation).

    What was done

    Audit closure and limitations

    Closed 2026-09-29: all 90 original findings are fixed, with none partial or open. The main suite, parse, full friction, and citation gates pass as recorded above. The 255 addenda below are historical secondary observations; not every one was independently re-verified or separately dispositioned during remediation, and they are not counted among the 90 findings. Windows-only validation remains excluded from this completion target and unavailable here (no Windows host or Wine). Linux/GDB checks previously ran in an Ubuntu container on a Fedora host.

    Findings

    Compilers

    idseveritystatusdefect
    D01criticalfixedAssigning to an element whose container the right-hand side grows through 'inout' wrote into a recycled buffer or lost a map update; fixed in FRICTION #143 with place-store sequencing
    D02highfixedtychoc held StructDef*/Variant* across recursive generic resolution and table growth; pointer stability fixed in FRICTION #134, with the associated nested-enum subtree blowup closed in #200
    D03highfixedtychoc1 took cubic memory and time on an elif chain: 800 arms (30 KB source) needed 5.3 GB, 1250 arms were OOM-killed; elif emission is linear in #138/#139 and nested-loop parity is fixed in #202
    D04highfixedtychoc1 lowered '/' and '%' to bare C operators for fixed-width integers and element-wise array arithmetic; guarded in FRICTION #135
    D05highfixedAn empty literal '[]' was accepted for '[N]T' or 'vector[N]T'; rejected in FRICTION #136
    D06highfixedOut-of-range numeric literals wrapped silently in destination-coercion paths; fit checks added in FRICTION #142
    D07highfixedtychoc1 dropped const lengths for fixed arrays/vectors; resolved and validated in FRICTION #137
    D08highfixedtychoc1 emitted unguarded C '/' and '%' for sized integers and array division; guarded in FRICTION #135
    D09highfixed'total := total(xs)' emitted invalid C when a local shared its initializer function's name; fixed in FRICTION #140
    D10highfixedtychoc1 dispatched shadowed builtins inconsistently with its resolver warning; fixed in FRICTION #140
    D11mediumfixedtychoc read_file() overflowed on non-seekable source paths; fixed and pinned in FRICTION #141
    D12mediumfixedBoth compilers now escape quotes, backslashes and control bytes in -g #line paths; pinned in FRICTION #144
    D13mediumfixedNested generic method chains and constructors now reuse inferred argument/receiver types rather than recursively re-emitting or re-resolving subtrees; pinned by FRICTION #200 (tests/emitter_nested_calls.sh)
    D14mediumfixedAn elif chain was lowered to nested C blocks; emitted iteratively and pinned in FRICTION #139
    D15mediumfixedtychoc1 program_in retained growing struct-definition prefixes quadratically; fixed and stress-tested in FRICTION #145
    D16mediumfixedFlat integer + chains remain iterative with the 2000-term diagnostic; other left-deep binary spines now get a located 1024-depth diagnostic in both compilers, and the self-hosted typechecker guards recursive expression trees (FRICTION #157)
    D17mediumfixedFlat simple-int sums and interpolated-string joins now emit at constant bracket depth; pinned under clang in FRICTION #146
    D18mediumfixedLarge constant scalar array literals now use a block-scope static table plus memcpy in both emitters; nonconstant literals retain element-wise ownership handling; 15,625 elements compile under gcc -O3 in tests/array_literal_scale.sh
    D19mediumfixedF-string C joins now assemble pieces linearly; lambdas append captures/parameters and restore binding-table length marks rather than snapshotting every enclosing local table; 4,000 holes and 8,000 capturing lambdas stay below 1.2 GiB in tests/emitter_memory_scale.sh
    D20mediumfixedtychoc1 emitted a synthesized tuple type rather than its destination type when tuple elements adapted; fixed and covered across contexts in FRICTION #147
    D21mediumfixedPacked-field inout uses an aligned local and memcpy to/from the byte-offset field address in both emitters, including FFI calls; direct, array-element and custom-subscript paths run under UBSan in tests/packed_inout.ty, and extern calls in tests/packed_inout_ffi.sh
    D22mediumfixedFixed-width float conversions now use the shared total float-to-u64 wrapping helper, matching native behavior; FRICTION #150 covers finite out-of-int64, negative, and NaN inputs
    D23mediumfixedtychoc1's if/elif memory use grew super-linearly; the flat AST fix is recorded in FRICTION #138
    D24mediumfixedF-string holes now trim surrounding whitespace, reject trailing tokens and skip char literals while finding the close brace; pinned by fstring_hole_whitespace and two reject fixtures
    D25mediumfixedtychoc1 typechecked but could not emit documented pow(x, y) or SoA for-in; both lowerings are covered by tests/emit_pow_soa_iter.ty
    D26mediumfixedCompiler-generated AST temporaries use a reserved 'Ztycho_' identifier namespace rejected by the lexer; collision cases for for-in, compound place/multi-assign, and '_mx36_0_0' are pinned in FRICTION #154
    D27mediumfixedFixed with D22: self-hosted fixed-width float conversions use 'tycho_f2u64' rather than the trapping 'tycho_f2i'; pinned in FRICTION #150
    D28mediumfixedEnum payload members and payload-struct typedefs use prefixed C-only names, keeping C keywords/macros and user enum names out of the generated namespace; tested in FRICTION #156
    D29mediumfixedFloat hash bits are canonicalized for both scalar composite leaves and float arrays, so +0.0 and -0.0 hash equally; nested-key overwrite is pinned in FRICTION #151
    D30mediumfixedSelf-hosted composite-key validation now matches native leaf types and rejects sized-int/f32 fields before emission; covered by two reject fixtures
    D31lowfixedProcedure, constant, variant and generic registries now use append-aware indexes, and both compilers cache source-line excerpts; stress-tested in FRICTION #198/#201 (tests/proc_signature_scale.sh, tests/name_registry_scale.sh)
    D32lowfixed--symbols now runs resolution and typechecking before printing, exits nonzero on invalid input, and emits no partial index (FRICTION #197)
    D33lowfixedBoth CLIs reject an explicitly empty -o before binary or --emit-c output, so neither creates a stray .c file (FRICTION #197)
    D34lowfixedBoth CLIs reject separated -L/-I without a non-option directory argument; a following linker flag is no longer swallowed as a path (FRICTION #197)
    D35lowfixedNative resolver recursion depth is reset at statement/procedure longjmp recovery boundaries; a generated 2105-error check confirms neither compiler fabricates depth diagnostics (FRICTION #155)
    D36lowfixedVariant ownership now consults the declared package prefix instead of splitting arbitrary type names at '__'; generic instance C names gain a suffix when they collide with source aggregate names; covered in FRICTION #158
    D37lowfixedShift lowering now preserves unsigned count bits before width checks, so u64 counts ≥ 2^63 yield zero rather than a false negative-count abort; FRICTION #153
    D38lowfixedNative generic parameter-pattern diagnostics now use nominal_name(); tests/package_diagnostic_name.sh asserts both compilers print source-spelled (not mangled) function names

    Runtime and emitted code

    idseveritystatusdefect
    D39highfixedArray reserve checks multiplication before allocation; map reserve bounds capacity before doubling/index sizing, and both abort before wrapped allocations (FRICTION #178, tests/audit_runtime_guards.sh)
    D40mediumfixedThe self-hosted emitter now lowers the documented pow(x, y) builtin; tests/emit_pow_soa_iter.ty covers it (FRICTION #148)
    D41mediumfixedChannel capacity is checked against the cell-array allocation size before multiplication; the runtime-guard gate checks overflow in both compilers (FRICTION #178)

    corelib shims

    idseveritystatusdefect
    D42highfixedRegex subjects are capped at 4,096 bytes, bounding the pathological unanchored search; oversize input is refused and pinned by the runtime-guard gate (FRICTION #178)
    D43highfixedCrypto hex inputs now carry explicit lengths or reject embedded NULs before C-string parsing; crypto regression cases pin the surface (FRICTION #164)
    D44highfixedHMAC, signature verification and AEAD paths reject NUL-truncated inputs under the same binary-safe crypto boundary fix (FRICTION #164)
    D45mediumfixedio.copy detects identical files by device/inode before opening the destination; corelib I/O tests cover same-path and alias cases
    D46mediumfixedSQLite path and SQL entry points reject embedded NULs before crossing the C-string ABI; corelib tests verify the database/statement is not changed (FRICTION #164)
    D47mediumfixedHTTP request construction rejects control characters in Content-Type; tests/http_header_guard.sh pins header injection rejection (FRICTION #178)
    D48mediumfixedRegex group indices are checked against the compiled capture count before allocating or reading capture slots (FRICTION #178)
    D49mediumfixedStream lines and child-process output cross the FFI as length-carrying bytes, preserving embedded NULs (FRICTION #164)
    D50lowfixedInflate growth now reaches the inclusive configured cap, probes past it, and distinguishes over-limit from truncated input; a reduced-cap C gate verifies both boundaries (FRICTION #165)
    D51lowfixedPOSIX TZ numeric fields and rule ranges are bounded before conversion; oversized-hour/minute/second and long-digit inputs are pinned (FRICTION #166)
    D52lowfixedBoth compilers reject aggregate-return handle destructors whose C ABI needs hidden out-parameters; reject fixtures cover bytes and arrays (FRICTION #167)
    D53lowfixedFFI docs distinguish C int (i32) from Tycho int and describe to_i32 as narrowing (FRICTION #181)

    Network (core:net, core:http, core:httpd, server/)

    idseveritystatusdefect
    D54highfixedRequest framing rejects every Transfer-Encoding field and empty Content-Length, distinguishing absent from present-empty headers (FRICTION #180)
    D55mediumfixedHTTP Content-Type control characters are rejected before header serialization (FRICTION #178)
    D56mediumfixedtls.connect_timeout and tls.set_timeout_ms are public, bounded to signed 32-bit milliseconds, and listed in surface.lock (FRICTION #179)
    D57lowfixedparse_request rejects invalid field names/values, obsolete folding, extra request-line tokens and unsupported versions (FRICTION #180)
    D58lowfixedRange digit limits count significant digits, not leading zeroes; padded positions and suffix lengths are pinned in server/run.sh (FRICTION #180)
    D59lowfixedThe net shim validates descriptor range before narrowing to the platform int type; corelib net tests pin large descriptors (FRICTION #178)
    D60lowfixedBoth package suggestion paths exclude _-prefixed private symbols; a package reject fixture pins the no-suggestion diagnostic (FRICTION #196)

    Archive and text parsers

    idseveritystatusdefect
    D61highfixedTOML parent tables now merge prior implicit subtables, and nested array-of-table headers attach to the latest element; regression cases preserve both paths (FRICTION #182)
    D62mediumfixedresolve_under now detects and refuses dangling symlink components instead of climbing past them; ordinary missing leaves still return for 404 (FRICTION #195)
    D63mediumfixedTOML parsing now bounds input to 64 KiB, entries/headers to 512 and nesting to 128, including arrays and dotted paths; boundary refusals are pinned (FRICTION #183)
    D64mediumfixedzip.create_checked validates classic ZIP names/counts/sizes/offsets before writing; legacy create is fail-fast, not truncating (FRICTION #184)
    D65mediumfixedAll zlib entry points reject lengths that exceed uInt before narrowing (FRICTION #165)
    D66mediumfixedTOML uses a quote-aware dotted-key scanner for assignments/headers, decodes quoted basic keys, rejects trailing literal-string text and surrogate escapes (FRICTION #185)
    D67mediumfixedJSON object parsing indexes keys with a per-object hash map while preserving last-wins-at-first-position duplicates; 20,000-member parse is pinned in corelib (FRICTION #186)
    D68mediumfixedInline rendering caches found ]/) positions, so unmatched link/image openers reuse terminators instead of rescanning; 20,000-opener fixtures are pinned (FRICTION #187)
    D69mediumfixedMeta carries its local offset and extract_entry enables one-directory-pass iteration; EOCD search is limited to the legal 65,557-byte suffix and rejects trailing junk (FRICTION #188)
    D70mediumfixedGzip decompression consumes concatenated members and rejects trailing non-member data (FRICTION #165)

    tools/tycho-db

    idseveritystatusdefect
    D71highfixedCheckpoints write/sync a sibling temp file, atomically rename it, sync the directory, then rewind the WAL; a file-size-limit regression preserves and reopens the old store (FRICTION #168)
    D72highfixedThe self-hosted emitter now discovers bytes and nested match/select accumulators; a 50,000-iteration two-compiler gate runs under an RSS limit (FRICTION #177)
    D73mediumfixedSorted-array shifts were replaced with a type-tagged hash index; 20,000 reverse-order inserts and a reopen/rebuild stay under 192 MiB (FRICTION #175)
    D74mediumfixedCREATE/INSERT validate, append to the WAL, then apply; the NotWritten regression proves later statements cannot observe a refused mutation (FRICTION #169)
    D75mediumfixedwal.open_db acquires a process-lifetime exclusive .lock before reading the store; a CLI competing with the server is refused (FRICTION #170)
    D76mediumfixedStore decoding validates row shape/types, integer encodings, tags, truncation and trailing bytes; corrupted fixtures fail closed with no stdout (FRICTION #171)
    D77lowfixedA corrupt store is no longer rebuilt from a zero-length WAL; the table's missing-store half was refuted in the addenda and is not a defect (FRICTION #172)
    D78lowfixedServer lines have an absolute one-second deadline and sessions a 60-second cap; an idle-peer/second-client test verifies the daemon resumes service (FRICTION #173)
    D79lowfixedSELECT and EXPLAIN validate indexed-key types before probing; the addenda's separate WHERE-column claim was refuted (FRICTION #174)

    Found by the 1000-line program

    idseveritystatusdefect
    D80criticalfixedThe self-hosted emitter now handles bytes and accumulators in match/select scopes; large 50,000-iteration fixtures run under a memory limit with both compilers (FRICTION #177)
    D81highfixedThe self-hosted receive lowering allocates the Option and payload copy in cs.arena; direct-return, bound and or_else cases survive arena churn under both compilers (FRICTION #176)
    D82mediumfixedBoth emitters copy map replacements into the owner and recursively recycle nested arrays, maps, structs, enums, tuples, options/results, SOA values and captured closures; composite-key/value deletion also reclaims owned payloads (FRICTION #193, tests/map_overwrite_scale.sh)
    D83mediumfixedQualified generic-enum construction now resolves templates within the named package and supports inferred payload or explicit type arguments; a two-compiler package fixture covers both forms (FRICTION #191)
    D84mediumfixed[string] writes copy/recycle old values and variadic packs/arguments use loop scratch; a two-million-iteration combined regression runs below 96 MiB under both compilers (FRICTION #192)
    D85mediumfixedSQLite path and SQL functions reject embedded NULs before the C-string boundary (FRICTION #164)
    D86lowfixedThe enum reference now states bare None equality is ungrounded; tychoc1 rejects bare Option/Result comparisons during typechecking with a located diagnostic and has three reject fixtures (FRICTION #189)
    D87lowfixedSelf-hosted builtin-shadow advice now uses the declaration's package prefix; a subpackage len warning fixture pins its qualified-call suggestion (FRICTION #190)
    D88lowfixedBodyTooLarge now separates a framed entity over budget from an incomplete oversized header; server and tycho-kvsrv return 413 vs 431 with reason text, pinned by corelib and wire tests (FRICTION #194)
    D89lowfixedThe SQLite example and stale corelib append/path/NUL documentation are corrected; docs-fence and corelib-reference gates pin them (FRICTION #181)

    Gates

    idseveritystatusdefect
    D90mediumfixedtools/tycho-debug/run.sh accepts both GDB 15's No line 999 in file and GDB 17's No compiled code for line 999; all seven make debug-check legs pass on Fedora/GDB 17.2, including idle Ctrl-C

    Detail

    Line numbers are as of b9df4aa and are written as prose, not as live citations.

    D01. Assigning to an element whose container the right-hand side grows through 'inout' writes into a buffer the arena has already recycled: free-list corruption and SIGSEGV (tychoc1+gcc, tychoc+clang), or a lost update (map, both compilers+gcc)

    critical, unverified; compilers: both.

    D02. tychoc heap-use-after-free: a StructDef*/Variant* pointer is held across resolve_expr while a nested generic instantiation reallocs g_structs/g_enums. SIGSEGV on a 195-byte valid program

    high, verified; compilers: tychoc.

    D03. tychoc1 takes cubic memory and time on an elif chain: 800 arms (30 KB source) need 5.3 GB, 1250 arms are OOM-killed

    high, verified; compilers: tychoc1. Current disposition: the elif path is linear in FRICTION #138/#139; the native/self-hosted nested-loop depth mismatch is fixed in #202. The separate compile-cost observation for 250 nested blocks remains a bounded (256-level) syntax/resource limit, not a constant-cost guarantee.

    D04. tychoc1 lowers '/' and '%' to bare C operators for every fixed-width integer and for element-wise array arithmetic. A zero divisor, or (-2^63) / -1, kills the process with SIGFPE (a literal 0 gives SIGILL) instead of the spec'd clean abort.

    high, unverified; compilers: tychoc1.

    D05. An empty literal '[]' is accepted for a fixed-size '[N]T' or 'vector[N]T'. tychoc emits an uninitialised C aggregate (reads of uninitialised stack, SIGSEGV with string elements), and tychoc1 builds a length-0 "fixed" array.

    high, unverified; compilers: both.

    D06. Out-of-range numeric literals wrap silently instead of being refused. tychoc1 does this in variadic, UFCS, lambda-body, m.get default, subscript-assignment, value-match and f32 vector/array routes. Both compilers do it for the element-wise broadcast scalar on a sized-int array.

    high, unverified; compilers: both.

    D07. tychoc1 drops the length of '[N]T' when N is an int const: wrong-arity literals, 'push' onto a fixed array, zero or negative N, and mismatched fixed-array parameters all compile, and 'len' is wrong. The same const 'vector[N]T' fails with an internal error or invalid C.

    high, unverified; compilers: tychoc1.

    D08. tychoc1 emits unguarded C '/' and '%' for every sized integer type and for element-wise array division, so division by zero is SIGFPE/SIGILL instead of 'tycho: division by zero', and i32/i64/[int] MIN/-1 trap

    high, unverified; compilers: tychoc1.

    D09. 'total := total(xs)', a local named after the same-package function that initializes it, is accepted by both compilers and emits C that does not compile

    high, unverified; compilers: both.

    D10. tychoc1 calls a user function that shadows a builtin (keys, map_get, to_u8, ...) where tychoc -- and tychoc1's own warning -- say the builtin wins: a silent miscompile

    high, reproduced.

    D11. tychoc read_file() aborts (heap buffer overflow) on any non-seekable source path (pipe, FIFO, process substitution, piped /dev/stdin)

    medium, verified; compilers: tychoc.

    D12. -g emitted unescaped source paths in #line directives and DWARF, corrupting debug paths or producing malformed C

    medium, verified; compilers: both. Current disposition: fixed in FRICTION #144; both emitters escape quotes, backslashes, newlines, tabs, and other control bytes, pinned by tests/debug_path.sh.

    D13. Exponential compile time and memory on nested calls. tychoc1: method-call chains (generic or not), nested generic calls, nested generic struct constructors. tychoc: nested generic enum constructors

    medium, verified; compilers: both. Current disposition: fixed in FRICTION #200.

    D14. An elif chain is lowered to one nested C block per arm: tychoc SIGSEGVs at ~7000 arms, clang (a supported cc) rejects chains of 255+ arms, and the emitted C grows quadratically (5000 arms -> 200 MB of C)

    medium, verified; compilers: both.

    D15. tychoc1 program_in copies the whole struct-definition text once per field: 900 structs x 20 fields (405 KB) need 4 GB, 2000 structs x 10 fields need 5.6 GB

    medium, verified; compilers: tychoc1.

    D16. tychoc1 has neither of tychoc's expression-depth guards: a flat 1400-term sum aborts tychoc1 with an unlocated 'stack overflow' that tychoc compiles, and 256- to ~2000-deep nested calls are accepted against spec §3.9

    medium, unverified; compilers: both.

    D17. Emitted C nests one bracket level per term of a flat '+' chain or f-string piece, so clang (a supported cc) rejects accepted programs: a 256-term sum, a 130-hole f-string

    medium, unverified; compilers: both.

    D18. A constant array literal is emitted as one store per element inside a statement expression, and gcc -O3 needs ~1.2 ms per element: a 15,625-element table (61 KB) builds in 21 s, 125,000 in 153 s, 250,000 in over 280 s

    medium, unverified; compilers: both.

    D19. tychoc1 takes quadratic memory on one long f-string (4000 holes on one line: 5 GB, 14 s) and on lambdas in a function with many locals (8000 lambdas: 5.7 GB)

    medium, unverified; compilers: tychoc1.

    D20. tychoc1 accepts a tuple literal whose elements adapt to a typed tuple, e.g. 'p: (float, u8) = (1, 2)', and emits C that cc rejects.

    medium, unverified; compilers: tychoc1.

    D21. Both compilers pass a misaligned pointer when a field of a 'packed struct' is taken as an 'inout' argument, which is undefined behaviour in the emitted C.

    medium, unverified; compilers: both.

    D22. tychoc1 makes 'to_u8'..'to_i64' of a float or f32 abort on NaN, infinity or any value outside the int64 range (including 'to_u64(1e19)', which fits a u64). The spec says these conversions are total, and tychoc gives the defined wrapped result.

    medium, unverified; compilers: tychoc1.

    D23. tychoc1's memory use grows super-linearly with the length of an if/elif chain: 500 arms take 1.3 GB, and 1000 arms (2004 lines) exceed 8 GB and die. tychoc compiles the same file in 0.1 s.

    medium, unverified; compilers: tychoc1.

    D24. An f-string hole parses its first expression and silently discards the rest, so 'f"{price:.2f}"', 'f"{n )))}"' and 'f"{n, 99}"' compile and print the bare value. A space after '{', which the spec itself writes as '{ expr }', is rejected as "unexpected indentation".

    medium, unverified; compilers: both.

    D25. tychoc1 type-checks two documented constructs and then dies in its emitter with an unlocated internal error: the builtin 'pow(x, y)', and 'for x in s' over an 'soa'. tychoc compiles both.

    medium, unverified; compilers: tychoc1.

    D26. Compiler-generated temporaries share the user identifier namespace: a user variable named '_fc0', '_cx0' (tychoc) or '_mx36_0_0' (tychoc1) is silently replaced by the compiler's temp and the program prints a wrong answer

    medium, unverified; compilers: both.

    D27. tychoc1 lowers 'to_u8'..'to_i64' of a float through the aborting 'tycho_f2i', so the spec-total conversions abort on NaN, inf and out-of-range input, including the in-range 'to_u64(1e19)'

    medium, unverified; compilers: tychoc1.

    D28. Enum payload-variant names reach C unmangled, so a variant named 'EOF', 'NULL', 'BUFSIZ', 'INT64_MAX', 'linux', 'unix', 'default', 'case', 'while' or 'switch' fails the C build; 'enum Shape {Circle(..)}' next to 'enum Shape_Circle' collides on the typedef 'E_Shape_Circle'

    medium, unverified; compilers: both.

    D29. Composite map keys with a float leaf break the '=='/hash contract: 'K(0.0) == K(-0.0)' is true but the hashes differ, so a map holds two '=='-equal keys

    medium, unverified; compilers: both.

    medium, unverified; compilers: tychoc1.

    D31. tychoc front-end (parse+resolve) is quadratic O(N^2) in top-level procedure count; ~15x slower than tychoc1 and widening

    low, fixed; compilers: tychoc. Current disposition: FRICTION #198 and #201.

    D32. --symbols disagrees on a name-resolution error: tychoc exits 1, tychoc1 exits 0 and still prints the index

    low, verified; compilers: both.

    D33. -o with an empty string: tychoc errors, tychoc1 silently ignores it and builds a default-named binary (fail-open)

    low, verified; compilers: both.

    D34. Trailing bare -L/-I (no value): tychoc fails, tychoc1 swallows -lm and builds (fail-open)

    low, verified; compilers: both.

    D35. tychoc never resets g_resolve_depth after recovering from an error, so after enough ordinary errors every later expression, valid code included, is reported as 'expression too deeply nested to type-check (max 2000)'

    low, unverified; compilers: tychoc.

    D36. tychoc1 mishandles user type names that contain '__': a main-package enum's variants are refused in 'match', and a struct named like a generic instance ('Box__int') breaks the emitter

    low, unverified; compilers: tychoc1.

    D37. A u64 shift count of 2^63 or more aborts with 'tycho: negative shift count', although the spec defines a count ≥ width as 0 and a u64 cannot be negative

    low, unverified; compilers: both.

    D38. tychoc prints a mangled package function name in the parameter-pattern mismatch error ('argument 2 of math__clamp'), a site the FRICTION #41 fix missed; tychoc1 says 'clamp'

    low, fixed 2026-09-27.

    D39. reserve(xs, n) on a composite-element array with attacker-controlled n has no size_t-overflow guard: (size_t)n*sizeof(elem) wraps to a tiny allocation under a huge cap -> heap-buffer-overflow / heap corruption

    high, verified; compilers: both.

    D40. Shipping self-hosted compiler tychoc1 cannot compile the documented builtin pow(x, y); tychoc accepts it (parity break + spec violation)

    medium, verified; compilers: tychoc1 rejects, tychoc accepts.

    D41. channel(T, cap) with a large capacity overflows the cell-array size computation in tycho_chan_new -> tiny allocation, then the init loop writes out of bounds (heap-buffer-overflow / SIGSEGV)

    medium, verified; compilers: both.

    D42. core:regex — a pattern UNDER the repetition-product ceiling still causes a 40+ second match-time DoS; the ceiling bounds compile memory, not match time

    high, verified; compilers: both.

    D43. core:crypto — the interior-NUL sweep (FRICTION #75–#78, declared COMPLETE) missed the entire crypto hex surface; sha256/sha512/hmac/pbkdf2-salt/aead/ed25519-sign/ed25519-verify/x25519/key_from_hex all truncate a hex argument at the first NUL, giving collisions

    high, verified; compilers: both.

    D44. core:crypto hex inputs stop at the first NUL, so verify, HMAC and AEAD-decrypt accept input that was never signed or encrypted

    high, verified; compilers: both.

    medium, verified; compilers: both.

    D46. core:sqlite open() and exec() act on the NUL-truncated prefix: open creates or opens a different database file, and exec runs a shorter statement

    medium, verified; compilers: both.

    D47. core:http post/post_bytes pass a CR/LF in content_type into the outgoing request headers

    medium, verified; compilers: both.

    D48. regex.group_start/group_end/group: a huge group index wraps the allocation size and rx_exec writes past a 0-byte heap block

    medium, verified; compilers: both.

    D49. io.read_line/fold_lines and os.run/exec_out silently cut data at the first NUL, while io.read/read_lines keep it

    medium, verified; compilers: both.

    D50. core:compress — a valid gzip/deflate stream well under the documented 1 GiB ceiling is refused as TooBig; the doubling loop's real limit is ~512 MiB and it mislabels legitimate data a 'decompression bomb'

    low, verified; compilers: both.

    D51. datetime.offset_at accepts out-of-range and overflowing POSIX TZ offsets (returns 8044431881044758032) where glibc clamps and the docs say bad input answers 0

    low, verified; compilers: both.

    D52. A handle destructor declared -> bytes/[int]/[float] passes the free: check, then the scope-exit call fails in cc with 'too few arguments'

    low, verified; compilers: both.

    D53. docs/reference/ffi.md still says u8/u16/i8/i16/i32/i64 are plain int to Tycho and that to_i32 returns int; both compilers reject that

    low, verified; compilers: both.

    D54. httpd.framing fails open on a repeated Transfer-Encoding whose last value is empty (and on an empty Content-Length): request is framed as bodyless, the connection stays alive, and the body bytes are answered as a second request

    high, verified; compilers: both.

    D55. core:http post/post_bytes: CR/LF in the content_type argument injects arbitrary request headers onto the wire

    medium, verified; compilers: both.

    D56. core:tls: the documented bounded-connect API (tls.connect_timeout, tls.set_timeout_ms) cannot be called from any program; the functions are package-private

    medium, verified; compilers: both.

    D57. httpd.parse_request accepts header and request-line grammar that RFC 9112 requires rejecting: whitespace before the colon, an obs-folded continuation line honoured as its own field (both give a real Content-Length), and a request line with more than three tokens or any version string

    low, verified; compilers: both.

    D58. tycho-httpd Range: a position with leading zeros that is 16+ characters long is treated as 10^15, giving a false 416 for a satisfiable range and the whole file for a small suffix

    low, verified; compilers: both.

    D59. core:net shim truncates 64-bit fds to int after checking only 'fd < 0', so net.close_fd(fd + 2^32) closes an unrelated live descriptor instead of failing closed

    low, verified; compilers: both.

    D60. Cross-package "did you mean" suggests a package-private (_-prefixed) symbol, which the next compile then refuses

    low, verified; compilers: both.

    D61. core:toml silently drops data from VALID documents: a parent table declared after its subtable, and subtables of arrays-of-tables

    high, verified; compilers: both.

    medium, verified; compilers: both.

    D63. core:toml can be crashed and memory-exhausted by small inputs: no nesting cap, and quadratic time and memory in keys and headers

    medium, verified; compilers: both.

    D64. zip.create silently wraps a name length >= 64 KiB and an entry count > 65535 (to_u16/to_u32), writing archives whose own reader lists forged entries and hides real ones

    medium, verified; compilers: tychoc.

    D65. core:compress shim truncates lengths >= 4 GiB to uInt: compress/raw_compress silently encode only len mod 2^32 bytes and report success

    medium, verified; compilers: n/a.

    D66. core:toml mis-parses quoted keys and quoted headers, and invents values from malformed literal strings and lone surrogate escapes

    medium, verified; compilers: both.

    D67. core:json parse is quadratic in the number of members of one object (the duplicate-key scan)

    medium, verified; compilers: tychoc.

    D68. markdown.inline is still O(n^2) when the terminator EXISTS: '['*n + ']' (the no_rb memo only caches absence)

    medium, verified; compilers: tychoc.

    D69. core:zip: every extract() re-reads the whole central directory and scans backwards over the whole file for the EOCD, so reading all entries is quadratic, and trailing junk multiplies the cost

    medium, verified; compilers: tychoc.

    D70. compress.decompress returns only the FIRST gzip member and accepts trailing garbage, as Ok

    medium, verified; compilers: both.

    D71. The checkpoint rewrites the store in place, so a crash, kill -9 or full disk during it destroys every committed row

    high, verified; compilers: both.

    D72. tychoc1 drops the in-place append for 'bytes' accumulators and for accumulators declared in a match arm (tychoc keeps both), so tycho-db's torn-log recovery needs 7.5 GB for a 789 KB log

    high, verified; compilers: tychoc1.

    D73. KEY index maintenance is quadratic in time and memory: 689 KB of INSERTs over TCP grows the server to 1.6 GB, and reopening a 560 KB store takes 3.8 GB

    medium, verified; compilers: both.

    D74. A mutation whose WAL append fails is still applied in memory: the client gets 'err cannot write', yet the row is served and checkpointed, and after a crash the database can refuse to open

    medium, verified; compilers: both.

    D75. Nothing locks the store: two processes on one database silently lose acknowledged commits (300 of 300 in one run)

    medium, verified; compilers: both.

    D76. store.decode trusts row arity, value types and trailing bytes: a one-byte corruption panics the process mid-query ('index 0 out of bounds'), and others silently change rows

    medium, verified; compilers: both.

    D77. open_db fails open on two unrecoverable states: a corrupt store beside a zero-length log is 'rebuilt' as an empty database and overwritten, and a missing store beside a checkpointed log opens with only the post-checkpoint suffix

    low, verified; compilers: both.

    D78. One idle TCP connection pins the serialised server forever: there is no read timeout, so every other client hangs

    low, verified; compilers: both.

    D79. The index path turns a cross-type comparison into an empty answer where the scan path reports an error, and WHERE columns are checked only against examined rows

    low, verified; compilers: both.

    D80. tychoc1: a string/bytes accumulator declared inside a match arm is never compiled to in-place append, so the loop takes O(n²) memory. json.stringify of a 20,000-element array uses 2.1 GB, and a 94 KB POST kills a tychoc1-built JSON server

    critical, verified; compilers: tychoc1.

    D81. tychoc1: a recv() used as an expression is built in the function's own _scope, so it is used after free when it escapes the function (SIGSEGV or a corrupted Option), and it grows memory without limit inside a loop

    high, verified; compilers: tychoc1.

    D82. Overwriting a heap value in a long-lived map never reclaims the old value, so tycho-kvsrv grows about 1 KB of RSS per PUT to the same key without limit (both compilers)

    medium, verified; compilers: both.

    D83. tychoc1 rejects constructing a generic enum's variant through a package qualifier ('box.Has(41)', 'box.Nothing$(int)') with internal, location-less messages. tychoc compiles and runs both

    medium, verified; compilers: tychoc1.

    D84. tychoc1 builds per-iteration values in the function arena where tychoc uses the loop scratch or recycles: '[string]' element overwrite (no MM-9) and variadic packs grow without limit in a loop

    medium, verified; compilers: tychoc1.

    D85. core:sqlite passes the database path and exec SQL to C as NUL-terminated char* with no interior-NUL guard: open() opens a different file and exec() runs a truncated statement, both returning Ok

    medium, verified; compilers: both.

    D86. The documented 'Some(1) == None' / 'opt == None' is rejected by both compilers. tychoc1's rejection is a location-less emit-phase die, and tychoc's names the mangled type ('util__Id')

    low, verified; compilers: both.

    D87. tychoc1's builtin-collision warning tells a sub-package author that 'package main' has no prefix and to rename. The function is in 'package res' and 'res.close(...)' works; tychoc gives the right advice

    low, verified; compilers: tychoc1.

    D88. tycho-httpd answers '431 Request Header Fields Too Large' to a small head whose Content-Length exceeds the budget, where 413 applies. core:httpd's single TooLarge conflates the two

    low, verified; compilers: both.

    D89. Wrong docs hit while building the app: the core:sqlite header usage example does not compile, and the corelib reference says io.append is 'read-rewrite, not atomic' although it is O_APPEND

    low, verified; compilers: both.

    D90. make debug-check pins gdb's wording: gdb 15.1 (Ubuntu 24.04) prints 'No line 999 in file', the gate requires 'No compiled code for line 999', so make ci is red on this host

    medium, fixed 2026-09-27.

    Negative results

    Classes each lane probed and found nothing in, which is what says where the map is blank rather than unvisited (audit-brief.md §7).

    What the verifiers added

    Each verifier re-ran its finding and often found more: a second defect beside the first, a cause the finder had wrong, or a part of the claim that did not hold. Those are listed here by finding. "secondary defect" rows are defects in their own right and are not counted in the totals above.

    D02 (addenda). tychoc heap-use-after-free: a StructDef*/Variant* pointer is held across resolve_expr while a nested generic ...

    D03 (addenda). tychoc1 takes cubic memory and time on an elif chain: 800 arms (30 KB source) need 5.3 GB, 1250 arms are ...

    D11 (addenda). tychoc read_file() aborts (heap buffer overflow) on any non-seekable source path (pipe, FIFO, process ...

    D12 (addenda). tychoc1 -g emits an un-escaped source path in #line directives and DWARF, corrupting debug paths (backslash ...

    D13 (addenda). Exponential compile time and memory on nested calls. tychoc1: method-call chains (generic or not), nested ...

    D14 (addenda). An elif chain is lowered to one nested C block per arm: tychoc SIGSEGVs at ~7000 arms, clang (a supported cc) ...

    D15 (addenda). tychoc1 program_in copies the whole struct-definition text once per field: 900 structs x 20 fields (405 KB) ...

    D31 (addenda). tychoc front-end (parse+resolve) is quadratic O(N^2) in top-level procedure count; ~15x slower than tychoc1 ...

    D32 (addenda). --symbols disagrees on a name-resolution error: tychoc exits 1, tychoc1 exits 0 and still prints the index

    D33 (addenda). -o with an empty string: tychoc errors, tychoc1 silently ignores it and builds a default-named binary ...

    D34 (addenda). Trailing bare -L/-I (no value): tychoc fails, tychoc1 swallows -lm and builds (fail-open)

    D39 (addenda). reserve(xs, n) on a composite-element array with attacker-controlled n has no size_t-overflow guard: ...

    D40 (addenda). Shipping self-hosted compiler tychoc1 cannot compile the documented builtin pow(x, y); tychoc accepts it ...

    D41 (addenda). channel(T, cap) with a large capacity overflows the cell-array size computation in tycho_chan_new -> tiny ...

    D42 (addenda). core:regex — a pattern UNDER the repetition-product ceiling still causes a 40+ second match-time DoS; the ...

    D43 (addenda). core:crypto — the interior-NUL sweep (FRICTION #75–#78, declared COMPLETE) missed the entire crypto hex ...

    D44 (addenda). core:crypto hex inputs stop at the first NUL, so verify, HMAC and AEAD-decrypt accept input that was never ...

    D45 (addenda). core:io — io.copy(src,dst) silently truncates dst to 0 bytes and returns Ok when src and dst are the same ...

    D46 (addenda). core:sqlite open() and exec() act on the NUL-truncated prefix: open creates or opens a different database ...

    D47 (addenda). core:http post/post_bytes pass a CR/LF in content_type into the outgoing request headers

    D48 (addenda). regex.group_start/group_end/group: a huge group index wraps the allocation size and rx_exec writes past a ...

    D49 (addenda). io.read_line/fold_lines and os.run/exec_out silently cut data at the first NUL, while io.read/read_lines keep ...

    D50 (addenda). core:compress — a valid gzip/deflate stream well under the documented 1 GiB ceiling is refused as TooBig; the ...

    D51 (addenda). datetime.offset_at accepts out-of-range and overflowing POSIX TZ offsets (returns 8044431881044758032) where ...

    D53 (addenda). docs/reference/ffi.md still says u8/u16/i8/i16/i32/i64 are plain int to Tycho and that to_i32 returns int; ...

    D54 (addenda). httpd.framing fails open on a repeated Transfer-Encoding whose last value is empty (and on an empty ...

    D55 (addenda). core:http post/post_bytes: CR/LF in the content_type argument injects arbitrary request headers onto the wire

    D56 (addenda). core:tls: the documented bounded-connect API (tls.connect_timeout, tls.set_timeout_ms) cannot be called from ...

    D57 (addenda). httpd.parse_request accepts header and request-line grammar that RFC 9112 requires rejecting: whitespace ...

    D58 (addenda). tycho-httpd Range: a position with leading zeros that is 16+ characters long is treated as 10^15, giving a ...

    D59 (addenda). core:net shim truncates 64-bit fds to int after checking only 'fd < 0', so net.close_fd(fd + 2^32) closes an ...

    D60 (addenda). Cross-package "did you mean" suggests a package-private (_-prefixed) symbol, which the next compile then ...

    D61 (addenda). core:toml silently drops data from VALID documents: a parent table declared after its subtable, and subtables ...

    D63 (addenda). core:toml can be crashed and memory-exhausted by small inputs: no nesting cap, and quadratic time and memory ...

    D64 (addenda). zip.create silently wraps a name length >= 64 KiB and an entry count > 65535 (to_u16/to_u32), writing ...

    D65 (addenda). core:compress shim truncates lengths >= 4 GiB to uInt: compress/raw_compress silently encode only len mod ...

    D66 (addenda). core:toml mis-parses quoted keys and quoted headers, and invents values from malformed literal strings and ...

    D67 (addenda). core:json parse is quadratic in the number of members of one object (the duplicate-key scan)

    D68 (addenda). markdown.inline is still O(n^2) when the terminator EXISTS: '['*n + ']' (the no_rb memo only caches absence)

    D69 (addenda). core:zip: every extract() re-reads the whole central directory and scans backwards over the whole file for ...

    D70 (addenda). compress.decompress returns only the FIRST gzip member and accepts trailing garbage, as Ok

    D71 (addenda). The checkpoint rewrites the store in place, so a crash, kill -9 or full disk during it destroys every ...

    D72 (addenda). tychoc1 drops the in-place append for 'bytes' accumulators and for accumulators declared in a match arm ...

    D73 (addenda). KEY index maintenance is quadratic in time and memory: 689 KB of INSERTs over TCP grows the server to 1.6 GB, ...

    D74 (addenda). A mutation whose WAL append fails is still applied in memory: the client gets 'err cannot write', yet the row ...

    D75 (addenda). Nothing locks the store: two processes on one database silently lose acknowledged commits (300 of 300 in one ...

    D76 (addenda). store.decode trusts row arity, value types and trailing bytes: a one-byte corruption panics the process ...

    D77 (addenda). open_db fails open on two unrecoverable states: a corrupt store beside a zero-length log is 'rebuilt' as an ...

    D78 (addenda). One idle TCP connection pins the serialised server forever: there is no read timeout, so every other client ...

    D79 (addenda). The index path turns a cross-type comparison into an empty answer where the scan path reports an error, and ...

    D80 (addenda). tychoc1: a string/bytes accumulator declared inside a match arm is never compiled to in-place append, so the ...

    D81 (addenda). tychoc1: a recv() used as an expression is built in the function's own _scope, so it is used after free when ...

    D82 (addenda). Overwriting a heap value in a long-lived map never reclaims the old value, so tycho-kvsrv grows about 1 KB of ...

    D83 (addenda). tychoc1 rejects constructing a generic enum's variant through a package qualifier ('box.Has(41)', ...

    D84 (addenda). tychoc1 builds per-iteration values in the function arena where tychoc uses the loop scratch or recycles: ...

    D85 (addenda). core:sqlite passes the database path and exec SQL to C as NUL-terminated char* with no interior-NUL guard: ...

    D86 (addenda). The documented 'Some(1) == None' / 'opt == None' is rejected by both compilers. tychoc1's rejection is a ...

    D88 (addenda). tycho-httpd answers '431 Request Header Fields Too Large' to a small head whose Content-Length exceeds the ...

    Lower-severity defects from the finders' notes

    Each finder was asked for its ten best findings; these are the rest, as the finders described them. None was independently verified.

    archives

    codegen

    db

    net

    parity

    program

    scale

    Refuted

    Kept so the next session does not re-report them.

    Observed during the session, outside any lane

    Historical partial-fix snapshots from the baseline session

    This section records two proposed diffs as they appeared when the original session stopped. Their State notes are historical, not a description of the current working tree: later remediation was implemented and documented in the audit rows and FRICTION ledger. The current tree is still uncommitted, and current validation status is summarized above.

    Size-multiply overflows before an allocation (g1-alloc-overflow)

    Staged, uncommitted work in /home/user/fix-g1-alloc-overflow (base b9df4aa), 24 files. Also saved as scratchpad/partial-g1-alloc-overflow.patch. It fixes five size-overflow defects. 1. The array reserve bound was too loose (runtime/tycho_rt.c@tycho_cap_check). The limit changes from SIZE_MAX/elem to PTRDIFF_MAX/elem. Before, 8*(2^61-1) passed the check and then wrapped inside arena_alloc. The patch also adds a backstop in block_get: it calls tycho_oom when cap > PTRDIFF_MAX - sizeof(HBlock). Fixture: tests/abort/reserve_range_top.{ty,err}. 2. Reserve on struct-element arrays had no cap check (src/tychoc.c@gen_program, the tycho_arr_C%d_reserve emitter, and compiler/emit/emit.ty@_arr_defs). Both compilers now emit a call to tycho_cap_check(n, sizeof(et)). In tychoc.c the fprintf gets one extra 'ct' argument to match. Fixture: tests/abort/reserve_composite.{ty,err}. 3. Map reserve could spin forever in its doubling loop. The patch adds runtime/tycho_rt.c@tycho_map_cap_check, which aborts when n > 2147483000. It is called in tycho_map_{si,sf,ii,if}_reserve, in tychoc.c's tycho_mapc%d_reserve and in emit.ty@_map_defs. In tychoc.c the ec/ic loop previously ran BEFORE the 'cap <= 0' guard; it is now moved after the guard and the new check. Fixtures: tests/abort/reserve_map_range and reserve_mapc_range (.ty/.err). 4. Channel capacity could overflow (runtime/tycho_rt.c@tycho_chan_new). It now aborts with "channel capacity N out of range" when cap > PTRDIFF_MAX/sizeof(HCell)/2. Separately, ty_aligned_alloc64 (_WIN32 branch only) now returns NULL if n + 71 would wrap. Fixtures: tests/conc/abort/chan_cap_{spin,wrap}.{ty,err}. tests/rtparity/run.py gets the new message in EXPECT_MSG_RUNTIME. 5. Regex group index was unchecked (corelib/regex/regex_shim.c@rx_group). An n above re_nsub now returns -1 before malloc. Doc comments in corelib/regex/regex.ty@group_start and @group are updated. There is a new 'grp_big' case in corelib/test/regex/main.ty, plus a line in regex.out. Supporting changes: - tests/run.sh: flat abort binaries now run under 'ulimit -t 15', so a hang counts as a failure. - compiler/verdict_diff.py: EXPECT goes from 1431 to 1437, which counts the six new abort fixtures. - docs/spec/17-runtime.md: the abort list now covers an oversized channel capacity. - docs/spec/appendix-e-conformance.md: the §30.2 row lists three of the new fixtures.

    State: Never gated; nothing in the diff shows any lane was run. What the diff shows: - The six .err goldens and the regex.out line appear to be written by hand, not recorded. Flat tests/abort goldens are compared exactly with cmp, so any wording or newline mismatch will fail. - The chan_cap .err files leave out the "tycho: " prefix. That works because tests/conc/run.sh matches with a substring grep. - The EXPECT=1437 count assumes all six fixtures reach both compilers. It has not been checked. - Appendix E lists reserve_composite, reserve_map_range and chan_cap_wrap, but not reserve_range_top, reserve_mapc_range or chan_cap_spin. - Two limits are policy choices that no longer follow the element size: the map cap is hardcoded at 2147483000, and the channel bound is roughly 2^55 cells. - tycho_cap_check is static. The per-type reserve both compilers emit relies on it and on tycho_map_cap_check being in the same translation unit, as the patch comment notes. - The ty_aligned_alloc64 change is Windows-only and has no fixture. - No FRICTION or changelog entry is included. - The work is still uncommitted.

    The interior-NUL sweep (g2-nul-sweep)

    Staged in /home/user/fix-g2-nul-sweep (base b9df4aa), 29 files. The change finishes the interior-NUL sweep from FRICTION 75-78 and addresses four defects. 1. core:crypto hex arguments were cut at a NUL. 'corelib/crypto/crypto_shim.c@hexdec_ct' and '@hexdec' now take 'tycho_int slen' and no longer call strlen. A negative length fails closed. All 12 hex-taking 'cx_*' externs gained a '*len' parameter, and the wrappers in 'corelib/crypto/crypto.ty' pass 'len(arg)'. '@cx_ct_equal' drops its separate strlen comparison (the FRICTION 76 check). In 'scripts/crypto_hygiene.sh', the prototypes and calls are updated, NUL-inside-length accept/reject cases are added, and the valgrind strlen suppressions are removed. New fixture legs: 21 'hexnul_*' lines in 'corelib/test/crypto/main.ty' and 'crypto.out', covering sha, hmac, key import, ed25519 sign/verify forgery, aead, x25519 and pbkdf2 salt, with controls. 2. core:sqlite 'open', 'exec' and '_run_stmt' acted on the SQL or path before the NUL. 'corelib/sqlite/sqlite.ty' adds a private '_has_nul' and a '_NUL_SQL' const, refuses with Err, and puts a '# nul: guard' verdict on 'sqlite3_prepare_v2'. New 'sqlnul *' legs in 'corelib/test/sqlite/main.ty' and 'sqlite.out', with helpers 'ires' and 'count_v'. 3. 'io.read_line' / 'fold_lines' cut a line at a NUL. 'corelib/io/io_shim.c@iox_read_line' now returns 'bytes' with an inout status and an exact-size malloc copy. 'corelib/io/io.ty@_next_line' wraps it. New 'nul_content_checks' in 'corelib/test/io/main.ty' and 'io.out'. 4. 'os.run' / 'exec_out' cut output at a NUL. 'OsRun' gains 'len'. 'corelib/os/os_shim.c@osx_run_out' now returns 'bytes' and hands the buffer over by setting the handle's 'out' to NULL. 'osx_spawn' and 'osx_spawn_win' pass 'outlen'. New 'out_nul_run' and 'out_nul_exec_out' legs in 'corelib/test/os'. Also added: 'scripts/nul_extern_check.py', a new checker with a selfcheck. It runs in the Makefile 'surface-check' target. The '# nul:' verdict it reads is added on 'corelib/strings/strings.ty@strx_parse_double'. The three census goldens were re-recorded, and docs were updated: SECURITY.md, CONTRIBUTING.md, audit-brief, builtins, corelib.md, spec 14 and 18. The docs also withdraw the base64/hex/url "0x00 caveat".

    State: Missing or not visible in the diff: - No FRICTION.md entry (the last entry is still 132). The docs mention "FRICTION 75-78" and "#78" but nothing is added to record this change. - No 'surface.lock' change, even though several extern signatures changed. Whether that file needs a change is unverified. - The census goldens were re-recorded, so the counts shifted (for example 'iox_read_line' 80 to 40, 'to_str' +58). Their provenance can't be checked from the diff. - The comment at compiler/emit/emit.ty (line 8139 at b9df4aa) still names 'iox_read_line' as the example of an 'Option(string)' return. That extern no longer returns one. Risks visible in the diff: - 'iox_read_line' sets '*out = NULL' with length 0 for an empty line. This relies on the runtime bytes wrapper accepting a NULL pointer. - 'osx_run_out' is now destructive: a second call on the same handle returns empty. - The docs remove the codec 0x00 caveat and assert that 'chr(0)' appends a byte. That rests on existing behaviour this diff does not show or test. - The crypto test calls 'key_free' on handles that may be null. - The Windows spawn path changed but has no host test, as its own comment says. - 'nul_extern_check.py' matches regexes against argument text and looks at only 8 lines per call, so its verdicts are heuristic. It was never gated. Nothing in the diff shows lanes run or passing.