[Bug]: Directory watcher leaks descriptors when initial handlers fail
- Dominant language
- Swift
- Stars
- 49.9k
- Forks
- 1.8k
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 22
Description
### I have done the following
- [x] I have searched the existing issues
- [x] I have reproduced the issue using the `main` branch of this project
### Steps to reproduce
During testing of my Container Compose plugin, I found a deterministic descriptor leak on stock `apple/container` `main` at `48145ac7fb177d9fb14e015a0bdea4c642b36729`.
1. Create a temporary directory and a `DirectoryWatcher` for that path.
2. Invoke the watcher's initial-start path with a handler that throws.
3. Repeat the failing start 32 times, matching the public watcher's retry behaviour without waiting 32 seconds.
4. Enumerate `/dev/fd`, resolve each descriptor with `fcntl(F_GETPATH)`, and count only descriptors whose path is the watched directory.
5. Observe exactly 32 leaked descriptors on stock `main`.
The public `startWatching` loop retries once per second while no dispatch source exists, so a persistently failing initial handler leaks one descriptor per retry.
The same audit found that `_startWatching` treats descriptor `0` as an open failure even though `open` succeeds for every non-negative descriptor. If standard input is closed and `open` returns `0`, the watcher rejects and leaks that valid descriptor.
### Problem description
`DirectoryWatcher._startWatching` opens the watched directory before its initial enumeration and handler call. The dispatch source that normally owns and closes the descriptor is created only after those operations succeed. If either operation throws, the function returns without transferring ownership or closing the descriptor.
The failure path should close the descriptor before throwing. Descriptor `0` should be accepted as a successful `open` result. A regression test should count descriptors for the exact watched path rather than relying on a process-wide threshold, which can pass despite a leak or fail because unrelated tests opened files.
This is the focused `DirectoryWatcher` leak described by #1773. It is independently reproducible, but it does not by itself establish the VM virtiofs behaviour reported in #1097, so the proposed fix should not close #1097.
### Environment
- OS: macOS 26.5.1 (25F80)
- Xcode: 26.6 (17F113)
- Swift: 6.3.3
- Container: source `main` at `48145ac7fb177d9fb14e015a0bdea4c642b36729`
### Code of Conduct
- [x] I agree to follow this project's Code of Conduct
Contributor guide
Research direction
Start at DirectoryWatcher._startWatching and the public startWatching retry loop, then reproduce the failure with a throwing initial handler and repeated starts. Done means failed initial enumeration or handler execution closes the descriptor, descriptor 0 is accepted, and a regression test counts descriptors resolved to the exact watched path.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- swift
- Domain
- tooling
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100