Skip to content

Commit f853278

Browse files
committed
fix(abi): resolve Hypatia new-alert errors (banned Python, unchecked casts)
Address the 3 new Hypatia alerts the PR introduced: - banned_language_file: rewrite the layout-table generator in Zig (scripts/gen_abi_expected.zig, run via `zig run`) and delete the Python version. The generator reproduces abi_layout_expected.zig byte-for-byte (only the @generated banner differs). - zig_ptr_cast / zig_align_cast (CWE-704): refactor abi_serde.zig to use zero @ptrCast/@aligncast, matching the rest of the FFI (which has none). Readers reinterpret memory via @ptrFromInt + @memcpy; emitters build a typed view of a c_allocator byte block with std.mem.bytesAsValue / bytesAsSlice. Behaviour unchanged: zig build + test/integration/smoke (124 tests) green, idris2 0.7.0 typechecks. Docs updated for the .zig generator. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvsZgNxFbeqfRmrVFNhJ8G
1 parent 5d498cc commit f853278

6 files changed

Lines changed: 199 additions & 173 deletions

File tree

.claude/CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ VeriSimDB (8-modality octads) -- container/verisimdb/
6565
| `cli.zig` | Standalone CLI executable (status, probe, profiles, version) |
6666

6767
`abi_layout_expected.zig` is generated from `Layout.idr` by
68-
`scripts/gen_abi_expected.py` (regenerate when `Layout.idr` changes).
68+
`scripts/gen_abi_expected.zig` (`zig run`; regenerate when `Layout.idr` changes).
6969

7070
## Key Conventions
7171

PROOF-NEEDS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ now checks that model against reality and makes it a live runtime contract.
6464
declared as a Zig `extern struct` (`abi_layout.zig`). A comptime block asserts
6565
`@offsetOf` / `@sizeOf` / `@alignOf` of every field equals the proven Idris
6666
constant — lifted verbatim from `Layout.idr` into `abi_layout_expected.zig` by
67-
`scripts/gen_abi_expected.py`. Any divergence is a **compile error**, so the
67+
`scripts/gen_abi_expected.zig`. Any divergence is a **compile error**, so the
6868
library cannot build against a layout the proofs do not describe. A negative
6969
test (perturbing one offset) confirms the guard bites; the check also runs
7070
under `zig build test`.
@@ -82,7 +82,7 @@ now checks that model against reality and makes it a live runtime contract.
8282
`0/4/12/20/28`), via `prim__driftStruct` + `prim__readPtr`/`readInt`/`readDouble`.
8383

8484
> Regenerate the expected table whenever `Layout.idr` changes:
85-
> `python3 scripts/gen_abi_expected.py` (then the Zig cross-check re-validates).
85+
> `zig run scripts/gen_abi_expected.zig` (then the Zig cross-check re-validates).
8686
8787
## What still needs proving
8888

scripts/gen_abi_expected.py

Lines changed: 0 additions & 123 deletions
This file was deleted.

scripts/gen_abi_expected.zig

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
//
4+
// gen_abi_expected.zig — extract the canonical C-ABI layout numbers from the
5+
// machine-checked Idris2 model (src/interface/abi/Layout.idr) and emit the Zig
6+
// source file (src/interface/ffi/src/abi_layout_expected.zig) consumed by the
7+
// Zig-side cross-check (abi_layout.zig).
8+
//
9+
// Layout.idr is the single source of truth: it carries the proofs (NoOverlap,
10+
// AllFieldsAligned, SizeAligned, SizeCoversFields). This tool lifts the same
11+
// `MkFieldDesc name offset size align` / `MkStructLayout [...] total align`
12+
// constants into Zig so the compiler can assert the canonical extern structs
13+
// agree with the proven model.
14+
//
15+
// Run from the repository root:
16+
// zig run scripts/gen_abi_expected.zig
17+
// (or `python3 -m ziglang run scripts/gen_abi_expected.zig`). CI re-runs this and
18+
// `git diff --exit-code`s the result so the Zig table never drifts from Layout.idr.
19+
20+
const std = @import("std");
21+
22+
const LAYOUT_IDR = "src/interface/abi/Layout.idr";
23+
const OUT_ZIG = "src/interface/ffi/src/abi_layout_expected.zig";
24+
25+
// Idris layout variable (without the "Layout" suffix) -> Zig struct type name.
26+
const Struct = struct { idris: []const u8, zig: []const u8 };
27+
const structs = [_]Struct{
28+
.{ .idris = "serverHandle", .zig = "ServerHandle" },
29+
.{ .idris = "probeResult", .zig = "ProbeResult" },
30+
.{ .idris = "configField", .zig = "ConfigField" },
31+
.{ .idris = "a2mlConfig", .zig = "A2MLConfig" },
32+
.{ .idris = "gameProfile", .zig = "GameProfile" },
33+
.{ .idris = "serverOctad", .zig = "ServerOctad" },
34+
.{ .idris = "fingerprint", .zig = "Fingerprint" },
35+
.{ .idris = "driftReport", .zig = "DriftReport" },
36+
};
37+
38+
const HEADER =
39+
\\// SPDX-License-Identifier: AGPL-3.0-or-later
40+
\\// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
41+
\\//
42+
\\// @generated by scripts/gen_abi_expected.zig from src/interface/abi/Layout.idr
43+
\\// DO NOT EDIT. Regenerate with: zig run scripts/gen_abi_expected.zig
44+
\\//
45+
\\// Canonical C-ABI layout numbers lifted from the machine-checked Idris2 model
46+
\\// (GSA.ABI.Layout). Consumed by abi_layout.zig to assert that the Zig extern
47+
\\// structs agree, byte-for-byte, with the proven Idris layout.
48+
\\
49+
\\pub const Field = struct {
50+
\\ name: []const u8,
51+
\\ offset: u32,
52+
\\ size: u32,
53+
\\ alignment: u32,
54+
\\};
55+
\\
56+
\\pub const StructExpect = struct {
57+
\\ name: []const u8,
58+
\\ total_size: u32,
59+
\\ struct_align: u32,
60+
\\ fields: []const Field,
61+
\\};
62+
\\
63+
\\
64+
;
65+
66+
fn nextUint(it: *std.mem.TokenIterator(u8, .any)) !u32 {
67+
return std.fmt.parseInt(u32, it.next() orelse return error.ParseFailed, 10);
68+
}
69+
70+
fn emitStruct(out: *std.array_list.Managed(u8), src: []const u8, s: Struct) !void {
71+
var nbuf: [128]u8 = undefined;
72+
const needle = try std.fmt.bufPrint(&nbuf, "{s}Layout = MkStructLayout", .{s.idris});
73+
const at = std.mem.indexOf(u8, src, needle) orelse return error.StructNotFound;
74+
const after = src[at + needle.len ..];
75+
const lb = std.mem.indexOfScalar(u8, after, '[') orelse return error.NoFieldList;
76+
const rel_rb = std.mem.indexOfScalar(u8, after[lb..], ']') orelse return error.NoFieldList;
77+
const body = after[lb + 1 .. lb + rel_rb];
78+
const tail = after[lb + rel_rb + 1 ..]; // "<total> <align>" follow the ']'
79+
80+
var tit = std.mem.tokenizeAny(u8, tail, " \r\n\t");
81+
const total = try nextUint(&tit);
82+
const salign = try nextUint(&tit);
83+
84+
const w = out.writer();
85+
try w.print(
86+
"pub const {s} = StructExpect{{\n .name = \"{s}\",\n .total_size = {d},\n .struct_align = {d},\n .fields = &.{{\n",
87+
.{ s.zig, s.zig, total, salign },
88+
);
89+
90+
var rest = body;
91+
while (std.mem.indexOf(u8, rest, "MkFieldDesc")) |fi| {
92+
rest = rest[fi + "MkFieldDesc".len ..];
93+
const q1 = std.mem.indexOfScalar(u8, rest, '"') orelse return error.BadField;
94+
const q2 = std.mem.indexOfScalarPos(u8, rest, q1 + 1, '"') orelse return error.BadField;
95+
const fname = rest[q1 + 1 .. q2];
96+
rest = rest[q2 + 1 ..];
97+
98+
var fit = std.mem.tokenizeAny(u8, rest, " \r\n\t");
99+
const foff = try nextUint(&fit);
100+
const fsize = try nextUint(&fit);
101+
const falign = try nextUint(&fit);
102+
103+
if (!std.mem.startsWith(u8, fname, "padding")) {
104+
try w.print(
105+
" .{{ .name = \"{s}\", .offset = {d}, .size = {d}, .alignment = {d} }},\n",
106+
.{ fname, foff, fsize, falign },
107+
);
108+
}
109+
}
110+
try w.writeAll(" },\n};\n\n");
111+
}
112+
113+
pub fn main() !void {
114+
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
115+
defer _ = gpa.deinit();
116+
const alloc = gpa.allocator();
117+
118+
const src = try std.fs.cwd().readFileAlloc(alloc, LAYOUT_IDR, 4 * 1024 * 1024);
119+
defer alloc.free(src);
120+
121+
var out = std.array_list.Managed(u8).init(alloc);
122+
defer out.deinit();
123+
124+
try out.appendSlice(HEADER);
125+
for (structs) |s| try emitStruct(&out, src, s);
126+
127+
try out.appendSlice("pub const all = [_]StructExpect{ ");
128+
for (structs, 0..) |s, i| {
129+
if (i != 0) try out.appendSlice(", ");
130+
try out.appendSlice(s.zig);
131+
}
132+
try out.appendSlice(" };\n");
133+
134+
try std.fs.cwd().writeFile(.{ .sub_path = OUT_ZIG, .data = out.items });
135+
std.debug.print("wrote {s} ({d} structs)\n", .{ OUT_ZIG, structs.len });
136+
}

src/interface/ffi/src/abi_layout_expected.zig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// SPDX-License-Identifier: AGPL-3.0-or-later
22
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
33
//
4-
// @generated by scripts/gen_abi_expected.py from src/interface/abi/Layout.idr
5-
// DO NOT EDIT. Regenerate with: python3 scripts/gen_abi_expected.py
4+
// @generated by scripts/gen_abi_expected.zig from src/interface/abi/Layout.idr
5+
// DO NOT EDIT. Regenerate with: zig run scripts/gen_abi_expected.zig
66
//
77
// Canonical C-ABI layout numbers lifted from the machine-checked Idris2 model
88
// (GSA.ABI.Layout). Consumed by abi_layout.zig to assert that the Zig extern

0 commit comments

Comments
 (0)