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 · 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.
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 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, 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.
See also: Jails and host proxying · Native interposition · The contract model.