a801f719897c
logAction JSON-escapes ctx.machine.name before embedding it in a JSONL log entry, so a config machine name containing a quote or backslash no longer produces an invalid line that renderJsonLogLine can only dump raw (NINA-020)
- time
- 2026-07-02 13:49 UTC · 3 months ago
- machine
- koh2
- agent
- claude-sonnet-5
- session
- fable audit fix — commands.zig
- parent
- e923ab8ccb1a
↓ nina-a801f719.tar.gzatlas at this save
files changed1 file
src/commands.zig+48 −1src/commands.zig+48 −1
@@ -2281,14 +2281,61 @@22812281 22822282 // --- Log helper ---22832283 2284+// Escapes a string for embedding inside a JSON string literal. Truncates2285+// (rather than overflows `buf`) if the escaped form doesn't fit — used for a2286+// single log line where losing the tail of an implausibly long machine name is2287+// preferable to corrupting the line.2288+fn jsonEscape(buf: []u8, s: []const u8) []const u8 {2289+ var i: usize = 0;2290+ for (s) |ch| {2291+ const esc: ?u8 = switch (ch) {2292+ '"' => '"',2293+ '\\' => '\\',2294+ '\n' => 'n',2295+ '\r' => 'r',2296+ '\t' => 't',2297+ else => null,2298+ };2299+ if (esc) |e| {2300+ if (i + 2 > buf.len) break;2301+ buf[i] = '\\';2302+ buf[i + 1] = e;2303+ i += 2;2304+ } else {2305+ if (i + 1 > buf.len) break;2306+ buf[i] = ch;2307+ i += 1;2308+ }2309+ }2310+ return buf[0..i];2311+}2312+22842313 fn logAction(ctx: Ctx, action: []const u8, gen: u32) void {22852314 var ts_buf: [32]u8 = undefined;22862315 const ts = nowIso(ctx.io, &ts_buf);2316+ // machine.name is config-parsed and may legally contain `"` or `\`2317+ // (NINA-020) — embedding it unescaped produced an invalid JSON line that2318+ // renderJsonLogLine could only fall back to showing raw.2319+ var name_buf: [128]u8 = undefined;2320+ const machine_name = jsonEscape(&name_buf, ctx.machine.name);22872321 var buf: [256]u8 = undefined;22882322 const entry = std.fmt.bufPrint(&buf, "{{\"ts\":\"{s}\",\"machine\":\"{s}\",\"command\":\"{s}\",\"outcome\":\"success\",\"gen_before\":null,\"gen_after\":{d},\"duration_ms\":0}}\n", .{2289- ts, ctx.machine.name, action, gen,2323+ ts, machine_name, action, gen,22902324 }) catch return;22912325 exec.appendNinaLog(ctx.io, ctx.environ, entry);2326+}2327+2328+test "jsonEscape lets a machine name containing quotes and backslashes round-trip through JSON" {2329+ var esc_buf: [128]u8 = undefined;2330+ const escaped = jsonEscape(&esc_buf, "kyoshi\" evil\\host");2331+2332+ var line_buf: [256]u8 = undefined;2333+ const entry = std.fmt.bufPrint(&line_buf, "{{\"ts\":\"2026-01-01T00:00:00Z\",\"machine\":\"{s}\",\"command\":\"apply\",\"outcome\":\"success\",\"gen_before\":null,\"gen_after\":5,\"duration_ms\":0}}\n", .{escaped}) catch unreachable;2334+2335+ const parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, entry, .{});2336+ defer parsed.deinit();2337+ try std.testing.expect(parsed.value == .object);2338+ try std.testing.expectEqualStrings("kyoshi\" evil\\host", parsed.value.object.get("machine").?.string);22922339 }22932340 22942341 // Wall-clock UTC as ISO-8601. Uses Io.Clock.real (the settable Unix-epoch clock);