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.
ARCHITECTURE · 21
Trace and observability.
Virtualizing an application means answering three questions: what the application touches, which hop failed when something breaks, and whether anything escapes virtualization. Each question used to be answered by hand-rolled one-off diagnostics; now one substrate and three front-ends answer them.
|
Note
|
The tfs::trace bus and schema, trace run / explain / cover, and the procmon converter are shipped, with golden parity against retrace-correlate on the shared fixture tree. The remainder is dogfood rather than code, and it is planned: the retrace libc-layer legs on both platforms, the kernel-layer leg, and incident-13 capture archival. |
The laws
The interception bus
The bus lives in the Rust tfs crate, the shipping VFS implementation on
every platform. Every interception point that the driver and TFS already
implement emits one structured event per decision. The bus is armed by
TEBAKO_TRACE=<host-path> or by the driver’s --tebako-trace argument (the
argument wins, and 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. When 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), and every event carries pid/tid so front-ends can regroup.
| OP | EMITTED WHERE | VERDICTS |
|---|---|---|
|
mount table insert/remove |
|
|
path dispatch |
|
|
dlmap2file / dlalias2file |
|
|
exec routing — one decision, two syscall surfaces |
|
|
exec-cache extraction |
|
|
policy bind / check |
|
|
the driver’s --tebako-image triple resolution |
|
The stream is JSONL, one JSON object per line, streamable, diffable, and
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
The command runs a composition under policy: record with the bus armed,
then synthesizes a suggested manifest, a commented YAML fragment that is
never applied. Host reads become ro grants, and 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, where 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"; and a spawned
host-absolute executable becomes an entrypoint/runtime-dep note. Floor,
store, and exec-cache paths are excluded, because the automatic surface is
never declared. Each entry carries its evidence line, and the author
reviews, flips ro↔rw, deletes noise, and fills why.
tebako trace explain: diagnosis
The command 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 and is extended as incidents teach new signatures; it is 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
This command is the escapes report. The question of what tebako does not intercept 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, and loader-internal
probes like macOS’s dyld rpath class, which only a kernel-layer producer
sees). 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, and 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, and self-contained, because the invariants leave no other home (no shell-outs in shipped artifacts, and no C/C++ linkage outside dwarfs-t). The format is retrace’s single source, and 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, because existence is information.
See also: Jails and host proxying · Native interposition · The contract model.