Syva v0.2 Runtime Verification

June 13, 2026 · View on GitHub

Syva's normal CI and Lima checks verify Linux userspace build/test coverage and eBPF object compilation. They do not prove runtime BPF LSM load, attach, or enforcement.

Use this procedure before tagging v0.2 on a privileged Linux host.

Requirements

  • Linux host or VM with BPF LSM enabled.
  • Root privileges, or equivalent privileges for loading and attaching BPF LSM programs.
  • protoc, clang/LLVM, Rust stable, Rust nightly with rust-src, and bpf-linker.
  • A syva group for the local syva-core Unix socket.
  • Kernel config and boot parameters that allow BPF LSM programs to load.
  • The release eBPF object. make verify-runtime builds it before running the ignored runtime tests.

Check the LSM list:

cat /sys/kernel/security/lsm

The output should include bpf.

Create the socket group if needed:

sudo groupadd --system syva

Command

From the repository root:

sudo -E make verify-runtime

This runs:

cargo run -p xtask -- verify-runtime

The xtask command checks Linux, root, and the syva group, then runs the release eBPF build and ignored runtime tests explicitly:

cargo run -p xtask -- build-ebpf
cargo test -p syva-core --test local_mode_starts_server -- --ignored --nocapture
cargo test -p syva-core --test local_mode_register_then_list -- --ignored --nocapture

Expected Output

Both ignored tests should run and pass:

test local_mode_starts_server ... ok
test local_mode_register_then_list ... ok

The command must exit non-zero if prerequisites are met but runtime startup, BPF attach, self-tests, or local core RPC behavior fail.

Expected runtime logs include the release object path and nine attached LSM hooks:

loading eBPF object: .../target/bpfel-unknown-none/release/syva-ebpf
attached LSM program program=syva_file_open hook=file_open
attached LSM program program=syva_bprm_check hook=bprm_check_security
attached LSM program program=syva_ptrace_check hook=ptrace_access_check
attached LSM program program=syva_task_kill hook=task_kill
attached LSM program program=syva_mmap_file hook=mmap_file
attached LSM program program=syva_unix_connect hook=unix_stream_connect

Syva v0.2 does not attach a cgroup movement LSM hook. cgroup_attach_task is not a BPF-LSM hook on supported mainline kernels, so cgroup movement / zone escape protection remains a documented gap until it is implemented with a valid cgroup BPF mechanism or another kernel-supported hook.

Integration Test: kernel actually blocks a forbidden action

verify-runtime proves the engine loads, attaches, and passes its self-tests. It does not exercise an end-to-end enforcement decision. verify-integration does: it deploys the local core and proves the kernel blocks a forbidden cross-zone file read while allowing a same-zone read.

sudo -E make verify-integration

This runs the same privileged preflight (Linux, root, syva group, BPF LSM), builds the release eBPF object, then runs one ignored test:

cargo test -p syva-core --test integration_file_open_enforcement -- --ignored --nocapture

Scenario (process + cgroup)

This host has no container runtime, so the test uses the supported enforcement key directly: it creates a cgroup-v2 leaf, attaches it to a zone through the real AttachContainer gRPC, then runs a workload process inside that cgroup. This proves kernel enforcement for cgroup/workload membership; container runtime integration (containerd / k8s adapters) is a separate follow-up.

  1. Register syva-it-zone-a and syva-it-zone-b.
  2. Create zone-a-allowed.txt (registered to zone-a) and zone-b-secret.txt (registered to zone-b). Both are root-owned and world-readable, so any denial is Syvä's, not filesystem permissions.
  3. Control: the unzoned test process reads the secret file successfully.
  4. Create a cgroup, attach it to syva-it-zone-a.
  5. Positive: a workload in the zone-a cgroup reads the zone-a file → succeeds.
  6. Negative: the same workload reads the zone-b file → blocked by file_open.

The test fails if the forbidden read succeeds; it cannot pass by accident, because any setup error leaves the workload unzoned, which file_open allows, which fails the negative control.

Expected output

=== syva integration evidence: file_open cross-zone enforcement ===
host cgroup attached to zone-a: cgroup_id=...
positive (zone-a -> zone-a file): exit=0, content read -> ALLOWED
negative (zone-a -> zone-b file): exit=Some(1) stderr="cat: .../zone-b-secret.txt: Operation not permitted"
  -> kernel DENIED the open with EPERM (Operation not permitted); no content read
file_open deny: before=0 after=1 deny_delta=1 (workload-attributable)
file_open allow: 63 (GLOBAL counter: every allowed open system-wide, ...)
test file_open_cross_zone_read_is_blocked ... ok

Evidence notes:

  • The blocked open surfaces to the workload as EPERM (Operation not permitted), because the file_open hook denies by returning -1. It is not EACCES.
  • deny_delta is the file_open deny counter measured immediately before and after the negative control. It is workload-attributable and must be 1: the workload is the only zoned process, and unzoned callers are always allowed, so no other process can produce a deny.
  • The file_open allow counter is global — the hook runs on every open() system-wide and counts unzoned/system opens as allowed, so it varies run to run and is not workload-specific.
  • The test asserts the workload's open fails, no secret content is read, the error is EPERM, and deny_delta == 1. It is safe to rerun (unique cgroup/dir per run, cleaned up even on panic).

Container Integration Test: a real container is blocked

verify-integration proves enforcement using a process moved into a cgroup. verify-container-integration proves the same file_open enforcement against a real container started by a container runtime.

sudo -E make verify-container-integration

This is a privileged Linux / BPF-LSM + container-runtime test. It needs a Docker-CLI-compatible runtime (docker, nerdctl, or podman); override with SYVA_CONTAINER_RUNTIME=<runtime>. With none present it fails clearly and does not fall back to a process test (use verify-integration for that). It runs the same privileged preflight, builds the release eBPF object, and runs one ignored test.

Scenario

The container's bind-mounted files share inodes with the host, so RegisterHostPath (inode → zone) applies inside the container. Enforcement keys on the container's host cgroup id (read from /proc/<main-pid>/cgroup), so the container is attached to a zone via the real AttachContainer gRPC — exactly as an adapter would. The container's main process performs the opens (its cat children inherit the cgroup); podman exec is avoided because the cgroup namespace hides exec processes' host cgroup.

The allowed and protected files live in the same bind mount with identical permissions and AppArmor treatment; only the Syvä zone registration differs, so a differential block is provably Syvä's, not the runtime's. A host (unzoned) read of the secret succeeds first, proving intrinsic readability.

Expected output

=== syva container integration contract ===
PASS:  container in zone-a can read its own zone-a file.
BLOCK: container in zone-a cannot read protected zone-b file.
HOOK:  file_open
EXPECTED DENIAL: EPERM / Operation not permitted
EXPECTED KERNEL EVIDENCE: file_open deny_delta=1
--- container setup ---
runtime: podman (podman version 4.9.3)
image:   docker.io/library/busybox
container: name=syva-it-container-a-<pid> id=<id>
cgroup:  host_path=/machine.slice/libpod-<id>.scope/container cgroup_id=<n> -> zone=syva-it-zone-a
--- allowed operation (zone-a -> zone-a file) ---
exit=0 -> ALLOWED, content present
--- blocked operation (zone-a -> zone-b file) ---
exit=1 stderr="cat: can't open '/work/zone-b/secret.txt': Operation not permitted"
  -> kernel DENIED the open with EPERM (Operation not permitted); no secret read
file_open deny: before=0 after=1 deny_delta=1 (container-workload attributable)
file_open allow: <N> (GLOBAL counter: allowed opens system-wide, ...)

Same counter semantics as verify-integration: deny_delta is workload-attributable (the container is the only zoned workload); allow is a global counter and is much larger here because a container start opens many files — none zoned, so all allowed. The test is safe to rerun (unique container/dir per run, removed even on panic).

Egress CIDR Allowlist Test

verify-egress-cidr proves the per-zone egress allowlist. Network-locked zones are registered with IPv4 any-port, IPv4 CIDR:port, and IPv6 CIDR:port exceptions:

sudo -E make verify-egress-cidr
  • connect/sendto to the allowlisted IPv4 IP: NOT denied (the any-port CIDR carve-out);
  • connect to an allowlisted IPv4 CIDR:port: NOT denied on that port, but EPERM with socket_connect deny movement on the same IP and a different port;
  • connect to an allowlisted IPv6 CIDR:port: NOT denied for that destination/port, while a different IPv6 destination is EPERM with socket_connect deny movement;
  • connect/sendto to a different non-loopback IP (TEST-NET 192.0.2.1): EPERM, with socket_connect / socket_sendmsg deny counters moving;
  • bind to the allowlisted IP: still EPERM — the allowlist governs egress destinations, never local listeners.

Grammar: bare IPv4/IPv6 CIDRs allow any port (10.0.0.0/8, 2001:db8::/32), IPv4 ports use CIDR:port (10.0.0.0/8:5432), and IPv6 ports require brackets ([2001:db8::/32]:443). Invalid entries are skipped. Limitation: one exact CIDR prefix stores either one port or any-port; repeating the same exact prefix with another port overwrites the previous value.

Cross-Zone TCP by Pod IP Test

verify-cross-zone-tcp proves exact IPv4 pod-IP-to-zone enforcement. The test creates two non-loopback dummy-interface IPv4 addresses, maps one to zone-a and one to zone-b with SetIpZone, and runs a zone-a workload:

sudo -E make verify-cross-zone-tcp
  • connect to the zone-a IP: NOT denied (same zone);
  • connect to the zone-b IP: EPERM, with socket_connect deny moving by 1;
  • after AllowComm(zone-a, zone-b), connect to the same zone-b IP is NOT denied.

This is zone-pair enforcement on resolvable IPv4 pod IPs, not a claim that all networking is closed. Unmapped destinations fall back to the existing network lock and egress allowlist behavior. syva-k8s populates the map from a cluster-wide pod watch; enforcement is eventually consistent until every node's adapter observes pod IP add/change/delete. Host-network pods are skipped because they do not have a distinct pod IP. IPv6 IP-to-zone enforcement is not implemented yet.

Cgroup Escape Detection Test

verify-cgroup-escape proves the cgroup-escape detector. BPF-LSM cannot prevent a cgroup migration on supported kernels, so this is honestly a detector: a best-effort fentry on cgroup_attach_task reads a migrating task's source cgroup (before the move) and records an escape when a zoned task leaves for an unzoned or different-zone cgroup.

sudo -E make verify-egress-cidr
sudo -E make verify-cgroup-escape

The test parks a workload in a zoned cgroup, attaches that cgroup to a zone, then moves the workload to an unzoned sibling cgroup. It asserts:

  • syva_escape_detector_attached 1 (detection is active),
  • syva_cgroup_escape_detected_total goes from 0 to 1, and
  • security status degrades.

The migration itself is not blocked — the assertions are about detection and surfacing, never prevention. If the kernel lacks fentry support for the target, the detector stays off (syva_escape_detector_attached 0) and LSM enforcement is unaffected.

Network Lock Integration Test

verify-network-lock proves the network lock end to end. An Isolated (network-locked) zone is reachable on loopback only — the three network hooks deny its non-loopback operations — while a Bridged (network-open) zone, registered through the proto network mode, is allowed out.

sudo -E make verify-network-lock

For a workload cgroup in the locked zone:

  • socket_connect — a non-loopback outbound connect fails with EPERM;
  • socket_sendmsg — an unconnected UDP sendto to a non-loopback address fails with EPERM (this is the hole socket_connect alone cannot close — connect() is optional for UDP);
  • socket_bind — binding a non-loopback listener fails with EPERM;
  • loopback connect SUCCEEDS.

Each denial bumps its own per-hook deny counter by exactly 1. A workload in the open (Bridged) zone runs the same non-loopback connect and does NOT get EPERM, proving the per-zone lock/open switch (network_mode) works. If a hook were unattached, the locked op would reach the network (ECONNREFUSED/timeout, never EPERM) and the counter would not move.

Residual (documented, not closed by this gate): raw/packet sockets (AF_PACKET, needs CAP_NET_RAW) bypass the family check, and the loopback carve-out trusts anything on 127.0.0.0/8 (a localhost proxy could forward out).

Inode Identity Integration Test

verify-inode-identity proves the composite (dev, ino) file identity: an inode-number collision across two filesystems must not cause cross-zone confusion.

sudo -E make verify-inode-identity

The test mounts two fresh tmpfs filesystems and constructs a real collision — a zoned secret.txt on filesystem A and an unrelated file on filesystem B with the same inode number but a different device (asserted as a precondition). For a workload cgroup attached to zone-a:

  • the unzoned same-ino file on filesystem B is READ successfully (file_open deny delta 0) — under an ino-only key this open would have been zone-confused and denied;
  • the genuinely zoned filesystem-A secret is DENIED with EPERM (file_open deny delta exactly 1) — enforcement itself still works.

The kernel-side device is the superblock s_dev, which userspace learns through the in-kernel inode probe at registration time (converting stat st_dev is wrong on filesystems like btrfs that synthesize per-subvolume device numbers). The startup inode self-test validates the full file → inode → superblock offset chain plus the dev encoding before enforcement begins.

Residual (documented, not closed by this gate): subvolumes of one btrfs filesystem share a superblock, so same-ino files in sibling subvolumes of the same filesystem still alias.

Enriched Events Integration Test

verify-events proves the operator-facing deny-event stream: a zoned workload's denied cat and denied connect arrive on WatchEvents carrying zone names, the kernel-captured process comm, the registered host path and inode (file denial), the destination ip:port (network denial), a decision label, and non-empty templated reason fields.

sudo -E make verify-events

Every asserted field travels a different layer (kernel struct, registration index, registry reverse index, sockaddr read), so a regression in any one of them fails a specific assertion.

Audit Mode Integration Test

verify-audit-mode proves the observe-only rollout path: the core is started with --mode audit, the same zone/file/cgroup setup as the enforcement test is applied, and the assertions invert the negative control —

sudo -E make verify-audit-mode
  • the zone-a workload READS the zone-b secret successfully (no EPERM),
  • the file_open deny counter still increments by exactly 1 (the would-deny decision was recorded), and
  • /healthz reports enforcement_mode: "audit" while staying healthy (audit is an operator choice, not a degradation).

If audit mode silently failed open everywhere the counter would not move; if it failed closed (still enforcing) the read would fail. Only a genuine record-but-allow path passes. The default mode is enforce; a missing ENFORCEMENT_MODE map value means enforce.

Kubernetes Membership Integration Test

verify-k8s-membership proves the first syva-k8s membership watcher end to end. It starts a local syva-core, starts syva-k8s against the current Kubernetes context, creates an annotated pod on the local node, waits for the adapter to resolve the pod container's real host cgroup id, and then proves file_open blocks that pod from reading a zone-b file.

sudo -E make verify-k8s-membership

This is a privileged Linux / BPF-LSM + single-node Kubernetes test. k3s is the preferred environment for Lima because it keeps the proof small and node-local. kind or another local cluster can work if the pod runtime exposes container cgroups through the host /proc and /sys/fs/cgroup visible to the adapter. The target fails clearly when Linux, root, BPF LSM, kubectl, the cluster, the pod image, host /proc, host cgroup v2, or adapter/core access is missing. It does not silently skip.

Contract

The target prints this contract before it runs:

=== syva k8s membership integration contract ===
PASS: annotated pod in zone-a can read its own zone-a file.
BLOCK: annotated pod in zone-a cannot read protected zone-b file.
ASSIGNMENT: syva.false.systems/zone=zone-a
HOOK: file_open
EXPECTED DENIAL: EPERM / Operation not permitted
EXPECTED KERNEL EVIDENCE: file_open deny_delta=1

Scenario

  1. Build the release eBPF object and local binaries.
  2. Start syva-core.
  3. Start syva-k8s with the local node name, host /proc, host /sys/fs/cgroup, and the local core socket.
  4. Register syva-it-zone-a and syva-it-zone-b.
  5. Register a zone-a allowed host path and a zone-b protected host path.
  6. Create a uniquely named namespace and a pod annotated with syva.false.systems/zone: syva-it-zone-a.
  7. Wait for the pod to become Running.
  8. Resolve the runtime container ID, host cgroup-v2 path, and host cgroup inode/id.
  9. Wait for AttachContainer evidence: syva_k8s_membership_attach_total{result="applied"}, syva_k8s_memberships_active 1, and syvactl status showing one active membership.
  10. Let the pod read /work/zone-a/allowed.txt; it must succeed and return the expected content.
  11. Measure the file_open deny counter.
  12. Let the pod read /work/zone-b/secret.txt; it must fail, stderr must contain Operation not permitted, stdout must not contain the secret, and file_open deny_delta must be exactly 1.
  13. Delete the pod and wait for detach evidence: syva_k8s_membership_detach_total{result="applied"} and syva_k8s_memberships_active 0.

The output includes the namespace, node, pod name, runtime container ID, host PID used for resolution, host cgroup path, cgroup inode/id, attach result, allowed read result, blocked read result, EPERM, deny_delta=1, metrics evidence, and detach result. The test is rerun-safe: names and paths are based on syva-k8s-it-<pid>, and cleanup deletes the pod, namespace, local processes, and /tmp/syva-k8s-it-<pid>.

Known limitations of this proof: assignment remains annotation-only; selector assignment, namespace defaults, leader election, finalizers, CRD status, runtime resolver coverage beyond the visible containerd/CRI/Docker-style cgroup layouts, and cgroup movement protection remain out of scope.

Release Evidence

Attach the command, host/kernel information, and output to the release notes or release PR:

uname -a
cat /sys/kernel/security/lsm
sudo -E make verify-runtime
sudo -E make verify-integration
sudo -E make verify-container-integration
sudo -E make verify-k8s-membership
sudo -E make verify-audit-mode
sudo -E make verify-network-lock
sudo -E make verify-egress-cidr
sudo -E make verify-cross-zone-tcp
sudo -E make verify-cgroup-escape

v0.2.0 Release Evidence

Syva v0.2.0 was tagged after the privileged release gates passed on the merge commit.

  • Tag: v0.2.0
  • Merge commit: 244c91c
  • Host: Linux lima-syva-dev 6.8.0-124-generic #124-Ubuntu SMP PREEMPT_DYNAMIC Tue May 26 12:38:50 UTC 2026 aarch64
  • LSM: lockdown,capability,landlock,yama,apparmor,bpf
  • Release eBPF object: syva-ebpf/target/bpfel-unknown-none/release/syva-ebpf

Privileged release gates:

  • sudo -E make verify-runtime passed.
    • Loaded the release eBPF object.
    • Loaded 6 eBPF programs.
    • Attached all 9 supported BPF-LSM hooks.
    • Passed cgroup, inode, and Unix stream self-tests.
    • Passed local_mode_starts_server.
    • Passed local_mode_register_then_list.
  • sudo -E make verify-integration passed.
    • Process/cgroup cross-zone file_open was denied with EPERM.
    • file_open deny_delta=1.
  • sudo -E make verify-container-integration passed.
    • Podman container in zone-a was blocked from reading a zone-b file.
    • The blocked read failed with EPERM.
    • file_open deny_delta=1.

Supported v0.2.0 hook set:

  • syva_file_open -> file_open
  • syva_bprm_check -> bprm_check_security
  • syva_ptrace_check -> ptrace_access_check
  • syva_task_kill -> task_kill
  • syva_mmap_file -> mmap_file
  • syva_unix_connect -> unix_stream_connect

Known v0.2.0 gaps:

  • Cgroup movement / zone escape protection is not enforced in v0.2.0. cgroup_attach_task is not a BPF-LSM hook on supported mainline kernels.
  • Runtime verification remains a privileged BPF-LSM release gate.
  • Kubernetes adapter behavior is not proven end to end.
  • Only file_open has end-to-end process and container denial evidence.
  • INODE_ZONE_MAP remains inode-only; /proc and /sys coverage remains incomplete.

Lima

Lima remains the supported macOS bridge for Linux build/test/eBPF object compilation:

make lima-check

Do not treat Lima as runtime enforcement evidence unless the runtime command above is run successfully inside that VM with BPF LSM support and privileges.