lichen.page.review from the reviewed point 0b92cb61 to stack eb5a1653, as one PR per subsystem (review set)

product: 01 Sandbox: the lichen-sandbox crate and the flat-file store on it #48

targeting main from oct/01-sandbox
Closed opened by and assigned to notplants-bot.bsky.social
targeting main from oct/01-sandbox

superseded by the full set from 09-23, #17–#37

description: Review set for lichen.page.review, 0b92cb61 (reviewed) → eb5a1653 (stack). PR 1 of 16. 30 files, +2987/−1262. One commit, c3bdd2b7dcb0, which sets these files to their content at eb5a1653. Targets main (the reviewed point plus tests and generated files). #2 stacks on this one. Tests and generated files are not in any PR; they are in main.

lichen's kernel confinement now lives in a crate of its own, lichen-sandbox. Most of it moved out of lichen-flat-file (Landlock rulesets, seccomp filters, the sandboxed process mode, confined threads, the egress filter, the child environment), and the site process's memory bound moved out of lichen-server. lichen-flat-file keeps the store, its transactions and its file operations, and is now built on lichen-sandbox, which it re-exports under the old names so callers did not change.

Two behaviours change, beyond the move:

  • A call's processes are one process group (SITE-FRONT-BOUNDARY decision 6, unit 3). Every child lichen starts leads a group of its own. In a site process, setsid and setpgid are denied by seccomp inside that group, so nothing a call starts can leave it. When the call ends (success, error, cancel, timeout) the group is killed while its leader is still unreaped, then reaped. This replaces the old "register every child by pidfd and sweep" design (lichen-flat-file/src/subtree.rs, deleted). Interpreters and mdBook, whose spawns lichen does not control, join the call's group through lichen sandbox call-join.
  • The front server sweeps a dead site process's orphans (reaper.rs, moved here from lichen-server): it is a subreaper, and tells a site's orphan from its own children by their seccomp filter count.

The crate also gets a start-up probe of the kernel (probe.rs): a mode that needs a feature the kernel lacks refuses to start, instead of running with less.


info: Plans: LICHEN-SANDBOX-AND-PERMISSIONS-2026-10-13.md (L1: the crate; its design note LICHEN-SANDBOX-L0-DESIGN-2026-10-15.md), SITE-FRONT-BOUNDARY-2026-10-17.md (decision 6 and unit 3: call groups; unit 11d: in_confined_site_process), OPERATOR-REVIEW-REVISIONS-2026-10-13.md (N3, R5: one wrapper that reads use_sandbox; N5: keep lichen-flat-file generic). All in lichen-orchestrator machine-docs/plans/.

Files

The new crate:

  • crates/lichen-sandbox/Cargo.toml, README.md, src/lib.rs — the crate, its rules (restrict before running, fail closed, only narrow), and its module list. It depends on no lichen crate.
  • src/beneath.rs — openat2/RESOLVE_BENEATH path resolution and the confined file operations, taken out of lichen-flat-file/src/fsops.rs.
  • src/call_group.rs — new: a call's process group, its seccomp deny filter, and the kill-then-reap order. Read this first.
  • src/subtree.rs — new and small: "am I a site process", and whether it is confined.
  • src/reaper.rs — the front server's sweep of a dead site process's orphans (was lichen-server/src/site_process/reaper.rs, deleted in #3; git sees it as a new file).
  • src/probe.rs — new: what the kernel offers, and the refusal when a mode needs more.
  • src/egress_unsupported.rs — new: the non-Linux stubs, now with the same item shapes as Linux.

Moved from lichen-flat-file (renames, with their changes):

  • src/sandbox.rs — the sandboxed process mode; adds call-hold/call-join, and the network flag in Spec (only the front server's push of a hub has it, unit 6).
  • src/confine.rs — confined threads and the runners of a tree's children, now on call groups.
  • src/script_child.rs — Python's child; its subtree kill loop replaced by CallChild.
  • src/egress.rs — adds probe_listener, read by the probe.
  • src/ruleset.rs — adds scope_only, a child's own nested domain (signals and abstract sockets).
  • src/capstd.rs, src/config.rs, src/child_env.rs — small follow-ups of the move.
  • src/memory.rs — moved from lichen-server/src/site_process/memory.rs; items made pub.

lichen-flat-file, now on lichen-sandbox:

  • src/lib.rs, Cargo.toml — re-exports the moved modules; drops libc, seccompiler, landlock, cap-std as direct dependencies.
  • src/fsops.rs — keeps the writes; resolution moved to lichen_sandbox::beneath (−528).
  • src/subtree.rs, src/egress_unsupported.rs — deleted (replaced by call groups; moved).
  • src/reserved.rs — new tree_private_area: <root>/.lichen/sites/<tree>/, the per-site area #4 uses; the marker text now says not to remove it.
  • src/store.rs — read_limited; mutate_metadata renamed mutate_without_advance (N1).
  • src/lock.rs, src/txn.rs, README.md — wording.

lichen's side:

  • crates/lichen-site/src/sandbox.rs — the site's interface to confinement. A script worker thread has no confinement of its own any more (N3): in a site process the process's sandbox covers it; with use_sandbox = false it runs with the server's access.
  • Cargo.toml (workspace) — adds crates/lichen-sandbox as a member.

Moved in: the workspace Cargo.toml (the starting map's CI bucket; its one change adds this crate) and lichen-site/src/sandbox.rs (site core; it is lichen's interface to this crate). Moved out: permission_router.rs and route_names.rs, to #2.

Lives elsewhere

  • permission_router.rs and route_names.rs are in lichen-sandbox but are about who may call a route, so they are in #2.
  • What each spawn is allowed (lichen-site/src/permissions.rs) is in #2; the site process's own launch is in #3.
  • Tests (crates/lichen-sandbox/tests/, src/tests/, fixtures) are in main, not here.
  • SANDBOX.md is in #16.

Inline tests kept in this PR's files: beneath.rs, call_group.rs and lichen-site/src/sandbox.rs each have an inline #[cfg(test)] module.

Commits 0