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 withrust-src, andbpf-linker.- A
syvagroup for the localsyva-coreUnix socket. - Kernel config and boot parameters that allow BPF LSM programs to load.
- The release eBPF object.
make verify-runtimebuilds 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.
- Register
syva-it-zone-aandsyva-it-zone-b. - Create
zone-a-allowed.txt(registered to zone-a) andzone-b-secret.txt(registered to zone-b). Both are root-owned and world-readable, so any denial is Syvä's, not filesystem permissions. - Control: the unzoned test process reads the secret file successfully.
- Create a cgroup, attach it to
syva-it-zone-a. - Positive: a workload in the zone-a cgroup reads the zone-a file → succeeds.
- 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 thefile_openhook denies by returning-1. It is not EACCES. deny_deltais thefile_opendeny counter measured immediately before and after the negative control. It is workload-attributable and must be1: the workload is the only zoned process, and unzoned callers are always allowed, so no other process can produce a deny.- The
file_openallowcounter is global — the hook runs on everyopen()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/sendtoto the allowlisted IPv4 IP: NOT denied (the any-port CIDR carve-out);connectto an allowlisted IPv4CIDR:port: NOT denied on that port, butEPERMwithsocket_connectdeny movement on the same IP and a different port;connectto an allowlisted IPv6CIDR:port: NOT denied for that destination/port, while a different IPv6 destination isEPERMwithsocket_connectdeny movement;connect/sendtoto a different non-loopback IP (TEST-NET192.0.2.1):EPERM, withsocket_connect/socket_sendmsgdeny counters moving;bindto the allowlisted IP: stillEPERM— 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
connectto the zone-a IP: NOT denied (same zone);connectto the zone-b IP:EPERM, withsocket_connectdeny moving by 1;- after
AllowComm(zone-a, zone-b),connectto 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_totalgoes from0to1, 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 withEPERM;socket_sendmsg— an unconnected UDPsendtoto a non-loopback address fails withEPERM(this is the holesocket_connectalone cannot close —connect()is optional for UDP);socket_bind— binding a non-loopback listener fails withEPERM;- 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_opendeny 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_opendeny 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_opendeny counter still increments by exactly 1 (the would-deny decision was recorded), and /healthzreportsenforcement_mode: "audit"while stayinghealthy(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
- Build the release eBPF object and local binaries.
- Start
syva-core. - Start
syva-k8swith the local node name, host/proc, host/sys/fs/cgroup, and the local core socket. - Register
syva-it-zone-aandsyva-it-zone-b. - Register a zone-a allowed host path and a zone-b protected host path.
- Create a uniquely named namespace and a pod annotated with
syva.false.systems/zone: syva-it-zone-a. - Wait for the pod to become
Running. - Resolve the runtime container ID, host cgroup-v2 path, and host cgroup inode/id.
- Wait for
AttachContainerevidence:syva_k8s_membership_attach_total{result="applied"},syva_k8s_memberships_active 1, andsyvactl statusshowing one active membership. - Let the pod read
/work/zone-a/allowed.txt; it must succeed and return the expected content. - Measure the
file_opendeny counter. - Let the pod read
/work/zone-b/secret.txt; it must fail, stderr must containOperation not permitted, stdout must not contain the secret, andfile_open deny_deltamust be exactly1. - Delete the pod and wait for detach evidence:
syva_k8s_membership_detach_total{result="applied"}andsyva_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-runtimepassed.- 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-integrationpassed.- Process/cgroup cross-zone
file_openwas denied withEPERM. file_open deny_delta=1.
- Process/cgroup cross-zone
sudo -E make verify-container-integrationpassed.- 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_opensyva_bprm_check -> bprm_check_securitysyva_ptrace_check -> ptrace_access_checksyva_task_kill -> task_killsyva_mmap_file -> mmap_filesyva_unix_connect -> unix_stream_connect
Known v0.2.0 gaps:
- Cgroup movement / zone escape protection is not enforced in v0.2.0.
cgroup_attach_taskis 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_openhas end-to-end process and container denial evidence. INODE_ZONE_MAPremains inode-only;/procand/syscoverage 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.