Skip to content

fix(tail): free dir-path alloc when a directory watch fails - #310

Open
detail-app[bot] wants to merge 2 commits into
masterfrom
detail/bug-fix/fix-tail-free-dir-path-alloc-when-a-directory-watc-56251b
Open

detail-app[bot] wants to merge 2 commits into
masterfrom
detail/bug-fix/fix-tail-free-dir-path-alloc-when-a-directory-watc-56251b

Conversation

@detail-app

@detail-app detail-app Bot commented Sep 16, 2026

Copy link
Copy Markdown

Detail bug report: View on Detail

Fixes ENG-711

Bug

In the tail watcher, ensureDirectoryWatch (in both kqueue_macos.zig and uring_linux.zig) allocates a dupe'd copy of the directory path and guards it with errdefer … free. On failure it then swallows the error with a bare return; (a normal void return from a !void function). In Zig, errdefer only runs on an error return, so errdefer never fires on these paths and the path buffer is leaked.

The kqueue impact is severe: collectDirty calls ensureDirectoryWatches (which iterates every tracked path) on every poll cycle (default poll_ms = 200, i.e. 5×/s). A trailing literal path whose parent directory doesn't exist re-enters the slow path every cycle and leaks owned_dir_path each time, unbounded, for the lifetime of the process. The common trigger is starting edge tail before the application has created its log directory.

Fix

  • kqueue_macos.zig: explicitly free(owned_dir_path) before the two normal-return failure paths — openDir returning error.FileNotFound, and kevent(...) < 0 — preserving the existing "swallow as no-op" behavior.
  • uring_linux.zig: same one-line fix on the inotifyAddWatch(...) catch path.
  • uring_linux.zig (supporting change): the inotifyInit1 / inotifyAddWatch wrappers called std.posix.errno(rc) on a raw std.os.linux syscall return. Because the build links libc, std.posix.errno resolves to std.c.errno, which only treats a return of -1 as an error — but raw Linux syscalls return -errno (a large usize), so every real inotify_add_watch/inotify_init1 failure was misclassified as SUCCESS and then panicked at .SUCCESS => @intCast(rc). This meant the uring error path crashed the process instead of leaking or being handled. Switching to std.os.linux.errno (type-identical to std.c.E on Linux) makes the wrappers return their typed errors so the leak fix's catch { free; return; } actually executes.

Testing

  • Unit regression tests added to each backend (and registered in src/tail/mod.zig so zig build test discovers them): they call ensureDirectoryWatch 50× against a literal path whose parent directory doesn't exist under a tmp dir, relying on std.testing.allocator's per-test leak detection. Pre-fix these would surface a leak; post-fix they pass.
  • Verified the uring test is a real guard by temporarily reverting only the leak fix (keeping the errno fix): the run reported exactly 50 leaked allocations and failed; after restoring the fix it passes with 0 leaks.
  • Reproduced the pre-fix uring crash end-to-end: built edge-tail (Debug) with the uring changes reverted, ran it with --io-engine uring over a missing-dir literal, deleted the glob-matched file to force eviction → rebuildIndexesensureDirectoryWatch on the missing dir, and captured the panic integer does not fit in destination type at inotifyAddWatch's .SUCCESS => @intCast(rc) with a full stack trace through collect → refreshPaths → evictExpiredUnmatched → removeTracked → rebuildIndexes → ensureDirectoryWatch → inotifyAddWatch (SIGABRT). Post-fix, the same scenario survives.
  • End-to-end on Linux: the poll and uring backends both tail a growing file and emit appended lines to stdout (no regression); the uring backend survives an eviction-triggered rebuild over a missing-dir literal post-fix.
  • Routine checks: zig build test (Debug and ReleaseSafe) passes (521/522, 1 pre-existing skip, 0 leaks), zig build and zig build tail succeed, zig fmt --check and ziglint are clean.
  • Could not run on macOS (kqueue regression test, kqueue end-to-end after-startup-before-dir leak RSS sampling): this is a Linux environment with no macOS SDK or Mach-O execution capability. I confirmed the kqueue file and its test block compile cleanly for aarch64-macos (zig build-obj / zig test -fno-emit-bin exit 0), but executing the macOS binary yields Exec format error (no emulator present). The kqueue leak fix follows the identical pattern as the uring fix and is unit-guarded by the same kind of regression test; it needs a macOS arm64 host for runtime verification.

Automatic Fixes PRs can be configured here.

Comment thread src/tail/mod.zig
_ = @import("read_scheduler/common.zig");
_ = @import("read_scheduler/poll.zig");
_ = @import("watch_backend/poll.zig");
_ = @import("watch_backend/uring_linux.zig");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium tail/mod.zig:35

Registering watch_backend/uring_linux.zig makes its regression test fail the entire Linux suite when IoUring.init returns error.PermissionDenied or error.SystemOutdated under seccomp or older kernels. The test must skip these unavailable-environment errors instead of propagating them.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/tail/mod.zig around line 35:

Registering `watch_backend/uring_linux.zig` makes its regression test fail the entire Linux suite when `IoUring.init` returns `error.PermissionDenied` or `error.SystemOutdated` under seccomp or older kernels. The test must skip these unavailable-environment errors instead of propagating them.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 2f45578.

macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Sep 16, 2026
@macroscopeapp

macroscopeapp Bot commented Sep 16, 2026

Copy link
Copy Markdown

Approvability

Verdict: Approved at 2f45578

Macroscope's review found this PR approvable — This is a small, localized tail-watcher bug fix that frees duplicated directory paths on failed watches and corrects Linux errno handling, with platform-specific regression tests. Existing successful watch behavior remains unchanged, and unavailable io_uring environments are skipped by the test.

You can add or adjust custom eligibility rules. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant