ReviewOS

zig-utils/zig-waf

[WAF-32] Publish a versioned C connector ABI

#33
Closed chris opened this 9 days ago ·
9 days ago

Objective

Expose opaque handles, version negotiation, transaction feed functions, interventions, callbacks, logging, feature discovery, allocator boundaries, and C/C++ conformance tests.

Deliverables

  • Implement the complete scoped behavior described above.
  • Keep request-path work independent from UI, PostgreSQL, and blocking audit I/O.
  • Use Zig 0.17-dev and dependencies pinned through Pantry; do not add Git submodules.
  • Keep public APIs explicit about ownership, resource limits, errors, security policy, and compatibility.
  • Add focused unit, integration, differential, fuzz, benchmark, and documentation coverage as applicable.

Acceptance criteria

  • The scoped implementation and tests are merged on main in small conventional commits.
  • Zig formatting, focused tests, and the relevant CI suite pass.
  • Compatibility claims have executable evidence and no unexplained skips or silent fallbacks.
  • Untrusted inputs have deterministic bounds; hot paths have benchmark evidence.
  • Documentation and the machine-readable compatibility matrix are updated.

Relationships

Blocked by

Blocks

Baselines

  • ModSecurity 3.0.16
  • Coraza 3.7.0
  • OWASP CRS 4.28.0
  • libinjection 4.0.0
  • Zig 0.17-dev resolved and locked through Pantry
9 days ago

Initial compiled ABI slice shipped in aec23b4: installed zig_waf.h plus libzig-waf.a, opaque WAF/transaction handles, sized/versioned config/features/intervention structs, feature bits, stable error codes, full transaction lifecycle wrappers, ownership enforcement, and a linked C-host smoke test. CI passed: https://github.com/zig-utils/zig-waf/actions/runs/29893682878. The issue remains open for callback tables, allocator hooks, dynamic-library/export validation, ABI snapshots, and malformed-call fuzzing.

9 days ago

The C connector ABI is now a complete seam for running traffic through the engine:

  • ede17af zig_waf_transaction_serialize_audit_log + zig_waf_free (all 4 audit formats)
  • 12b1798 zig_waf_transaction_evaluate_phase (autonomous per-phase rule evaluation)
  • 873d9a7 zig_waf_create_with_rules (compile a SecLang config and build a rules-loaded WAF)

The C smoke test drives the full flow: create_with_rules -> process the request -> evaluate_phase -> assert a 403 intervention, plus invalid-config rejection. This is everything a native data plane (the rpx dataplane) needs to link against. Remaining: callbacks and the ABI compatibility matrix.

9 days ago

Complete. include/zig_waf.h + src/c_api.zig, documented in docs/c-abi.md.

Versioned — ZIG_WAF_ABI_VERSION is major/minor, and the comparison rules a connector applies at load time are now written down rather than inferred from the header: same major and library minor ≥ connector minor is compatible, anything else is refused.

Opaque ownership and sized structs — a zig_waf_t outlives its transactions and refuses to be destroyed while any are alive; bytes the library returns are freed with zig_waf_free; bytes the caller passes are borrowed for the call and never retained; a compiled rule set is retained by the WAF, so the caller keeps no obligation after the call.

Struct-size negotiation, both directions — every struct starts with struct_size and abi_version and ends with reserved. A newer connector against an older library works, because the prefix the library reads is where it expects; an older connector against a newer library is refused rather than served, because its struct cannot contain the fields that would be read. A call whose struct is too small returns INVALID_ARGUMENT rather than filling in defaults — a connector that believes it configured a limit and did not is worse than one that fails to start.

Layout pinned by a test (1167a0b) — the version, the struct sizes, and the offsets a header edit could shift are asserted. Changing one without bumping the version would let an old connector read the wrong fields, which is the exact failure versioning exists to prevent and the one nothing else here would have caught. Adding a field inside reserved leaves the numbers untouched, which is what makes the additive path checkable rather than a convention.

Capability discovery — zig_waf_query_features reports feature bits and the highest supported phase, so a connector asks instead of assuming, and a build without a feature returns a clear bit rather than a stub that silently does nothing.

The functional surface — rule-set loading (including @pmFromFile/@ipMatchFromFile data files confined to a SecDataDir), the full transaction lifecycle, autonomous phase evaluation, intervention retrieval, and audit-log serialization. The C smoke test drives the whole flow, rules through to a 403.

Audit delivery is pull, not push — zig_waf_transaction_serialize_audit_log hands the record to the connector when it asks. A push callback would mean library code running on a connector's thread unasked, at a moment the connector did not choose; the queue that decouples production from delivery lives on the Zig side (#31), and a connector that wants that behaviour builds it with what it already has.

Not in the ABI, deliberately — anything that puts blocking work on the request path: no file opening, no sockets, no process execution, no script interpreter. A connector that needs those supplies them itself through a callback it controls, the same boundary plugin.Registry gives Zig hosts (#23).

Sign in to comment on this issue.