fix(kqueue): copy watch path under lock in thread_fn to prevent use-after-free

This commit is contained in:
CJ van den Berg 2026-03-14 23:44:31 +01:00
parent c4bb73dfe1
commit 2e7838515b
Signed by: neurocyte
GPG key ID: 8EB1E1BB660E3FB9
2 changed files with 43 additions and 21 deletions

View file

@ -119,32 +119,49 @@ fn thread_fn(self: *@This(), allocator: std.mem.Allocator) void {
const fd: std.posix.fd_t = @intCast(ev.ident);
// Check if this is a file watch: NOTE_WRITE/NOTE_EXTEND modified.
// Copy the path under the lock so a concurrent remove_watch cannot
// free it before we finish using it.
var file_path_buf: [std.fs.max_path_bytes]u8 = undefined;
var file_path_len: usize = 0;
self.file_watches_mutex.lock();
var fwit = self.file_watches.iterator();
const file_path: ?[]const u8 = while (fwit.next()) |entry| {
if (entry.value_ptr.* == fd) break entry.key_ptr.*;
} else null;
while (fwit.next()) |entry| {
if (entry.value_ptr.* == fd) {
@memcpy(file_path_buf[0..entry.key_ptr.*.len], entry.key_ptr.*);
file_path_len = entry.key_ptr.*.len;
break;
}
}
self.file_watches_mutex.unlock();
if (file_path) |fp| {
if (file_path_len > 0) {
const fp = file_path_buf[0..file_path_len];
if (ev.fflags & (NOTE_WRITE | NOTE_EXTEND) != 0)
self.handler.change(fp, EventType.modified, .file) catch return;
continue;
}
// Otherwise look up the directory path for this fd.
// Same copy-under-lock pattern.
var dir_path_buf: [std.fs.max_path_bytes]u8 = undefined;
var dir_path_len: usize = 0;
self.watches_mutex.lock();
var wit = self.watches.iterator();
const dir_path: ?[]const u8 = while (wit.next()) |entry| {
if (entry.value_ptr.* == fd) break entry.key_ptr.*;
} else null;
while (wit.next()) |entry| {
if (entry.value_ptr.* == fd) {
@memcpy(dir_path_buf[0..entry.key_ptr.*.len], entry.key_ptr.*);
dir_path_len = entry.key_ptr.*.len;
break;
}
}
self.watches_mutex.unlock();
if (dir_path == null) continue;
if (dir_path_len == 0) continue;
const dir_path = dir_path_buf[0..dir_path_len];
if (ev.fflags & NOTE_DELETE != 0) {
self.handler.change(dir_path.?, EventType.deleted, .dir) catch return;
self.handler.change(dir_path, EventType.deleted, .dir) catch return;
} else if (ev.fflags & NOTE_RENAME != 0) {
self.handler.change(dir_path.?, EventType.renamed, .dir) catch return;
self.handler.change(dir_path, EventType.renamed, .dir) catch return;
} else if (ev.fflags & NOTE_WRITE != 0) {
self.scan_dir(allocator, dir_path.?) catch {};
self.scan_dir(allocator, dir_path) catch {};
}
}
}

View file

@ -103,35 +103,40 @@ fn thread_fn(self: *@This(), allocator: std.mem.Allocator) void {
if (ev.filter != EVFILT_VNODE) continue;
const fd: std.posix.fd_t = @intCast(ev.ident);
// Copy the path under the lock so a concurrent remove_watch cannot
// free it before we finish using it.
var watch_path_buf: [std.fs.max_path_bytes]u8 = undefined;
var watch_path_len: usize = 0;
var is_file: bool = false;
self.watches_mutex.lock();
var wit = self.watches.iterator();
var watch_path: ?[]const u8 = null;
var is_file: bool = false;
while (wit.next()) |entry| {
if (entry.value_ptr.*.fd == fd) {
watch_path = entry.key_ptr.*;
@memcpy(watch_path_buf[0..entry.key_ptr.*.len], entry.key_ptr.*);
watch_path_len = entry.key_ptr.*.len;
is_file = entry.value_ptr.*.is_file;
break;
}
}
self.watches_mutex.unlock();
if (watch_path == null) continue;
if (watch_path_len == 0) continue;
const watch_path = watch_path_buf[0..watch_path_len];
if (is_file) {
// Explicit file watch: emit events with .file type directly.
if (ev.fflags & NOTE_DELETE != 0) {
self.handler.change(watch_path.?, EventType.deleted, .file) catch return;
self.handler.change(watch_path, EventType.deleted, .file) catch return;
} else if (ev.fflags & NOTE_RENAME != 0) {
self.handler.change(watch_path.?, EventType.renamed, .file) catch return;
self.handler.change(watch_path, EventType.renamed, .file) catch return;
} else if (ev.fflags & (NOTE_WRITE | NOTE_EXTEND) != 0) {
self.handler.change(watch_path.?, EventType.modified, .file) catch return;
self.handler.change(watch_path, EventType.modified, .file) catch return;
}
} else {
if (ev.fflags & NOTE_DELETE != 0) {
self.handler.change(watch_path.?, EventType.deleted, .dir) catch return;
self.handler.change(watch_path, EventType.deleted, .dir) catch return;
} else if (ev.fflags & NOTE_RENAME != 0) {
self.handler.change(watch_path.?, EventType.renamed, .dir) catch return;
self.handler.change(watch_path, EventType.renamed, .dir) catch return;
} else if (ev.fflags & NOTE_WRITE != 0) {
self.scan_dir(allocator, watch_path.?) catch {};
self.scan_dir(allocator, watch_path) catch {};
}
}
}