horizon

module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 28, 2026 License: Apache-2.0

README

Horizon

A Go-shaped DSL for writing kernel capabilities — verifier-aware checks, readable BPF C, typed Go bindings, and Continuum capability manifests, all in one workbench.

Horizon is not a Go compiler. It is a small Go-shaped DSL for writing verifier-friendly eBPF programs that lower to readable BPF C.

What Horizon solves

Verifier errors are cryptic. Horizon emits source maps with every build. hzn diagnose remaps clang and verifier logs back through those source maps and adds Horizon-specific remediation hints for nil checks, ringbuf lifetimes, bounded loops, helper availability, and stack usage. Common verifier messages route through a vendored catalog with stable HZN31xx codes so editors and CI see consistent remediation text.

Resource lifetimes are implicit. Ringbuf reservations, map lookups, and packet headers are first-class resource types with tracked states (maybe_nil → live → consumed). Missing nil checks, double submits, writes after submit, and live-on-return are rejected before clang runs. Tracking spans intra-function aliasing, &&-chained nil checks, bounded loop iterations, and user helpers that take resource handles as parameters.

Build pipelines are scattered. hzn workbench produces readable BPF C, source maps, typed Go bindings, a capability manifest, diagnostics, and a report with provenance — in one command. hzn build adds clang compilation.

Object files do not explain their power. Every program declares a capability with explicit danger metadata. Danger is recorded as an axis triple (mode × scope × reversibility); legacy flat words such as observe, mutate, drop, block, and privileged map deterministically onto the axes. The generated manifest carries the contract, including helper side-effects per program.

The Workbench

hzn workbench is the authoring path. One command, every artifact:

hzn workbench ./examples/execwatch -o dist

produces:

  • Readable BPF C (exec.bpf.c).
  • Source map (exec.hznmap.json) for clang and verifier diagnostics.
  • Typed Go bindings (bindings.go) wrapping cilium/ebpf with typed map, ringbuf, and attach helpers.
  • Capability manifest (exec.cap.json) — see "Capability manifests" below.
  • Diagnostics (exec.diagnostics.json) including source-line context and Horizon-specific remediation hints.
  • Report (exec.report.json) with artifact hashes, sizes, generator provenance, and a summary of programs, maps, capability danger levels, and minimum kernel requirements.

Use -compile to also run clang and emit .bpf.o. Use -preflight to run host readiness checks against the generated manifest.

A program end-to-end

The same program, expressed four ways.

Author it (examples/execwatch/exec.hzn):

package probes

import bpf "m31labs.dev/horizon/runtime/kernel"

type ExecEvent struct {
    ts_ns u64
    pid u32
    ppid u32
    uid u32
    comm [16]u8
}

map ExecEvents ringbuf[ExecEvent]

capability ExecObserve danger observe = "kernel.process.exec.observe"

@capability(ExecObserve)
@tracepoint("sched:sched_process_exec")
func OnExec(ctx tracepoint.Exec) i32 {
    event := ExecEvents.reserve()
    if event == nil {
        return 0
    }
    event.ts_ns = bpf.ktime_get_ns()
    event.pid = bpf.current_pid()
    event.ppid = bpf.current_ppid()
    event.uid = bpf.current_uid()
    bpf.current_comm(&event.comm)
    ExecEvents.submit(event)
    return 0
}

Generated BPF C (dist/exec.bpf.c, excerpt):

/* Code generated by hzn; DO NOT EDIT. */
/* ... */
struct hzn_type_ExecEvent {
    __u64 ts_ns;
    __u32 pid;
    __u32 ppid;
    __u32 uid;
    __u8 comm[16];
};
_Static_assert(sizeof(struct hzn_type_ExecEvent) == 40, "horizon: struct ExecEvent size mismatch");
/* ... */

struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 1 << 24);
} ExecEvents SEC(".maps");

static __always_inline struct hzn_type_ExecEvent *ExecEvents_reserve(void) {
    return bpf_ringbuf_reserve(&ExecEvents, sizeof(struct hzn_type_ExecEvent), 0);
}

static __always_inline void ExecEvents_submit(struct hzn_type_ExecEvent *value) {
    bpf_ringbuf_submit(value, 0);
}

SEC("tracepoint/sched/sched_process_exec")
int OnExec(struct trace_event_raw_sched_process_exec *ctx) {
    (void)ctx;
    struct hzn_type_ExecEvent *event = ExecEvents_reserve();
    if (event == 0) {
        return 0;
    }
    event->ts_ns = hzn_ktime_get_ns();
    event->pid = hzn_current_pid();
    event->ppid = hzn_current_ppid();
    event->uid = hzn_current_uid();
    hzn_current_comm(&event->comm, sizeof(event->comm));
    ExecEvents_submit(event);
    return 0;
}

Generated Go binding (dist/exec.bindings.go, excerpt):

// Code generated by hzn; DO NOT EDIT.

package bindings

// ...

type ExecEvent struct {
    TsNs uint64
    Pid  uint32
    Ppid uint32
    Uid  uint32
    Comm [16]uint8
}

type Objects struct {
    ExecEvents *ebpf.Map     `ebpf:"ExecEvents"`
    OnExec     *ebpf.Program `ebpf:"OnExec"`
}

func LoadObjects(path string) (*Objects, error) {
    return LoadObjectsWithOptions(path, LoadOptions{RemoveMemlock: true})
}

func LoadObjectsWithOptions(path string, opts LoadOptions) (*Objects, error) { /* ... */ }

func (o *Objects) ReadExecEvents(ctx context.Context, handle func(ExecEvent) error) error { /* ... */ }

func (o *Objects) AttachOnExec() (link.Link, error) {
    if o == nil || o.OnExec == nil {
        return nil, fmt.Errorf("OnExec program is not loaded")
    }
    return link.Tracepoint("sched", "sched_process_exec", o.OnExec, nil)
}

Capability manifest (dist/exec.cap.json, excerpt):

{
  "schema": "m31labs.dev/horizon/capability/v1",
  "package": "probes",
  "programs": [
    {
      "name": "OnExec",
      "kind": "tracepoint",
      "attach": "sched:sched_process_exec",
      "section": "tracepoint/sched:sched_process_exec",
      "capabilities": [
        "kernel.process.exec.observe"
      ]
    }
  ],
  "capabilities": [
    {
      "name": "kernel.process.exec.observe",
      "kind": "source",
      "danger": {
        "mode": "observe",
        "reversibility": "none",
        "scope": "event"
      },
      "program": "OnExec",
      "section": "tracepoint/sched:sched_process_exec",
      "emits": "ExecEvent",
      "maps": {
        "read": [],
        "write": [],
        "events": ["ExecEvents"]
      }
    }
  ]
}

Install

Until the m31labs.dev/horizon vanity import metadata is live, install the CLI from a clone:

git clone https://github.com/M31-Labs/horizon.git
cd horizon
go install ./cmd/hzn

After the vanity import path is serving Go metadata, tagged releases will be installable with:

go install m31labs.dev/horizon/cmd/hzn@latest

Language tour

.hzn -> gotreesitter parser -> AST -> BPF IR -> validation -> C -> clang -> .bpf.o -> bindings + capabilities
What Horizon keeps small

It keeps the kernel-side language deliberately small:

  • tracepoint programs
  • kprobe and kretprobe programs
  • uprobe and uretprobe programs
  • fentry and fexit programs
  • raw tracepoint programs
  • XDP programs
  • TC classifier programs
  • cgroup connect programs
  • LSM programs
  • sockops programs
  • struct_ops programs
  • typed structs and fixed arrays
  • boolean literals and typed boolean expressions
  • package-scoped declarations across multiple .hzn files
  • multi-file packages and cross-package imports resolved under vendor/
  • source-level scalar type aliases such as type Port = u16
  • grouped type declarations for related aliases and records
  • integer constants with optional scalar widths
  • grouped constants for related limits, flags, and action values
  • signed integer literals such as -1 for signed scalar fields and helpers
  • scoped if name := expr; condition declarations for short-lived nullable resources
  • ringbuf event output
  • hash, array, per-CPU, and LRU maps
  • explicit @max_entries(...) map sizing
  • nil-checked map lookups
  • bounded counted loops
  • expression switch statements with explicit non-fallthrough cases
  • explicit integer scalar conversions such as u64(pid)
  • explicit local variable declarations such as var pid u32 = bpf.current_pid()
  • explicitly typed constants such as const Port u16 = 443
  • signed constants such as const Errno i32 = -1
  • typed enum value groups for named integer actions and flags
  • named capability aliases with explicit danger metadata such as capability ExecObserve danger observe = "kernel.process.exec.observe"
  • scalar user helper functions that lower to static __always_inline C
  • compiler-known kernel helpers
  • typed kprobe argument and kretprobe return helpers
  • readable generated BPF C
  • generated BPF C and Go bindings marked as generated artifacts
  • embedded gotreesitter highlight, locals, and symbol queries for editor integrations
  • source maps with declaration and function/section context for diagnostics
  • typed Go bindings and Continuum capability manifests
Programs and capabilities
package probes

import bpf "m31labs.dev/horizon/runtime/kernel"

type ExecEvent struct {
    ts_ns u64
    pid  u32
    ppid u32
    uid  u32
    comm [16]u8
}

map ExecEvents ringbuf[ExecEvent]

capability ExecObserve danger observe = "kernel.process.exec.observe"

@capability(ExecObserve)
@tracepoint("sched:sched_process_exec")
func OnExec(ctx tracepoint.Exec) i32 {
    event := ExecEvents.reserve()
    if event == nil {
        return 0
    }

    event.ts_ns = bpf.ktime_get_ns()
    event.pid = bpf.current_pid()
    event.ppid = bpf.current_ppid()
    event.uid = bpf.current_uid()
    bpf.current_comm(&event.comm)

    ExecEvents.submit(event)
    return 0
}

Stateful programs can use typed maps. Lookup results are nullable and must be checked before dereference.

Capability strings can be named once at package scope with explicit danger metadata and referenced from entrypoint attributes. This keeps the source readable while preserving the manifest's stable Continuum capability name. When explicit danger is omitted, Horizon infers it from the program body; when it is present, the manifest uses the more dangerous of the declaration and the program's inferred behavior. If a capability name ends in a known danger word, such as .block or .drop, that suffix is also treated as a manifest floor.

capability ExecObserve danger observe = "kernel.process.exec.observe"

@capability(ExecObserve)
@tracepoint("sched:sched_process_exec")
func OnExec(ctx tracepoint.Exec) i32 {
    return 0
}
Enums

Use enum for named integer value sets. Enum values are explicit, typed constants; Horizon does not infer iota-like values or widen them through C's usual implicit conversions.

enum Verdict i32 {
    VerdictPass = 0
    VerdictDrop = 1
}

capability ExecObserve danger observe = "kernel.process.exec.observe"

@capability(ExecObserve)
@tracepoint("sched:sched_process_exec")
func OnExec(ctx tracepoint.Exec) i32 {
    if bpf.current_pid() == 0 {
        return VerdictPass
    }
    return VerdictDrop
}
Types and constants

Use source-level type aliases for domain names over scalar and bool widths. Aliases are authoring names only: generated C uses the underlying BPF scalar type, and aliases cannot hide pointers, resource handles, context types, packet headers, maps, or structs.

Aliases and related records can be grouped when that makes the domain model easier to scan.

type (
    Pid = u32
    Port = u16

    SocketEvent struct {
        pid  Pid
        port Port
    }
)

Related constants can be grouped without changing their C-facing type. This is useful for map sizes, packet ports, bit masks, and other named verifier-visible limits.

const (
    CountEntries u32 = 4096
    HTTPS Port = 443
    BucketMask u32 = 0x0f
)
Var declarations

Use var when a local needs an explicit C-facing type. := remains the preferred shape for nullable resources because Horizon tracks lookup, reserve, and packet-header ownership from the helper call.

capability ExecObserve danger observe = "kernel.process.exec.observe"

@capability(ExecObserve)
@tracepoint("sched:sched_process_exec")
func OnExec(ctx tracepoint.Exec) i32 {
    var pid u32 = bpf.current_pid()
    var bucket u32 = pid & 0x0f
    return i32(bucket)
}
Switch

Use switch for scalar dispatch that should become readable C switch. Cases do not fall through, and case values must be compile-time constants: integer or bool literals, package constants, enum values, or compiler-known action/protocol constants.

capability WebDrop danger drop = "kernel.network.xdp.drop"

@capability(WebDrop)
@xdp
func DropWeb(ctx xdp.Context) i32 {
    tcp := xdp.tcp(ctx)
    if tcp == nil {
        return xdp.Pass
    }

    switch xdp.ntohs(tcp.dst_port) {
    case 80, 443:
        return xdp.Drop
    default:
        return xdp.Pass
    }
}
Maps
const FirstSeen u32 = 1

type Count struct {
    seen u32
}

map Counts hash[u32, Count]

capability ExecCount danger observe = "kernel.process.exec.count.observe"

@capability(ExecCount)
@tracepoint("sched:sched_process_exec")
func OnExec(ctx tracepoint.Exec) i32 {
    pid := bpf.current_pid()
    if Counts.update(pid, Count{seen: FirstSeen}) != 0 {
        return 0
    }

    if count := Counts.lookup(pid); count != nil {
        count.seen = count.seen + 1
    }
    return 0
}
Helpers

Reusable logic belongs in small scalar helpers, not in raw C fragments. A sectionless func is a Horizon user helper: it can be called from eBPF entrypoints or other helpers, must be acyclic, and lowers to readable static __always_inline C so clang and the verifier still see the final code. Helper return values are scalar or bool. Helper parameters are scalar, bool, or a nullable resource handle (ringbuf reservation, map lookup pointer, or packet header); Horizon summarizes each helper's effect on the handle and propagates resource state across the call, so callers see the transition without having to re-derive it.

func should_count(pid u32) bool {
    return pid != 0
}

func normalize_pid(pid u32) u32 {
    if should_count(pid) {
        return pid
    }
    return 1
}

capability ExecObserve danger observe = "kernel.process.exec.observe"

@capability(ExecObserve)
@tracepoint("sched:sched_process_exec")
func OnExec(ctx tracepoint.Exec) i32 {
    pid := normalize_pid(bpf.current_pid())
    if should_count(pid) {
        return 0
    }
    return 0
}
Map sizing and shapes

Maps can be sized deliberately with @max_entries(...). Ringbuf sizes are byte sizes and must be powers of two; hash and array sizes are entry counts. Use a literal or an integer constant; Horizon resolves constants before emitting C map definitions.

const CountEntries u32 = 4096
const EventBytes u32 = 262144

@max_entries(CountEntries)
map Counts hash[u32, Count]

@max_entries(EventBytes)
map ExecEvents ringbuf[ExecEvent]

Per-CPU maps are explicit when each CPU should get its own value slot. In kernel-side .hzn, they use the same safe lookup/update/delete flow as ordinary maps; generated Go bindings expose per-CPU values as typed slices.

type Count struct {
    seen u64
}

map ExecCounts percpu_hash[u32, Count]

capability ExecCount danger observe = "kernel.process.exec.count.observe"

@capability(ExecCount)
@tracepoint("sched:sched_process_exec")
func CountExec(ctx tracepoint.Exec) i32 {
    pid := bpf.current_pid()
    if ExecCounts.update(pid, Count{seen: 1}) != 0 {
        return 0
    }

    count := ExecCounts.lookup(pid)
    if count == nil {
        return 0
    }
    count.seen = count.seen + 1
    return 0
}

LRU maps are explicit when state should stay bounded and let the kernel evict least-recently-used entries instead of surfacing map-full failures.

type Flow struct {
    bytes u64
}

@max_entries(8192)
map Flows lru_hash[u64, Flow]

@max_entries(8192)
map CPUFlows lru_percpu_hash[u64, Flow]
Packet path

Packet-path programs use explicit section attributes, compiler-checked packet loads, nil-checked headers, and named action values instead of raw pointer arithmetic or integer returns.

package probes

capability DropCapability danger drop = "kernel.network.xdp.drop"

@capability(DropCapability)
@xdp
func DropTCP(ctx xdp.Context) i32 {
    tcp := xdp.tcp(ctx)
    if tcp == nil {
        return xdp.Pass
    }

    port := xdp.ntohs(tcp.dst_port)
    if (port == 443) && ((tcp.data_off & 0x0f) != 0) {
        return xdp.Drop
    }

    return xdp.Pass
}
Probes

Tracing programs can also attach to kernel symbols with explicit kprobe and kretprobe section attributes. Probe contexts stay opaque; use compiler-known helpers such as kprobe.arg1(ctx) and kretprobe.ret(ctx) rather than raw register arithmetic.

package probes

import bpf "m31labs.dev/horizon/runtime/kernel"

type OpenEvent struct {
    pid u32
    dfd i32
    path [256]u8
}

map OpenEvents ringbuf[OpenEvent]

capability FileOpenObserve danger observe = "kernel.file.open.observe"

@capability(FileOpenObserve)
@kprobe("do_sys_openat2")
func OnOpen(ctx kprobe.Context) i32 {
    event := OpenEvents.reserve()
    if event == nil {
        return 0
    }

    event.pid = bpf.current_pid()
    event.dfd = i32(kprobe.arg1(ctx))
    if bpf.probe_read_user_str(&event.path, kprobe.arg2(ctx)) < 0 {
        OpenEvents.discard(event)
        return 0
    }

    OpenEvents.submit(event)
    return 0
}

@capability(FileOpenObserve)
@kretprobe("do_sys_openat2")
func OnOpenReturn(ctx kretprobe.Context) i32 {
    rc := kretprobe.ret(ctx)
    if rc < 0 {
        return 0
    }
    return 0
}
TC

TC classifier programs are explicit about direction and return named TC actions, not raw integers.

package probes

capability TCObserve danger observe = "kernel.network.tc.observe"

@capability(TCObserve)
@tc("ingress")
func PassIngress(ctx tc.Context) i32 {
    return tc.OK
}
Cgroup connect

Cgroup connect programs make policy decisions with named allow/deny actions. Compiler-known helpers expose small, typed pieces of the kernel context instead of making authors poke raw struct bpf_sock_addr fields.

package probes

capability ConnectBlock danger block = "kernel.network.connect.block"

@capability(ConnectBlock)
@cgroup("connect4")
func BlockSMTP(ctx cgroup.Connect) i32 {
    if cgroup.family(ctx) != cgroup.FamilyIPv4 {
        return cgroup.Allow
    }
    if cgroup.protocol(ctx) != cgroup.ProtocolTCP {
        return cgroup.Allow
    }
    if cgroup.dst_port(ctx) == 25 && cgroup.dst_ip4(ctx) != cgroup.ip4(127, 0, 0, 1) {
        return cgroup.Deny
    }
    return cgroup.Allow
}
LSM

LSM programs are policy hooks with opaque contexts. They return named allow/deny actions so authoring stays explicit about security impact.

package probes

capability FileOpenBlock danger block = "kernel.file.open.block"

@capability(FileOpenBlock)
@lsm("file_open")
func DenyFileOpen(ctx lsm.Context) i32 {
    return lsm.Deny
}

Safety Model

Horizon makes verifier-sensitive behavior explicit before clang runs.

Resource typing
  • ringbuf reservations must be nil-checked, submitted, or discarded exactly once
  • scoped if name := expr; condition declarations lower to a C block before the if, so nullable lookup/header/reservation locals can be kept short-lived without leaking outside the branch
  • writes after ringbuf submit/discard are rejected
  • map lookup results must be nil-checked before field access
  • nullable map, packet, and ringbuf resource pointers cannot be copied or aliased
  • raw address-taking and explicit pointer dereference are rejected; use compiler-known resource/header helpers and direct fixed-array helper operands instead
  • source-authored pointer types such as *u32 are rejected; nullable pointers only come from compiler-known map lookup, ringbuf reserve, and packet helpers
  • ringbuf maps emit typed events and must use declared struct value types, not scalars or compiler-owned packet/header structs
  • fixed array fields are address-only; pass &event.comm directly to helpers instead of copying arrays
  • packet headers returned by xdp.eth(ctx), xdp.ipv4(ctx), xdp.tcp(ctx), and xdp.udp(ctx) must be nil-checked before field access
Type and width discipline
  • conditions must be typed boolean expressions; integers and pointers need explicit comparison
  • integer, bitwise, comparison, and boolean operators are typed before C emission
  • integer width changes are explicit; write u64(pid) or u16(port) instead of relying on implicit C coercions
  • integer literals, literal-backed constants, and literal-backed conversions are checked against their target scalar width before C emission
  • unary negation is only allowed for signed scalar values or direct integer literals; unsigned values must be converted deliberately before signed arithmetic
  • division and modulo by literal zero are rejected before C emission
  • dynamic division and modulo require a divisor proven non-zero by a simple guard or non-zero constant
  • literal shift counts must be non-negative and smaller than the left operand width
  • dynamic shift counts must be proven non-negative and below the shifted value width with a simple guard
  • constants can carry scalar widths, and generated C preserves those widths
  • constants are immutable; use locals for values that change inside a program
  • grouped constants are package-scoped constants with the same scalar, bool, and literal rules as standalone constants
  • enum values are explicit typed integer constants; there is no implicit iota or untyped C enum widening
  • type aliases are source-level names for scalar and bool widths only; they cannot target structs, pointer syntax, compiler-owned context/header types, or resource-bearing values
  • grouped type declarations are package-scoped type declarations with the same alias and struct rules as standalone types
  • var declarations require an explicit scalar, bool, or declared struct type and cannot store nullable resources or compiler-owned packet/context types
  • switch values must be scalar or bool, case values must be constant and type-compatible, and Horizon emits explicit C break statements so cases never fall through
  • short variable declarations introduce fresh local names only; use = to update existing locals, and do not shadow maps or compiler namespaces
Capability discipline
  • hzn check, hzn emit-c, hzn bindgen, hzn workbench, hzn build, and hzn capabilities reject attachable programs without capability coverage
  • capability aliases can declare danger as a legacy flat word (observe, mutate, drop, block, privileged) or as an explicit axis triple ("mode,scope,reversibility"); manifests emit the triple and never understate inferred program danger or a known danger suffix in the capability name
  • capability names must be unique across attachable programs, keeping generated manifests unambiguous for downstream policy and deployment tooling
  • known kernel.* capability namespaces must match known attach surfaces, so an XDP program cannot claim a process exec capability or a cgroup connect program cannot claim a file-open capability
  • capability manifests require each program's capability list to match the top-level capability entries, so Continuum consumers can trust either index
  • capability manifest programs, maps, types, and type fields must have unique names; schema consumers never have to guess which duplicate wins
  • default generated Go binding names must be valid and collision-free, so public APIs are checked as part of hzn check
Verifier-aware constraints
  • only bounded counted loops with numeric literal or integer const upper bounds are accepted
  • helper availability is checked against the program kind
  • compiler-known helpers have typed signatures and section availability rules before C emission
  • every program must return an explicit i32 on every control-flow path
  • bare return is rejected; tracing programs should use return 0, while packet and policy programs should return named actions
  • tracepoints must use category:event attach strings, and kprobe, kretprobe, and LSM attach strings must be non-empty section tokens rather than path-like fragments
  • XDP programs must return named actions such as xdp.Pass and xdp.Drop, not raw integers
  • TC programs must declare @tc("ingress") or @tc("egress") and return named actions such as tc.OK and tc.Shot, not raw integers
  • cgroup connect programs must declare @cgroup("connect4") or @cgroup("connect6"), use typed context helpers such as cgroup.protocol(ctx) and cgroup.dst_ip4(ctx), and return named actions such as cgroup.Allow and cgroup.Deny, not raw integers
  • cgroup context reads lower through typed generated wrappers, so generated C diagnostics map back to the authored cgroup.* helper call
  • LSM programs must declare an explicit hook such as @lsm("file_open") and return named actions such as lsm.Allow and lsm.Deny, not raw integers
  • kprobe arguments, safe user string reads, and kretprobe return registers are exposed through typed helper calls, not direct pt_regs access
  • sectionless functions are user helpers, not eBPF programs; they are emitted as static __always_inline C, must be non-recursive, and currently accept and return only scalar or bool values
  • eBPF entrypoint functions cannot be called like helpers; share logic through sectionless helpers so attachable programs remain explicit
  • struct fields must be unique, and structs are finite by-value records; recursive struct shapes are rejected before C emission
  • stored data types for structs and keyed maps must be scalars, fixed arrays, or declared Horizon structs; compiler-owned context and packet header types stay helper-only
  • package-scoped declarations cannot use compiler namespace names such as bpf, xdp, tc, cgroup, lsm, kprobe, or tracepoint
  • map sizing is explicit through @max_entries(...); integer constants are resolved before C emission, and ringbuf sizes must be powers of two
  • map update/delete results must be checked with an explicit comparison
Generated-artifact discipline
  • generated BPF C and Go bindings include standard generated-file headers so downstream tools and reviewers can distinguish authored source from artifacts
  • generated C validation only permits Horizon-owned SEC(...) shapes for license, maps, and supported program sections
  • generated C validation permits raw bpf_* calls only inside compiler-owned helper or map wrappers, never inside source-authored helper functions
  • generated map wrappers source-map back to the authored lookup, update, delete, reserve, submit, or discard call
  • generated BPF C is compiled with clang warnings treated as errors
  • generated C stays readable so clang and verifier logs remain inspectable
  • internal generated C constants and struct tags are prefixed to avoid collisions with kernel headers
  • generated C emits only the helper and map wrappers the program actually uses
  • parser failures are surfaced as stable diagnostics and never produce generated C

Commands

hzn check ./examples/execwatch
hzn check ./examples/execwatch -json
hzn new -list
hzn new ./scratch/execwatch
hzn new ./scratch/xdpdrop -template xdpdrop
hzn fmt ./examples/execwatch
hzn fmt ./examples -w
hzn fmt ./examples -check
hzn doctor
hzn doctor -capabilities dist/exec.cap.json
hzn version
hzn --version
hzn version -json
make setup-vmlinux
make ci-go
hzn workbench ./examples/execwatch -o dist
hzn workbench ./examples/execwatch -o dist -json
hzn workbench ./examples/execwatch -o dist -compile
hzn workbench ./examples/execwatch -o dist -preflight
hzn capabilities ./examples/execwatch -o dist/exec.cap.json
hzn build ./examples/cgroupconnect -o dist
sudo go run ./examples/cgroupconnect/cmd/cgroupconnect -obj dist/connect.bpf.o -cgroup /sys/fs/cgroup
hzn build ./examples/execwatch -o dist
go run ./examples/execwatch/cmd/execwatch -obj dist/exec.bpf.o
hzn build ./examples/execcount -o dist
sudo go run ./examples/execcount/cmd/execcount -obj dist/count.bpf.o -timeout 10s
hzn build ./examples/lsmfile -o dist
sudo go run ./examples/lsmfile/cmd/lsmfile -obj dist/file.bpf.o
hzn build ./examples/openwatch -o dist
sudo go run ./examples/openwatch/cmd/openwatch -obj dist/open.bpf.o
hzn build ./examples/tcpconnect -o dist
sudo go run ./examples/tcpconnect/cmd/tcpconnect -obj dist/tcp.bpf.o
hzn build ./examples/tcpass -o dist
sudo go run ./examples/tcpass/cmd/tcpass -obj dist/tc.bpf.o -iface lo
hzn build ./examples/xdpdrop -o dist
sudo go run ./examples/xdpdrop/cmd/xdpdrop -obj dist/xdp.bpf.o -iface lo
hzn diagnose dist/exec.verifier.log --map dist/exec.hznmap.json
hzn diagnose dist/exec.verifier.log --map dist/exec.hznmap.json -json -fail-on-error

hzn fmt gives .hzn files a canonical AST-based style for local editing and CI. Use -w to update files in place and -check to fail when files need formatting. The formatter preserves standalone and inline line comments.

hzn new creates an opinionated starter .hzn program that already follows Horizon's safety rules: named capabilities, explicit danger, typed helpers, nil-checked resources, and named packet actions. The default execwatch template emits a tracepoint/ringbuf source program. -template also supports execcount, openwatch, tcpconnect, kretprobe, xdpdrop, tcpass, cgroupconnect, and lsmfile; run hzn new -list to inspect the current catalog. Generated starters are meant to pass hzn check, hzn fmt -check, and hzn workbench immediately.

hzn workbench is the authoring path: it validates source and writes readable BPF C, a source map, typed Go bindings, a capability manifest, diagnostics, and a report with source file hashes plus artifact kinds, byte sizes, and SHA-256 hashes. The report also includes a compact summary of source count, program kinds, map kinds, capability danger levels, declared type count, and the minimum kernel version implied by generated capabilities. Each run removes stale artifacts for the target output base before writing new ones, records replaced paths, and includes generator, version, Go toolchain, available VCS, and timestamp provenance in the report. Workbench artifact generation requires every attachable eBPF program to declare at least one capability, so generated manifests cannot silently omit a program. Use -preflight when workbench should run the same host readiness checks as hzn doctor -capabilities against the generated manifest and include that result in the report. Invalid programs still produce <name>.diagnostics.json and <name>.report.json, including parser failures before typechecking or C emission can run. Clang failures are remapped into the same diagnostics artifact, so editors and automation can show actionable feedback without scraping terminal output. Diagnostics include source-line context and markers when the authored file is available. Remapped diagnostics keep the generated BPF C location plus source-map metadata such as Horizon function, section, and AST node. Verifier diagnostics retain the recent verifier instruction/source-comment context that led to the error. Common verifier failures also carry Horizon-specific remediation hints for nil checks, ringbuf lifetimes, bounded loops, helper availability, and stack usage. Use -compile or hzn build when the local clang/BPF C toolchain should also produce a .bpf.o. Both accept -clang-timeout=<duration> (default 30s, also configurable via HZN_CLANG_TIMEOUT).

hzn diagnose remaps clang and verifier logs through an .hznmap.json source map. By default it exits successfully when it can explain the log, even when the log contains errors. Use -fail-on-error in CI or editor tasks that should exit non-zero after emitting remapped diagnostics.

Generated Go bindings expose typed helpers around the loaded objects: ringbuf maps get Read<Name>(context.Context, func(Event) error), hash maps get Lookup<Name>, Update<Name>, ForEach<Name>, and Delete<Name>, array maps get Lookup<Name>, Update<Name>, and ForEach<Name>, LRU hash maps use the same typed hash helpers, per-CPU variants expose typed []Value slices for lookup/update/iteration, and attachable programs get section-specific attach methods. The raw *ebpf.Map and *ebpf.Program fields remain available for advanced users, but ordinary consumers should not need to hand-roll cilium loader, memlock, or map-access boilerplate. Bindings also embed CapabilityManifestJSON and expose CapabilityManifest() so applications can inspect the generated capability contract without manually locating the sidecar manifest. Ringbuf readers close themselves on context cancellation so blocking reads unwind through the supplied context. LoadObjects removes the memlock limit by default; use LoadObjectsWithOptions when callers need explicit cilium collection options or custom rlimit behavior. hzn check validates default generated binding names, so collisions with loader, object, attach, map-helper, and reader APIs fail before artifacts are written. make ci-go typechecks generated bindings for every example so attach helpers, typed map helpers, and ringbuf readers stay valid against cilium/ebpf. CI-oriented Make targets keep green logs compact and print captured command output only when a gate fails.

Generated BPF C and generated Go bindings include scalar width, struct size, and field offset assertions, so clang or go test fails early if an emitted type no longer matches Horizon's ABI model.

Capability manifests include programs, map access, emitted event names, map key and value types, and struct size/align/field-offset schemas for declared Horizon types. They also include minimum kernel requirements for program types, map kinds, and compiler-known helpers, plus deploy-time permission and host feature requirements for attach surfaces such as tracefs, XDP, tc, cgroup v2, and BPF LSM. Continuum consumers can inspect what a program observes, emits, and needs from a target host without parsing BPF C. Compiler-known helpers stay explicit: bpf.ktime_get_ns() lowers to bpf_ktime_get_ns() and returns a typed u64 monotonic kernel timestamp. bpf.current_ppid() lowers through a typed CO-RE task read, so the generated C requires libbpf's bpf_core_read.h and a vmlinux.h that includes struct task_struct layout.

hzn doctor checks the local eBPF C toolchain: clang BPF support, libbpf headers including CO-RE helpers, bpftool/LLVM utilities, kernel BTF, and a usable vmlinux.h. With -capabilities, it also reads a generated capability manifest and checks the target host against the manifest's minimum kernel, permission, and attach-feature requirements, including per-capability requirements when a manifest carries them. Use make setup-vmlinux on BTF-enabled Linux hosts to generate /usr/local/include/vmlinux.h.

Capability manifests

Every Horizon build emits a capability manifest describing what the program observes, emits, mutates, and needs from a target host. The manifest is the contract Horizon hands to Continuum for governance and deployment.

The current manifest schema is m31labs.dev/horizon/capability/v1. v0 manifests are rejected by capability.LoadManifest() with an HZN3304 error; regenerate them as v1 (see docs/migrations/v0-to-v1-manifest.md).

A manifest contains:

  • Programs: each attachable program with its section, attach string, and declared capability name.
  • Capabilities: each capability's danger axes (mode × scope × reversibility), program/section, emitted event, map access, and per-program helper_effects summary of what the helpers observed, mutated, or required.
  • Maps: each map's kind (ringbuf, hash, array, percpu_hash, lru_hash, ...), key/value type schemas, @max_entries(...) sizing, and optional @steady_state_entries(...) / @access_freq(...) capacity-planning annotations.
  • Types: declared struct schemas with size, align, and field offsets.
  • Emitted events: ringbuf event names linked to their struct schema.
  • Minimum kernel requirements: feature versions implied by attach surfaces, map kinds, and compiler-known helpers.
  • Deploy-time requirements: tracefs availability, XDP support, TC ingress/egress, cgroup v2, BPF LSM, plus required Linux capabilities per attach surface.
  • Origin tagging: capabilities, maps, and types contributed by a vendored imported package carry an origin field naming the import alias they came from, so multi-package builds keep cross-package provenance visible.

hzn doctor -capabilities <manifest> checks the local host against a manifest before deploy. Continuum consumes the same manifest to govern which hosts may load which capabilities, what danger floor is acceptable per environment, and how a capability composes with the rest of an operator's policy surface.

What Horizon won't do

  • Compile arbitrary Go. Horizon is a Go-shaped DSL, not a Go compiler.
  • Hide the generated C. Generated BPF C is readable, source-mapped, and meant to be inspected by reviewers and the verifier alike.
  • Replace clang or the verifier. Horizon checks what it can before clang runs; clang and the verifier remain the source of truth on the kernel side.
  • Make unsafe kernel capabilities look safe. Every program declares explicit danger metadata; manifests never understate inferred danger.
  • Silently emit a capability-free object. hzn check, hzn emit-c, hzn bindgen, hzn workbench, hzn build, and hzn capabilities reject attachable programs without capability coverage.
  • Pretend the kernel is userspace. Verifier obligations, bounded loops, resource lifetimes, and helper availability stay visible in the source language.

Status

Latest tagged release: v0.1.2. v0.2 is in progress on main (Phase 0/1/2 shipped behind v0.2.0-phase{0,1,2} tags); the [Unreleased] section of CHANGELOG.md tracks what has landed so far.

License

Apache-2.0. See LICENSE.

Directories

Path Synopsis
cmd
hzn command
hzn-helpergen command
hzn-helpergen walks a pinned libbpf source tree and produces candidate helper-registry entries for review against the hand-curated internal/registry/helpers-v1.json.
hzn-helpergen walks a pinned libbpf source tree and produces candidate helper-registry entries for review against the hand-curated internal/registry/helpers-v1.json.
Package compiler — build matrix.
Package compiler — build matrix.
examples
internal
registry
Package registry loads the canonical capability-namespace registry vendored from the Hyphae spec at ~/.hyphae/spaces/m31labs-horizon/specs/capability-namespaces-v1.json.
Package registry loads the canonical capability-namespace registry vendored from the Hyphae spec at ~/.hyphae/spaces/m31labs-horizon/specs/capability-namespaces-v1.json.
Package parser — build-tag scanning.
Package parser — build-tag scanning.
runtime
Package-level helper-effect summarization for cross-call resource tracking.
Package-level helper-effect summarization for cross-call resource tracking.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL