Skip to content

fix: null file slot on backend track failure to prevent double-close - #316

Open
detail-app[bot] wants to merge 1 commit into
masterfrom
detail/bug-fix/fix-null-file-slot-on-backend-track-failure-to-pre-c3619e
Open

detail-app[bot] wants to merge 1 commit into
masterfrom
detail/bug-fix/fix-null-file-slot-on-backend-track-failure-to-pre-c3619e

Conversation

@detail-app

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

Copy link
Copy Markdown

Detail bug report: View on Detail

Fixes ENG-717

Bug

Watcher.openTracked (src/tail/watch.zig) committed a freshly-opened file handle into self.files.items[idx] before the final try self.backendTrackOpenFile(...), but its errdefer file.close(self.io) only closed the file on error — it left the slot pointing at the now-closed handle. When backendTrackOpenFile errored after the slot commit (e.g. macOS kqueue error.AccessDenied from ensureDirectoryWatch on a search-but-not-read directory, or Linux io_uring error.OutOfMemory from the directory-watch hashmap puts), Watcher.init's errdefer self.deinit() — and the production runtime's defer watcher.deinit() — re-close()d the already-closed fd.

  • In Debug this hits kernel EBADFstd.Io.Threaded.closeFd's recoverableOsBugDetected()unreachable panic.
  • In ReleaseSafe/ReleaseFast the EBADF is swallowed, but the deinit invariant ("every non-null files[i] is an open handle") is still violated.

Introduced in a32652c, which appended the try after the slot commit without updating the errdefer.

Fix

Two local changes so a backend-registration failure propagates cleanly through Watcher.init/deinit:

  • openTracked's errdefer now nulls the slot in the same block that closes the file, restoring the invariant deinit relies on:
    errdefer {
        file.close(self.io);
        self.files.items[idx] = null;
    }
  • Watcher.init had a latent double-free on the same path: its errdefer allocator.free(out_copy) (and the input_copy errdefer) fire after errdefer self.deinit() (LIFO) and re-free memory deinit already released. Removed the redundant out_copy errdefer (the dupe is moved into self.output_path by a non-errorable struct literal, so the errdefer covers no errorable window) and detached input_copy = .empty once self takes ownership. Without this, the report's primary macOS repro (which fails during Watcher.init) would merely move the crash from the file double-close to the out_copy double-free.

Testing

  • New unit test in src/tail/watch.zig drives the post-slot-commit failure window via the Linux io_uring backend (OOM in backendTrackOpenFile) with an allocation-failure sweep and asserts Watcher.init either succeeds or returns a clean error, never panics. Verified by negative control: reverting the slot fix makes the test reproduce the exact EBADF → unreachable panic from the bug (the documented call chain is openTracked → backendTrackOpenFile → init errdefer self.deinit() → deinit f.close() → Threaded.closeFd EBADF → recoverableOsBugDetected); restoring the fix turns it green.
  • Routine checks pass: zig build; zig build test (Debug, where the panic is observable) and zig build test -Doptimize=ReleaseSafe (the CI mode); zig fmt --check; ziglint; zig build -Doptimize=ReleaseSafe; the tail distribution build; both HTTP frontends (httpz/stdio); and task do (the project's pre-merge flow).
  • Black-box integration suite (bench/logging, pytest): glob discovery, checkpoint recovery/resume, append, concurrency stress, and the non-DAC error-handling tests pass, with no regressions in rotation lifecycle. Two pre-existing, unrelated failures were confirmed against the unmodified source — a literal-missing-path re-discovery case, and two chmod 0 tests that hang when run as root (root bypasses DAC).
  • The macOS kqueue --x-directory repro could not be exercised on this Linux host (the kqueue backend is comptime-gated to .macos). It is covered by code equivalence: the io_uring OOM driver exercises the same openTracked errdefer/deinit path under test, and openTracked is the sole site that commits a freshly-opened handle into a slot.

Automatic Fixes PRs can be configured here.

@macroscopeapp

macroscopeapp Bot commented Sep 16, 2026

Copy link
Copy Markdown

Approvability

Verdict: Approved at 564ed1f

Macroscope's review found this PR approvable — This is a focused cleanup fix in one watcher module that restores the file-slot invariant on backend registration failures and prevents duplicate memory ownership cleanup. Existing successful behavior remains unchanged, and the added regression test is isolated to testing.

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