authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-11-18 12:03:06+00:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-11-19 09:44:22+00:00
log806470b492f15c4e3350ce4a48e607f2c8e94ff8
tree3bbc7184dc62cc4f1e583c9e907620517f44e4c7
parent4ea472808437c932f6240c0df5e4d5912d282052
signaturelock-open Commit is signed but in an unrecognized format.

compiler: fix crash if file contents change during update

When reporting a compile error, we would load the new file, but assume we could apply old AST/token indices (etc) to it, potentially causing crashes. Instead, if the file stat has changed since it was loaded, just emit an error that the file was modified mid-update.

2 files changed, 48 insertions(+), 39 deletions(-)

src/Compilation.zig+10-9
...@@ -3935,17 +3935,13 @@ pub fn getAllErrorsAlloc(comp: *Compilation) error{OutOfMemory}!ErrorBundle {...@@ -3935,17 +3935,13 @@ pub fn getAllErrorsAlloc(comp: *Compilation) error{OutOfMemory}!ErrorBundle {
3935 for (zcu.failed_imports.items) |failed| {3935 for (zcu.failed_imports.items) |failed| {
3936 assert(zcu.alive_files.contains(failed.file_index)); // otherwise it wouldn't have been added3936 assert(zcu.alive_files.contains(failed.file_index)); // otherwise it wouldn't have been added
3937 const file = zcu.fileByIndex(failed.file_index);3937 const file = zcu.fileByIndex(failed.file_index);
3938 const source = file.getSource(zcu) catch |err| {
3939 try unableToLoadZcuFile(zcu, &bundle, file, err);
3940 continue;
3941 };
3942 const tree = file.getTree(zcu) catch |err| {3938 const tree = file.getTree(zcu) catch |err| {
3943 try unableToLoadZcuFile(zcu, &bundle, file, err);3939 try unableToLoadZcuFile(zcu, &bundle, file, err);
3944 continue;3940 continue;
3945 };3941 };
3946 const start = tree.tokenStart(failed.import_token);3942 const start = tree.tokenStart(failed.import_token);
3947 const end = start + tree.tokenSlice(failed.import_token).len;3943 const end = start + tree.tokenSlice(failed.import_token).len;
3948 const loc = std.zig.findLineColumn(source.bytes, start);3944 const loc = std.zig.findLineColumn(tree.source, start);
3949 try bundle.addRootErrorMessage(.{3945 try bundle.addRootErrorMessage(.{
3950 .msg = switch (failed.kind) {3946 .msg = switch (failed.kind) {
3951 .file_outside_module_root => try bundle.addString("import of file outside module path"),3947 .file_outside_module_root => try bundle.addString("import of file outside module path"),
...@@ -4338,7 +4334,7 @@ pub fn addModuleErrorMsg(...@@ -4338,7 +4334,7 @@ pub fn addModuleErrorMsg(
4338 const err_span = err_src_loc.span(zcu) catch |err| {4334 const err_span = err_src_loc.span(zcu) catch |err| {
4339 return unableToLoadZcuFile(zcu, eb, err_src_loc.file_scope, err);4335 return unableToLoadZcuFile(zcu, eb, err_src_loc.file_scope, err);
4340 };4336 };
4341 const err_loc = std.zig.findLineColumn(err_source.bytes, err_span.main);4337 const err_loc = std.zig.findLineColumn(err_source, err_span.main);
43424338
4343 var ref_traces: std.ArrayListUnmanaged(ErrorBundle.ReferenceTrace) = .empty;4339 var ref_traces: std.ArrayListUnmanaged(ErrorBundle.ReferenceTrace) = .empty;
4344 defer ref_traces.deinit(gpa);4340 defer ref_traces.deinit(gpa);
...@@ -4434,7 +4430,7 @@ pub fn addModuleErrorMsg(...@@ -4434,7 +4430,7 @@ pub fn addModuleErrorMsg(
4434 const span = note_src_loc.span(zcu) catch |err| {4430 const span = note_src_loc.span(zcu) catch |err| {
4435 return unableToLoadZcuFile(zcu, eb, note_src_loc.file_scope, err);4431 return unableToLoadZcuFile(zcu, eb, note_src_loc.file_scope, err);
4436 };4432 };
4437 const loc = std.zig.findLineColumn(source.bytes, span.main);4433 const loc = std.zig.findLineColumn(source, span.main);
44384434
4439 const omit_source_line = loc.eql(err_loc) or (last_note_loc != null and loc.eql(last_note_loc.?));4435 const omit_source_line = loc.eql(err_loc) or (last_note_loc != null and loc.eql(last_note_loc.?));
4440 last_note_loc = loc;4436 last_note_loc = loc;
...@@ -4489,7 +4485,7 @@ fn addReferenceTraceFrame(...@@ -4489,7 +4485,7 @@ fn addReferenceTraceFrame(
4489 try unableToLoadZcuFile(zcu, eb, src.file_scope, err);4485 try unableToLoadZcuFile(zcu, eb, src.file_scope, err);
4490 return error.AlreadyReported;4486 return error.AlreadyReported;
4491 };4487 };
4492 const loc = std.zig.findLineColumn(source.bytes, span.main);4488 const loc = std.zig.findLineColumn(source, span.main);
4493 try ref_traces.append(gpa, .{4489 try ref_traces.append(gpa, .{
4494 .decl_name = try eb.printString("{s}{s}", .{ name, if (inlined) " [inlined]" else "" }),4490 .decl_name = try eb.printString("{s}{s}", .{ name, if (inlined) " [inlined]" else "" }),
4495 .src_loc = try eb.addSourceLocation(.{4491 .src_loc = try eb.addSourceLocation(.{
...@@ -4545,8 +4541,13 @@ pub fn unableToLoadZcuFile(...@@ -4545,8 +4541,13 @@ pub fn unableToLoadZcuFile(
4545 file: *Zcu.File,4541 file: *Zcu.File,
4546 err: Zcu.File.GetSourceError,4542 err: Zcu.File.GetSourceError,
4547) Allocator.Error!void {4543) Allocator.Error!void {
4544 const msg = switch (err) {
4545 error.OutOfMemory => |e| return e,
4546 error.FileChanged => try eb.addString("file contents changed during update"),
4547 else => |e| try eb.printString("unable to load: {t}", .{e}),
4548 };
4548 try eb.addRootErrorMessage(.{4549 try eb.addRootErrorMessage(.{
4549 .msg = try eb.printString("unable to load: {t}", .{err}),4550 .msg = msg,
4550 .src_loc = try file.errorBundleWholeFileSrc(zcu, eb),4551 .src_loc = try file.errorBundleWholeFileSrc(zcu, eb),
4551 });4552 });
4552}4553}
src/Zcu.zig+38-30
...@@ -925,8 +925,11 @@ pub const File = struct {...@@ -925,8 +925,11 @@ pub const File = struct {
925 /// allocated into `gpa`.925 /// allocated into `gpa`.
926 path: Compilation.Path,926 path: Compilation.Path,
927927
928 /// Populated only when emitting error messages; see `getSource`.
928 source: ?[:0]const u8,929 source: ?[:0]const u8,
930 /// Populated only when emitting error messages; see `getTree`.
929 tree: ?Ast,931 tree: ?Ast,
932
930 zir: ?Zir,933 zir: ?Zir,
931 zoir: ?Zoir,934 zoir: ?Zoir,
932935
...@@ -1033,25 +1036,27 @@ pub const File = struct {...@@ -1033,25 +1036,27 @@ pub const File = struct {
1033 }1036 }
1034 }1037 }
10351038
1036 pub const Source = struct {
1037 bytes: [:0]const u8,
1038 stat: Cache.File.Stat,
1039 };
1040
1041 pub const GetSourceError = error{1039 pub const GetSourceError = error{
1042 OutOfMemory,1040 OutOfMemory,
1043 FileTooBig,1041 FileChanged,
1044 Streaming,1042 } || std.Io.File.OpenError || std.Io.File.Reader.Error;
1045 } || std.fs.File.OpenError || std.fs.File.ReadError;
10461043
1047 pub fn getSource(file: *File, zcu: *const Zcu) GetSourceError!Source {1044 /// This must only be called in error conditions where `stat` *is* populated. It returns the
1045 /// contents of the source file, assuming the stat has not changed since it was originally
1046 /// loaded.
1047 pub fn getSource(file: *File, zcu: *const Zcu) GetSourceError![:0]const u8 {
1048 const gpa = zcu.gpa;1048 const gpa = zcu.gpa;
1049 const io = zcu.comp.io;1049 const io = zcu.comp.io;
10501050
1051 if (file.source) |source| return .{1051 if (file.source) |source| return source;
1052 .bytes = source,1052
1053 .stat = file.stat,1053 switch (file.status) {
1054 };1054 .never_loaded => unreachable, // stat must be populated
1055 .retryable_failure => unreachable, // stat must be populated
1056 .astgen_failure, .success => {},
1057 }
1058
1059 assert(file.stat.size <= std.math.maxInt(u32)); // `PerThread.updateFile` checks this
10551060
1056 var f = f: {1061 var f = f: {
1057 const dir, const sub_path = file.path.openInfo(zcu.comp.dirs);1062 const dir, const sub_path = file.path.openInfo(zcu.comp.dirs);
...@@ -1059,40 +1064,43 @@ pub const File = struct {...@@ -1059,40 +1064,43 @@ pub const File = struct {
1059 };1064 };
1060 defer f.close();1065 defer f.close();
10611066
1062 const stat = try f.stat();1067 const stat = f.stat() catch |err| switch (err) {
1068 error.Streaming => {
1069 // Since `file.stat` is populated, this was previously a file stream; since it is
1070 // now not a file stream, it must have changed.
1071 return error.FileChanged;
1072 },
1073 else => |e| return e,
1074 };
10631075
1064 if (stat.size > std.math.maxInt(u32))1076 if (stat.inode != file.stat.inode or
1065 return error.FileTooBig;1077 stat.size != file.stat.size or
1078 stat.mtime.nanoseconds != file.stat.mtime.nanoseconds)
1079 {
1080 return error.FileChanged;
1081 }
10661082
1067 const source = try gpa.allocSentinel(u8, @intCast(stat.size), 0);1083 const source = try gpa.allocSentinel(u8, @intCast(file.stat.size), 0);
1068 errdefer gpa.free(source);1084 errdefer gpa.free(source);
10691085
1070 var file_reader = f.reader(io, &.{});1086 var file_reader = f.reader(io, &.{});
1071 file_reader.size = stat.size;1087 file_reader.size = stat.size;
1072 file_reader.interface.readSliceAll(source) catch return file_reader.err.?;1088 file_reader.interface.readSliceAll(source) catch return file_reader.err.?;
10731089
1074 // Here we do not modify stat fields because this function is the one
1075 // used for error reporting. We need to keep the stat fields stale so that
1076 // updateFile can know to regenerate ZIR.
1077
1078 file.source = source;1090 file.source = source;
1079 errdefer comptime unreachable; // don't error after populating `source`1091 errdefer comptime unreachable; // don't error after populating `source`
10801092
1081 return .{1093 return source;
1082 .bytes = source,
1083 .stat = .{
1084 .size = stat.size,
1085 .inode = stat.inode,
1086 .mtime = stat.mtime,
1087 },
1088 };
1089 }1094 }
10901095
1096 /// This must only be called in error conditions where `stat` *is* populated. It returns the
1097 /// parsed AST of the source file, assuming the stat has not changed since it was originally
1098 /// loaded.
1091 pub fn getTree(file: *File, zcu: *const Zcu) GetSourceError!*const Ast {1099 pub fn getTree(file: *File, zcu: *const Zcu) GetSourceError!*const Ast {
1092 if (file.tree) |*tree| return tree;1100 if (file.tree) |*tree| return tree;
10931101
1094 const source = try file.getSource(zcu);1102 const source = try file.getSource(zcu);
1095 file.tree = try .parse(zcu.gpa, source.bytes, file.getMode());1103 file.tree = try .parse(zcu.gpa, source, file.getMode());
1096 return &file.tree.?;1104 return &file.tree.?;
1097 }1105 }
10981106