authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-02-25 17:26:19-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-02-26 11:42:03-08:00
log0fc7c9f57c0042cfe6dab9deb3b5fe2f9404d744
treef554469401cd9df8a762a098dc0091a79e0caf7d
parenta70307e7ffc608643bbd940796eaeb5bca6bbc8f

switch from "id" to "nonce"

mainly this addresses the following use case: 1. Someone creates a template with build.zig.zon, id field included (note that zig init does not create this problem since it generates fresh id every time it runs). 2. User A uses the template, changing package name to "example" but not id field. 3. User B uses the same template, changing package name also to "example", also not changing the id field. Here, both packages have unintentional conflicting logical ids. By making the field a combination of name checksum + random id, this accident is avoided. "nonce" is an OK name for this. Also relaxes errors on remote packages when using `zig fetch`.

7 files changed, 100 insertions(+), 59 deletions(-)

build.zig.zon+1-1
......@@ -12,5 +12,5 @@
1212 },
1313 },
1414 .paths = .{""},
15 .id = 0x1cb6,
15 .nonce = 0xc1ce10810000f013,
1616}
doc/build.zig.zon.md+17-8
......@@ -22,21 +22,30 @@ Zig package namespace.
2222
2323Must be a valid bare Zig identifier (don't `@` me), limited to 32 bytes.
2424
25### `id`
25### `nonce`
2626
2727Together with name, this represents a globally unique package identifier. This
28field should be initialized with a 16-bit random number when the package is
29first created, and then *never change*. This allows Zig to unambiguously detect
30when one package is an updated version of another.
28field is auto-initialized by the toolchain when the package is first created,
29and then *never changes*. This allows Zig to unambiguously detect when one
30package is an updated version of another.
3131
32When forking a Zig project, this id should be regenerated with a new random
33number if the upstream project is still maintained. Otherwise, the fork is
34*hostile*, attempting to take control over the original project's identity.
32When forking a Zig project, this nonce should be regenerated if the upstream
33project is still maintained. Otherwise, the fork is *hostile*, attempting to
34take control over the original project's identity. The nonce can be regenerated
35by deleting the field and running `zig build`.
3536
36`0x0000` is invalid because it obviously means a random number wasn't used.
37This 64-bit integer is the combination of a 16-bit id component, a 32-bit
38checksum, and 16 bits of reserved zeroes.
39
40The id component within the nonce has these restrictions:
41
42`0x0000` is reserved for legacy packages.
3743
3844`0xffff` is reserved to represent "naked" packages.
3945
46The checksum is computed from `name` and serves to protect Zig users from
47accidental id collisions.
48
4049### `version`
4150
4251String. Required.
lib/init/build.zig.zon+11-10
......@@ -13,17 +13,18 @@
1313 .version = "0.0.0",
1414
1515 // Together with name, this represents a globally unique package
16 // identifier. This field should be initialized with a 16-bit random number
17 // when the package is first created, and then *never change*. This allows
18 // unambiguous detection when one package is an updated version of another.
16 // identifier. This field is generated by the Zig toolchain when the
17 // package is first created, and then *never changes*. This allows
18 // unambiguous detection of one package being an updated version of
19 // another.
1920 //
20 // When forking a Zig project, this id should be regenerated with a new
21 // random number if the upstream project is still maintained. Otherwise,
22 // the fork is *hostile*, attempting to take control over the original
23 // project's identity. Thus it is recommended to leave the comment on the
24 // following line intact, so that it shows up in code reviews that modify
25 // the field.
26 .id = $i, // Changing this has security and trust implications.
21 // When forking a Zig project, this id should be regenerated (delete the
22 // field and run `zig build`) if the upstream project is still maintained.
23 // Otherwise, the fork is *hostile*, attempting to take control over the
24 // original project's identity. Thus it is recommended to leave the comment
25 // on the following line intact, so that it shows up in code reviews that
26 // modify the field.
27 .nonce = $i, // Changing this has security and trust implications.
2728
2829 // Tracks the earliest Zig version that the package considers to be a
2930 // supported use case.
src/Package.zig+27-6
......@@ -10,9 +10,29 @@ pub const multihash_len = 1 + 1 + Hash.Algo.digest_length;
1010pub const multihash_hex_digest_len = 2 * multihash_len;
1111pub const MultiHashHexDigest = [multihash_hex_digest_len]u8;
1212
13pub fn randomId() u16 {
14 return std.crypto.random.intRangeLessThan(u16, 0x0001, 0xffff);
15}
13pub const Nonce = packed struct(u64) {
14 id: u16,
15 reserved: u16 = 0,
16 checksum: u32,
17
18 pub fn generate(name: []const u8) Nonce {
19 return .{
20 .id = std.crypto.random.intRangeLessThan(u16, 0x0001, 0xffff),
21 .checksum = std.hash.Crc32.hash(name),
22 };
23 }
24
25 pub fn validate(n: Nonce, name: []const u8) bool {
26 switch (n.id) {
27 0x0000, 0xffff => return false,
28 else => return std.hash.Crc32.hash(name) == n.checksum,
29 }
30 }
31
32 pub fn int(n: Nonce) u64 {
33 return @bitCast(n);
34 }
35};
1636
1737/// A user-readable, file system safe hash that identifies an exact package
1838/// snapshot, including file contents.
......@@ -72,9 +92,10 @@ pub const Hash = struct {
7292 }
7393
7494 /// Produces "$name-$semver-$hashplus".
75 /// * name is the name field from build.zig.zon, truncated at 32 bytes and must
76 /// be a valid zig identifier
77 /// * semver is the version field from build.zig.zon, truncated at 32 bytes
95 /// * name is the name field from build.zig.zon, asserted to be at most 32
96 /// bytes and assumed be a valid zig identifier
97 /// * semver is the version field from build.zig.zon, asserted to be at
98 /// most 32 bytes
7899 /// * hashplus is the following 39-byte array, base64 encoded using -_ to make
79100 /// it filesystem safe:
80101 /// - (2 bytes) LE u16 Package ID
src/Package/Fetch.zig+9-3
......@@ -44,6 +44,8 @@ omit_missing_hash_error: bool,
4444/// which specifies inclusion rules. This is intended to be true for the first
4545/// fetch task and false for the recursive dependencies.
4646allow_missing_paths_field: bool,
47allow_missing_nonce: bool,
48allow_name_string: bool,
4749/// If true and URL points to a Git repository, will use the latest commit.
4850use_latest_commit: bool,
4951
......@@ -372,7 +374,7 @@ pub fn run(f: *Fetch) RunError!void {
372374 };
373375
374376 if (remote.hash) |expected_hash| {
375 var prefixed_pkg_sub_path_buffer: [100]u8 = undefined;
377 var prefixed_pkg_sub_path_buffer: [Package.Hash.max_len + 2]u8 = undefined;
376378 prefixed_pkg_sub_path_buffer[0] = 'p';
377379 prefixed_pkg_sub_path_buffer[1] = fs.path.sep;
378380 const hash_slice = expected_hash.toSlice();
......@@ -647,8 +649,8 @@ fn loadManifest(f: *Fetch, pkg_root: Cache.Path) RunError!void {
647649
648650 f.manifest = try Manifest.parse(arena, ast.*, .{
649651 .allow_missing_paths_field = f.allow_missing_paths_field,
650 .allow_missing_id = f.allow_missing_paths_field,
651 .allow_name_string = f.allow_missing_paths_field,
652 .allow_missing_nonce = f.allow_missing_nonce,
653 .allow_name_string = f.allow_name_string,
652654 });
653655 const manifest = &f.manifest.?;
654656
......@@ -750,6 +752,8 @@ fn queueJobsForDeps(f: *Fetch) RunError!void {
750752 .job_queue = f.job_queue,
751753 .omit_missing_hash_error = false,
752754 .allow_missing_paths_field = true,
755 .allow_missing_nonce = true,
756 .allow_name_string = true,
753757 .use_latest_commit = false,
754758
755759 .package_root = undefined,
......@@ -2319,6 +2323,8 @@ const TestFetchBuilder = struct {
23192323 .job_queue = &self.job_queue,
23202324 .omit_missing_hash_error = true,
23212325 .allow_missing_paths_field = false,
2326 .allow_missing_nonce = true, // so we can keep using the old testdata .tar.gz
2327 .allow_name_string = true, // so we can keep using the old testdata .tar.gz
23222328 .use_latest_commit = true,
23232329
23242330 .package_root = undefined,
src/Package/Manifest.zig+25-25
......@@ -52,7 +52,7 @@ pub const ParseOptions = struct {
5252 /// Deprecated, to be removed after 0.14.0 is tagged.
5353 allow_name_string: bool = true,
5454 /// Deprecated, to be removed after 0.14.0 is tagged.
55 allow_missing_id: bool = true,
55 allow_missing_nonce: bool = true,
5656};
5757
5858pub const Error = Allocator.Error;
......@@ -81,7 +81,7 @@ pub fn parse(gpa: Allocator, ast: Ast, options: ParseOptions) Error!Manifest {
8181 .paths = .{},
8282 .allow_missing_paths_field = options.allow_missing_paths_field,
8383 .allow_name_string = options.allow_name_string,
84 .allow_missing_id = options.allow_missing_id,
84 .allow_missing_nonce = options.allow_missing_nonce,
8585 .minimum_zig_version = null,
8686 .buf = .{},
8787 };
......@@ -157,7 +157,7 @@ const Parse = struct {
157157 paths: std.StringArrayHashMapUnmanaged(void),
158158 allow_missing_paths_field: bool,
159159 allow_name_string: bool,
160 allow_missing_id: bool,
160 allow_missing_nonce: bool,
161161 minimum_zig_version: ?std.SemanticVersion,
162162
163163 const InnerError = error{ ParseFailure, OutOfMemory };
......@@ -175,7 +175,7 @@ const Parse = struct {
175175 var have_name = false;
176176 var have_version = false;
177177 var have_included_paths = false;
178 var have_id = false;
178 var nonce: ?Package.Nonce = null;
179179
180180 for (struct_init.ast.fields) |field_init| {
181181 const name_token = ast.firstToken(field_init) - 2;
......@@ -192,9 +192,8 @@ const Parse = struct {
192192 } else if (mem.eql(u8, field_name, "name")) {
193193 p.name = try parseName(p, field_init);
194194 have_name = true;
195 } else if (mem.eql(u8, field_name, "id")) {
196 p.id = try parseId(p, field_init);
197 have_id = true;
195 } else if (mem.eql(u8, field_name, "nonce")) {
196 nonce = try parseNonce(p, field_init);
198197 } else if (mem.eql(u8, field_name, "version")) {
199198 p.version_node = field_init;
200199 const version_text = try parseString(p, field_init);
......@@ -218,14 +217,23 @@ const Parse = struct {
218217 }
219218 }
220219
221 if (!have_id and !p.allow_missing_id) {
222 try appendError(p, main_token, "missing top-level 'id' field; suggested value: 0x{x}", .{
223 Package.randomId(),
224 });
225 }
226
227220 if (!have_name) {
228221 try appendError(p, main_token, "missing top-level 'name' field", .{});
222 } else {
223 if (nonce) |n| {
224 if (!n.validate(p.name)) {
225 return fail(p, main_token, "invalid nonce: 0x{x}; if this is a new or forked package, use this value: 0x{x}", .{
226 n.int(), Package.Nonce.generate(p.name).int(),
227 });
228 }
229 p.id = n.id;
230 } else if (!p.allow_missing_nonce) {
231 try appendError(p, main_token, "missing top-level 'nonce' field; suggested value: 0x{x}", .{
232 Package.Nonce.generate(p.name).int(),
233 });
234 } else {
235 p.id = 0;
236 }
229237 }
230238
231239 if (!have_version) {
......@@ -377,7 +385,7 @@ const Parse = struct {
377385 }
378386 }
379387
380 fn parseId(p: *Parse, node: Ast.Node.Index) !u16 {
388 fn parseNonce(p: *Parse, node: Ast.Node.Index) !Package.Nonce {
381389 const ast = p.ast;
382390 const node_tags = ast.nodes.items(.tag);
383391 const main_tokens = ast.nodes.items(.main_token);
......@@ -387,20 +395,12 @@ const Parse = struct {
387395 }
388396 const token_bytes = ast.tokenSlice(main_token);
389397 const parsed = std.zig.parseNumberLiteral(token_bytes);
390 const n = switch (parsed) {
391 .int => |n| n,
392 .big_int, .float => return fail(p, main_token, "expected u16 integer literal, found {s}", .{
398 switch (parsed) {
399 .int => |n| return @bitCast(n),
400 .big_int, .float => return fail(p, main_token, "expected u64 integer literal, found {s}", .{
393401 @tagName(parsed),
394402 }),
395403 .failure => |err| return fail(p, main_token, "bad integer literal: {s}", .{@tagName(err)}),
396 };
397 const casted = std.math.cast(u16, n) orelse
398 return fail(p, main_token, "integer value {d} does not fit into u16", .{n});
399 switch (casted) {
400 0x0000, 0xffff => return fail(p, main_token, "id value 0x{x} reserved; use 0x{x} instead", .{
401 casted, Package.randomId(),
402 }),
403 else => return casted,
404404 }
405405 }
406406
src/main.zig+10-6
......@@ -4752,10 +4752,10 @@ fn cmdInit(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
47524752 };
47534753 var ok_count: usize = 0;
47544754
4755 const id = Package.randomId();
4755 const nonce: Package.Nonce = .generate(sanitized_root_name);
47564756
47574757 for (template_paths) |template_path| {
4758 if (templates.write(arena, fs.cwd(), sanitized_root_name, template_path, id)) |_| {
4758 if (templates.write(arena, fs.cwd(), sanitized_root_name, template_path, nonce)) |_| {
47594759 std.log.info("created {s}", .{template_path});
47604760 ok_count += 1;
47614761 } else |err| switch (err) {
......@@ -5225,6 +5225,8 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
52255225 .job_queue = &job_queue,
52265226 .omit_missing_hash_error = true,
52275227 .allow_missing_paths_field = false,
5228 .allow_missing_nonce = false,
5229 .allow_name_string = false,
52285230 .use_latest_commit = false,
52295231
52305232 .package_root = undefined,
......@@ -7125,6 +7127,8 @@ fn cmdFetch(
71257127 .job_queue = &job_queue,
71267128 .omit_missing_hash_error = true,
71277129 .allow_missing_paths_field = false,
7130 .allow_missing_nonce = true,
7131 .allow_name_string = true,
71287132 .use_latest_commit = true,
71297133
71307134 .package_root = undefined,
......@@ -7464,10 +7468,10 @@ fn loadManifest(
74647468 0,
74657469 ) catch |err| switch (err) {
74667470 error.FileNotFound => {
7467 const id = Package.randomId();
7471 const nonce: Package.Nonce = .generate(options.root_name);
74687472 var templates = findTemplates(gpa, arena);
74697473 defer templates.deinit();
7470 templates.write(arena, options.dir, options.root_name, Package.Manifest.basename, id) catch |e| {
7474 templates.write(arena, options.dir, options.root_name, Package.Manifest.basename, nonce) catch |e| {
74717475 fatal("unable to write {s}: {s}", .{
74727476 Package.Manifest.basename, @errorName(e),
74737477 });
......@@ -7525,7 +7529,7 @@ const Templates = struct {
75257529 out_dir: fs.Dir,
75267530 root_name: []const u8,
75277531 template_path: []const u8,
7528 id: u16,
7532 nonce: Package.Nonce,
75297533 ) !void {
75307534 if (fs.path.dirname(template_path)) |dirname| {
75317535 out_dir.makePath(dirname) catch |err| {
......@@ -7551,7 +7555,7 @@ const Templates = struct {
75517555 state = .start;
75527556 },
75537557 'i' => {
7554 try templates.buffer.writer().print("0x{x}", .{id});
7558 try templates.buffer.writer().print("0x{x}", .{nonce.int()});
75557559 state = .start;
75567560 },
75577561 'v' => {