authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-03-26 20:02:33-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-03-27 14:57:05+01:00
log053b5e3bddc086c43bc44e11a0106a2f15a0f3af
tree50bc6d1b9d049d4e2a9d257278e13c3601cb1948
parent2d3694ab42b73ba09124d5f1ddbfd9adaea79c47

Package.Manifest: add missing hash validation

closes #31225 reverts e96d86064eb81977f254fa8f36481b7d150cb3b6 which was an inadequate attempt to address the same problem (lack of hash validation).

3 files changed, 25 insertions(+), 16 deletions(-)

src/Package.zig+19-8
...@@ -44,6 +44,8 @@ pub const Fingerprint = packed struct(u64) {...@@ -44,6 +44,8 @@ pub const Fingerprint = packed struct(u64) {
44pub const Hash = struct {44pub const Hash = struct {
45 /// Maximum size of a package hash. Unused bytes at the end are45 /// Maximum size of a package hash. Unused bytes at the end are
46 /// filled with zeroes.46 /// filled with zeroes.
47 ///
48 /// Assumed to be already validated.
47 bytes: [max_len]u8,49 bytes: [max_len]u8,
4850
49 pub const Algo = std.crypto.hash.sha2.Sha256;51 pub const Algo = std.crypto.hash.sha2.Sha256;
...@@ -52,21 +54,35 @@ pub const Hash = struct {...@@ -52,21 +54,35 @@ pub const Hash = struct {
52 /// Example: "nnnn-vvvv-hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh"54 /// Example: "nnnn-vvvv-hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh"
53 pub const max_len = 32 + 1 + 32 + 1 + (32 + 32 + 200) / 6;55 pub const max_len = 32 + 1 + 32 + 1 + (32 + 32 + 200) / 6;
5456
57 /// Asserts `s` is valid.
55 pub fn fromSlice(s: []const u8) Hash {58 pub fn fromSlice(s: []const u8) Hash {
56 assert(s.len <= max_len);59 assert(validate(s) == .ok);
57 var result: Hash = undefined;60 var result: Hash = undefined;
58 @memcpy(result.bytes[0..s.len], s);61 @memcpy(result.bytes[0..s.len], s);
59 @memset(result.bytes[s.len..], 0);62 @memset(result.bytes[s.len..], 0);
60 return result;63 return result;
61 }64 }
6265
66 pub const Validation = enum { ok, short, long, incomplete };
67
68 pub fn validate(s: []const u8) Validation {
69 if (s.len > max_len) return .long;
70 if (s.len < 44) return .short;
71 const n_dashes = std.mem.countScalar(u8, s[0 .. s.len - 44], '-');
72 if (n_dashes < 2) return .incomplete;
73 return .ok;
74 }
75
76 test validate {
77 try std.testing.expectEqual(.short, validate(""));
78 }
79
63 pub fn toSlice(ph: *const Hash) []const u8 {80 pub fn toSlice(ph: *const Hash) []const u8 {
64 var end: usize = ph.bytes.len;81 var end: usize = ph.bytes.len;
65 while (end > 0) {82 while (true) {
66 end -= 1;83 end -= 1;
67 if (ph.bytes[end] != 0) return ph.bytes[0 .. end + 1];84 if (ph.bytes[end] != 0) return ph.bytes[0 .. end + 1];
68 }85 }
69 return ph.bytes[0..0];
70 }86 }
7187
72 pub fn eql(a: *const Hash, b: *const Hash) bool {88 pub fn eql(a: *const Hash, b: *const Hash) bool {
...@@ -188,11 +204,6 @@ test Hash {...@@ -188,11 +204,6 @@ test Hash {
188 try std.testing.expectEqualStrings("nasm-2.16.1-3-vrr-ygAAoADH9XG3tOdvPNuHen_d-XeHndOG-nNXmved", result.toSlice());204 try std.testing.expectEqualStrings("nasm-2.16.1-3-vrr-ygAAoADH9XG3tOdvPNuHen_d-XeHndOG-nNXmved", result.toSlice());
189}205}
190206
191test "empty hash" {
192 const hash = Hash.fromSlice("");
193 try std.testing.expectEqualStrings("", hash.toSlice());
194}
195
196test {207test {
197 _ = Fetch;208 _ = Fetch;
198}209}
src/Package/Fetch.zig+1-1
...@@ -783,7 +783,7 @@ fn runResource(...@@ -783,7 +783,7 @@ fn runResource(
783 const hash_tok = f.hash_tok.unwrap().?;783 const hash_tok = f.hash_tok.unwrap().?;
784 if (!computed_package_hash.eql(&declared_hash)) {784 if (!computed_package_hash.eql(&declared_hash)) {
785 return f.fail(hash_tok, try eb.printString(785 return f.fail(hash_tok, try eb.printString(
786 "hash mismatch: manifest declares '{s}' but the fetched package has '{s}'",786 "hash mismatch: manifest declares {s} but the fetched package has {s}",
787 .{ declared_hash.toSlice(), computed_package_hash.toSlice() },787 .{ declared_hash.toSlice(), computed_package_hash.toSlice() },
788 ));788 ));
789 }789 }
src/Package/Manifest.zig+5-7
...@@ -420,12 +420,10 @@ const Parse = struct {...@@ -420,12 +420,10 @@ const Parse = struct {
420 const ast = p.ast;420 const ast = p.ast;
421 const tok = ast.nodeMainToken(node);421 const tok = ast.nodeMainToken(node);
422 const h = try parseString(p, node);422 const h = try parseString(p, node);
423423 switch (Package.Hash.validate(h)) {
424 if (h.len > Package.Hash.max_len) {424 .ok => return h,
425 return fail(p, tok, "hash length exceeds maximum: {d}", .{h.len});425 else => |t| return fail(p, tok, "invalid hash: {t}", .{t}),
426 }426 }
427
428 return h;
429 }427 }
430428
431 /// TODO: try to DRY this with AstGen.identifierTokenString429 /// TODO: try to DRY this with AstGen.identifierTokenString
...@@ -632,7 +630,7 @@ test "basic" {...@@ -632,7 +630,7 @@ test "basic" {
632 \\ .dependencies = .{630 \\ .dependencies = .{
633 \\ .bar = .{631 \\ .bar = .{
634 \\ .url = "https://example.com/baz.tar.gz",632 \\ .url = "https://example.com/baz.tar.gz",
635 \\ .hash = "1220f1b680b6065fcfc94fe777f22e73bcb7e2767e5f4d99d4255fe76ded69c7a35f",633 \\ .hash = "libmp3lame-3.100.1-6-67wlF_KvEwDRCT3pTpcDzi5KGntWCEoM-WtvVPEWdlk5",
636 \\ },634 \\ },
637 \\ },635 \\ },
638 \\}636 \\}
...@@ -664,7 +662,7 @@ test "basic" {...@@ -664,7 +662,7 @@ test "basic" {
664 manifest.dependencies.values()[0].location.url,662 manifest.dependencies.values()[0].location.url,
665 );663 );
666 try testing.expectEqualStrings(664 try testing.expectEqualStrings(
667 "1220f1b680b6065fcfc94fe777f22e73bcb7e2767e5f4d99d4255fe76ded69c7a35f",665 "libmp3lame-3.100.1-6-67wlF_KvEwDRCT3pTpcDzi5KGntWCEoM-WtvVPEWdlk5",
668 manifest.dependencies.values()[0].hash orelse return error.TestFailed,666 manifest.dependencies.values()[0].hash orelse return error.TestFailed,
669 );667 );
670668