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;
145145pub const hex_digest_len = bin_digest_len * 2;
146146pub 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";
148150const manifest_file_size_max = 50 * 1024 * 1024;
149151
150152/// 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;
152154/// fastest options right now.
153155pub const Hasher = crypto.auth.siphash.SipHash128(1, 3);
154156
155/// Initial state, that can be copied.
156pub const hasher_init: Hasher = Hasher.init(&[_]u8{0} ** Hasher.key_length);
157/// Initial state with random bytes, that can be copied.
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
158167pub const File = struct {
159168 prefixed_path: ?PrefixedPath,
......@@ -391,208 +400,179 @@ pub const Manifest = struct {
391400 @memcpy(manifest_file_path[0..self.hex_digest.len], &self.hex_digest);
392401 manifest_file_path[hex_digest_len..][0..ext.len].* = ext.*;
393402
394 if (self.files.items.len == 0) {
395 // If there are no file inputs, we check if the manifest file exists instead of
396 // comparing the hashes on the files used for the cached item
397 while (true) {
398 if (self.cache.manifest_dir.openFile(&manifest_file_path, .{
399 .mode = .read_write,
400 .lock = .Exclusive,
401 .lock_nonblocking = self.want_shared_lock,
402 })) |manifest_file| {
403 self.manifest_file = manifest_file;
404 self.have_exclusive_lock = true;
405 break;
406 } else |open_err| switch (open_err) {
407 error.WouldBlock => {
408 self.manifest_file = try self.cache.manifest_dir.openFile(&manifest_file_path, .{
409 .lock = .Shared,
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;
403 while (true) {
404 if (self.cache.manifest_dir.createFile(&manifest_file_path, .{
405 .read = true,
406 .truncate = false,
407 .lock = .Exclusive,
408 .lock_nonblocking = self.want_shared_lock,
409 })) |manifest_file| {
410 self.manifest_file = manifest_file;
411 self.have_exclusive_lock = true;
412 break;
413 } else |err| switch (err) {
414 error.WouldBlock => {
415 self.manifest_file = try self.cache.manifest_dir.openFile(&manifest_file_path, .{
416 .mode = .read_write,
417 .lock = .Shared,
418 });
446419 break;
447 } else |err| switch (err) {
448 error.WouldBlock => {
449 self.manifest_file = try self.cache.manifest_dir.openFile(&manifest_file_path, .{
450 .lock = .Shared,
451 });
452 break;
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 }
420 },
421 // There are no dir components, so you would think that this was
422 // unreachable, however we have observed on macOS two processes racing
423 // to do openat() with O_CREAT manifest in ENOENT.
424 error.FileNotFound => continue,
425 else => |e| return e,
460426 }
461427 }
462428
463429 self.want_refresh_timestamp = true;
464430
465 const file_contents = try self.manifest_file.?.reader().readAllAlloc(gpa, manifest_file_size_max);
466 defer gpa.free(file_contents);
467
468 const input_file_count = self.files.items.len;
469 var any_file_changed = false;
470 var line_iter = mem.tokenize(u8, file_contents, "\n");
471 var idx: usize = 0;
472 while (line_iter.next()) |line| {
473 defer idx += 1;
474
475 const cache_hash_file = if (idx < input_file_count) &self.files.items[idx] else blk: {
476 const new = try self.files.addOne(gpa);
477 new.* = .{
478 .prefixed_path = null,
479 .contents = null,
480 .max_file_size = null,
481 .stat = undefined,
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;
431 while (true) {
432 const file_contents = try self.manifest_file.?.reader().readAllAlloc(gpa, manifest_file_size_max);
433 defer gpa.free(file_contents);
434
435 const input_file_count = self.files.items.len;
436 var any_file_changed = false;
437 var line_iter = mem.tokenize(u8, file_contents, "\n");
438 var idx: usize = 0;
439 if (if (line_iter.next()) |line| !std.mem.eql(u8, line, manifest_header) else true) {
440 if (try self.upgradeToExclusiveLock()) continue;
441 self.manifest_dirty = true;
442 while (idx < input_file_count) : (idx += 1) {
443 const ch_file = &self.files.items[idx];
444 self.populateFileHash(ch_file) catch |err| {
445 self.failed_file_index = idx;
446 return err;
447 };
508448 }
449 return false;
509450 }
510
511 if (cache_hash_file.prefixed_path == null) {
512 cache_hash_file.prefixed_path = .{
513 .prefix = prefix,
514 .sub_path = try gpa.dupe(u8, file_path),
451 while (line_iter.next()) |line| {
452 defer idx += 1;
453
454 const cache_hash_file = if (idx < input_file_count) &self.files.items[idx] else blk: {
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;
515464 };
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| {
530 self.failed_file_index = idx;
531 return err;
532 };
533 const size_match = actual_stat.size == cache_hash_file.stat.size;
534 const mtime_match = actual_stat.mtime == cache_hash_file.stat.mtime;
535 const inode_match = actual_stat.inode == cache_hash_file.stat.inode;
466 var iter = mem.tokenize(u8, line, " ");
467 const size = iter.next() orelse return error.InvalidFormat;
468 const inode = iter.next() orelse return error.InvalidFormat;
469 const mtime_nsec_str = iter.next() orelse return error.InvalidFormat;
470 const digest_str = iter.next() orelse return error.InvalidFormat;
471 const prefix_str = iter.next() orelse return error.InvalidFormat;
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) {
538 self.manifest_dirty = true;
490 if (cache_hash_file.prefixed_path == null) {
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 = .{
541 .size = actual_stat.size,
542 .mtime = actual_stat.mtime,
543 .inode = actual_stat.inode,
497 const pp = cache_hash_file.prefixed_path.?;
498 const dir = self.cache.prefixes()[pp.prefix].handle;
499 const this_file = dir.openFile(pp.sub_path, .{ .mode = .read_only }) catch |err| switch (err) {
500 error.FileNotFound => {
501 if (try self.upgradeToExclusiveLock()) continue;
502 return false;
503 },
504 else => return error.CacheUnavailable,
544505 };
506 defer this_file.close();
545507
546 if (self.isProblematicTimestamp(cache_hash_file.stat.mtime)) {
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| {
508 const actual_stat = this_file.stat() catch |err| {
554509 self.failed_file_index = idx;
555510 return err;
556511 };
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)) {
559 cache_hash_file.bin_digest = actual_digest;
560 // keep going until we have the input file digests
561 any_file_changed = true;
544 if (!any_file_changed) {
545 self.hash.hasher.update(&cache_hash_file.bin_digest);
562546 }
563547 }
564548
565 if (!any_file_changed) {
566 self.hash.hasher.update(&cache_hash_file.bin_digest);
549 if (any_file_changed) {
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;
567555 }
568 }
569556
570 if (any_file_changed) {
571 // cache miss
572 // keep the manifest file open
573 self.unhit(bin_digest, input_file_count);
574 try self.upgradeToExclusiveLock();
575 return false;
576 }
557 if (idx < input_file_count) {
558 if (try self.upgradeToExclusiveLock()) continue;
559 self.manifest_dirty = true;
560 while (idx < input_file_count) : (idx += 1) {
561 const ch_file = &self.files.items[idx];
562 self.populateFileHash(ch_file) catch |err| {
563 self.failed_file_index = idx;
564 return err;
565 };
566 }
567 return false;
568 }
577569
578 if (idx < input_file_count) {
579 self.manifest_dirty = true;
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 };
570 if (self.want_shared_lock) {
571 try self.downgradeToSharedLock();
586572 }
587 try self.upgradeToExclusiveLock();
588 return false;
589 }
590573
591 if (self.want_shared_lock) {
592 try self.downgradeToSharedLock();
574 return true;
593575 }
594
595 return true;
596576 }
597577
598578 pub fn unhit(self: *Manifest, bin_digest: BinDigest, input_file_count: usize) void {
......@@ -854,19 +834,13 @@ pub const Manifest = struct {
854834 defer contents.deinit();
855835
856836 const writer = contents.writer();
857 var encoded_digest: [hex_digest_len]u8 = undefined;
858
837 try writer.writeAll(manifest_header ++ "\n");
859838 for (self.files.items) |file| {
860 _ = fmt.bufPrint(
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", .{
839 try writer.print("{d} {d} {d} {} {d} {s}\n", .{
866840 file.stat.size,
867841 file.stat.inode,
868842 file.stat.mtime,
869 &encoded_digest,
843 fmt.fmtSliceHexLower(&file.bin_digest),
870844 file.prefixed_path.?.prefix,
871845 file.prefixed_path.?.sub_path,
872846 });
......@@ -895,8 +869,8 @@ pub const Manifest = struct {
895869 self.have_exclusive_lock = false;
896870 }
897871
898 fn upgradeToExclusiveLock(self: *Manifest) !void {
899 if (self.have_exclusive_lock) return;
872 fn upgradeToExclusiveLock(self: *Manifest) !bool {
873 if (self.have_exclusive_lock) return false;
900874 assert(self.manifest_file != null);
901875
902876 // WASI does not currently support flock, so we bypass it here.
......@@ -910,6 +884,7 @@ pub const Manifest = struct {
910884 try manifest_file.lock(.Exclusive);
911885 }
912886 self.have_exclusive_lock = true;
887 return true;
913888 }
914889
915890 /// Obtain only the data needed to maintain a lock on the manifest file.