From 0c54ae3c0e0099dc8b382ea9fe9b80b37dbaee06 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 12 Oct 2026 20:34:17 -0700 Subject: [PATCH] zig build: validate package hash consistency Any time the same package (based on fingerprint) appears in the dependency tree, if the semantic version compares equal, the hash must be identical, otherwise one of three things has gone wrong and the maintainer must address it. --- lib/compiler/Maker.zig | 77 ++++++++++++++++++++++++++++++++++ lib/compiler/Maker/Package.zig | 8 +++- 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/lib/compiler/Maker.zig b/lib/compiler/Maker.zig index f85a66096c3f513f50faf0e6038337b865213233..0ae85d4fa3bf3ba2b6b78514eb3a8cc4a5a1429b 100644 --- a/lib/compiler/Maker.zig +++ b/lib/compiler/Maker.zig @@ -1397,6 +1397,8 @@ fn configure(graph: *Graph, options: ConfigureOptions) !ScannedConfig { process.exit(0); } + try validateFingerprints(gpa, &job_queue); + // Create the dependencies.zig file for configurer to // obtain via `@import("@dependencies")`. { @@ -1714,6 +1716,81 @@ fn configure(graph: *Graph, options: ConfigureOptions) !ScannedConfig { }; } +const ValidFingerprintKey = struct { + project_id: Package.ProjectId, + version: std.SemanticVersion, + + const Context = struct { + pub fn hash(this: @This(), a: ValidFingerprintKey) u32 { + _ = this; + var hasher: std.hash.Wyhash = .init(a.project_id.fingerprint_id); + hasher.update(&a.project_id.padded_name); + hasher.update(@ptrCast((&a.version.major)[0..1])); + hasher.update(@ptrCast((&a.version.minor)[0..1])); + hasher.update(@ptrCast((&a.version.patch)[0..1])); + if (a.version.pre) |pre| hasher.update(pre); + return @truncate(hasher.final()); + } + pub fn eql(this: @This(), a: ValidFingerprintKey, b: ValidFingerprintKey, i: usize) bool { + _ = this; + _ = i; + return a.project_id.eql(&b.project_id) and a.version.order(b.version) == .eq; + } + }; +}; + +const ValidFingerprintTable = std.array_hash_map.Custom( + ValidFingerprintKey, + struct { fetch: *Fetch, pkg_hash: Package.Hash, printed: bool }, + ValidFingerprintKey.Context, + true, +); + +/// Ensure that the dependency tree did not have any fingerprint matches with the same semantic version but +/// differing package hashes. +fn validateFingerprints(gpa: Allocator, job_queue: *const Fetch.JobQueue) !void { + var any_errors = false; + + var table: ValidFingerprintTable = .empty; + defer table.deinit(gpa); + + for (job_queue.table.keys(), job_queue.table.values()) |pkg_hash, fetch| { + if (!fetch.have_manifest) continue; + const gop = try table.getOrPut(gpa, .{ + .project_id = .init(fetch.manifest.name, fetch.manifest.id), + .version = fetch.manifest.version, + }); + if (gop.found_existing) { + any_errors = true; + const fingerprint: Package.Fingerprint = .init(fetch.manifest.id, fetch.manifest.name); + const stripped_version: std.SemanticVersion = .{ + .major = fetch.manifest.version.major, + .minor = fetch.manifest.version.minor, + .patch = fetch.manifest.version.patch, + .pre = fetch.manifest.version.pre, + }; + if (!gop.value_ptr.printed) { + gop.value_ptr.printed = true; + log.err("contradictory hash for package with fingerprint 0x{x} and version {f}: {s}", .{ + fingerprint.int(), stripped_version, gop.value_ptr.pkg_hash.toSlice(), + }); + } + log.err("contradictory hash for package with fingerprint 0x{x} and version {f}: {s}", .{ + fingerprint.int(), stripped_version, pkg_hash.toSlice(), + }); + } else { + gop.value_ptr.* = .{ .fetch = fetch, .pkg_hash = pkg_hash, .printed = false }; + } + } + if (any_errors) { + log.info("potential cause: package ownership dispute", .{}); + log.info("potential cause: maintainer neglected to bump version after making changes", .{}); + log.info("potential cause: dependency on unreleased changes", .{}); + log.info("hint: use --print-tree to visualize all package versions and hashes", .{}); + return error.FailedButCacheIntact; + } +} + fn printPkgTree( terminal: Io.Terminal, arena: Allocator, diff --git a/lib/compiler/Maker/Package.zig b/lib/compiler/Maker/Package.zig index 8baef22f375f925534380176f618a470f988117e..c31ecf6916ce4d8d279cc9dbe9f529ee99355372 100644 --- a/lib/compiler/Maker/Package.zig +++ b/lib/compiler/Maker/Package.zig @@ -8,13 +8,17 @@ pub const Fingerprint = packed struct(u64) { id: u32, checksum: u32, - pub fn generate(rng: std.Random, name: []const u8) Fingerprint { + pub fn init(id: u32, name: []const u8) Fingerprint { return .{ - .id = rng.intRangeLessThan(u32, 1, 0xffffffff), + .id = id, .checksum = std.hash.Crc32.hash(name), }; } + pub fn generate(rng: std.Random, name: []const u8) Fingerprint { + return .init(rng.intRangeLessThan(u32, 1, 0xffffffff), name); + } + pub fn validate(n: Fingerprint, name: []const u8) bool { switch (n.id) { 0x00000000, 0xffffffff => return false, -- 2.54.0