Compare commits

...

2 Commits

Author SHA1 Message Date
Jarred Sumner
1a88df0cda Add crash reporting for fs_watch and fs_watchfile 2026-02-18 21:01:55 -08:00
Jarred Sumner
dc4dd3aa5b fix(fs): create owned copies of watch event paths on Windows (#27099)
`onPathUpdateWindows` stored the `Event` directly from
`PathWatcherManager.onFileUpdate` without creating an owned copy. The
event's path was a `[]const u8` sub-slice of the watchlist's storage,
auto-coerced to `StringOrBytesToDecode{.bytes_to_free}`. When
`FSWatchTaskWindows.run()` later accessed `path.string` (for utf8
encoding), it reinterpreted the raw pointer bytes as a `bun.String`,
whose `tag` field contained the LSB of a heap pointer — almost always
an invalid enum value, causing `panic: switch on corrupt value` in
`String.deref()`.

The original implementation in #9972 properly created owned copies per
encoding (`bun.String.createUTF8` for utf8, `allocator.dupeZ` for
others), but this was lost during the watcher refactor in #10492.

Restore correct ownership by creating a `bun.String` for utf8 encoding
or a duped `[]const u8` for other encodings before enqueuing the task.

Closes #27099, closes #27108.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 20:55:57 -08:00
3 changed files with 21 additions and 1 deletions

View File

@@ -91,6 +91,8 @@ pub const Features = struct {
pub var yaml_parse: usize = 0;
pub var cpu_profile: usize = 0;
pub var heap_snapshot: usize = 0;
pub var fs_watch: usize = 0;
pub var fs_watchfile: usize = 0;
comptime {
@export(&napi_module_register, .{ .name = "Bun__napi_module_register_count" });

View File

@@ -486,9 +486,11 @@ pub const StatWatcher = struct {
/// After a restat found the file changed, this calls the listener function.
pub fn swapAndCallListenerOnMainThread(this: *StatWatcher) void {
bun.analytics.Features.fs_watchfile += 1;
defer this.deref(); // Balance the ref from restat().
const prev_jsvalue = this.last_jsvalue.swap();
const globalThis = this.globalThis;
const current_jsvalue = statToJSStats(globalThis, &this.getLastStat(), this.bigint) catch return; // TODO: properly propagate exception upwards
this.last_jsvalue.set(globalThis, current_jsvalue);

View File

@@ -261,6 +261,7 @@ pub const FSWatcher = struct {
pub fn onPathUpdatePosix(ctx: ?*anyopaque, event: Event, is_file: bool) void {
const this = bun.cast(*FSWatcher, ctx.?);
bun.analytics.Features.fs_watch += 1;
if (this.verbose) {
switch (event) {
@@ -281,6 +282,7 @@ pub const FSWatcher = struct {
pub fn onPathUpdateWindows(ctx: ?*anyopaque, event: Event, is_file: bool) void {
const this = bun.cast(*FSWatcher, ctx.?);
bun.analytics.Features.fs_watch += 1;
if (this.verbose) {
switch (event) {
@@ -299,9 +301,23 @@ pub const FSWatcher = struct {
return;
}
// The event's path comes from PathWatcherManager.onFileUpdate as a
// []const u8 sub-slice of the watchlist's file_path storage, auto-coerced
// to StringOrBytesToDecode{.bytes_to_free}. We must create a properly
// owned copy: either a bun.String for utf8 encoding or a duped []const u8
// for other encodings.
const owned_event: Event = switch (event) {
inline .rename, .change => |path, t| @unionInit(Event, @tagName(t), if (this.encoding == .utf8)
FSWatchTaskWindows.StringOrBytesToDecode{ .string = bun.String.cloneUTF8(path.bytes_to_free) }
else
FSWatchTaskWindows.StringOrBytesToDecode{ .bytes_to_free = bun.default_allocator.dupe(u8, path.bytes_to_free) catch return }),
.@"error" => |err| .{ .@"error" = err.clone(bun.default_allocator) },
inline else => |value, t| @unionInit(Event, @tagName(t), value),
};
const task = bun.new(FSWatchTaskWindows, .{
.ctx = this,
.event = event,
.event = owned_event,
});
this.eventLoop().enqueueTask(jsc.Task.init(task));
}