Skip to content

watcher: inotify arm panics on every Linux start (raw syscall checked with the libc errno accessor) - #698

Merged
justrach merged 1 commit into
justrach:release/0.2.5842from
poelzi:fix/inotify-errno-crash
Aug 22, 2026
Merged

watcher: inotify arm panics on every Linux start (raw syscall checked with the libc errno accessor)#698
justrach merged 1 commit into
justrach:release/0.2.5842from
poelzi:fix/inotify-errno-crash

Conversation

@poelzi

@poelzi poelzi commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Problem

FileChangeWatch guards two raw std.os.linux.* syscalls with the wrong errno accessor:

const wd_rc = std.os.linux.inotify_add_watch(...);  // raw syscall: -errno packed into a usize
if (std.posix.errno(wd_rc) != .SUCCESS) return;     // libc accessor: `if (rc == -1) ... else .SUCCESS`
const wd: i32 = @intCast(wd_rc);                    // panics on the negative rc

codedb links libc, so std.posix.errno resolves to std.c.errno, which reports failure only for rc == -1. A usize never equals -1, so the guard always returned .SUCCESS and the negative rc fell straight through to @intCast.

Verified against the pinned toolchain (0.17.0-dev.813+2153f8143):

inotify_add_watch("/tmp/definitely-not-here") ->
  rc = 18446744073709551614 (0xfffffffffffffffe)
  std.posix.errno(rc)     = SUCCESS   <-- wrong
  std.os.linux.errno(rc)  = NOENT     <-- correct

Why it fires for everyone on Linux

armInotify watches /tmp/codedb-notify — the optional muonry interop file, which codedb itself never creates. On any machine without muonry that watch returns -ENOENT on every arm, so:

  • Debug / ReleaseSafe — hard panic (integer does not fit in destination type) in the watcher thread.
  • ReleaseFast@intCast is unchecked, so the value truncates to a garbage watch descriptor instead. That is why shipped releases never surfaced it: /tmp/codedb-notify also fails silently, and a failing directory watch (EACCES, watch-limit ENOSPC) inserts a bogus wd -> rel mapping that can misattribute later events.

The kqueue path already tolerated the file's absence (else |_| {}); only inotify did not.

Failing test

Reproduces today on release/0.2.5842 as a crash in the existing test:

$ zig build test-index -Dtest-filter="issue-690"
thread panic: integer does not fit in destination type
src/watcher.zig:1494:25: in addInotifyWatch
src/watcher.zig:1482:29: in armInotify
src/watcher.zig:1336:28: in arm
src/watcher.zig:1621:14: in incrementalLoop
Build Summary: 2/3 tests passed (1 crashed)

Fix

Classify raw returns by the -errno range instead. rawLinuxSyscallFailed mirrors std.os.linux.errno without referencing the linux-only namespace, so the boundary behaviour stays unit-testable on every target. Applied at both call sites — inotify_init1 had the identical bug.

I scanned src/ for other raw std.os.linux.* call sites; these two are the only ones.

Verification

  • New test watcher: raw linux syscall failures are detected by the -errno range pins success values, -ENOENT, and the -1 / -4095 / -4096 boundaries.
  • Both tests confirmed to actually catch the bug: with the old classification restored, issue-690 crashes and the new test fails; with the fix, both pass.
  • Full suite green: zig build test 25/25 steps, 807/811 passed, 4 skipped, 0 failed, 0 crashed (previously 1 crash).
  • Daemon smoke-tested with /tmp/codedb-notify absent — arms and runs clean.

`armInotify` and `addInotifyWatch` classify raw `std.os.linux.*` returns with
`std.posix.errno`. That is the libc accessor — `if (rc == -1) ... else .SUCCESS`
— and a raw syscall returns `-errno` packed into a usize, which never equals
-1. So the guard reported `.SUCCESS` for every failure and the negative rc fell
through to `@intCast(...)`: a panic in Debug/ReleaseSafe, a truncated garbage
watch descriptor in ReleaseFast.

It fires on startup for everyone on Linux. `armInotify` watches
`/tmp/codedb-notify` — the optional muonry interop file that codedb itself
never creates — so a machine without muonry takes `-ENOENT` on every arm. The
kqueue path already tolerated its absence (`else |_| {}`); only inotify did not.
Reproduced as a hard crash in `test_index` "issue-690".

Classify with the -errno range instead. `rawLinuxSyscallFailed` mirrors
std.os.linux.errno without referencing the linux-only namespace, so the
boundary behaviour is unit-testable on every target.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqTyTJVJ4cd1MzFF54Hrbh
@justrach
justrach marked this pull request as ready for review August 22, 2026 16:31
@justrach
justrach merged commit 1cd8e06 into justrach:release/0.2.5842 Aug 22, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants