Skip to content
All posts
6 min readtebakopackagingrelease

Tracing and checking packaged apps

Every packaged application raises two questions: what it actually touches on the machine, and whether it actually works. The trace commands answer the first question, payload checks answer the second, and this post presents both with worked examples.

The Tebako team

github.com/tamatebako

Every packaged application raises two questions that hand-written, disposable diagnostics answer poorly: what the payload actually touches on the machine, and whether the payload actually works. Tebako ships a toolkit for each — the trace commands and payload checks — and this post presents both with worked examples. Both are also covered as step-by-step walkthroughs in the guides: Trace a packaged app and Check a payload.

The trace toolkit: see what your payload touches

Every tebako interception point — mount, path dispatch, dlopen, exec, materialize, jail verdict, resolve — now emits one structured JSON event onto a stream inside the virtual-filesystem layer. The stream is identical on Linux, macOS, and Windows, and it records operating-system facts only, so every runtime is covered equally.

Turn it on with one environment variable:

TEBAKO_TRACE=/tmp/capture.jsonl

Tracing is deliberately safe to leave on: if the capture channel fails, tebako prints a warning to stderr and nothing else happens — the application’s exit code and behavior are never affected.

trace cover and payload checks tebako trace cover — what escaped the mounts inside stream TEBAKO_TRACE=*.jsonl outside capture retrace / procmon tebako trace cover outside minus inside escapes: files touched below the mounts, straight on the host exit 0 = no escapes · exit 1 = the list tebako check — does the payload work the manifest declares checks: entry, fixtures, expect tebako check runs the real entrypoint PASS behavior is right SKIP prerequisite missing FAIL exit 79, reason named a check fails only when its prerequisites are present and its behavior is wrong verification proves the bytes — a check proves the payload works

Figure 1 — Trace cover compares an outside capture with the inside stream, and a payload check declares its own acceptance test; these are the two tools of this release.

Four commands read that stream:

  • tebako trace run <pkg>discovery: it runs a package with the stream armed and drafts a manifest fragment; the fragment records the host paths the package touched (as needs: grants), the files it read through raw file descriptors (as materialize: candidates), and the shared libraries and host executables it loaded. The draft is commented YAML, and tebako never applies it automatically; the operator reviews it, adjusts read and write permissions, and records why each grant exists.

  • tebako trace explain <capture.jsonl>diagnosis: it replays a capture through the chain of steps a file access goes through (mount → manifest read → resolve → materialize → OS bind) and names the first step that failed, with a likely cause. Exit code 0 means the chain is green; exit code 1 names the failing step.

  • tebako trace covercoverage: it produces the report described below.

  • tebako trace import procmon <capture.csv> — the Windows path, which is also described below.

A worked example: the escape the jail cannot show you

Suppose a packaged application has been jailed with a deny policy and behaves correctly. One question remains: whether anything reaches the host below the virtual filesystem entirely — through a native library’s own fopen, or through a probe made by the system loader. The jail journal cannot answer it, because an access that bypasses the virtual filesystem is invisible from inside it by definition.

The answer is to compare an inside capture with an outside capture of the same run:

$ tebako trace run ./myapp --capture inside.jsonl --out draft.yaml -- serve ./public
$ # …the same run again, captured outside the process…
$ tebako trace cover --inside inside.jsonl --outside retrace.json --prefix /mnt/tfs
escape /mnt/tfs/secret/keys.pem func=open tid=603 pid=601 class=read
escape /mnt/tfs/tmp/.lock func=openat tid=604 pid=601 class=read
$ echo $?
1

Two files under the mounted prefix were read through a path the inside stream never saw — reads that bypassed the interception layer and hit the host’s files directly. Exit 0 means no escapes; exit 1 lists them, so the verdict can gate CI. Stderr adds the coverage percentage per surface and which layer produced the capture.

One caveat, which the report also states: an outside capture can only certify escapes that go through the layer it watches. A capture at the libc boundary (retrace) certifies libc-routed escapes; certifying raw syscalls and loader-internal probes takes a kernel-layer capture (--layer kernel).

On Windows: import a Procmon capture

On Windows, the outside capture that works today is Procmon’s CSV export. An in-process converter normalizes it into the same JSON shape:

PS> tebako trace import procmon .\sassc-run.csv > outside.json
tebako: trace import: entries=1832 bad-rows=0
PS> tebako trace cover --inside inside.jsonl --outside outside.json --prefix C:/pkg/scss --pid 9012 --layer kernel
escape C:/pkg/scss/_a.scss func=QueryOpen tid=0 pid=9012 class=probe
escape C:/pkg/scss/_hidden.scss func=CreateFile tid=0 pid=9012 class=probe

That output is a real case: a native Sass importer probing for partials with raw Win32 calls, underneath every libc hook — the kind of access that only a kernel-layer capture can see. The libc-boundary alternative on Windows (retrace’s live-injection backends) does not work at retrace v2.14.0, because the injector crashes inside the child’s engine boot. Until that failure is fixed upstream, the Procmon path carries Windows coverage, and a CI check watches for the fix so that the project notices when either side changes.

Payload checks: does it work, written down

A check is the payload’s own acceptance test, declared in its in-image manifest and carried inside its image; it states that, given the declared needs, the payload performs its one real task. Integrity verification proves that the bytes are the publisher’s; a check proves that the payload works once the bytes are present.

Here is the canonical example — the metanorma formula’s Homebrew test, written as a declaration:

checks:
  html-xml:
    entry: /bin/metanorma
    argv: ["--type", "iso", "{scratch}/test-iso.adoc", "--agree-to-terms"]
    fixtures: /__tpkg__/check/html-xml
    expect: { exit: 0, files: ["test-iso.xml", "test-iso.html"] }
    timeout: 180

tebako check runs it against an installed payload on the local machine, against a just-pressed image in feedstock CI (--runtime / --runtime-image), against a pressed package, or against a composition document (which can declare checks for the composition itself):

$ tebako check metanorma
slice metanorma: html-xml PASS 41s
slice metanorma: pdf SKIP (no jvm in the composition)
$ echo $?
0

A few deliberate rules keep checks useful:

  • A missing prerequisite skips with a clear reason, and never fails. A check fails only when its prerequisites are present and its behavior is wrong.

  • Assertions are deliberately simple: files must exist and be non-empty, plus one optional stdout pattern. Fragile byte-for-byte golden outputs are not supported on purpose.

  • Any failure exits with code 79 (EX_TEBAKO_CHECK) and names the expectation that failed.

  • --record runs the checks under the record policy, so the same acceptance test doubles as needs discovery; --keep-scratch preserves the scratch directory while a failing check is being debugged.

What’s next

Two things remain open, and both are known. First, the Windows libc-layer live capture waits on the upstream retrace fix described above. Second, the metanorma feedstock’s html-xml gate turns on once the feedstock re-pins its workflows to this CLI — the check engine shipped with its first release, and feedstocks consume released binaries only, because releases are the interface between them. Wiring checks into tebako press and tebako install gates is the remaining work.

Get it

brew upgrade tamatebako/tap/tebako

Alternatively, download the binaries for the target platform from the releases page. Every asset is built and tested in CI before it is uploaded.

The team welcomes reports of surprising behavior in the issue tracker.