TYCHO Search

Search documentation

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

    Probe: uninitialised arena reads, 2026-09-11

    Status: probe record. The instrumented runtime was thrown away; this is the artifact. Nothing was committed to runtime/tycho_rt.c.

    Verdict: 371 programs, 0 uninitialised arena reads; 972 instrumented tool runs, 0 reads. Probed 2026-09-11, tools sweep closed 2026-09-18. Outcome: no lane added — the method stays recorded for re-runs after codegen work touching initialisation.

    Why

    Rule: the four sanitizer lanes cover addresses (ASan), undefined behaviour (UBSan), leaks (LSan), and races (TSan). None of them sees a read of memory that was never written.

    That gap matters here. arena_alloc is a bump pointer over a block the runtime got from malloc: it hands back memory that is *not* zeroed and that ASan considers valid, because the enclosing block is a live allocation. So a codegen path that reads a struct field or array slot before writing it is invisible to every lane in the tree, and prints whatever the arena last held. It also tends to pass a golden — a fresh page reads as zeros, so the output looks right until the allocation pattern shifts.

    Valgrind's memcheck closes that gap. This tree already uses valgrind client requests for the constant-time crypto proof (docs/internals/ffi-review-2026-08-14.md), so the technique is established; it had never been pointed at the arena.

    What was run

    Two sweeps over the 288 flat fixtures in tests/*.ty, each built and then run under valgrind --error-exitcode=42 --errors-for-leak-kinds=none.

    1. Plain memcheck — 288 ran, 0 errors. Weak by construction: valgrind sees the arena as one large malloc and cannot tell a slot inside it from any other byte. This sweep tests roughly what ASan already tests.

    2. Arena-instrumented memcheck — 288 ran, 0 errors. A copy of runtime/tycho_rt.c was patched so every arena handout is marked undefined:

    static inline void *arena_alloc_vg(Arena *a, size_t n) {
        void *p = arena_alloc_i(a, n);
        if (p) VALGRIND_MAKE_MEM_UNDEFINED(p, n);
        return p;
    }
    #define arena_alloc(a, n) arena_alloc_vg((a), (n))
    

    The macro matters: internal callers reach arena_alloc_i through it, so patching only the exported arena_alloc function would have instrumented almost nothing. Fixtures were built against the patched copy with ./tychoc1 <f> --runtime <copy>, which is why the repo needed no change.

    The control, because a clean sweep from an unproven instrument is worth nothing

    A detector that cannot report is indistinguishable from a clean tree. So the instrument was made to fire on purpose — a C program including the patched runtime, allocating 64 bytes from a real Arena and branching on them without writing:

    exit=42
    ==2128933== Conditional jump or move depends on uninitialised value(s)
    

    It reports. The 288/0 above was measured against a working detector.

    3. Extended to corelib and the examples — 83 ran, 0 errors, 0 failed to build. All 46 packages under corelib/test/ and all 37 under examples/corelib/, same instrumented runtime. This is the stronger half of the result: the flat fixtures are mostly minimal feature probes, while these are real programs that allocate, grow and partially fill buffers — bignum, decimal, regex, json, compress, sha256, zip — the shape that produces a read of a slot nobody wrote. The libcurl-, libpng- and sqlite-backed packages built and ran too, so nothing was silently skipped.

    371 programs total, 0 uninitialised arena reads.

    What this does and does not establish

    Does: across 371 programs — every flat fixture, every corelib test package and every corelib example — no execution reads an arena slot it never wrote, with an instrument demonstrated to catch exactly that.

    Does not: prove the property. The corpus exercises the paths the corpus exercises — a codegen path nothing reaches is as untested here as anywhere. And memcheck reports the read, not the missing write, so a real finding would still need tracing back to the construct that failed to initialise.

    4. tools/ — closed 2026-09-18, and it was not the job this record expected

    This record first called driving 30 programs "a bigger job than this probe". The arguments already existed: 27 of the 30 tools ship their own run.sh gate that feeds them real inputs, and every one resolves its compiler as TYCHOC="${TYCHOC:-./tychoc1}". So nothing had to be invented and no gate had to be edited — a wrapper standing in for tychoc1 does both halves:

    1. appends --runtime <instrumented copy>, and
    2. replaces the produced binary with a shim that runs it under valgrind -q --log-file=....

    Findings go to a log, deliberately not to --error-exitcode. The gates' exit codes and stdout have to stay byte-identical or they stop testing what they test, and a probe that quietly breaks the suite it is riding on is worse than no probe.

    The instrument was proved in BOTH directions this time:

    controlexpectedmeasured
    read an arena slot nobody wrotemust fireexit 42, Conditional jump or move depends on uninitialised value(s)
    the same slot, written firstmust stay silentexit 0, nothing reported

    The negative half is not decoration: without it a scanner that matches anything is indistinguishable from a working one.

    27 tools, 972 instrumented runs, 0 uninitialised arena reads.

    Two of the gaps this record first reported were closed the same day:

    What is NOT covered, named rather than rounded up:

    tycho-chess was the third gap and it is closed too: leaving it running. The gate passes *under memcheck* — 24 published and oracle perft totals, the divides against golden, 5 search positions deterministic and TT-invariant, parallel == serial — across 63 instrumented runs, 0 reads. It was stopped at 41 runs earlier for cost: perft is pure compute, so it buys minutes of memcheck per arena allocation it does not make.

    One failure that is the INSTRUMENT, not the tree. tycho-flow fails under memcheck: *"the pool drained out of order only 77 of 200 runs"*. Running the same gate with the same compiler and no valgrind is green and reorders on essentially all 200 — memcheck serialises threads, so an assertion that needs real parallelism degrades. Recorded because the next run pointing valgrind at a concurrency gate will meet it again.

    Found on the way: five of these gates silently discard the TYCHOC override and run the C bootstrap instead of the shipped compiler — FRICTION 93. That is why the sweep initially reported "no instrumented run" for them.

    Why no lane was added

    It found nothing, and a lane costs time on every run forever. The method is cheap to re-run from this record when there is a reason — after codegen work that touches initialisation, or if a golden ever differs in a way that smells like stale bytes. Recorded rather than institutionalised.