watcher(linux): drop inotify event on WatchItemIndex overflow instead of aborting - #34308
watcher(linux): drop inotify event on WatchItemIndex overflow instead of aborting#34308robobun wants to merge 2 commits into
Conversation
… of aborting WatchItemIndex is u16 and nothing caps watchlist length. With >65535 watched paths, a watchlist position found at >=65536 panicked on try_from().unwrap() in watch_loop_cycle, and with panic = "abort" that kills the whole process from the FileWatcher thread. The Zig baseline used an unchecked @intcast here (ReleaseFast on Linux), which silently truncated and routed the event to the wrong entry. Wrong, but the process lived. Restore that tolerance by logging and skipping the event, same as the adjacent None arm for an unknown watch descriptor.
|
Warning Review limit reached
Next review available in: 4 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
Comment |
|
Updated 3:16 AM PT - Jul 16th, 2026
❌ @robobun, your commit e49c403 has 3 failures in
🧪 To try this PR locally: bunx bun-pr 34308That installs a local version of the PR into your bun-34308 --bun |
Why no regression testThe trigger condition is a watchlist position
A fail-before/pass-after test has to run under The change itself is mechanical: |
… u32 idx == 65535 passed the try_from guard (valid u16) but collides with NO_WATCH_ITEM and the process_inotify_event_batch dedup sentinel; a batch whose first sorted event had index 65535 would merge into head[0] of an empty slice and panic. Drop idx >= u16::MAX at the guard and widen the sentinel to u32::MAX, matching WindowsWatcher.rs.
There was a problem hiding this comment.
LGTM — the follow-up commit addresses the idx == u16::MAX sentinel collision I flagged, and the widened u32 dedup sentinel now matches WindowsWatcher.rs.
What was reviewed:
- Guard condition
idx < WatchItemIndex::MAX as usizecorrectly excludes both ≥65536 and theNO_WATCH_ITEM(65535) reserved value; theas WatchItemIndexcast is bounded. last_event_id: u32 = u32::MAXwithu32::from(u16)comparisons — lossless, no real index can collide, so thei=0split_at_mut(0)path is unreachable.- Drop path mirrors the adjacent
Nonearm (events_processed += 1; continue), so batching bookkeeping stays consistent. - No-test rationale (66k-module load infeasible under debug+ASAN) is documented in-thread; existing
watch-many-dirs.test.tsstill exercises the surrounding code.
Extended reasoning...
Overview
Two localized edits in src/watcher/INotifyWatcher.rs (Linux-only, cfg-gated): (1) replace WatchItemIndex::try_from(idx).unwrap() with a match guard that accepts idx < u16::MAX and otherwise logs via scoped_log! and skips the event, mirroring the adjacent unknown-watch-descriptor arm; (2) widen the dedup-loop sentinel last_event_id from WatchItemIndex (u16) to u32, matching the identical fix already present at WindowsWatcher.rs:515-517. Net diff is ~15 lines.
Security risks
None. This is defensive integer-bounds handling on an internal watchlist position derived from .iter().position(). No user-controlled input reaches a new sink; the change strictly narrows the set of inputs that reach downstream code and turns a process-abort into a logged event drop.
Level of scrutiny
Low-to-moderate. The trigger requires >65535 concurrently watched paths in a single --hot/--watch process — an extreme edge that the Zig baseline silently truncated and the Rust port turned into an abort. The change is purely defensive: for the <65535 common case, behavior is byte-identical (guard passes, sentinel comparison still works because u32::from(u16) is lossless). The only behavioral delta is at the boundary, where abort becomes drop-and-continue.
Other factors
- My earlier review on this PR flagged that the first revision still admitted
idx == 65535, which collides with bothNO_WATCH_ITEMand the oldWatchItemIndex::MAXdedup sentinel (leading tohead[0]on an empty slice ati=0). Commit e49c403 applied both suggested fixes: the guard now uses<(strict), and the sentinel is widened tou32::MAXwith the same comment as the Windows sibling. I re-verifiedWatchItemIndex = u16andNO_WATCH_ITEM = WatchItemIndex::MAXinWatcher.rs:48,51. - The drop arm increments
events_processedbeforecontinue, so the outerwhile events_processed < events.len()loop advances correctly and no event is double-processed or skipped. - No automated regression test ships with this PR. CLAUDE.md normally requires one, but the author documented in-thread that reproducing needs 66k+ imported modules under
bun bd, which did not complete in 180s under debug+ASAN. Given the fix is a mechanical bounds guard + sentinel widening that mirrors an already-landed sibling (WindowsWatcher.rs), andcargo check/clippyplus the existing inotify stress test pass, I think that rationale is acceptable for a maintainer to accept — but it's called out here so a human can override if they disagree.
|
CI on build 73736: the red lanes are unrelated to this diff. |
Two unguarded int casts in the file watcher turn what Zig handled tolerantly into a process abort from the FileWatcher thread (
Cargo.tomlsetspanic = "abort", andtry_fromis a real runtime check not compiled out in release).Site 1: kevent(2) returning -1 (macOS/FreeBSD)
src/watcher/KEventWatcher.rs:84-87: the coalesce guard iscount < 64, so a kevent error return of -1 falls intousize::try_from(-1).expect("int cast")and aborts.Already fixed in #31695 (uses the EINTR-retrying
bun_sys::keventwrapper socountisusizeand real errors propagate asErr). Not touched here.Site 2: watchlist position >= 65536 (Linux)
src/watcher/INotifyWatcher.rs:454:Nothing caps watchlist length (
Watcher.rs:687/:783), so a project with >65535 watched paths can reach a position that doesn't fit inu16, and the.unwrap()aborts the process from the watcher thread. The Zig baseline (INotifyWatcher.zig:281) used@intCast, which is unchecked in Linux ReleaseFast builds, so the same input silently truncated and routed the event to the wrong entry. Wrong, but the process lived.Fix: on overflow,
scoped_log!and skip the event (events_processed += 1; continue;), matching the adjacentNonearm for an unknown watch descriptor.Verification
Source-level finding; reproducing requires >65535 inotify watches on a single module graph, which is impractical and flaky in CI (depends on
fs.inotify.max_user_watches).cargo check -p bun_watcherandcargo clippy -p bun_watcherare clean, andtest/cli/hot/watch-many-dirs.test.ts(the existing inotify stress test) still passes with the debug build.