authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-05-11 12:51:56-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2023-05-11 12:51:56-07:00
log6c41d435194d1777e1a2e50879c8d293a1f1e65f
treecfe2d3089306735067b9971bcb3fb921ed23863b
parentaa9ea612168836e2c2867dcd79c768155d1e3bfb
parent6e292f66db8e7b85ea8b116872b3ca074db7885a
signaturebadge-question-mark Signed by PGP key 4AEE18F83AFDEB23

Merge pull request #15641 from jacobly0/cache-race

Cache: fix race condition

1 files changed, 163 insertions(+), 188 deletions(-)

lib/std/Build/Cache.zig+163-188
...@@ -145,6 +145,8 @@ pub const bin_digest_len = 16;...@@ -145,6 +145,8 @@ pub const bin_digest_len = 16;
145pub const hex_digest_len = bin_digest_len * 2;145pub const hex_digest_len = bin_digest_len * 2;
146pub const BinDigest = [bin_digest_len]u8;146pub const BinDigest = [bin_digest_len]u8;
147147
148/// This is currently just an arbitrary non-empty string that can't match another manifest line.
149const manifest_header = "0";
148const manifest_file_size_max = 50 * 1024 * 1024;150const manifest_file_size_max = 50 * 1024 * 1024;
149151
150/// The type used for hashing file contents. Currently, this is SipHash128(1, 3), because it152/// The type used for hashing file contents. Currently, this is SipHash128(1, 3), because it
...@@ -152,8 +154,15 @@ const manifest_file_size_max = 50 * 1024 * 1024;...@@ -152,8 +154,15 @@ const manifest_file_size_max = 50 * 1024 * 1024;
152/// fastest options right now.154/// fastest options right now.
153pub const Hasher = crypto.auth.siphash.SipHash128(1, 3);155pub const Hasher = crypto.auth.siphash.SipHash128(1, 3);
154156
155/// Initial state, that can be copied.157/// Initial state with random bytes, that can be copied.
156pub const hasher_init: Hasher = Hasher.init(&[_]u8{0} ** Hasher.key_length);158/// Refresh this with new random bytes when the manifest
159/// format is modified in a non-backwards-compatible way.
160pub const hasher_init: Hasher = Hasher.init(&[_]u8{
161 0x33, 0x52, 0xa2, 0x84,
162 0xcf, 0x17, 0x56, 0x57,
163 0x01, 0xbb, 0xcd, 0xe4,
164 0x77, 0xd6, 0xf0, 0x60,
165});
157166
158pub const File = struct {167pub const File = struct {
159 prefixed_path: ?PrefixedPath,168 prefixed_path: ?PrefixedPath,
...@@ -391,208 +400,179 @@ pub const Manifest = struct {...@@ -391,208 +400,179 @@ pub const Manifest = struct {
391 @memcpy(manifest_file_path[0..self.hex_digest.len], &self.hex_digest);400 @memcpy(manifest_file_path[0..self.hex_digest.len], &self.hex_digest);
392 manifest_file_path[hex_digest_len..][0..ext.len].* = ext.*;401 manifest_file_path[hex_digest_len..][0..ext.len].* = ext.*;
393402
394 if (self.files.items.len == 0) {403 while (true) {
395 // If there are no file inputs, we check if the manifest file exists instead of404 if (self.cache.manifest_dir.createFile(&manifest_file_path, .{
396 // comparing the hashes on the files used for the cached item405 .read = true,
397 while (true) {406 .truncate = false,
398 if (self.cache.manifest_dir.openFile(&manifest_file_path, .{407 .lock = .Exclusive,
399 .mode = .read_write,408 .lock_nonblocking = self.want_shared_lock,
400 .lock = .Exclusive,409 })) |manifest_file| {
401 .lock_nonblocking = self.want_shared_lock,410 self.manifest_file = manifest_file;
402 })) |manifest_file| {411 self.have_exclusive_lock = true;
403 self.manifest_file = manifest_file;412 break;
404 self.have_exclusive_lock = true;413 } else |err| switch (err) {
405 break;414 error.WouldBlock => {
406 } else |open_err| switch (open_err) {415 self.manifest_file = try self.cache.manifest_dir.openFile(&manifest_file_path, .{
407 error.WouldBlock => {416 .mode = .read_write,
408 self.manifest_file = try self.cache.manifest_dir.openFile(&manifest_file_path, .{417 .lock = .Shared,
409 .lock = .Shared,418 });
410 });
411 break;
412 },
413 error.FileNotFound => {
414 if (self.cache.manifest_dir.createFile(&manifest_file_path, .{
415 .read = true,
416 .truncate = false,
417 .lock = .Exclusive,
418 .lock_nonblocking = self.want_shared_lock,
419 })) |manifest_file| {
420 self.manifest_file = manifest_file;
421 self.manifest_dirty = true;
422 self.have_exclusive_lock = true;
423 return false; // cache miss; exclusive lock already held
424 } else |err| switch (err) {
425 // There are no dir components, so you would think
426 // that this was unreachable, however we have
427 // observed on macOS two processes racing to do
428 // openat() with O_CREAT manifest in ENOENT.
429 error.WouldBlock, error.FileNotFound => continue,
430 else => |e| return e,
431 }
432 },
433 else => |e| return e,
434 }
435 }
436 } else {
437 while (true) {
438 if (self.cache.manifest_dir.createFile(&manifest_file_path, .{
439 .read = true,
440 .truncate = false,
441 .lock = .Exclusive,
442 .lock_nonblocking = self.want_shared_lock,
443 })) |manifest_file| {
444 self.manifest_file = manifest_file;
445 self.have_exclusive_lock = true;
446 break;419 break;
447 } else |err| switch (err) {420 },
448 error.WouldBlock => {421 // There are no dir components, so you would think that this was
449 self.manifest_file = try self.cache.manifest_dir.openFile(&manifest_file_path, .{422 // unreachable, however we have observed on macOS two processes racing
450 .lock = .Shared,423 // to do openat() with O_CREAT manifest in ENOENT.
451 });424 error.FileNotFound => continue,
452 break;425 else => |e| return e,
453 },
454 // There are no dir components, so you would think that this was
455 // unreachable, however we have observed on macOS two processes racing
456 // to do openat() with O_CREAT manifest in ENOENT.
457 error.FileNotFound => continue,
458 else => |e| return e,
459 }
460 }426 }
461 }427 }
462428
463 self.want_refresh_timestamp = true;429 self.want_refresh_timestamp = true;
464430
465 const file_contents = try self.manifest_file.?.reader().readAllAlloc(gpa, manifest_file_size_max);431 while (true) {
466 defer gpa.free(file_contents);432 const file_contents = try self.manifest_file.?.reader().readAllAlloc(gpa, manifest_file_size_max);
467433 defer gpa.free(file_contents);
468 const input_file_count = self.files.items.len;434
469 var any_file_changed = false;435 const input_file_count = self.files.items.len;
470 var line_iter = mem.tokenize(u8, file_contents, "\n");436 var any_file_changed = false;
471 var idx: usize = 0;437 var line_iter = mem.tokenize(u8, file_contents, "\n");
472 while (line_iter.next()) |line| {438 var idx: usize = 0;
473 defer idx += 1;439 if (if (line_iter.next()) |line| !std.mem.eql(u8, line, manifest_header) else true) {
474440 if (try self.upgradeToExclusiveLock()) continue;
475 const cache_hash_file = if (idx < input_file_count) &self.files.items[idx] else blk: {441 self.manifest_dirty = true;
476 const new = try self.files.addOne(gpa);442 while (idx < input_file_count) : (idx += 1) {
477 new.* = .{443 const ch_file = &self.files.items[idx];
478 .prefixed_path = null,444 self.populateFileHash(ch_file) catch |err| {
479 .contents = null,445 self.failed_file_index = idx;
480 .max_file_size = null,446 return err;
481 .stat = undefined,447 };
482 .bin_digest = undefined,
483 };
484 break :blk new;
485 };
486
487 var iter = mem.tokenize(u8, line, " ");
488 const size = iter.next() orelse return error.InvalidFormat;
489 const inode = iter.next() orelse return error.InvalidFormat;
490 const mtime_nsec_str = iter.next() orelse return error.InvalidFormat;
491 const digest_str = iter.next() orelse return error.InvalidFormat;
492 const prefix_str = iter.next() orelse return error.InvalidFormat;
493 const file_path = iter.rest();
494
495 cache_hash_file.stat.size = fmt.parseInt(u64, size, 10) catch return error.InvalidFormat;
496 cache_hash_file.stat.inode = fmt.parseInt(fs.File.INode, inode, 10) catch return error.InvalidFormat;
497 cache_hash_file.stat.mtime = fmt.parseInt(i64, mtime_nsec_str, 10) catch return error.InvalidFormat;
498 _ = fmt.hexToBytes(&cache_hash_file.bin_digest, digest_str) catch return error.InvalidFormat;
499 const prefix = fmt.parseInt(u8, prefix_str, 10) catch return error.InvalidFormat;
500 if (prefix >= self.cache.prefixes_len) return error.InvalidFormat;
501
502 if (file_path.len == 0) {
503 return error.InvalidFormat;
504 }
505 if (cache_hash_file.prefixed_path) |pp| {
506 if (pp.prefix != prefix or !mem.eql(u8, file_path, pp.sub_path)) {
507 return error.InvalidFormat;
508 }448 }
449 return false;
509 }450 }
510451 while (line_iter.next()) |line| {
511 if (cache_hash_file.prefixed_path == null) {452 defer idx += 1;
512 cache_hash_file.prefixed_path = .{453
513 .prefix = prefix,454 const cache_hash_file = if (idx < input_file_count) &self.files.items[idx] else blk: {
514 .sub_path = try gpa.dupe(u8, file_path),455 const new = try self.files.addOne(gpa);
456 new.* = .{
457 .prefixed_path = null,
458 .contents = null,
459 .max_file_size = null,
460 .stat = undefined,
461 .bin_digest = undefined,
462 };
463 break :blk new;
515 };464 };
516 }
517
518 const pp = cache_hash_file.prefixed_path.?;
519 const dir = self.cache.prefixes()[pp.prefix].handle;
520 const this_file = dir.openFile(pp.sub_path, .{ .mode = .read_only }) catch |err| switch (err) {
521 error.FileNotFound => {
522 try self.upgradeToExclusiveLock();
523 return false;
524 },
525 else => return error.CacheUnavailable,
526 };
527 defer this_file.close();
528465
529 const actual_stat = this_file.stat() catch |err| {466 var iter = mem.tokenize(u8, line, " ");
530 self.failed_file_index = idx;467 const size = iter.next() orelse return error.InvalidFormat;
531 return err;468 const inode = iter.next() orelse return error.InvalidFormat;
532 };469 const mtime_nsec_str = iter.next() orelse return error.InvalidFormat;
533 const size_match = actual_stat.size == cache_hash_file.stat.size;470 const digest_str = iter.next() orelse return error.InvalidFormat;
534 const mtime_match = actual_stat.mtime == cache_hash_file.stat.mtime;471 const prefix_str = iter.next() orelse return error.InvalidFormat;
535 const inode_match = actual_stat.inode == cache_hash_file.stat.inode;472 const file_path = iter.rest();
473
474 cache_hash_file.stat.size = fmt.parseInt(u64, size, 10) catch return error.InvalidFormat;
475 cache_hash_file.stat.inode = fmt.parseInt(fs.File.INode, inode, 10) catch return error.InvalidFormat;
476 cache_hash_file.stat.mtime = fmt.parseInt(i64, mtime_nsec_str, 10) catch return error.InvalidFormat;
477 _ = fmt.hexToBytes(&cache_hash_file.bin_digest, digest_str) catch return error.InvalidFormat;
478 const prefix = fmt.parseInt(u8, prefix_str, 10) catch return error.InvalidFormat;
479 if (prefix >= self.cache.prefixes_len) return error.InvalidFormat;
480
481 if (file_path.len == 0) {
482 return error.InvalidFormat;
483 }
484 if (cache_hash_file.prefixed_path) |pp| {
485 if (pp.prefix != prefix or !mem.eql(u8, file_path, pp.sub_path)) {
486 return error.InvalidFormat;
487 }
488 }
536489
537 if (!size_match or !mtime_match or !inode_match) {490 if (cache_hash_file.prefixed_path == null) {
538 self.manifest_dirty = true;491 cache_hash_file.prefixed_path = .{
492 .prefix = prefix,
493 .sub_path = try gpa.dupe(u8, file_path),
494 };
495 }
539496
540 cache_hash_file.stat = .{497 const pp = cache_hash_file.prefixed_path.?;
541 .size = actual_stat.size,498 const dir = self.cache.prefixes()[pp.prefix].handle;
542 .mtime = actual_stat.mtime,499 const this_file = dir.openFile(pp.sub_path, .{ .mode = .read_only }) catch |err| switch (err) {
543 .inode = actual_stat.inode,500 error.FileNotFound => {
501 if (try self.upgradeToExclusiveLock()) continue;
502 return false;
503 },
504 else => return error.CacheUnavailable,
544 };505 };
506 defer this_file.close();
545507
546 if (self.isProblematicTimestamp(cache_hash_file.stat.mtime)) {508 const actual_stat = this_file.stat() catch |err| {
547 // The actual file has an unreliable timestamp, force it to be hashed
548 cache_hash_file.stat.mtime = 0;
549 cache_hash_file.stat.inode = 0;
550 }
551
552 var actual_digest: BinDigest = undefined;
553 hashFile(this_file, &actual_digest) catch |err| {
554 self.failed_file_index = idx;509 self.failed_file_index = idx;
555 return err;510 return err;
556 };511 };
512 const size_match = actual_stat.size == cache_hash_file.stat.size;
513 const mtime_match = actual_stat.mtime == cache_hash_file.stat.mtime;
514 const inode_match = actual_stat.inode == cache_hash_file.stat.inode;
515
516 if (!size_match or !mtime_match or !inode_match) {
517 self.manifest_dirty = true;
518
519 cache_hash_file.stat = .{
520 .size = actual_stat.size,
521 .mtime = actual_stat.mtime,
522 .inode = actual_stat.inode,
523 };
524
525 if (self.isProblematicTimestamp(cache_hash_file.stat.mtime)) {
526 // The actual file has an unreliable timestamp, force it to be hashed
527 cache_hash_file.stat.mtime = 0;
528 cache_hash_file.stat.inode = 0;
529 }
530
531 var actual_digest: BinDigest = undefined;
532 hashFile(this_file, &actual_digest) catch |err| {
533 self.failed_file_index = idx;
534 return err;
535 };
536
537 if (!mem.eql(u8, &cache_hash_file.bin_digest, &actual_digest)) {
538 cache_hash_file.bin_digest = actual_digest;
539 // keep going until we have the input file digests
540 any_file_changed = true;
541 }
542 }
557543
558 if (!mem.eql(u8, &cache_hash_file.bin_digest, &actual_digest)) {544 if (!any_file_changed) {
559 cache_hash_file.bin_digest = actual_digest;545 self.hash.hasher.update(&cache_hash_file.bin_digest);
560 // keep going until we have the input file digests
561 any_file_changed = true;
562 }546 }
563 }547 }
564548
565 if (!any_file_changed) {549 if (any_file_changed) {
566 self.hash.hasher.update(&cache_hash_file.bin_digest);550 if (try self.upgradeToExclusiveLock()) continue;
551 // cache miss
552 // keep the manifest file open
553 self.unhit(bin_digest, input_file_count);
554 return false;
567 }555 }
568 }
569556
570 if (any_file_changed) {557 if (idx < input_file_count) {
571 // cache miss558 if (try self.upgradeToExclusiveLock()) continue;
572 // keep the manifest file open559 self.manifest_dirty = true;
573 self.unhit(bin_digest, input_file_count);560 while (idx < input_file_count) : (idx += 1) {
574 try self.upgradeToExclusiveLock();561 const ch_file = &self.files.items[idx];
575 return false;562 self.populateFileHash(ch_file) catch |err| {
576 }563 self.failed_file_index = idx;
564 return err;
565 };
566 }
567 return false;
568 }
577569
578 if (idx < input_file_count) {570 if (self.want_shared_lock) {
579 self.manifest_dirty = true;571 try self.downgradeToSharedLock();
580 while (idx < input_file_count) : (idx += 1) {
581 const ch_file = &self.files.items[idx];
582 self.populateFileHash(ch_file) catch |err| {
583 self.failed_file_index = idx;
584 return err;
585 };
586 }572 }
587 try self.upgradeToExclusiveLock();
588 return false;
589 }
590573
591 if (self.want_shared_lock) {574 return true;
592 try self.downgradeToSharedLock();
593 }575 }
594
595 return true;
596 }576 }
597577
598 pub fn unhit(self: *Manifest, bin_digest: BinDigest, input_file_count: usize) void {578 pub fn unhit(self: *Manifest, bin_digest: BinDigest, input_file_count: usize) void {
...@@ -854,19 +834,13 @@ pub const Manifest = struct {...@@ -854,19 +834,13 @@ pub const Manifest = struct {
854 defer contents.deinit();834 defer contents.deinit();
855835
856 const writer = contents.writer();836 const writer = contents.writer();
857 var encoded_digest: [hex_digest_len]u8 = undefined;837 try writer.writeAll(manifest_header ++ "\n");
858
859 for (self.files.items) |file| {838 for (self.files.items) |file| {
860 _ = fmt.bufPrint(839 try writer.print("{d} {d} {d} {} {d} {s}\n", .{
861 &encoded_digest,
862 "{s}",
863 .{fmt.fmtSliceHexLower(&file.bin_digest)},
864 ) catch unreachable;
865 try writer.print("{d} {d} {d} {s} {d} {s}\n", .{
866 file.stat.size,840 file.stat.size,
867 file.stat.inode,841 file.stat.inode,
868 file.stat.mtime,842 file.stat.mtime,
869 &encoded_digest,843 fmt.fmtSliceHexLower(&file.bin_digest),
870 file.prefixed_path.?.prefix,844 file.prefixed_path.?.prefix,
871 file.prefixed_path.?.sub_path,845 file.prefixed_path.?.sub_path,
872 });846 });
...@@ -895,8 +869,8 @@ pub const Manifest = struct {...@@ -895,8 +869,8 @@ pub const Manifest = struct {
895 self.have_exclusive_lock = false;869 self.have_exclusive_lock = false;
896 }870 }
897871
898 fn upgradeToExclusiveLock(self: *Manifest) !void {872 fn upgradeToExclusiveLock(self: *Manifest) !bool {
899 if (self.have_exclusive_lock) return;873 if (self.have_exclusive_lock) return false;
900 assert(self.manifest_file != null);874 assert(self.manifest_file != null);
901875
902 // WASI does not currently support flock, so we bypass it here.876 // WASI does not currently support flock, so we bypass it here.
...@@ -910,6 +884,7 @@ pub const Manifest = struct {...@@ -910,6 +884,7 @@ pub const Manifest = struct {
910 try manifest_file.lock(.Exclusive);884 try manifest_file.lock(.Exclusive);
911 }885 }
912 self.have_exclusive_lock = true;886 self.have_exclusive_lock = true;
887 return true;
913 }888 }
914889
915 /// Obtain only the data needed to maintain a lock on the manifest file.890 /// Obtain only the data needed to maintain a lock on the manifest file.