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.
This commit is contained in:
Matthew Lugg 2025-11-18 12:03:06 +00:00
parent 4ea4728084
commit 806470b492
No known key found for this signature in database
GPG key ID: 3F5B7DCCBF4AF02E
2 changed files with 48 additions and 39 deletions

View file

@ -3935,17 +3935,13 @@ pub fn getAllErrorsAlloc(comp: *Compilation) error{OutOfMemory}!ErrorBundle {
for (zcu.failed_imports.items) |failed| {
assert(zcu.alive_files.contains(failed.file_index)); // otherwise it wouldn't have been added
const file = zcu.fileByIndex(failed.file_index);
const source = file.getSource(zcu) catch |err| {
try unableToLoadZcuFile(zcu, &bundle, file, err);
continue;
};
const tree = file.getTree(zcu) catch |err| {
try unableToLoadZcuFile(zcu, &bundle, file, err);
continue;
};
const start = tree.tokenStart(failed.import_token);
const end = start + tree.tokenSlice(failed.import_token).len;
const loc = std.zig.findLineColumn(source.bytes, start);
const loc = std.zig.findLineColumn(tree.source, start);
try bundle.addRootErrorMessage(.{
.msg = switch (failed.kind) {
.file_outside_module_root => try bundle.addString("import of file outside module path"),
@ -4338,7 +4334,7 @@ pub fn addModuleErrorMsg(
const err_span = err_src_loc.span(zcu) catch |err| {
return unableToLoadZcuFile(zcu, eb, err_src_loc.file_scope, err);
};
const err_loc = std.zig.findLineColumn(err_source.bytes, err_span.main);
const err_loc = std.zig.findLineColumn(err_source, err_span.main);
var ref_traces: std.ArrayListUnmanaged(ErrorBundle.ReferenceTrace) = .empty;
defer ref_traces.deinit(gpa);
@ -4434,7 +4430,7 @@ pub fn addModuleErrorMsg(
const span = note_src_loc.span(zcu) catch |err| {
return unableToLoadZcuFile(zcu, eb, note_src_loc.file_scope, err);
};
const loc = std.zig.findLineColumn(source.bytes, span.main);
const loc = std.zig.findLineColumn(source, span.main);
const omit_source_line = loc.eql(err_loc) or (last_note_loc != null and loc.eql(last_note_loc.?));
last_note_loc = loc;
@ -4489,7 +4485,7 @@ fn addReferenceTraceFrame(
try unableToLoadZcuFile(zcu, eb, src.file_scope, err);
return error.AlreadyReported;
};
const loc = std.zig.findLineColumn(source.bytes, span.main);
const loc = std.zig.findLineColumn(source, span.main);
try ref_traces.append(gpa, .{
.decl_name = try eb.printString("{s}{s}", .{ name, if (inlined) " [inlined]" else "" }),
.src_loc = try eb.addSourceLocation(.{
@ -4545,8 +4541,13 @@ pub fn unableToLoadZcuFile(
file: *Zcu.File,
err: Zcu.File.GetSourceError,
) Allocator.Error!void {
const msg = switch (err) {
error.OutOfMemory => |e| return e,
error.FileChanged => try eb.addString("file contents changed during update"),
else => |e| try eb.printString("unable to load: {t}", .{e}),
};
try eb.addRootErrorMessage(.{
.msg = try eb.printString("unable to load: {t}", .{err}),
.msg = msg,
.src_loc = try file.errorBundleWholeFileSrc(zcu, eb),
});
}

View file

@ -925,8 +925,11 @@ pub const File = struct {
/// allocated into `gpa`.
path: Compilation.Path,
/// Populated only when emitting error messages; see `getSource`.
source: ?[:0]const u8,
/// Populated only when emitting error messages; see `getTree`.
tree: ?Ast,
zir: ?Zir,
zoir: ?Zoir,
@ -1033,25 +1036,27 @@ pub const File = struct {
}
}
pub const Source = struct {
bytes: [:0]const u8,
stat: Cache.File.Stat,
};
pub const GetSourceError = error{
OutOfMemory,
FileTooBig,
Streaming,
} || std.fs.File.OpenError || std.fs.File.ReadError;
FileChanged,
} || std.Io.File.OpenError || std.Io.File.Reader.Error;
pub fn getSource(file: *File, zcu: *const Zcu) GetSourceError!Source {
/// This must only be called in error conditions where `stat` *is* populated. It returns the
/// contents of the source file, assuming the stat has not changed since it was originally
/// loaded.
pub fn getSource(file: *File, zcu: *const Zcu) GetSourceError![:0]const u8 {
const gpa = zcu.gpa;
const io = zcu.comp.io;
if (file.source) |source| return .{
.bytes = source,
.stat = file.stat,
};
if (file.source) |source| return source;
switch (file.status) {
.never_loaded => unreachable, // stat must be populated
.retryable_failure => unreachable, // stat must be populated
.astgen_failure, .success => {},
}
assert(file.stat.size <= std.math.maxInt(u32)); // `PerThread.updateFile` checks this
var f = f: {
const dir, const sub_path = file.path.openInfo(zcu.comp.dirs);
@ -1059,40 +1064,43 @@ pub const File = struct {
};
defer f.close();
const stat = try f.stat();
const stat = f.stat() catch |err| switch (err) {
error.Streaming => {
// Since `file.stat` is populated, this was previously a file stream; since it is
// now not a file stream, it must have changed.
return error.FileChanged;
},
else => |e| return e,
};
if (stat.size > std.math.maxInt(u32))
return error.FileTooBig;
if (stat.inode != file.stat.inode or
stat.size != file.stat.size or
stat.mtime.nanoseconds != file.stat.mtime.nanoseconds)
{
return error.FileChanged;
}
const source = try gpa.allocSentinel(u8, @intCast(stat.size), 0);
const source = try gpa.allocSentinel(u8, @intCast(file.stat.size), 0);
errdefer gpa.free(source);
var file_reader = f.reader(io, &.{});
file_reader.size = stat.size;
file_reader.interface.readSliceAll(source) catch return file_reader.err.?;
// Here we do not modify stat fields because this function is the one
// used for error reporting. We need to keep the stat fields stale so that
// updateFile can know to regenerate ZIR.
file.source = source;
errdefer comptime unreachable; // don't error after populating `source`
return .{
.bytes = source,
.stat = .{
.size = stat.size,
.inode = stat.inode,
.mtime = stat.mtime,
},
};
return source;
}
/// This must only be called in error conditions where `stat` *is* populated. It returns the
/// parsed AST of the source file, assuming the stat has not changed since it was originally
/// loaded.
pub fn getTree(file: *File, zcu: *const Zcu) GetSourceError!*const Ast {
if (file.tree) |*tree| return tree;
const source = try file.getSource(zcu);
file.tree = try .parse(zcu.gpa, source.bytes, file.getMode());
file.tree = try .parse(zcu.gpa, source, file.getMode());
return &file.tree.?;
}