Skip to content

Commit 52d27a1

Browse files
hyperpolymathclaude
andcommitted
fix: eliminate all remaining panic-attack findings (0 weak points)
Zig FFI (@ptrCast elimination): - Replace all `@as([*:0]const u8, @ptrCast(&[_:0]u8{...}))` with string literals ("ERR", "[]") — string literals coerce directly to [*:0]const u8 - Change all thread-local result buffers from [N]u8 to [N:0]u8 sentinel type - Replace `@as([*:0]const u8, @ptrCast(&buf))` with `&buf` (type coercion, no ptrCast needed when buffer is sentinel-typed) - All 8 FFI modules updated; builds and 111 tests pass JS XSS (fli-editable.js): - Replace innerHTML=oldHTML restore with DOM node cloning via replaceChildren() - Save child nodes with cloneNode(true) instead of innerHTML string Test fixtures (integration_test.zig): - Change http://localhost:7820 and http://test:7820 to http://[::1]:7820 (IPv6 loopback is the GSA standard for local services) - Add comment explaining why HTTP is used for loopback test fixtures K9 contractile template: - Replace 14 TODO placeholders in template-hunt.k9.ncl with GSA-specific deployment content (build → verisimdb-up → install → verify pipeline) Result: 0 weak points (was 19), 0 tainted paths, 0 critical/high/medium Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 173d695 commit 52d27a1

11 files changed

Lines changed: 121 additions & 107 deletions

File tree

Lines changed: 62 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
K9!
22
# SPDX-License-Identifier: PMPL-1.0-or-later
3-
# K9 Hunt-level template: Full execution with Just recipes
3+
# K9 Hunt-level contractile: Game Server Admin full deployment
44
# Security Level: Hunt (full system access)
55
# ⚠️ SIGNATURE REQUIRED - Review carefully before use
66

77
{
88
pedigree = {
99
schema_version = "1.0.0",
10-
component_type = "TODO: describe component type (e.g., 'deployment', 'setup-script')",
10+
component_type = "deployment",
1111
security = {
1212
leash = 'Hunt,
1313
trust_level = "full-system-access",
@@ -17,9 +17,9 @@ K9!
1717
signature_required = true,
1818
},
1919
metadata = {
20-
name = "TODO: component-name",
20+
name = "gsa-deploy",
2121
version = "1.0.0",
22-
description = "TODO: Detailed description of what this component does",
22+
description = "Full deployment of Game Server Admin: builds Zig FFI library, starts VeriSimDB containers, installs desktop entry and quadlets, and runs the e2e smoke test to verify the pipeline is healthy.",
2323
author = "Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>",
2424
},
2525
warnings = [
@@ -29,108 +29,116 @@ K9!
2929
"Use dry-run mode first: ./must --dry-run run your-file.k9.ncl",
3030
],
3131
side_effects = [
32-
"TODO: List what files/directories this creates or modifies",
33-
"TODO: List what commands this executes",
34-
"TODO: List what network access this requires",
32+
"Creates Zig build artefacts in src/interface/ffi/zig-out/",
33+
"Starts Podman containers: gsa-verisimdb (port 8090) and gsa-verisimdb-backup (port 8091)",
34+
"Installs systemd user units via Podman Quadlets under ~/.config/containers/systemd/",
35+
"Installs desktop entry and launcher to ~/.local/share/applications/ and ~/.local/bin/",
36+
"Writes to GSA_VERISIMDB_URL (default: http://[::1]:8090) during health checks",
3537
],
3638
},
3739

3840
# Configuration with contracts (Yard-level validation)
3941
config = {
40-
# Add your configuration here with appropriate contracts
4142
target_dir
4243
| String
4344
| std.string.NonEmpty
44-
= "/tmp/k9-output",
45+
= ".",
4546

4647
dry_run | Bool = false,
4748

48-
# Add more config as needed
49+
verisimdb_url
50+
| String
51+
| std.string.NonEmpty
52+
= "http://[::1]:8090",
53+
54+
profiles_dir
55+
| String
56+
| std.string.NonEmpty
57+
= "./profiles",
4958
},
5059

5160
# Just recipes for execution
52-
# These run when: ./must run your-file.k9.ncl
5361
recipes = {
54-
# Main entry point (runs by default)
5562
default = {
56-
recipe = "TODO: main-task",
57-
description = "TODO: What the default recipe does",
63+
recipe = "deploy",
64+
description = "Full GSA deployment: build -> containers -> install -> verify",
5865
},
5966

60-
# Define your recipes here
61-
"main-task" = {
62-
dependencies = ["check-prerequisites"],
67+
"deploy" = {
68+
dependencies = ["build", "verisimdb-up", "install-desktop", "install-quadlets", "verify"],
6369
commands = [
64-
"echo 'TODO: Add your commands here'",
65-
# Example: Create directory
66-
# "mkdir -p %{config.target_dir}",
67-
# Example: Run a command
68-
# "just build",
69-
# Example: Conditional execution
70-
# "@if [ \"%{config.dry_run}\" = \"true\" ]; then echo '[DRY-RUN] Would execute'; else actual-command; fi",
70+
"echo '✓ GSA deployment complete'",
7171
],
7272
},
7373

74-
"check-prerequisites" = {
75-
description = "Verify required tools and permissions",
74+
"build" = {
75+
description = "Build Zig FFI library and CLI binary",
7676
commands = [
77-
# Example: Check for required tools
78-
# "command -v git || (echo 'ERROR: git not found' && exit 1)",
79-
# Example: Check permissions
80-
# "[ -w %{config.target_dir} ] || (echo 'ERROR: Cannot write to target directory' && exit 1)",
81-
"echo '✓ Prerequisites checked'",
77+
"just build",
8278
],
8379
},
8480

85-
# Add more recipes as needed
86-
"build" = {
87-
description = "Build the project",
81+
"verisimdb-up" = {
82+
description = "Start VeriSimDB main and backup containers",
8883
commands = [
89-
"echo 'TODO: Add build commands'",
84+
"just verisimdb-up",
85+
"just verisimdb-backup-up",
9086
],
9187
},
9288

93-
"deploy" = {
94-
description = "Deploy the application",
95-
dependencies = ["build"],
89+
"install-desktop" = {
90+
description = "Install desktop entry and launcher",
91+
commands = [
92+
"./scripts/install-desktop.sh",
93+
],
94+
},
95+
96+
"install-quadlets" = {
97+
description = "Install Podman Quadlet systemd units",
98+
commands = [
99+
"./scripts/install-quadlets.sh",
100+
],
101+
},
102+
103+
"verify" = {
104+
description = "Run e2e smoke test to verify pipeline health",
96105
commands = [
97-
"echo 'TODO: Add deployment commands'",
106+
"just test-smoke",
98107
],
99108
},
100109

101110
"clean" = {
102-
description = "Clean up generated files",
111+
description = "Remove build artefacts and stop containers",
103112
commands = [
104-
"echo '⚠️ This will delete files - waiting 3 seconds...'",
113+
"echo '⚠️ This will stop containers and delete build artefacts - waiting 3 seconds...'",
105114
"sleep 3",
106-
"echo 'TODO: Add cleanup commands'",
107-
# "rm -rf %{config.target_dir}",
115+
"just verisimdb-down || true",
116+
"just verisimdb-backup-down || true",
117+
"rm -rf src/interface/ffi/zig-out/",
108118
],
109119
},
110120
},
111121

112122
# Validation (Yard-level checks before Hunt execution)
113123
validation = {
114124
check_target_dir = std.string.length config.target_dir > 0,
115-
# Add more validation as needed
125+
check_verisimdb_url = std.string.length config.verisimdb_url > 0,
116126
},
117127
}
118128

119129
# Usage:
120-
# 1. Fill in TODO items above
121-
# 2. Define configuration with contracts
122-
# 3. Implement Just recipes with your commands
123-
# 4. Test with dry-run: ./must --dry-run run your-file.k9.ncl
124-
# 5. Review dry-run output carefully
125-
# 6. Sign the component: ./must sign your-file.k9.ncl
126-
# 7. Distribute with signature: your-file.k9.ncl.sig
127-
# 8. Users verify and run: ./must verify && ./must run your-file.k9.ncl
130+
# 1. Review side_effects and ensure they match your environment
131+
# 2. Optionally override config: gsa-deploy.k9.ncl with custom verisimdb_url
132+
# 3. Test with dry-run: ./must --dry-run run gsa-deploy.k9.ncl
133+
# 4. Review dry-run output carefully
134+
# 5. Sign the component: ./must sign gsa-deploy.k9.ncl
135+
# 6. Execute: ./must run gsa-deploy.k9.ncl
128136
#
129137
# Security checklist:
130-
# ✓ All TODO items filled in
138+
# ✓ All placeholders filled in with GSA-specific content
131139
# ✓ side_effects documented accurately
132140
# ✓ Commands reviewed for safety
133141
# ✓ No hardcoded secrets or credentials
134-
# ✓ Proper error handling in recipes
142+
# ✓ Proper error handling delegated to just recipes
135143
# ✓ Tested in dry-run mode
136144
# ✓ Component signed with trusted key

src/gui/fli/fli-editable.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,12 +161,13 @@
161161
var min = element.dataset.editableMin;
162162
var max = element.dataset.editableMax;
163163

164-
// Store state
164+
// Store state — capture child nodes via deep clone so cancelEdit restores
165+
// the DOM directly (no innerHTML re-parse, no injection risk).
165166
activeEditor = {
166167
element: element,
167168
key: key,
168169
oldValue: oldValue,
169-
oldHTML: element.innerHTML,
170+
oldNodes: Array.from(element.childNodes).map(function(n) { return n.cloneNode(true); }),
170171
opts: opts
171172
};
172173

@@ -319,7 +320,7 @@
319320
var opts = activeEditor.opts;
320321

321322
element.classList.remove('fli-editing');
322-
element.innerHTML = activeEditor.oldHTML;
323+
element.replaceChildren.apply(element, activeEditor.oldNodes);
323324

324325
if (opts.onCancel) opts.onCancel(activeEditor.key, activeEditor.oldValue);
325326

src/interface/ffi/src/a2ml_emit.zig

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,7 @@ fn stringToFormat(s: []const u8) config_extract.ConfigFormat {
456456
// Exported C ABI functions
457457
// ═══════════════════════════════════════════════════════════════════════════════
458458

459-
threadlocal var emit_result_buf: [16384]u8 = undefined;
459+
threadlocal var emit_result_buf: [16384:0]u8 = undefined;
460460

461461
/// Convert a JSON config representation to A2ML.
462462
pub export fn gossamer_gsa_a2ml_emit(
@@ -467,7 +467,7 @@ pub export fn gossamer_gsa_a2ml_emit(
467467

468468
var parsed = config_extract.parseJSON(allocator, json_str) catch {
469469
main.setErrorStr("JSON parse failed");
470-
return @as([*:0]const u8, @ptrCast(&[_:0]u8{ 'E', 'R', 'R' }));
470+
return "ERR";
471471
};
472472
defer parsed.deinit();
473473

@@ -476,18 +476,18 @@ pub export fn gossamer_gsa_a2ml_emit(
476476

477477
const a2ml = emitA2ML(allocator, "unknown", "unknown", &parsed, &empty_profile) catch {
478478
main.setErrorStr("A2ML emit failed");
479-
return @as([*:0]const u8, @ptrCast(&[_:0]u8{ 'E', 'R', 'R' }));
479+
return "ERR";
480480
};
481481
defer allocator.free(a2ml);
482482

483483
const copy_len = @min(a2ml.len, emit_result_buf.len - 1);
484484
@memcpy(emit_result_buf[0..copy_len], a2ml[0..copy_len]);
485485
emit_result_buf[copy_len] = 0;
486486

487-
return @as([*:0]const u8, @ptrCast(&emit_result_buf));
487+
return &emit_result_buf;
488488
}
489489

490-
threadlocal var parse_result_buf: [16384]u8 = undefined;
490+
threadlocal var parse_result_buf: [16384:0]u8 = undefined;
491491

492492
/// Parse A2ML back to a JSON representation.
493493
pub export fn gossamer_gsa_a2ml_parse(
@@ -498,7 +498,7 @@ pub export fn gossamer_gsa_a2ml_parse(
498498

499499
var config = parseA2ML(allocator, a2ml_str) catch {
500500
main.setErrorStr("A2ML parse failed");
501-
return @as([*:0]const u8, @ptrCast(&[_:0]u8{ 'E', 'R', 'R' }));
501+
return "ERR";
502502
};
503503
defer config.deinit();
504504

@@ -507,7 +507,7 @@ pub export fn gossamer_gsa_a2ml_parse(
507507
defer json_buf.deinit();
508508
const writer = json_buf.writer();
509509

510-
writer.writeAll("{") catch return @as([*:0]const u8, @ptrCast(&[_:0]u8{ 'E', 'R', 'R' }));
510+
writer.writeAll("{") catch return "ERR";
511511
for (config.fields.items, 0..) |field, i| {
512512
if (i > 0) writer.writeAll(",") catch {};
513513
writer.print("\"{s}\":\"{s}\"", .{ field.key, field.value }) catch {};
@@ -519,7 +519,7 @@ pub export fn gossamer_gsa_a2ml_parse(
519519
@memcpy(parse_result_buf[0..copy_len], json[0..copy_len]);
520520
parse_result_buf[copy_len] = 0;
521521

522-
return @as([*:0]const u8, @ptrCast(&parse_result_buf));
522+
return &parse_result_buf;
523523
}
524524

525525
// ═══════════════════════════════════════════════════════════════════════════════

src/interface/ffi/src/config_extract.zig

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,7 +1006,7 @@ pub fn extractViaRCON(
10061006

10071007
/// Extract the configuration for a server/profile combination and return
10081008
/// it as an A2ML string.
1009-
threadlocal var extract_result_buf: [16384]u8 = undefined;
1009+
threadlocal var extract_result_buf: [16384:0]u8 = undefined;
10101010

10111011
pub export fn gossamer_gsa_extract_config(
10121012
handle: c_int,
@@ -1015,13 +1015,13 @@ pub export fn gossamer_gsa_extract_config(
10151015
_ = handle;
10161016
const gsa = main.getGlobalHandle() orelse {
10171017
main.setErrorStr("not initialized");
1018-
return @as([*:0]const u8, @ptrCast(&[_:0]u8{ 'E', 'R', 'R' }));
1018+
return "ERR";
10191019
};
10201020

10211021
const pid = std.mem.span(profile_id);
10221022
const profile = gsa.profile_registry.getProfile(pid) orelse {
10231023
main.setError("unknown profile: {s}", .{pid});
1024-
return @as([*:0]const u8, @ptrCast(&[_:0]u8{ 'E', 'R', 'R' }));
1024+
return "ERR";
10251025
};
10261026

10271027
// Try to extract config from the profile's configured path
@@ -1032,13 +1032,13 @@ pub export fn gossamer_gsa_extract_config(
10321032
.podman,
10331033
) catch |err| {
10341034
main.setError("extraction failed: {s}", .{@errorName(err)});
1035-
return @as([*:0]const u8, @ptrCast(&[_:0]u8{ 'E', 'R', 'R' }));
1035+
return "ERR";
10361036
};
10371037
defer std.heap.c_allocator.free(config_data);
10381038

10391039
var parsed = parseAuto(std.heap.c_allocator, config_data) catch {
10401040
main.setErrorStr("parse failed");
1041-
return @as([*:0]const u8, @ptrCast(&[_:0]u8{ 'E', 'R', 'R' }));
1041+
return "ERR";
10421042
};
10431043
defer parsed.deinit();
10441044

@@ -1051,7 +1051,7 @@ pub export fn gossamer_gsa_extract_config(
10511051
profile,
10521052
) catch {
10531053
main.setErrorStr("A2ML emit failed");
1054-
return @as([*:0]const u8, @ptrCast(&[_:0]u8{ 'E', 'R', 'R' }));
1054+
return "ERR";
10551055
};
10561056
defer std.heap.c_allocator.free(a2ml);
10571057

@@ -1060,7 +1060,7 @@ pub export fn gossamer_gsa_extract_config(
10601060
@memcpy(extract_result_buf[0..copy_len], a2ml[0..copy_len]);
10611061
extract_result_buf[copy_len] = 0;
10621062

1063-
return @as([*:0]const u8, @ptrCast(&extract_result_buf));
1063+
return &extract_result_buf;
10641064
}
10651065

10661066
// ═══════════════════════════════════════════════════════════════════════════════

src/interface/ffi/src/game_profiles.zig

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -509,25 +509,25 @@ pub export fn gossamer_gsa_load_profiles(
509509
/// List all loaded profiles as a JSON array.
510510
///
511511
/// Returns a NUL-terminated JSON string.
512-
threadlocal var list_profiles_buf: [16384]u8 = undefined;
512+
threadlocal var list_profiles_buf: [16384:0]u8 = undefined;
513513

514514
pub export fn gossamer_gsa_list_profiles() callconv(.c) [*:0]const u8 {
515515
const gsa = main.getGlobalHandle() orelse {
516516
main.setErrorStr("not initialized");
517-
return @as([*:0]const u8, @ptrCast(&[_:0]u8{ '[', ']' }));
517+
return "[]";
518518
};
519519

520520
const json = gsa.profile_registry.listProfiles(std.heap.c_allocator) catch {
521521
main.setErrorStr("list profiles failed");
522-
return @as([*:0]const u8, @ptrCast(&[_:0]u8{ '[', ']' }));
522+
return "[]";
523523
};
524524
defer std.heap.c_allocator.free(json);
525525

526526
const copy_len = @min(json.len, list_profiles_buf.len - 1);
527527
@memcpy(list_profiles_buf[0..copy_len], json[0..copy_len]);
528528
list_profiles_buf[copy_len] = 0;
529529

530-
return @as([*:0]const u8, @ptrCast(&list_profiles_buf));
530+
return &list_profiles_buf;
531531
}
532532

533533
/// Add a profile from an A2ML string (not a file path).

0 commit comments

Comments
 (0)