milestone 19: hygiene sweep - dead ecs surface, single-source constants, tls classification, frontend state hazards, docker smoke network fix
CI / test (push) Successful in 1m46s
CI / test-aarch64 (push) Successful in 5m30s
CI / frontend (push) Successful in 46s
CI / cross (push) Successful in 8m12s
CI / docker (push) Successful in 3m46s

This commit is contained in:
2026-08-07 20:39:27 +02:00
parent 6f67940995
commit 6c507992e4
59 changed files with 1020 additions and 382 deletions
+67 -2
View File
@@ -318,12 +318,15 @@ fn load(
};
defer gpa.free(cert_pem);
const key_pem = readPem(gpa, io, key_path) catch |err| return switch (err) {
const key_pem = readKeyPem(gpa, io, key_path) catch |err| return switch (err) {
error.OutOfMemory => error.OutOfMemory,
error.TooLarge => error.KeyTooLarge,
error.Unreadable => error.KeyUnreadable,
};
defer gpa.free(key_pem);
defer {
std.crypto.secureZero(u8, key_pem);
gpa.free(key_pem);
}
const entry = try gpa.create(Entry);
errdefer gpa.destroy(entry);
@@ -342,6 +345,9 @@ fn load(
/// Mbed TLS wants PEM with a terminating zero byte counted in the length, so
/// the file lands in a sentinel-terminated allocation. The limit admits
/// exactly `max_pem_bytes` and rejects the first byte beyond it.
///
/// The certificate only. A private key goes through `readKeyPem`, which does
/// not leave copies behind.
fn readPem(
gpa: std.mem.Allocator,
io: std.Io,
@@ -361,6 +367,36 @@ fn readPem(
};
}
/// `readPem` for the private key, with the same cap and the same
/// sentinel-terminated result. The difference is that no copy of the key
/// survives this function: `readFileAllocOptions` grows its buffer as it reads,
/// and every intermediate copy it abandons stays legible in freed pages that no
/// wipe at the call site can reach. One fixed staging buffer never grows, and it
/// is wiped before it goes back to the allocator. The caller wipes the returned
/// slice the same way before freeing it (the idiom is auth.zig's `secureZero`
/// defers).
fn readKeyPem(
gpa: std.mem.Allocator,
io: std.Io,
path: []const u8,
) error{ OutOfMemory, TooLarge, Unreadable }![:0]u8 {
const staging = try gpa.alloc(u8, max_pem_bytes + 1);
defer {
std.crypto.secureZero(u8, staging);
gpa.free(staging);
}
const bytes = std.Io.Dir.cwd().readFile(io, path, staging) catch return error.Unreadable;
// A read that filled the staging buffer is ambiguous — `readFile` cannot
// say whether more followed — and one byte past `max_pem_bytes` is over the
// cap either way.
if (bytes.len == staging.len) return error.TooLarge;
const key = try gpa.allocSentinel(u8, bytes.len, 0);
@memcpy(key, bytes);
return key;
}
fn statSig(io: std.Io, path: []const u8) !FileSig {
const st = try std.Io.Dir.cwd().statFile(io, path, .{});
return .{ .mtime_ns = st.mtime.nanoseconds, .size = st.size };
@@ -481,6 +517,35 @@ test "the size cap admits 64 KiB and rejects one byte more" {
try testing.expectError(error.TooLarge, readPem(testing.allocator, io, env.cert_path));
}
test "the key PEM path keeps the shape and the cap of the certificate path" {
var env: TestEnv = undefined;
try env.init();
defer env.deinit();
const io = env.io();
const key = try readKeyPem(testing.allocator, io, env.key_path);
defer testing.allocator.free(key);
try testing.expectEqualStrings(fixtures.key_pem, key);
try testing.expectEqual(@as(u8, 0), key[key.len]);
const at_cap = try testing.allocator.alloc(u8, max_pem_bytes);
defer testing.allocator.free(at_cap);
@memset(at_cap, 'a');
try env.tmp.dir.writeFile(io, .{ .sub_path = "key.pem", .data = at_cap });
testing.allocator.free(try readKeyPem(testing.allocator, io, env.key_path));
const over = try testing.allocator.alloc(u8, max_pem_bytes + 1);
defer testing.allocator.free(over);
@memset(over, 'a');
try env.tmp.dir.writeFile(io, .{ .sub_path = "key.pem", .data = over });
try testing.expectError(error.TooLarge, readKeyPem(testing.allocator, io, env.key_path));
try testing.expectError(
error.Unreadable,
readKeyPem(testing.allocator, io, "./nxdns-no-such-key-9b31.pem"),
);
}
test "init fails typed on a missing file and on garbage PEM" {
var env: TestEnv = undefined;
try env.init();