jail:
default: open | deny # open = cwd + writes pass through
mounts:
- host: $HOME/sources
mount: /work
access: ro | rw
argument_files: auto-allowed # the input file you hand the command
# is allowed even under deny
ARCHITECTURE · 22
Jails and host proxying.
Every file access a payload makes already flows through the TFS layer — so that one choke point is where host access is decided. The policy is docker -v semantics: a default, a mount list, and named errors instead of surprises. Enforcement exists because coverage is total by construction.
|
Note
|
Status: shipped — policy model, the TFS choke, manifest∩user composition, press/run/shim surfaces, the platform floor, jail-deny journaling, record mode. |
The policy model.
| PROFILE | SPEC | MEANING |
|---|---|---|
full access |
|
host cwd + writes pass through — the historic behavior |
directory-scoped |
|
the working tree is visible; nothing else is |
file-scoped tight jail |
|
an untrusted payload against one input, nothing else |
Writes to an ro mount fail EROFS; paths outside every mount fail EPERM.
Payload images themselves are always read-only. Symlinks resolve inside the
granted view: realpath checked at bind time, re-validated on each open — a
symlink swapped in after install resolves to its target and escapes fail
EPERM. Precedence is locked: the package’s manifest REQUESTS access
(capabilities.host); the user can always TIGHTEN (tebako run --jail,
--mount src:/work:rw, --no-host). User policy always wins; signed payloads
may get wider latitude by policy choice, never by bypass. Deny defaults win;
each side’s grants are capped by the other side’s allowance at the same prefix
— ro is sticky.
The platform floor.
Under the deny default, every bound policy gains a fixed per-platform set of
read-only grants covering the surface a spawned interpreter physically cannot
boot without. The floor exists because the failure mode for a missing platform
grant was never a policy verdict — it was a crash in someone else’s library.
Denying the platform surface never produced a working process, so the floor
grants it always.
| PLATFORM | FLOOR ENTRIES (ro) | EVIDENCE |
|---|---|---|
macOS |
|
the JVM NULL-derefs in getMacOSXLocale when /usr reads are denied — a SIGSEGV in someone else’s library, 2026-08-13 |
windows |
|
the loader’s DLL root; the 32-bit view for 32-bit children; the GDI font tree AWT enumerates |
other unix |
|
an entry joins only with a proven platform-process consumer, cited by run — amended by evidence, never anticipation |
The rules: an authored mount covering a floor path SUPERSEDES it — the floor
never narrows what the author allowed, and widening a floor path to rw is one
authored grant away; there is no way to drop a floor path short of not denying.
Every bound grant implies its strict ancestors are traversable — exact-path
reads, never a prefix, never a write — so the platform’s canonicalization walks
pass by construction (the JVM reads its cwd and every ancestor at VM init).
Floor mounts are never serialized into TEBAKO_JAIL: the env grammar’s
right-split cannot carry a windows drive-qualified spelling, and it never needs
to — every consumer re-derives the floor at its own bind, so a spawned child’s
inherited spec plus its bind yields a policy identical to the parent’s. And the
boundary is honest: the floor is SYSTEM surface only — the workload’s own tool
tree and the user’s home stay authored grants. The floor’s promise is the end
of the segfault class: every missing grant surfaces as the workload’s own named
error, pinned in the audit journal.
One choke point.
tebako_fs_host_policy installs the policy as PROCESS state (an unmount never
resets it — fail-closed; a later call replaces it). "Host path" means any
path the mounts do not hold — outside every mount, or covered by a mount but
absent from its image. The app payload mounts at / and the host stays
reachable exactly through this rule. A mount’s access bit applies even under
open; longest host prefix wins on path-component boundaries; argument files
are an exact read-only grant, even under deny.
| GATED DECISION | ANSWER |
|---|---|
open (read and write modes) |
EPERM denied · EROFS on an ro mount |
stat · opendir |
EPERM denied |
dlmap2file |
EPERM denied — native-library load paths are policy surface |
extract_all destination |
a host write, gated as one |
the mount family’s own image read |
policy-gated once a policy is active — install the policy AFTER the mounts |
Because bootstrap, runtime driver, tebako run, and tfs exec all enforce in
the same Rust layer, every consumer behaves identically with no per-app work.
The bootstrap composes package jail: ∩ TEBAKO_JAIL at handoff and exports
the effective policy (+ TEBAKO_JAIL_SOURCE = manifest|user|manifest+user);
a malformed policy fails closed, exit 73. Every denial appends
event=jail-deny path=… op=read|write source=… to the audit journal —
best-effort: journaling never fails the operation it audits.
Record mode: discover the needs you didn’t know.
A slice author usually does not KNOW the host surface their executable touches.
policy: record (env form: TEBAKO_JAIL=record) is a third policy default
beside open/deny: every host passthrough check is ALLOWED and journaled as
event=jail-allow. Nothing is denied, so the workload cannot crash from policy
during discovery; spawned children inherit the spec and append to the same
journal.
$ TEBAKO_JAIL=record tebako run pkg -- input.docx
$ tfs needs --from-journal ~/.tebako/journal.log
needs:
host:
- path: $HOME/.cache/vendor-tool
access: rw # strongest observed op wins
optional: true # absent at generation time
why: "TODO — observed: 14 read, 3 write" # you replace this
The generator folds jail-deny lines in too — a denial is an UNMET need. It
aggregates on the atom-substituted form, excludes the automatic surface (floor,
store, exec-cache paths — never declared), re-substitutes symbolic atoms
longest-prefix-wins, omits relative paths (cwd-relative probes are not
declarable — declare $CWD explicitly), collapses ro ancestors the bind’s
traverse set already covers, and marks absent-at-generation paths
optional: true. Then the human gate: the record shows the OBSERVED MINIMUM —
the author flips ro↔rw where production differs, deletes noise, fills why,
and merges into D1 or D2. The generator never edits a manifest. Record is a
development mode: shipping a composition with it in force is a named lint
warning, and the store’s audit journal marks record-mode runs.
Scope, stated honestly.
Jails are FILESYSTEM-ONLY. capabilities.network is advisory metadata, not an
enforcement promise; OS-level network confinement (seatbelt/seccomp/WFP) is a
possible later layer, never claimed today. Coverage is every IO route through
TFS — all interpreted payloads, plus native binaries under the preload
interposition shim. The shim’s coverage is SYMBOL-level (tebako#439): a jail
holds only for the libc entry points the shim interposes — the named list
includes the LFS64/fortify/versioned twins (open64, openat_2, the
xstat family), because glibc hands _FILE_OFFSET_BITS=64 callers DISTINCT
symbols that never touch the plain names. Known-uncovered at this layer: the
remaining fortify open family and raw syscall(2) IO; statically-linked
binaries have no interposition point at all. And the trust note: tebako is the
EXECUTOR of everything it runs — mediating a child’s syscalls is the execution
contract, not hijacking; jails bind the child downward and claim nothing about
the executor or kernel upward.
See also: Declarative composition · Native interposition · Trace and observability.