lichen's kernel confinement is now a crate of its own, lichen-sandbox, and the store in lichen-flat-file is built on it. Since 09-23 that confinement has grown from "Landlock for a site's children" into a site process's whole sandbox: a sandboxed process mode entered before anything runs, seccomp filters, an egress that hands every connect to the front server, a memory bound, process groups that end with the call, and a kernel probe that refuses rather than runs with less. Children also start from an empty environment, and a new lichen-env crate empties the process's own environment at startup. Since FULL-SITE-PROCESS a site process also narrows itself: its admin thread keeps the full grants, and every other thread and every child holds narrower rules, each child in a nested domain of its own.
AST-verified changes
lichen-flat-file→lichen-sandbox(LICHEN-SANDBOX L1a, 4ed5afc6): 17 files (child_env,config,confine,egress,egress_unsupported,ruleset,sandbox,script_child,subtreeand their tests) renamed into the new crate, andensure_openat2,open_beneath,unsupported_heremoved fromfsops.rstobeneath.rs. Checked by notify-cbe5c0df: "equal, errors 0, items 527=527, moved 3, carried 244, stayed 280, renamed 17, visibility 2, module decls 23, comments 352"; adversary rerun notify-0783abee equal, controls RED. Allowed:pubonopen_beneathandunsupported_here;lichen_flat_file::→lichen_sandbox::andcrate::fsops::→crate::beneath::rewrites.lichen-server/src/site_process/{reaper,memory}.rs→lichen-sandbox/src/{reaper,memory}.rs(L1b-3, 13cc0482). Checked by notify-bd7f69c6: "equal, 1528=1528, carried 37, renamed 4, errors 0"; adversary notify-4471f15f the same. Allowed:lichen_flat_file::script_child::→crate::script_child::.- 20 items from
lichen-flat-file/src/fsops.rstolichen-sandbox/src/beneath.rs(Backend, the*_beneathfunctions,Staged,StagingDir, theopenat2modules), andcapstd.rsrenamed intolichen-sandbox(L1b-4, 509b9ace). Checked (run id not recorded on the move line): "AST equal, 607=607, moved 20, carried 10, stayed 577, errors 0"; adversary notify-4a9c7083 the same. Allowed:lichen_sandbox::beneath::→crate::beneath::. - Rename
site_area→tree_private_area,site_area_path→tree_private_area_path,SITES_DIR→PRIVATE_AREAS_DIR(ORR R2, 461886e6), back-substituted: "rename back-substituted AST-equal, control not" (run id not recorded; adversary A11494 equal, control RED). Itslichen-serverhalf no longer shows in this range's diff (state.rsat the tip no longer uses these names). - Rename
mutate_metadata→mutate_without_advance(ORR R3, 31c3e4bf), back-substituted. Checked by notify-ff079aed: "rename AST-equal + control"; adversary notify-22ea2438. - The crate rename
lichen-cms→lichen-site(P2c-1, d7c4a668) renamedsandbox.rsand the workspace member. Checked (run id not recorded): "ok (4365 items, 113 renamed, 1874 carried, 0 visibility)"; adversary A11324 equal. It touches most of the site crate; see each PR.
Roughly 1,300 of this PR's 7,799 changed lines are code these moves carried verbatim (mostly confine.rs and fsops.rs, removed from lichen-flat-file and added in lichen-sandbox). Most of the other moved files (child_env, egress, sandbox, script_child, reaper, memory) were created after 09-23, so in this diff they read as new code.
On this forge the 3 renamed files here show as a whole deletion and a whole addition, so its diff reads 9,475 changed lines; with rename detection (git diff -M base-0923 full/01-sandbox) it is 7,799.
What changed and why:
- One sandbox for a site process (SANDBOX-UNIFY, PER-SITE-PROCESS). A site process is this executable in its
sandboxmode (sandbox.rs): the front server writes what it may reach into one environment variable, and the process restricts itself (Landlock, seccomp,no_new_privs) before it runslichen site serve. Since FULL-SITE-PROCESS unit 1 the spec also carries narrow rules (Spec::narrow, inLICHEN_SANDBOX_NARROW): the site process counts its threads (threads_of), starts its admin thread with the full grants, then narrows its own thread (narrow_this_thread), so its main runtime and every child hold only the narrow rules. Every other child is an ordinary one: in a site process it enters a nested domain of its own, made before the fork and entered after it (1a, 6c); in local mode it has none. - Egress (
egress.rs, PER-SITE-PROCESS P3-1, P4-T). Landlock cannot name an address, so a site process installs a seccomp filter that hands eachconnectto the front server, which makes the connection itself if the address is allowed. With iroh on (FULL-SITE-PROCESS 6f1), a site process may also make a UDP socket and a netlink route socket for its endpoint; a UDPconnectis answered with a datagram socket the front server connected under the same rule (egress::Kind). Its children refuse both, and every child refuses a datagramsocketpair. - Children's environment (CHILD-ENV-CLEAR). Every child starts empty, then gets a base list and what its caller set by name (
child_env.rs).lichen-envsnapshots the process's own environment at startup and removes everything not on a named keep-list, so no secret lichen was started with stays readable in the environment. The keep-list now namesLICHEN_ROUTES_FD, a child's child-routes descriptor (FULL-SITE-PROCESS 6g). - A call's processes are one process group (SITE-FRONT-BOUNDARY decision 6,
call_group.rs). Every child leads a group; in a site processsetsidandsetpgidare denied inside it, and the group is killed when the call ends, while its leader is still unreaped. Interpreters and mdBook, whose spawns lichen does not control, join throughlichen sandbox call-join. - What a dead site process leaves (
reaper.rs): the front server is a subreaper and tells a site's orphans from its own children by theirSeccomp_filterscount. Memory (memory.rs): a cgroup of its own where the server's cgroup is delegated, otherwise an address-space limit (RLIMIT_AS, weaker; SECURITY-DEBTPSP-MEM1). - The crate split (LICHEN-SANDBOX L1). The mechanisms moved out of
lichen-flat-file, which keeps the store, transactions and file writes and re-exports the moved modules under their old names.probe.rsasks the kernel what it offers, and a mode that needs more refuses to start.
Files
lichen-sandbox (new crate):
src/lib.rs,Cargo.toml— the module list and the rule: mechanisms here, policies in their users; no lichen dependency. Readlib.rsfirst.src/sandbox.rs— the sandboxed process mode, its spec,call-hold/call-join, the site filters.src/call_group.rs— a call's process group, its deny filter, kill-then-reap.src/confine.rs— confined threads and the runners of ordinary children (it replaceslichen-flat-file/src/confine.rs, deleted here).src/egress.rs,src/egress_unsupported.rs— the connect hand-off, and the non-Linux twins with the same item shapes.src/child_env.rs— a child's environment.src/script_child.rs,src/subtree.rs— a script's child, which from FULL-SITE-PROCESS 6g may be given its end of a child-routes socket at descriptor 4 (ROUTES_FD), and the socket filters children carry; and "is this a site process".src/reaper.rs,src/memory.rs— moved from the front server's site process code.src/probe.rs,src/ruleset.rs— the kernel probe, and the one Landlock ruleset builder (with a child's nested scope-only domain).src/beneath.rs,src/capstd.rs,src/config.rs— resolution beneath a root (taken fromfsops.rs), the cap-std backend, the process-wide mode.
lichen-flat-file, now on lichen-sandbox:
Cargo.toml,src/lib.rs— depends onlichen-sandboxand re-exports its modules; dropslandlockandcap-stdas direct dependencies.src/fsops.rs— keeps the writes; resolution moved tobeneath.rs(−528).src/confine.rs— deleted (moved).src/reserved.rs— per-tree staging andtree_private_area(<root>/.lichen/sites/<tree>/, mode 0700), the area #20 keeps; the marker now says not to remove the directory.src/store.rs,src/txn.rs,src/lock.rs— the store no longer carries a command runner;read_limited,mutate_without_advance,outside_paths(the lock and staging a confined process is granted),private_area.
Elsewhere in this PR:
crates/lichen-env/— new crate, the environment snapshot.crates/lichen-site/src/sandbox.rs— the site's interface to all this: a script worker thread has no confinement of its own (N3), the SSR and mdBook preview policies, a child's home and caches inside the site's.lichen.Cargo.toml(workspace) — the new members,lichen-cms→lichen-site,lichen-shellandlichen-irohgone,clippy::mod_module_filesdenied (N8), and yrs 0.27.4.
Lives elsewhere
permission_router.rsandroute_names.rsare inlichen-sandboxbut are about who may call a route: #18. What each spawn is allowed (lichen-site/src/permissions.rs): #18.- The site process's launch by the front server: #19. The front server's answer to a site's
connect(the egress's other half): #19. - Every sandbox lichen sets, as data, side by side (
lichen-site/src/sandbox/spawns.rs, whichlichen-site/src/sandbox.rsdeclares): #39. The start-up order that narrows a site process (site_process/start.rs,admin.rs): #38. The child-routes socket's site side (site_process/child_routes.rs): #40. SANDBOX.md, the crate'sREADME.mdandclippy.tomlare in the base commit.
Category: Sandbox: the lichen-sandbox crate, the flat-file store on it, and lichen-env
Size: 29 files, +6,460/−1,339
Rounds: round 0 at stack ae42e3c0; at 29a7c15b and 6b650b2a none of this PR's files changed; round 1 at b66397c0 (FULL-SITE-PROCESS): the narrow sandbox in the sandbox mode (Spec::narrow, narrow_this_thread, the thread count first), every child of a site process in a nested domain entered after the fork, UDP and netlink route sockets for a site's iroh with UDP answered by the front, the child-routes descriptor in script_child.rs and lichen-env, and yrs 0.27.4. No AST-checked move touches this PR's files. Read this round in its interdiff: https://tangled.org/notplants-bot.bsky.social/lichen.page.october/pulls/17/round/1/interdiff
Plans and ledger: SANDBOX-UNIFY-2026-10-09.md, CHILD-ENV-CLEAR-2026-10-08.md (E1, E2), PER-SITE-PROCESS-2026-10-09.md (P3-1, P4-3 to P4-5, PSP-MEM1), LICHEN-SANDBOX-AND-PERMISSIONS-2026-10-13.md (L1) with LICHEN-SANDBOX-L0-DESIGN-2026-10-15.md, SITE-FRONT-BOUNDARY-2026-10-17.md (decision 6, unit 3), OPERATOR-REVIEW-REVISIONS-2026-10-13.md (N1, N3, N5), FULL-SITE-PROCESS-2026-10-25.md (units 1a, 1b, 6c, 6f1, 6g); builder ledger L1a–L1b-4 (lines 2702–2716).
Inline #[cfg(test)] modules: lichen-sandbox/src/beneath.rs, lichen-sandbox/src/call_group.rs, lichen-site/src/sandbox.rs. 13 files point to a test file with #[path]; 2 have test-only items.
Bucket moves: none from round 1. lichen-env is new in this range and joins the sandbox, as the starting map put it.
Round 1: #1 (Sandbox) covered these files from 0b92cb61, except lichen-env.