Feat/control socket introspection#153
Conversation
|
Thanks for the PR! Just design-level review. Layering looks right: Five things worth resolving before merge: 1. pid file and socket refer to different processes. 2. Name collision silently destroys a live sandbox. 3. Pruning races startup. 4. The 64 KB cap is client-side only. 5. No opt-out. Every |
84602b9 to
a5df3da
Compare
Add a per-sandbox Unix control socket under /dev/shm/sandlock-$UID/<name>/
that serves introspection requests. Each sandbox gets a runtime directory
containing a `pid` file and a `control.sock` Unix socket.
CLI changes:
- `sandlock list` → `sandlock ps` (NAME/PID/UPTIME/CMD columns via /proc)
- `sandlock config <name>` queries the control socket for effective policy
(JSON or --toml output)
- `sandlock kill <name>` reads pid from the per-sandbox pid file
- Removed network_registry.rs and all network.json references
- Removed --name incompatibility with --no-supervisor
Core changes:
- New `control` module: runtime dir helpers, control loop (tokio task),
wire protocol (4-byte BE length + JSON, v:1, verb:config), SO_PEERCRED
audit, stale-dir pruning via kill(pid,0) liveness check
- `profile.rs`: added Serialize to all section structs; added reverse
serializer (sandbox_to_profile, sandbox_to_toml, sandbox_to_json,
format_net_rule, format_http_rule, bind_ports_to_specs, time_start_str)
- `seccomp/state.rs`: added DeniedSet::denied_paths() accessor
- `sandbox.rs`: Runtime gains control_handle + control_dir fields; control
socket setup in do_create_stdio (supervisor path) + pid-file-only for
no_supervisor; control loop spawned after notif startup; cleanup in
wait() and Drop
Wire protocol: 4-byte big-endian length prefix + UTF-8 JSON.
Request: {"v":1,"verb":"config","args":{}}
Response: {"v":1,"ok":true,"data":{...ProfileInput...}}
Add 10 integration tests in sandlock-core exercising the control socket wire protocol (list, config, prune stale dirs, serializer round-trips). Add 8 CLI integration tests for ps, config, kill, and help commands. Make control socket setup best-effort (warn instead of fail) so nested sandboxes where /dev/shm is restricted by outer landlock still work. Update README.md: replace `sandlock list` with `sandlock ps`, add `sandlock config` examples, update port virtualization section.
- Dual PIDs in runtime dir pid file (child_pid + supervisor_pid) - Name collision detection in setup_runtime_dir (kill(pid,0) check) - Pruning race guard: atomic temp+rename pid write, 2s minimum-age - 64KB response cap server-side in write_response - control_socket opt-out knob on Sandbox/SandboxBuilder - Wire supervisor_pid through do_create_stdio, gated on control_socket - Update kill command for dual PIDs + killpg on child PID
a5df3da to
482da97
Compare
Document the `no_supervisor` field (serde-serialized, already in the struct and CLI) and the new `control_socket` builder-only field (introduced in RFC multikernel#68) in the Python synopsis, TOML profile example, `[program]` table, and Runtime kwargs section of the sandbox reference.
|
Thanks for the rework. Points 1, 3, 4, 5 from my previous review are properly addressed (two-line pid file with supervisor liveness, temp+rename plus the 2s grace, server-side cap, 1. Removing 2. The name-collision fix misses the 3. Smaller items, fine as part of this PR or a fast follow-up:
|
Summary
Replaces the
/dev/shmnetwork_registrywith a per-sandbox control socket architecture under/dev/shm/sandlock-$UID/<name>/. Every sandbox (CLI, Python SDK, embedded) gets its own runtime directory with a pid file and a Unix stream socket for introspection. Adds two new CLI subcommands (sandlock ps,sandlock config) and hardenssandlock killagainst race conditions.RFC: #68
Changes
New: per-sandbox runtime directory & control socket (
control.rs).pid.tmp→ rename →pid) with dual-line format:child_pid\nsupervisor_pid\nkill(supervisor_pid, 0)before overwriting an existing sandbox dir →ErrorKind::AlreadyExistslist_live_sandboxesto prevent pruning a sandbox that's still starting upwrite_response— oversized payloads return an error rather than being truncated silentlycleanup_runtime_dirbest-effort teardown (called from Drop paths, never panics)New CLI subcommands
sandlock ps— lists all running sandboxes with NAME, PID, UPTIME, and CMD. Uptime is computed from/proc/<pid>/statstarttime vs/proc/uptime. CMD is read from/proc/<pid>/cmdline, truncated to 60 chars.sandlock config <name>— fetches the effectiveSandboxpolicy from a running sandbox via control socket. Supports--tomlfor TOML output (round-trips throughProfileInput).sandlock killhardeningkill(supervisor_pid, 0)before attempting to killkillpg(child_pid, SIGKILL)+ directkill(supervisor_pid, SIGKILL)to cover the case where supervisor is in a different process groupcontrol_socketopt-out knobcontrol_socket: boolfield onSandbox(defaulttrue,#[serde(skip)]) andSandboxBuilderfalse, no runtime dir, pid file, or control-socket task is created — the sandbox is invisible tosandlock ps/sandlock configno_supervisorsandboxes create a control socket only whencontrol_socket=truenetwork_registryremovalnetwork_registry.rs(146 lines) — the old/dev/shm/sandlock-$UID/network.jsonshared file withflock-based synchronizationport_remapregistration path frommain.rs(~60 lines of spawn+register+unregister logic)format_net_ruleandformat_portsfrommain.rsintosandlock-coreas public free functions sosandlock configcan serializeNetRules to TOML/JSON without duplicating rendering logicsandlock_cli_test.rs: ported all tests to usesandlock_core::format_net_rulenetwork_registry::next_name()intosandlock-core(sandbox_prefix + PID + counter insandbox.rsdo_create_stdio/no_supervisorpath)Integration tests (10 new control socket tests)
test_control_setup_and_cleanup— runtime dir created with pid file and socket, cleaned up on shutdowntest_control_config_verb— control socket serves effective policy as JSONtest_control_config_verb_toml— control socket serves effective policy as TOMLtest_control_unknown_verb— unknown verb returns error responsetest_control_list_live—list_live_sandboxesdiscovers running sandboxestest_control_name_collision— duplicate name with live supervisor returnsAlreadyExiststest_control_prunes_stale_dirs— dead supervisor dirs are removedtest_control_prunes_stale_dirs_via_cli— CLI path also prunes dead dirstest_control_no_supervisor—no_supervisorsandboxes still get a control sockettest_control_socket_disabled—control_socket: falseskips runtime dir entirelyDocumentation
sandlock listreplaced withsandlock psandsandlock configexamplesno_supervisoradded to Python synopsis, TOML profile example, and[program]field table;control_socketadded to Runtime kwargs tableFiles changed
15 files, +1,787 / −290
crates/sandlock-core/src/control.rscrates/sandlock-core/src/profile.rscrates/sandlock-core/src/sandbox.rscrates/sandlock-core/src/sandbox/builder.rscrates/sandlock-core/src/seccomp/state.rscrates/sandlock-core/src/lib.rscrates/sandlock-core/tests/integration/test_control.rscrates/sandlock-core/tests/integration.rscrates/sandlock-cli/src/main.rscrates/sandlock-cli/src/network_registry.rscrates/sandlock-cli/tests/cli_test.rscrates/sandlock-cli/Cargo.tomlCargo.lockREADME.mddocs/sandbox-reference.mdTest plan
Design decisions
sandlock pscan read/proc/<child_pid>/statfor uptime whilesandlock killcan target the supervisor (which owns the socket)list_live_sandboxesfrom seeing a partially-written pid file during sandbox startupkill(supervisor_pid, 0)before overwriting prevents accidentally reusing a name that's still runninglist_live_sandboxesfrom pruning a sandbox that was created microseconds agocontrol_socket: boolopt-out — gives callers who don't want introspection overhead a clean escape hatch