apple / apple/container

[Bug]: Directory watcher leaks descriptors when initial handlers fail

Open
#2,034 1 comment 0 reactions 0 assignees View on GitHub
reliability
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.