Skip to content

ARCHITECTURE · 25

Trace and observability.

Virtualizing an application means answering three questions: what does it touch, which hop failed when it breaks, and is anything it does NOT virtualized. Each used to be answered by hand-rolled one-off diagnostics. Now there is one substrate and three front-ends.

Note

Status: shipped — the tfs::trace bus + schema, trace run / explain / cover, the procmon converter — golden-parity with retrace-correlate on the shared fixture tree.

Status: planned — the remainder is dogfood, not code: retrace libc-layer legs on both platforms, the kernel-layer leg, incident-13 capture archival.

The laws.

1. Observability never gates — a trace-channel failure degrades to a
   loud stderr note; the payload run and its exit code are unaffected.
   A tracer must never become a failure mode.
2. Runtime-independent — events name OS-level facts only (paths,
   verdicts, errnos), never language facts. Ruby, Julia, Python
   payloads are equally covered: every runtime bottoms out at the
   same libc/loader boundary the bus instruments.
3. Cross-platform inside; layered outside — the inside stream is
   identical on linux, macOS, windows. Outside captures join ONLY as
   retrace-shaped JSON — an import format, never a runtime dependency.
4. No shell-outs — captures are files parsed in-process.
5. The correlation law — the inside stream can never prove its own
   completeness: an escape is invisible from inside BY DEFINITION.
6. MECE with record mode — the bus's jail channel IS the composition
   record-mode journal, formalized; trace run extends discovery to the
   axes record mode does not cover (materialize:, entrypoints, runtime deps).
7. Human gate — a generated suggestion never edits a manifest.

The interception bus.

The bus lives in the Rust tfs crate — the shipping VFS implementation on every platform. Every interception point the driver and TFS already implement emits one structured event per decision. Armed by TEBAKO_TRACE=<host-path> or the driver’s --tebako-trace argument (the argument wins; it travels the env-scrubbed handoff). The channel opens at driver boot, before any mount; writes are append-only, process-internal, and never policy-gated. Disarmed, an emission point is one Option::is_none() branch — and the bus compiles into the runtime/driver side only, so the bootstrap’s size gate is untouched. Spawned children inherit the armed channel (fd inheritance on POSIX; re-open-for-append on windows); every event carries pid/tid so front-ends regroup.

OP EMITTED WHERE VERDICTS

mount

mount table insert/remove

ok / error:<errno>

open · stat

path dispatch

image:<mount> / host / denied:<rule> / error:<errno>

dlopen

dlmap2file / dlalias2file

materialized:<host-path> / host / error:<errno>

exec · spawn

exec routing — one decision, two syscall surfaces

routed:<entry> / host / error:<errno>

materialize

exec-cache extraction

ok:<host-path> / cache-hit / error:<errno>

jail

policy bind / check

allow:<rule> / deny:<rule> / record

resolve

the driver’s --tebako-image triple resolution

whole / slot:<n> / error:<errno>

The stream is JSONL — one JSON object per line, streamable, diffable, robust to a crashed tail — under the versioned schema trace-event.yaml (envelope v1: additive only, never bumped; ts/pid/tid/op/path/verdict/detail/dur_us/ errno). CI property-tests the whole op matrix against the schema. TEBAKO_DEBUG_TFS remains as the human-readable degraded view over the same emission points.

tebako trace run — discovery.

Runs a composition under policy: record with the bus armed, then synthesizes a suggested manifest — a commented YAML fragment, never applied. Host reads become ro grants, host writes become rw grants (strongest-observed-op wins); a VFS-resident file read through a RAW host fd (a native library’s own fopen — the exec-cache answer consumed it) becomes a materialize: candidate, caught automatically instead of discovered by a failing import; an in-image dlopen whose deps all resolve in-image earns a NOTE "closure-covered — nothing to declare"; a spawned host-absolute executable becomes an entrypoint/runtime-dep note. Floor, store, and exec-cache paths are excluded — the automatic surface is never declared. Each entry carries its evidence line; the author reviews, flips ro↔rw, deletes noise, fills why.

tebako trace explain — diagnosis.

Replays a capture into the hop chain — mount → manifest read → resolve → materialize → OS bind — and prints the first hop whose verdict is red, with a signature table seeded from the incident corpus. The table lives in data, extended as incidents teach new signatures — never hard-coded into the bus.

SIGNATURE IN THE STREAM NAMED HOP

no mount event for the env image + prelude-class child stderr

env image never mounted (handoff env lost)

dlopen verdict = the OS loader’s module-not-found, every dep materialized|host-system

the OS bind itself — bisect candidates printed

jail deny immediately preceding a dependent open error

policy denial (the EACCES class)

materialize error

exec-cache write failure

tebako trace cover — coverage.

The escapes report. "What isn’t tebako intercepting" is answerable ONLY by subtracting the inside stream from an outside capture — and only up to the outside capture’s own layer.

LAYER POSIX WINDOWS WHO SEES IT

inside the VFS

the tfs bus

the tfs bus

tebako

libc boundary

retrace preload

retrace inline hooks (preload-msvc / preload-mingw)

retrace — both platforms

kernel syscall

retrace ptrace; strace → strace2retrace; eBPF bridge

ETW / procmon (via converter)

kernel tracers

Escapes split in two at the libc boundary: libc-routed escapes (the app called open/CreateFileW directly — visible to retrace on both platforms today) and sub-libc escapes (raw syscall / NtCreateFile, loader-internal probes like macOS’s dyld rpath class — only a kernel-layer producer sees them). A report MUST name the producing layer of its outside input: a libc-boundary capture certifies libc-routed escapes only, and a raw-syscall fixture paired with one reports UNCERTIFIABLE, not covered. The outside format is the retrace trace document — tebako never authors a competing one; procmon/ETW join through an offline converter (tebako trace import procmon, pure Rust, in-process).

$ tebako trace cover --inside tfs.jsonl --outside retrace.json \
    --prefix /vfs/root [--pid N] [--window SECS] \
    [--exclude-probes] [--json] [--layer libc|kernel]
# stdout = the golden contract: one
#   escape <path> func=<f> tid=<t> pid=<p> class=<probe|read|write|none>
# per escape — byte-compared in CI against retrace's shared golden tree.
# exit 0 = no escapes · 1 = escapes found (CI-gateable) · 2 = usage/IO

The correlator is tebako-owned, safe Rust, self-contained — the invariants leave no other home (no shell-outs in shipped artifacts; no C/C++ linkage outside dwarfs-t). The FORMAT is retrace’s single source; the ALGORITHM has two implementations that must agree — the shared golden fixtures produce byte-identical escapes reports from both correlators in CI. Intentionally-unserved surfaces (documented passthroughs like the platform floor) are declared in the composition, so the report separates KNOWN-UNSERVED from ESCAPE. A NAME-NOT-FOUND probe on an unserved under-prefix path IS an escape — existence is information.