Skip to content

Commit 47f1ac3

Browse files
fix(cartridges): drop unchecked @ptrCast from all cartridge shims (CWE-704) (#89)
Closes the Hypatia `code_safety/zig_ptr_cast` alert ([#550](https://github.com/hyperpolymath/boj-server-cartridges/security/code-scanning/550)) raised on #85, which reported 8 unchecked pointer conversions (CWE-704) in `bug-filing-mcp/ffi/cartridge_shim.zig`. #85 merged before the alert was addressed, so the flagged code is now on `main` and the alert is still open. This fixes it there. ## Why 115 files `cartridge_shim.zig` is vendored byte-identical into 116 cartridges, so the same latent flaw sat in every copy — the alert only named `bug-filing-mcp` because that was the file #85 touched. Each other copy would raise the identical alert as soon as a PR touched it. Fixing them together also stops the vendored copies drifting apart. ## The fix The 8 casts were **redundant, not unsafe** — no behaviour change: - `std.mem.sliceTo(ptr, 0)` accepts a `[*c]const u8` directly and scans to the NUL, so neither the `@ptrCast` nor the `[*:0]` re-typing was ever needed. - Typing the test locals as `[*c]const u8` removes the cast at the call sites. The non-null precondition documented on `toolIs` is unchanged and still enforced by `invokeArgsNull`. `local-coord-mcp` **already carried exactly this fix** (post-#146). This adopts that as the canonical form rather than inventing a second one, which collapses the shim from three divergent variants to two. `npc-mcp` keeps its trimmed layout and takes the same transform. ## Verification Run against the **CI toolchain (zig 0.15.2)**, not my local default: - All **116/116** shims pass `zig test`. - **115/116** cartridges `zig build` clean. `database-mcp` fails only on a missing local `libsqlite3` — it fails identically on pristine `main`, so it is environmental and unrelated to this change. - No `@ptrCast` remains in any shim. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d1454f7 commit 47f1ac3

115 files changed

Lines changed: 1837 additions & 1149 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cartridges/cross-cutting/agentic/agent-mcp/ffi/cartridge_shim.zig

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,14 @@ pub fn invokeArgsNull(
5858
/// Compare a C-NUL-terminated tool-name pointer against a Zig string
5959
/// literal. Caller must have already verified `tool_name` is non-null
6060
/// (usually via `invokeArgsNull`).
61+
///
62+
/// Implementation note (CWE-704 fix, post-#146): uses
63+
/// `std.mem.sliceTo(ptr, 0)` which scans the C string up to the first
64+
/// NUL — no `@ptrCast` and no `[*:0]` re-typing. The earlier
65+
/// `std.mem.spanZ` call was removed in Zig 0.14+ and would not
66+
/// compile under the 0.15.1 CI pin.
6167
pub fn toolIs(tool_name: [*c]const u8, expected: []const u8) bool {
62-
const s = std.mem.span(@as([*:0]const u8, @ptrCast(tool_name)));
68+
const s = std.mem.sliceTo(tool_name, 0);
6369
return std.mem.eql(u8, s, expected);
6470
}
6571

@@ -124,19 +130,19 @@ test "writeResult: empty body" {
124130
}
125131

126132
test "toolIs: matches and rejects" {
127-
const name: [*:0]const u8 = "foo";
128-
try std.testing.expect(toolIs(@ptrCast(name), "foo"));
129-
try std.testing.expect(!toolIs(@ptrCast(name), "bar"));
130-
try std.testing.expect(!toolIs(@ptrCast(name), "foobar"));
131-
try std.testing.expect(!toolIs(@ptrCast(name), "fo"));
133+
const name: [*c]const u8 = "foo";
134+
try std.testing.expect(toolIs(name, "foo"));
135+
try std.testing.expect(!toolIs(name, "bar"));
136+
try std.testing.expect(!toolIs(name, "foobar"));
137+
try std.testing.expect(!toolIs(name, "fo"));
132138
}
133139

134140
test "invokeArgsNull: detects each null slot" {
135141
var buf: [4]u8 = undefined;
136142
var len: usize = 4;
137-
const name: [*:0]const u8 = "x";
138-
try std.testing.expect(!invokeArgsNull(@ptrCast(name), &buf, &len));
143+
const name: [*c]const u8 = "x";
144+
try std.testing.expect(!invokeArgsNull(name, &buf, &len));
139145
try std.testing.expect(invokeArgsNull(null, &buf, &len));
140-
try std.testing.expect(invokeArgsNull(@ptrCast(name), null, &len));
141-
try std.testing.expect(invokeArgsNull(@ptrCast(name), &buf, null));
146+
try std.testing.expect(invokeArgsNull(name, null, &len));
147+
try std.testing.expect(invokeArgsNull(name, &buf, null));
142148
}

cartridges/cross-cutting/agentic/claude-agents-power-mcp/ffi/cartridge_shim.zig

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,14 @@ pub fn invokeArgsNull(
5858
/// Compare a C-NUL-terminated tool-name pointer against a Zig string
5959
/// literal. Caller must have already verified `tool_name` is non-null
6060
/// (usually via `invokeArgsNull`).
61+
///
62+
/// Implementation note (CWE-704 fix, post-#146): uses
63+
/// `std.mem.sliceTo(ptr, 0)` which scans the C string up to the first
64+
/// NUL — no `@ptrCast` and no `[*:0]` re-typing. The earlier
65+
/// `std.mem.spanZ` call was removed in Zig 0.14+ and would not
66+
/// compile under the 0.15.1 CI pin.
6167
pub fn toolIs(tool_name: [*c]const u8, expected: []const u8) bool {
62-
const s = std.mem.span(@as([*:0]const u8, @ptrCast(tool_name)));
68+
const s = std.mem.sliceTo(tool_name, 0);
6369
return std.mem.eql(u8, s, expected);
6470
}
6571

@@ -124,19 +130,19 @@ test "writeResult: empty body" {
124130
}
125131

126132
test "toolIs: matches and rejects" {
127-
const name: [*:0]const u8 = "foo";
128-
try std.testing.expect(toolIs(@ptrCast(name), "foo"));
129-
try std.testing.expect(!toolIs(@ptrCast(name), "bar"));
130-
try std.testing.expect(!toolIs(@ptrCast(name), "foobar"));
131-
try std.testing.expect(!toolIs(@ptrCast(name), "fo"));
133+
const name: [*c]const u8 = "foo";
134+
try std.testing.expect(toolIs(name, "foo"));
135+
try std.testing.expect(!toolIs(name, "bar"));
136+
try std.testing.expect(!toolIs(name, "foobar"));
137+
try std.testing.expect(!toolIs(name, "fo"));
132138
}
133139

134140
test "invokeArgsNull: detects each null slot" {
135141
var buf: [4]u8 = undefined;
136142
var len: usize = 4;
137-
const name: [*:0]const u8 = "x";
138-
try std.testing.expect(!invokeArgsNull(@ptrCast(name), &buf, &len));
143+
const name: [*c]const u8 = "x";
144+
try std.testing.expect(!invokeArgsNull(name, &buf, &len));
139145
try std.testing.expect(invokeArgsNull(null, &buf, &len));
140-
try std.testing.expect(invokeArgsNull(@ptrCast(name), null, &len));
141-
try std.testing.expect(invokeArgsNull(@ptrCast(name), &buf, null));
146+
try std.testing.expect(invokeArgsNull(name, null, &len));
147+
try std.testing.expect(invokeArgsNull(name, &buf, null));
142148
}

cartridges/cross-cutting/agentic/claude-ai-mcp/ffi/cartridge_shim.zig

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,14 @@ pub fn invokeArgsNull(
5858
/// Compare a C-NUL-terminated tool-name pointer against a Zig string
5959
/// literal. Caller must have already verified `tool_name` is non-null
6060
/// (usually via `invokeArgsNull`).
61+
///
62+
/// Implementation note (CWE-704 fix, post-#146): uses
63+
/// `std.mem.sliceTo(ptr, 0)` which scans the C string up to the first
64+
/// NUL — no `@ptrCast` and no `[*:0]` re-typing. The earlier
65+
/// `std.mem.spanZ` call was removed in Zig 0.14+ and would not
66+
/// compile under the 0.15.1 CI pin.
6167
pub fn toolIs(tool_name: [*c]const u8, expected: []const u8) bool {
62-
const s = std.mem.span(@as([*:0]const u8, @ptrCast(tool_name)));
68+
const s = std.mem.sliceTo(tool_name, 0);
6369
return std.mem.eql(u8, s, expected);
6470
}
6571

@@ -124,19 +130,19 @@ test "writeResult: empty body" {
124130
}
125131

126132
test "toolIs: matches and rejects" {
127-
const name: [*:0]const u8 = "foo";
128-
try std.testing.expect(toolIs(@ptrCast(name), "foo"));
129-
try std.testing.expect(!toolIs(@ptrCast(name), "bar"));
130-
try std.testing.expect(!toolIs(@ptrCast(name), "foobar"));
131-
try std.testing.expect(!toolIs(@ptrCast(name), "fo"));
133+
const name: [*c]const u8 = "foo";
134+
try std.testing.expect(toolIs(name, "foo"));
135+
try std.testing.expect(!toolIs(name, "bar"));
136+
try std.testing.expect(!toolIs(name, "foobar"));
137+
try std.testing.expect(!toolIs(name, "fo"));
132138
}
133139

134140
test "invokeArgsNull: detects each null slot" {
135141
var buf: [4]u8 = undefined;
136142
var len: usize = 4;
137-
const name: [*:0]const u8 = "x";
138-
try std.testing.expect(!invokeArgsNull(@ptrCast(name), &buf, &len));
143+
const name: [*c]const u8 = "x";
144+
try std.testing.expect(!invokeArgsNull(name, &buf, &len));
139145
try std.testing.expect(invokeArgsNull(null, &buf, &len));
140-
try std.testing.expect(invokeArgsNull(@ptrCast(name), null, &len));
141-
try std.testing.expect(invokeArgsNull(@ptrCast(name), &buf, null));
146+
try std.testing.expect(invokeArgsNull(name, null, &len));
147+
try std.testing.expect(invokeArgsNull(name, &buf, null));
142148
}

cartridges/cross-cutting/agentic/model-router-mcp/ffi/cartridge_shim.zig

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,14 @@ pub fn invokeArgsNull(
5858
/// Compare a C-NUL-terminated tool-name pointer against a Zig string
5959
/// literal. Caller must have already verified `tool_name` is non-null
6060
/// (usually via `invokeArgsNull`).
61+
///
62+
/// Implementation note (CWE-704 fix, post-#146): uses
63+
/// `std.mem.sliceTo(ptr, 0)` which scans the C string up to the first
64+
/// NUL — no `@ptrCast` and no `[*:0]` re-typing. The earlier
65+
/// `std.mem.spanZ` call was removed in Zig 0.14+ and would not
66+
/// compile under the 0.15.1 CI pin.
6167
pub fn toolIs(tool_name: [*c]const u8, expected: []const u8) bool {
62-
const s = std.mem.span(@as([*:0]const u8, @ptrCast(tool_name)));
68+
const s = std.mem.sliceTo(tool_name, 0);
6369
return std.mem.eql(u8, s, expected);
6470
}
6571

@@ -124,19 +130,19 @@ test "writeResult: empty body" {
124130
}
125131

126132
test "toolIs: matches and rejects" {
127-
const name: [*:0]const u8 = "foo";
128-
try std.testing.expect(toolIs(@ptrCast(name), "foo"));
129-
try std.testing.expect(!toolIs(@ptrCast(name), "bar"));
130-
try std.testing.expect(!toolIs(@ptrCast(name), "foobar"));
131-
try std.testing.expect(!toolIs(@ptrCast(name), "fo"));
133+
const name: [*c]const u8 = "foo";
134+
try std.testing.expect(toolIs(name, "foo"));
135+
try std.testing.expect(!toolIs(name, "bar"));
136+
try std.testing.expect(!toolIs(name, "foobar"));
137+
try std.testing.expect(!toolIs(name, "fo"));
132138
}
133139

134140
test "invokeArgsNull: detects each null slot" {
135141
var buf: [4]u8 = undefined;
136142
var len: usize = 4;
137-
const name: [*:0]const u8 = "x";
138-
try std.testing.expect(!invokeArgsNull(@ptrCast(name), &buf, &len));
143+
const name: [*c]const u8 = "x";
144+
try std.testing.expect(!invokeArgsNull(name, &buf, &len));
139145
try std.testing.expect(invokeArgsNull(null, &buf, &len));
140-
try std.testing.expect(invokeArgsNull(@ptrCast(name), null, &len));
141-
try std.testing.expect(invokeArgsNull(@ptrCast(name), &buf, null));
146+
try std.testing.expect(invokeArgsNull(name, null, &len));
147+
try std.testing.expect(invokeArgsNull(name, &buf, null));
142148
}

cartridges/cross-cutting/build/bsp-mcp/ffi/cartridge_shim.zig

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,14 @@ pub fn invokeArgsNull(
5858
/// Compare a C-NUL-terminated tool-name pointer against a Zig string
5959
/// literal. Caller must have already verified `tool_name` is non-null
6060
/// (usually via `invokeArgsNull`).
61+
///
62+
/// Implementation note (CWE-704 fix, post-#146): uses
63+
/// `std.mem.sliceTo(ptr, 0)` which scans the C string up to the first
64+
/// NUL — no `@ptrCast` and no `[*:0]` re-typing. The earlier
65+
/// `std.mem.spanZ` call was removed in Zig 0.14+ and would not
66+
/// compile under the 0.15.1 CI pin.
6167
pub fn toolIs(tool_name: [*c]const u8, expected: []const u8) bool {
62-
const s = std.mem.span(@as([*:0]const u8, @ptrCast(tool_name)));
68+
const s = std.mem.sliceTo(tool_name, 0);
6369
return std.mem.eql(u8, s, expected);
6470
}
6571

@@ -124,19 +130,19 @@ test "writeResult: empty body" {
124130
}
125131

126132
test "toolIs: matches and rejects" {
127-
const name: [*:0]const u8 = "foo";
128-
try std.testing.expect(toolIs(@ptrCast(name), "foo"));
129-
try std.testing.expect(!toolIs(@ptrCast(name), "bar"));
130-
try std.testing.expect(!toolIs(@ptrCast(name), "foobar"));
131-
try std.testing.expect(!toolIs(@ptrCast(name), "fo"));
133+
const name: [*c]const u8 = "foo";
134+
try std.testing.expect(toolIs(name, "foo"));
135+
try std.testing.expect(!toolIs(name, "bar"));
136+
try std.testing.expect(!toolIs(name, "foobar"));
137+
try std.testing.expect(!toolIs(name, "fo"));
132138
}
133139

134140
test "invokeArgsNull: detects each null slot" {
135141
var buf: [4]u8 = undefined;
136142
var len: usize = 4;
137-
const name: [*:0]const u8 = "x";
138-
try std.testing.expect(!invokeArgsNull(@ptrCast(name), &buf, &len));
143+
const name: [*c]const u8 = "x";
144+
try std.testing.expect(!invokeArgsNull(name, &buf, &len));
139145
try std.testing.expect(invokeArgsNull(null, &buf, &len));
140-
try std.testing.expect(invokeArgsNull(@ptrCast(name), null, &len));
141-
try std.testing.expect(invokeArgsNull(@ptrCast(name), &buf, null));
146+
try std.testing.expect(invokeArgsNull(name, null, &len));
147+
try std.testing.expect(invokeArgsNull(name, &buf, null));
142148
}

cartridges/cross-cutting/debug/dap-mcp/ffi/cartridge_shim.zig

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,14 @@ pub fn invokeArgsNull(
5858
/// Compare a C-NUL-terminated tool-name pointer against a Zig string
5959
/// literal. Caller must have already verified `tool_name` is non-null
6060
/// (usually via `invokeArgsNull`).
61+
///
62+
/// Implementation note (CWE-704 fix, post-#146): uses
63+
/// `std.mem.sliceTo(ptr, 0)` which scans the C string up to the first
64+
/// NUL — no `@ptrCast` and no `[*:0]` re-typing. The earlier
65+
/// `std.mem.spanZ` call was removed in Zig 0.14+ and would not
66+
/// compile under the 0.15.1 CI pin.
6167
pub fn toolIs(tool_name: [*c]const u8, expected: []const u8) bool {
62-
const s = std.mem.span(@as([*:0]const u8, @ptrCast(tool_name)));
68+
const s = std.mem.sliceTo(tool_name, 0);
6369
return std.mem.eql(u8, s, expected);
6470
}
6571

@@ -124,19 +130,19 @@ test "writeResult: empty body" {
124130
}
125131

126132
test "toolIs: matches and rejects" {
127-
const name: [*:0]const u8 = "foo";
128-
try std.testing.expect(toolIs(@ptrCast(name), "foo"));
129-
try std.testing.expect(!toolIs(@ptrCast(name), "bar"));
130-
try std.testing.expect(!toolIs(@ptrCast(name), "foobar"));
131-
try std.testing.expect(!toolIs(@ptrCast(name), "fo"));
133+
const name: [*c]const u8 = "foo";
134+
try std.testing.expect(toolIs(name, "foo"));
135+
try std.testing.expect(!toolIs(name, "bar"));
136+
try std.testing.expect(!toolIs(name, "foobar"));
137+
try std.testing.expect(!toolIs(name, "fo"));
132138
}
133139

134140
test "invokeArgsNull: detects each null slot" {
135141
var buf: [4]u8 = undefined;
136142
var len: usize = 4;
137-
const name: [*:0]const u8 = "x";
138-
try std.testing.expect(!invokeArgsNull(@ptrCast(name), &buf, &len));
143+
const name: [*c]const u8 = "x";
144+
try std.testing.expect(!invokeArgsNull(name, &buf, &len));
139145
try std.testing.expect(invokeArgsNull(null, &buf, &len));
140-
try std.testing.expect(invokeArgsNull(@ptrCast(name), null, &len));
141-
try std.testing.expect(invokeArgsNull(@ptrCast(name), &buf, null));
146+
try std.testing.expect(invokeArgsNull(name, null, &len));
147+
try std.testing.expect(invokeArgsNull(name, &buf, null));
142148
}

cartridges/cross-cutting/fleet/fleet-mcp/ffi/cartridge_shim.zig

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,14 @@ pub fn invokeArgsNull(
5858
/// Compare a C-NUL-terminated tool-name pointer against a Zig string
5959
/// literal. Caller must have already verified `tool_name` is non-null
6060
/// (usually via `invokeArgsNull`).
61+
///
62+
/// Implementation note (CWE-704 fix, post-#146): uses
63+
/// `std.mem.sliceTo(ptr, 0)` which scans the C string up to the first
64+
/// NUL — no `@ptrCast` and no `[*:0]` re-typing. The earlier
65+
/// `std.mem.spanZ` call was removed in Zig 0.14+ and would not
66+
/// compile under the 0.15.1 CI pin.
6167
pub fn toolIs(tool_name: [*c]const u8, expected: []const u8) bool {
62-
const s = std.mem.span(@as([*:0]const u8, @ptrCast(tool_name)));
68+
const s = std.mem.sliceTo(tool_name, 0);
6369
return std.mem.eql(u8, s, expected);
6470
}
6571

@@ -124,19 +130,19 @@ test "writeResult: empty body" {
124130
}
125131

126132
test "toolIs: matches and rejects" {
127-
const name: [*:0]const u8 = "foo";
128-
try std.testing.expect(toolIs(@ptrCast(name), "foo"));
129-
try std.testing.expect(!toolIs(@ptrCast(name), "bar"));
130-
try std.testing.expect(!toolIs(@ptrCast(name), "foobar"));
131-
try std.testing.expect(!toolIs(@ptrCast(name), "fo"));
133+
const name: [*c]const u8 = "foo";
134+
try std.testing.expect(toolIs(name, "foo"));
135+
try std.testing.expect(!toolIs(name, "bar"));
136+
try std.testing.expect(!toolIs(name, "foobar"));
137+
try std.testing.expect(!toolIs(name, "fo"));
132138
}
133139

134140
test "invokeArgsNull: detects each null slot" {
135141
var buf: [4]u8 = undefined;
136142
var len: usize = 4;
137-
const name: [*:0]const u8 = "x";
138-
try std.testing.expect(!invokeArgsNull(@ptrCast(name), &buf, &len));
143+
const name: [*c]const u8 = "x";
144+
try std.testing.expect(!invokeArgsNull(name, &buf, &len));
139145
try std.testing.expect(invokeArgsNull(null, &buf, &len));
140-
try std.testing.expect(invokeArgsNull(@ptrCast(name), null, &len));
141-
try std.testing.expect(invokeArgsNull(@ptrCast(name), &buf, null));
146+
try std.testing.expect(invokeArgsNull(name, null, &len));
147+
try std.testing.expect(invokeArgsNull(name, &buf, null));
142148
}

cartridges/cross-cutting/health/boj-health-mcp/ffi/cartridge_shim.zig

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,14 @@ pub fn invokeArgsNull(
5858
/// Compare a C-NUL-terminated tool-name pointer against a Zig string
5959
/// literal. Caller must have already verified `tool_name` is non-null
6060
/// (usually via `invokeArgsNull`).
61+
///
62+
/// Implementation note (CWE-704 fix, post-#146): uses
63+
/// `std.mem.sliceTo(ptr, 0)` which scans the C string up to the first
64+
/// NUL — no `@ptrCast` and no `[*:0]` re-typing. The earlier
65+
/// `std.mem.spanZ` call was removed in Zig 0.14+ and would not
66+
/// compile under the 0.15.1 CI pin.
6167
pub fn toolIs(tool_name: [*c]const u8, expected: []const u8) bool {
62-
const s = std.mem.span(@as([*:0]const u8, @ptrCast(tool_name)));
68+
const s = std.mem.sliceTo(tool_name, 0);
6369
return std.mem.eql(u8, s, expected);
6470
}
6571

@@ -124,19 +130,19 @@ test "writeResult: empty body" {
124130
}
125131

126132
test "toolIs: matches and rejects" {
127-
const name: [*:0]const u8 = "foo";
128-
try std.testing.expect(toolIs(@ptrCast(name), "foo"));
129-
try std.testing.expect(!toolIs(@ptrCast(name), "bar"));
130-
try std.testing.expect(!toolIs(@ptrCast(name), "foobar"));
131-
try std.testing.expect(!toolIs(@ptrCast(name), "fo"));
133+
const name: [*c]const u8 = "foo";
134+
try std.testing.expect(toolIs(name, "foo"));
135+
try std.testing.expect(!toolIs(name, "bar"));
136+
try std.testing.expect(!toolIs(name, "foobar"));
137+
try std.testing.expect(!toolIs(name, "fo"));
132138
}
133139

134140
test "invokeArgsNull: detects each null slot" {
135141
var buf: [4]u8 = undefined;
136142
var len: usize = 4;
137-
const name: [*:0]const u8 = "x";
138-
try std.testing.expect(!invokeArgsNull(@ptrCast(name), &buf, &len));
143+
const name: [*c]const u8 = "x";
144+
try std.testing.expect(!invokeArgsNull(name, &buf, &len));
139145
try std.testing.expect(invokeArgsNull(null, &buf, &len));
140-
try std.testing.expect(invokeArgsNull(@ptrCast(name), null, &len));
141-
try std.testing.expect(invokeArgsNull(@ptrCast(name), &buf, null));
146+
try std.testing.expect(invokeArgsNull(name, null, &len));
147+
try std.testing.expect(invokeArgsNull(name, &buf, null));
142148
}

0 commit comments

Comments
 (0)