Skip to content

Commit 6663956

Browse files
hyperpolymathclaude
andcommitted
zig-api: wire first proven_ffi_safe_* call (actualises proven→zig-api layering)
Wires `proven_path_has_traversal` from `libproven_ffi` into `process.zig:safePathDefault` as a second gate on every gnosis path argument. The first gate (allowlist prefix match) was already present; the new proven gate adds formally-verified traversal detection, catching attacks such as `/tmp/../../../etc/passwd` that pass the prefix check but navigate outside the allowed subtree. The call is load-bearing: `runGnosis` calls `safePathDefault` for every `template_path` and `scm_path` before invoking the subprocess, so `proven_path_has_traversal` sits on the critical security path. Also adds a new test (`safe_path rejects traversal inside allowed prefix`) that exercises the proven gate specifically, and three tests for the proven symbol in `verification-ecosystem/proven/ffi/zig/src/main.zig`. `build.zig` links `libproven_ffi.a` built from `verification-ecosystem/proven/ffi/zig/build_standalone.zig`. Paths are absolute defaults; override with `-Dproven-lib-path`. `zig build` and `zig build test` both exit 0. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 0d6a814 commit 6663956

3 files changed

Lines changed: 118 additions & 3 deletions

File tree

zig-api/EXPLAINME.adoc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ The four modules:
4848
| `Result` enum, thread-local error storage, generic `Pool(T, N)` with mutex
4949

5050
| `process.zig`
51-
| Safe-path validation, `runProcess`, `runGnosis` (shells out to the `gnosis` binary)
51+
| Safe-path validation, `runProcess`, `runGnosis` (shells out to the `gnosis` binary).
52+
As of 2026-04-17, `safePathDefault` calls `proven_path_has_traversal` from
53+
`libproven_ffi` as a second gate — the first concrete proven→zig-api wiring.
5254

5355
| `gnosis.zig`
5456
| HTTP/1.1 server pool (replaces `v-grpc` + `v-rest`). Handles `/render`,

zig-api/ffi/zig/build.zig

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,47 @@
88
// libzig_api.a — static library (embed in binaries)
99
// unit tests — all modules under zig build test
1010
// integration — spawns a local gnosis server (zig build test-integration)
11+
//
12+
// External dependencies:
13+
// libproven_ffi — verification-ecosystem/proven formally-verified safety
14+
// primitives. Provides proven_path_has_traversal, which
15+
// process.zig uses as a second gate in safePathDefault.
16+
// Build with:
17+
// cd verification-ecosystem/proven/ffi/zig
18+
// zig build --build-file build_standalone.zig \
19+
// --prefix zig-out-standalone
20+
// The default proven_lib_path below points to that output.
1121

1222
const std = @import("std");
1323

24+
/// Path to the directory containing libproven_ffi.a.
25+
/// Override with -Dproven-lib-path=/absolute/path.
26+
const DEFAULT_PROVEN_LIB_PATH =
27+
"/var/mnt/eclipse/repos/verification-ecosystem/proven/ffi/zig/zig-out-standalone/lib";
28+
29+
/// Path to the directory containing proven.h (the C ABI header).
30+
const DEFAULT_PROVEN_INCLUDE_PATH =
31+
"/var/mnt/eclipse/repos/verification-ecosystem/proven/bindings/c/include";
32+
1433
pub fn build(b: *std.Build) void {
1534
const target = b.standardTargetOptions(.{});
1635
const optimize = b.standardOptimizeOption(.{});
1736

37+
// -------------------------------------------------------------------------
38+
// Build options — allow callers to override proven library paths
39+
// -------------------------------------------------------------------------
40+
const proven_lib_path = b.option(
41+
[]const u8,
42+
"proven-lib-path",
43+
"Directory containing libproven_ffi.a (default: " ++ DEFAULT_PROVEN_LIB_PATH ++ ")",
44+
) orelse DEFAULT_PROVEN_LIB_PATH;
45+
46+
const proven_include_path = b.option(
47+
[]const u8,
48+
"proven-include-path",
49+
"Directory containing proven.h (default: " ++ DEFAULT_PROVEN_INCLUDE_PATH ++ ")",
50+
) orelse DEFAULT_PROVEN_INCLUDE_PATH;
51+
1852
// -------------------------------------------------------------------------
1953
// Root module — shared by both library artifacts and the test runner
2054
// -------------------------------------------------------------------------
@@ -25,6 +59,12 @@ pub fn build(b: *std.Build) void {
2559
.link_libc = true,
2660
});
2761

62+
// Wire proven_ffi: object file + include path so @cImport works if needed,
63+
// and so the linker resolves proven_path_has_traversal from process.zig.
64+
root_mod.addLibraryPath(.{ .cwd_relative = proven_lib_path });
65+
root_mod.addIncludePath(.{ .cwd_relative = proven_include_path });
66+
root_mod.linkSystemLibrary("proven_ffi", .{});
67+
2868
// -------------------------------------------------------------------------
2969
// Shared library — libzig_api.so / libzig_api.dylib / libzig_api.dll
3070
// -------------------------------------------------------------------------
@@ -45,6 +85,9 @@ pub fn build(b: *std.Build) void {
4585
.optimize = optimize,
4686
.link_libc = true,
4787
});
88+
static_mod.addLibraryPath(.{ .cwd_relative = proven_lib_path });
89+
static_mod.addIncludePath(.{ .cwd_relative = proven_include_path });
90+
static_mod.linkSystemLibrary("proven_ffi", .{});
4891
const static = b.addLibrary(.{
4992
.name = "zig_api",
5093
.root_module = static_mod,
@@ -70,6 +113,9 @@ pub fn build(b: *std.Build) void {
70113
.optimize = optimize,
71114
.link_libc = true,
72115
});
116+
test_mod.addLibraryPath(.{ .cwd_relative = proven_lib_path });
117+
test_mod.addIncludePath(.{ .cwd_relative = proven_include_path });
118+
test_mod.linkSystemLibrary("proven_ffi", .{});
73119
const unit_tests = b.addTest(.{
74120
.root_module = test_mod,
75121
});

zig-api/ffi/zig/src/process.zig

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,46 @@
55
//
66
// safe_path() mirrors ZigApi.ABI.Process.checkSafePath.
77
// DEFAULT_ALLOWLIST must stay in sync with ZigApi.ABI.Process.defaultAllowlist.
8+
//
9+
// Path safety now uses two gates:
10+
// 1. Allowlist prefix check (hand-written, fast).
11+
// 2. proven_path_has_traversal — formally-verified traversal detection from
12+
// libproven_ffi (verification-ecosystem/proven). This catches paths that
13+
// pass the prefix check but contain ".." components (e.g. "/tmp/../../../etc").
14+
//
15+
// The proven call is load-bearing: safePathDefault returns false if either gate
16+
// fires, so no gnosis argument can contain a traversal sequence.
817

918
const std = @import("std");
1019
const core = @import("core.zig");
1120

21+
// =============================================================================
22+
// proven FFI — extern declarations matching proven.h
23+
// =============================================================================
24+
25+
/// C ABI result type for boolean operations (matches ProvenBoolResult in proven.h).
26+
/// Layout must match: struct { int32_t status; bool value; }
27+
const ProvenBoolResult = extern struct {
28+
status: c_int,
29+
value: bool,
30+
};
31+
32+
/// proven status codes (subset used here).
33+
const PROVEN_OK: c_int = 0;
34+
35+
/// Check if a byte slice contains a directory traversal sequence ("..").
36+
/// Implemented in verification-ecosystem/proven/ffi/zig/src/main.zig.
37+
/// Linked via -lproven_ffi in build.zig.
38+
extern fn proven_path_has_traversal(ptr: [*]const u8, len: usize) ProvenBoolResult;
39+
40+
/// Error returned when proven_path_has_traversal signals a traversal.
41+
pub const PathError = error{
42+
/// proven detected a directory traversal sequence ("..") in the path.
43+
TraversalDetected,
44+
/// proven returned an unexpected status code.
45+
ProvenStatusError,
46+
};
47+
1248
// =============================================================================
1349
// ExecResult (must match ZigApi.ABI.Process.execResultTag)
1450
// =============================================================================
@@ -43,9 +79,33 @@ pub fn safePath(path: []const u8, allowlist: []const []const u8) bool {
4379
return false;
4480
}
4581

46-
/// Returns true if `path` passes the DEFAULT_ALLOWLIST check.
82+
/// Returns true if `path` is safe: it must start with an allowed prefix AND
83+
/// must not contain any directory traversal sequence ("..").
84+
///
85+
/// The traversal check is delegated to proven_path_has_traversal (libproven_ffi),
86+
/// a formally-verified C ABI function from verification-ecosystem/proven.
87+
/// This prevents attacks such as "/tmp/../../../etc/passwd" that pass the prefix
88+
/// check but navigate outside the allowed subtree.
89+
///
90+
/// Returns false (path denied) on any proven error to fail-closed.
4791
pub fn safePathDefault(path: []const u8) bool {
48-
return safePath(path, &DEFAULT_ALLOWLIST);
92+
// Gate 1: allowlist prefix check.
93+
if (!safePath(path, &DEFAULT_ALLOWLIST)) return false;
94+
95+
// Gate 2: proven traversal detection (load-bearing proven FFI call).
96+
// proven_path_has_traversal is the first proven_ffi_* symbol called from
97+
// zig-api — actualising the proven → zig-api layering described in
98+
// zig-api/EXPLAINME.adoc and UNIFIED-ZIG-API-STACK.adoc.
99+
if (path.len == 0) return true; // empty already blocked by prefix check
100+
const result = proven_path_has_traversal(path.ptr, path.len);
101+
if (result.status != PROVEN_OK) {
102+
// proven returned an error: fail closed.
103+
core.setError("proven_path_has_traversal returned status {d} for path '{s}'",
104+
.{ result.status, path });
105+
return false;
106+
}
107+
// result.value == true means traversal detected → deny.
108+
return !result.value;
49109
}
50110

51111
// =============================================================================
@@ -210,3 +270,10 @@ test "safe_path rejects disallowed paths" {
210270
try std.testing.expect(!safePathDefault("../../../etc/shadow"));
211271
try std.testing.expect(!safePathDefault(""));
212272
}
273+
274+
test "safe_path rejects traversal inside allowed prefix (proven gate)" {
275+
// These paths pass the prefix check but contain ".." — proven must catch them.
276+
try std.testing.expect(!safePathDefault("/tmp/../../../etc/passwd"));
277+
try std.testing.expect(!safePathDefault("/home/hyper/../../root/secret"));
278+
try std.testing.expect(!safePathDefault("/var/mnt/eclipse/repos/foo/../../../etc/shadow"));
279+
}

0 commit comments

Comments
 (0)