| author | |
| committer | |
| log | 0fc7c9f57c0042cfe6dab9deb3b5fe2f9404d744 |
| tree | f554469401cd9df8a762a098dc0091a79e0caf7d |
| parent | a70307e7ffc608643bbd940796eaeb5bca6bbc8f |
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 @@ |
| 12 | 12 | }, |
| 13 | 13 | }, |
| 14 | 14 | .paths = .{""}, |
| 15 | .id = 0x1cb6, | |
| 15 | .nonce = 0xc1ce10810000f013, | |
| 16 | 16 | } |
doc/build.zig.zon.md+17-8| ... | ... | @@ -22,21 +22,30 @@ Zig package namespace. |
| 22 | 22 | |
| 23 | 23 | Must be a valid bare Zig identifier (don't `@` me), limited to 32 bytes. |
| 24 | 24 | |
| 25 | ### `id` | |
| 25 | ### `nonce` | |
| 26 | 26 | |
| 27 | 27 | Together with name, this represents a globally unique package identifier. This |
| 28 | field should be initialized with a 16-bit random number when the package is | |
| 29 | first created, and then *never change*. This allows Zig to unambiguously detect | |
| 30 | when one package is an updated version of another. | |
| 28 | field is auto-initialized by the toolchain when the package is first created, | |
| 29 | and then *never changes*. This allows Zig to unambiguously detect when one | |
| 30 | package is an updated version of another. | |
| 31 | 31 | |
| 32 | When forking a Zig project, this id should be regenerated with a new random | |
| 33 | number if the upstream project is still maintained. Otherwise, the fork is | |
| 34 | *hostile*, attempting to take control over the original project's identity. | |
| 32 | When forking a Zig project, this nonce should be regenerated if the upstream | |
| 33 | project is still maintained. Otherwise, the fork is *hostile*, attempting to | |
| 34 | take control over the original project's identity. The nonce can be regenerated | |
| 35 | by deleting the field and running `zig build`. | |
| 35 | 36 | |
| 36 | `0x0000` is invalid because it obviously means a random number wasn't used. | |
| 37 | This 64-bit integer is the combination of a 16-bit id component, a 32-bit | |
| 38 | checksum, and 16 bits of reserved zeroes. | |
| 39 | ||
| 40 | The id component within the nonce has these restrictions: | |
| 41 | ||
| 42 | `0x0000` is reserved for legacy packages. | |
| 37 | 43 | |
| 38 | 44 | `0xffff` is reserved to represent "naked" packages. |
| 39 | 45 | |
| 46 | The checksum is computed from `name` and serves to protect Zig users from | |
| 47 | accidental id collisions. | |
| 48 | ||
| 40 | 49 | ### `version` |
| 41 | 50 | |
| 42 | 51 | String. Required. |
lib/init/build.zig.zon+11-10| ... | ... | @@ -13,17 +13,18 @@ |
| 13 | 13 | .version = "0.0.0", |
| 14 | 14 | |
| 15 | 15 | // 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. | |
| 19 | 20 | // |
| 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. | |
| 27 | 28 | |
| 28 | 29 | // Tracks the earliest Zig version that the package considers to be a |
| 29 | 30 | // supported use case. |
src/Package.zig+27-6| ... | ... | @@ -10,9 +10,29 @@ pub const multihash_len = 1 + 1 + Hash.Algo.digest_length; |
| 10 | 10 | pub const multihash_hex_digest_len = 2 * multihash_len; |
| 11 | 11 | pub const MultiHashHexDigest = [multihash_hex_digest_len]u8; |
| 12 | 12 | |
| 13 | pub fn randomId() u16 { | |
| 14 | return std.crypto.random.intRangeLessThan(u16, 0x0001, 0xffff); | |
| 15 | } | |
| 13 | pub 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 | }; | |
| 16 | 36 | |
| 17 | 37 | /// A user-readable, file system safe hash that identifies an exact package |
| 18 | 38 | /// snapshot, including file contents. |
| ... | ... | @@ -72,9 +92,10 @@ pub const Hash = struct { |
| 72 | 92 | } |
| 73 | 93 | |
| 74 | 94 | /// 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 | |
| 78 | 99 | /// * hashplus is the following 39-byte array, base64 encoded using -_ to make |
| 79 | 100 | /// it filesystem safe: |
| 80 | 101 | /// - (2 bytes) LE u16 Package ID |
src/Package/Fetch.zig+9-3| ... | ... | @@ -44,6 +44,8 @@ omit_missing_hash_error: bool, |
| 44 | 44 | /// which specifies inclusion rules. This is intended to be true for the first |
| 45 | 45 | /// fetch task and false for the recursive dependencies. |
| 46 | 46 | allow_missing_paths_field: bool, |
| 47 | allow_missing_nonce: bool, | |
| 48 | allow_name_string: bool, | |
| 47 | 49 | /// If true and URL points to a Git repository, will use the latest commit. |
| 48 | 50 | use_latest_commit: bool, |
| 49 | 51 | |
| ... | ... | @@ -372,7 +374,7 @@ pub fn run(f: *Fetch) RunError!void { |
| 372 | 374 | }; |
| 373 | 375 | |
| 374 | 376 | 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; | |
| 376 | 378 | prefixed_pkg_sub_path_buffer[0] = 'p'; |
| 377 | 379 | prefixed_pkg_sub_path_buffer[1] = fs.path.sep; |
| 378 | 380 | const hash_slice = expected_hash.toSlice(); |
| ... | ... | @@ -647,8 +649,8 @@ fn loadManifest(f: *Fetch, pkg_root: Cache.Path) RunError!void { |
| 647 | 649 | |
| 648 | 650 | f.manifest = try Manifest.parse(arena, ast.*, .{ |
| 649 | 651 | .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, | |
| 652 | 654 | }); |
| 653 | 655 | const manifest = &f.manifest.?; |
| 654 | 656 | |
| ... | ... | @@ -750,6 +752,8 @@ fn queueJobsForDeps(f: *Fetch) RunError!void { |
| 750 | 752 | .job_queue = f.job_queue, |
| 751 | 753 | .omit_missing_hash_error = false, |
| 752 | 754 | .allow_missing_paths_field = true, |
| 755 | .allow_missing_nonce = true, | |
| 756 | .allow_name_string = true, | |
| 753 | 757 | .use_latest_commit = false, |
| 754 | 758 | |
| 755 | 759 | .package_root = undefined, |
| ... | ... | @@ -2319,6 +2323,8 @@ const TestFetchBuilder = struct { |
| 2319 | 2323 | .job_queue = &self.job_queue, |
| 2320 | 2324 | .omit_missing_hash_error = true, |
| 2321 | 2325 | .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 | |
| 2322 | 2328 | .use_latest_commit = true, |
| 2323 | 2329 | |
| 2324 | 2330 | .package_root = undefined, |
src/Package/Manifest.zig+25-25| ... | ... | @@ -52,7 +52,7 @@ pub const ParseOptions = struct { |
| 52 | 52 | /// Deprecated, to be removed after 0.14.0 is tagged. |
| 53 | 53 | allow_name_string: bool = true, |
| 54 | 54 | /// Deprecated, to be removed after 0.14.0 is tagged. |
| 55 | allow_missing_id: bool = true, | |
| 55 | allow_missing_nonce: bool = true, | |
| 56 | 56 | }; |
| 57 | 57 | |
| 58 | 58 | pub const Error = Allocator.Error; |
| ... | ... | @@ -81,7 +81,7 @@ pub fn parse(gpa: Allocator, ast: Ast, options: ParseOptions) Error!Manifest { |
| 81 | 81 | .paths = .{}, |
| 82 | 82 | .allow_missing_paths_field = options.allow_missing_paths_field, |
| 83 | 83 | .allow_name_string = options.allow_name_string, |
| 84 | .allow_missing_id = options.allow_missing_id, | |
| 84 | .allow_missing_nonce = options.allow_missing_nonce, | |
| 85 | 85 | .minimum_zig_version = null, |
| 86 | 86 | .buf = .{}, |
| 87 | 87 | }; |
| ... | ... | @@ -157,7 +157,7 @@ const Parse = struct { |
| 157 | 157 | paths: std.StringArrayHashMapUnmanaged(void), |
| 158 | 158 | allow_missing_paths_field: bool, |
| 159 | 159 | allow_name_string: bool, |
| 160 | allow_missing_id: bool, | |
| 160 | allow_missing_nonce: bool, | |
| 161 | 161 | minimum_zig_version: ?std.SemanticVersion, |
| 162 | 162 | |
| 163 | 163 | const InnerError = error{ ParseFailure, OutOfMemory }; |
| ... | ... | @@ -175,7 +175,7 @@ const Parse = struct { |
| 175 | 175 | var have_name = false; |
| 176 | 176 | var have_version = false; |
| 177 | 177 | var have_included_paths = false; |
| 178 | var have_id = false; | |
| 178 | var nonce: ?Package.Nonce = null; | |
| 179 | 179 | |
| 180 | 180 | for (struct_init.ast.fields) |field_init| { |
| 181 | 181 | const name_token = ast.firstToken(field_init) - 2; |
| ... | ... | @@ -192,9 +192,8 @@ const Parse = struct { |
| 192 | 192 | } else if (mem.eql(u8, field_name, "name")) { |
| 193 | 193 | p.name = try parseName(p, field_init); |
| 194 | 194 | 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); | |
| 198 | 197 | } else if (mem.eql(u8, field_name, "version")) { |
| 199 | 198 | p.version_node = field_init; |
| 200 | 199 | const version_text = try parseString(p, field_init); |
| ... | ... | @@ -218,14 +217,23 @@ const Parse = struct { |
| 218 | 217 | } |
| 219 | 218 | } |
| 220 | 219 | |
| 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 | ||
| 227 | 220 | if (!have_name) { |
| 228 | 221 | 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 | } | |
| 229 | 237 | } |
| 230 | 238 | |
| 231 | 239 | if (!have_version) { |
| ... | ... | @@ -377,7 +385,7 @@ const Parse = struct { |
| 377 | 385 | } |
| 378 | 386 | } |
| 379 | 387 | |
| 380 | fn parseId(p: *Parse, node: Ast.Node.Index) !u16 { | |
| 388 | fn parseNonce(p: *Parse, node: Ast.Node.Index) !Package.Nonce { | |
| 381 | 389 | const ast = p.ast; |
| 382 | 390 | const node_tags = ast.nodes.items(.tag); |
| 383 | 391 | const main_tokens = ast.nodes.items(.main_token); |
| ... | ... | @@ -387,20 +395,12 @@ const Parse = struct { |
| 387 | 395 | } |
| 388 | 396 | const token_bytes = ast.tokenSlice(main_token); |
| 389 | 397 | 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}", .{ | |
| 393 | 401 | @tagName(parsed), |
| 394 | 402 | }), |
| 395 | 403 | .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, | |
| 404 | 404 | } |
| 405 | 405 | } |
| 406 | 406 |
src/main.zig+10-6| ... | ... | @@ -4752,10 +4752,10 @@ fn cmdInit(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { |
| 4752 | 4752 | }; |
| 4753 | 4753 | var ok_count: usize = 0; |
| 4754 | 4754 | |
| 4755 | const id = Package.randomId(); | |
| 4755 | const nonce: Package.Nonce = .generate(sanitized_root_name); | |
| 4756 | 4756 | |
| 4757 | 4757 | 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)) |_| { | |
| 4759 | 4759 | std.log.info("created {s}", .{template_path}); |
| 4760 | 4760 | ok_count += 1; |
| 4761 | 4761 | } else |err| switch (err) { |
| ... | ... | @@ -5225,6 +5225,8 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { |
| 5225 | 5225 | .job_queue = &job_queue, |
| 5226 | 5226 | .omit_missing_hash_error = true, |
| 5227 | 5227 | .allow_missing_paths_field = false, |
| 5228 | .allow_missing_nonce = false, | |
| 5229 | .allow_name_string = false, | |
| 5228 | 5230 | .use_latest_commit = false, |
| 5229 | 5231 | |
| 5230 | 5232 | .package_root = undefined, |
| ... | ... | @@ -7125,6 +7127,8 @@ fn cmdFetch( |
| 7125 | 7127 | .job_queue = &job_queue, |
| 7126 | 7128 | .omit_missing_hash_error = true, |
| 7127 | 7129 | .allow_missing_paths_field = false, |
| 7130 | .allow_missing_nonce = true, | |
| 7131 | .allow_name_string = true, | |
| 7128 | 7132 | .use_latest_commit = true, |
| 7129 | 7133 | |
| 7130 | 7134 | .package_root = undefined, |
| ... | ... | @@ -7464,10 +7468,10 @@ fn loadManifest( |
| 7464 | 7468 | 0, |
| 7465 | 7469 | ) catch |err| switch (err) { |
| 7466 | 7470 | error.FileNotFound => { |
| 7467 | const id = Package.randomId(); | |
| 7471 | const nonce: Package.Nonce = .generate(options.root_name); | |
| 7468 | 7472 | var templates = findTemplates(gpa, arena); |
| 7469 | 7473 | 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| { | |
| 7471 | 7475 | fatal("unable to write {s}: {s}", .{ |
| 7472 | 7476 | Package.Manifest.basename, @errorName(e), |
| 7473 | 7477 | }); |
| ... | ... | @@ -7525,7 +7529,7 @@ const Templates = struct { |
| 7525 | 7529 | out_dir: fs.Dir, |
| 7526 | 7530 | root_name: []const u8, |
| 7527 | 7531 | template_path: []const u8, |
| 7528 | id: u16, | |
| 7532 | nonce: Package.Nonce, | |
| 7529 | 7533 | ) !void { |
| 7530 | 7534 | if (fs.path.dirname(template_path)) |dirname| { |
| 7531 | 7535 | out_dir.makePath(dirname) catch |err| { |
| ... | ... | @@ -7551,7 +7555,7 @@ const Templates = struct { |
| 7551 | 7555 | state = .start; |
| 7552 | 7556 | }, |
| 7553 | 7557 | 'i' => { |
| 7554 | try templates.buffer.writer().print("0x{x}", .{id}); | |
| 7558 | try templates.buffer.writer().print("0x{x}", .{nonce.int()}); | |
| 7555 | 7559 | state = .start; |
| 7556 | 7560 | }, |
| 7557 | 7561 | 'v' => { |