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 {
39353935 for (zcu.failed_imports.items) |failed| {
39363936 assert(zcu.alive_files.contains(failed.file_index)); // otherwise it wouldn't have been added
39373937 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 };
39423938 const tree = file.getTree(zcu) catch |err| {
39433939 try unableToLoadZcuFile(zcu, &bundle, file, err);
39443940 continue;
39453941 };
39463942 const start = tree.tokenStart(failed.import_token);
39473943 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);
39493945 try bundle.addRootErrorMessage(.{
39503946 .msg = switch (failed.kind) {
39513947 .file_outside_module_root => try bundle.addString("import of file outside module path"),
......@@ -4338,7 +4334,7 @@ pub fn addModuleErrorMsg(
43384334 const err_span = err_src_loc.span(zcu) catch |err| {
43394335 return unableToLoadZcuFile(zcu, eb, err_src_loc.file_scope, err);
43404336 };
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
43434339 var ref_traces: std.ArrayListUnmanaged(ErrorBundle.ReferenceTrace) = .empty;
43444340 defer ref_traces.deinit(gpa);
......@@ -4434,7 +4430,7 @@ pub fn addModuleErrorMsg(
44344430 const span = note_src_loc.span(zcu) catch |err| {
44354431 return unableToLoadZcuFile(zcu, eb, note_src_loc.file_scope, err);
44364432 };
4437 const loc = std.zig.findLineColumn(source.bytes, span.main);
4433 const loc = std.zig.findLineColumn(source, span.main);
44384434
44394435 const omit_source_line = loc.eql(err_loc) or (last_note_loc != null and loc.eql(last_note_loc.?));
44404436 last_note_loc = loc;
......@@ -4489,7 +4485,7 @@ fn addReferenceTraceFrame(
44894485 try unableToLoadZcuFile(zcu, eb, src.file_scope, err);
44904486 return error.AlreadyReported;
44914487 };
4492 const loc = std.zig.findLineColumn(source.bytes, span.main);
4488 const loc = std.zig.findLineColumn(source, span.main);
44934489 try ref_traces.append(gpa, .{
44944490 .decl_name = try eb.printString("{s}{s}", .{ name, if (inlined) " [inlined]" else "" }),
44954491 .src_loc = try eb.addSourceLocation(.{
......@@ -4545,8 +4541,13 @@ pub fn unableToLoadZcuFile(
45454541 file: *Zcu.File,
45464542 err: Zcu.File.GetSourceError,
45474543) 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 };
45484549 try eb.addRootErrorMessage(.{
4549 .msg = try eb.printString("unable to load: {t}", .{err}),
4550 .msg = msg,
45504551 .src_loc = try file.errorBundleWholeFileSrc(zcu, eb),
45514552 });
45524553}
src/Zcu.zig+38-30
......@@ -925,8 +925,11 @@ pub const File = struct {
925925 /// allocated into `gpa`.
926926 path: Compilation.Path,
927927
928 /// Populated only when emitting error messages; see `getSource`.
928929 source: ?[:0]const u8,
930 /// Populated only when emitting error messages; see `getTree`.
929931 tree: ?Ast,
932
930933 zir: ?Zir,
931934 zoir: ?Zoir,
932935
......@@ -1033,25 +1036,27 @@ pub const File = struct {
10331036 }
10341037 }
10351038
1036 pub const Source = struct {
1037 bytes: [:0]const u8,
1038 stat: Cache.File.Stat,
1039 };
1040
10411039 pub const GetSourceError = error{
10421040 OutOfMemory,
1043 FileTooBig,
1044 Streaming,
1045 } || std.fs.File.OpenError || std.fs.File.ReadError;
1041 FileChanged,
1042 } || std.Io.File.OpenError || std.Io.File.Reader.Error;
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 {
10481048 const gpa = zcu.gpa;
10491049 const io = zcu.comp.io;
10501050
1051 if (file.source) |source| return .{
1052 .bytes = source,
1053 .stat = file.stat,
1054 };
1051 if (file.source) |source| return source;
1052
1053 switch (file.status) {
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
10561061 var f = f: {
10571062 const dir, const sub_path = file.path.openInfo(zcu.comp.dirs);
......@@ -1059,40 +1064,43 @@ pub const File = struct {
10591064 };
10601065 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))
1065 return error.FileTooBig;
1076 if (stat.inode != file.stat.inode or
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);
10681084 errdefer gpa.free(source);
10691085
10701086 var file_reader = f.reader(io, &.{});
10711087 file_reader.size = stat.size;
10721088 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
10781090 file.source = source;
10791091 errdefer comptime unreachable; // don't error after populating `source`
10801092
1081 return .{
1082 .bytes = source,
1083 .stat = .{
1084 .size = stat.size,
1085 .inode = stat.inode,
1086 .mtime = stat.mtime,
1087 },
1088 };
1093 return source;
10891094 }
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.
10911099 pub fn getTree(file: *File, zcu: *const Zcu) GetSourceError!*const Ast {
10921100 if (file.tree) |*tree| return tree;
10931101
10941102 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());
10961104 return &file.tree.?;
10971105 }
10981106