authorgravatar for jacobly@ziglang.orgJacob Young <jacobly@ziglang.org> 2023-05-10 01:52:33-04:00
committergravatar for jacobly@ziglang.orgJacob Young <jacobly@ziglang.org> 2023-05-11 00:55:40-04:00
logfae6290387850bfddbe4e9c1255750a65568b763
tree3812c9ca4ee0436fe3dd77b1d7b237313fb666bb
parent2d2d79a05b8423c7638348dc5a89793c1e0cafce

Cache: fix race condition

When checking a cache entry with no input files for a hit, if `createFile` returned `error.WouldBlock` we would forget about the fact that the file has been created, and all future checks will assume that a cache hit has happened, even though one never has or does, leading to rare `FileNotFound` errors trying the access the protected files. This fix works by writing an extra byte to the manifest file to distinguish hits and misses when there no input files to write.

1 files changed, 48 insertions(+), 76 deletions(-)

lib/std/Build/Cache.zig+48-76
...@@ -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,72 +400,29 @@ pub const Manifest = struct {...@@ -391,72 +400,29 @@ 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
...@@ -469,6 +435,18 @@ pub const Manifest = struct {...@@ -469,6 +435,18 @@ pub const Manifest = struct {
469 var any_file_changed = false;435 var any_file_changed = false;
470 var line_iter = mem.tokenize(u8, file_contents, "\n");436 var line_iter = mem.tokenize(u8, file_contents, "\n");
471 var idx: usize = 0;437 var idx: usize = 0;
438 if (if (line_iter.next()) |line| !std.mem.eql(u8, line, manifest_header) else true) {
439 self.manifest_dirty = true;
440 while (idx < input_file_count) : (idx += 1) {
441 const ch_file = &self.files.items[idx];
442 self.populateFileHash(ch_file) catch |err| {
443 self.failed_file_index = idx;
444 return err;
445 };
446 }
447 try self.upgradeToExclusiveLock();
448 return false;
449 }
472 while (line_iter.next()) |line| {450 while (line_iter.next()) |line| {
473 defer idx += 1;451 defer idx += 1;
474452
...@@ -854,19 +832,13 @@ pub const Manifest = struct {...@@ -854,19 +832,13 @@ pub const Manifest = struct {
854 defer contents.deinit();832 defer contents.deinit();
855833
856 const writer = contents.writer();834 const writer = contents.writer();
857 var encoded_digest: [hex_digest_len]u8 = undefined;835 try writer.writeAll(manifest_header ++ "\n");
858
859 for (self.files.items) |file| {836 for (self.files.items) |file| {
860 _ = fmt.bufPrint(837 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,838 file.stat.size,
867 file.stat.inode,839 file.stat.inode,
868 file.stat.mtime,840 file.stat.mtime,
869 &encoded_digest,841 fmt.fmtSliceHexLower(&file.bin_digest),
870 file.prefixed_path.?.prefix,842 file.prefixed_path.?.prefix,
871 file.prefixed_path.?.sub_path,843 file.prefixed_path.?.sub_path,
872 });844 });