Re: [PATCH v2] Cygwin: open: Unlock fdtab before open_with_arch()
Takashi Yano <[email protected]> Mon, 3 Aug 2026 19:54:43 +0900
| Newsgroups | gmane.os.cygwin.patches |
|---|---|
| Message-ID | <[email protected]> |
Hi Johannes, On Wed, 22 Jul 2026 22:29:44 +0200 (CEST) Johannes Schindelin wrote: > Hi Takashi, > > Thank you for trying the experiment so quickly. To answer your closing > question first: yes, both outputs are exactly what a correct fix should > produce. Let me walk through why, and then flag two caveats about the > reserved-marker design before we build it out. > > On Wed, 22 Jul 2026, Takashi Yano wrote: > > > On Wed, 22 Jul 2026 20:10:12 +0900 > > Takashi Yano wrote: > > > Hi Johannes, > > > > > > Thanks for reviewing! > > > > > > On Tue, 21 Jul 2026 19:16:11 +0200 (CEST) > > > Johannes Schindelin wrote: > > > > This is the first point where the slot and its reference count agree > > > > again; Too late for anything that looked in between. And the cleanup only > > > > runs for non-negative descriptors, so it never covers the case above. > > > > > > > > Since it is already in `master`, a follow-up patch probably makes most > > > > sense. Two things to fix: release the lock when no descriptor is > > > > available, and stop a reserved-but-not-yet-open descriptor from looking > > > > like a fully open one to the rest of the fdtable. Reviving the old integer > > > > marker would mean teaching every consumer of the table about it, so it is > > > > not a drop-in. > > > > > > Ah, I got it. The user program cannot know that, but cygwin1.dll can > > > refere fdtab inside it. What about adding reserved flag to fdtab? > > Adding a reserved marker is the right approach, and it is what I had in > mind too. Keeping the reserved slot hidden from the rest of the table lets > `open()` remain the sole owner of the handler and delete it on any error > path, including the `FH_PROCESSFD` reopen, without any reference-count > juggling. > > The first caveat, and the bigger one: this is not a drop-in change. Every > place that treats a non-NULL slot as a live handler has to learn to skip > the reserved marker. Besides `cygheap_fdget`, that includes > `cygheap_fdenum::next`, `not_open`, and the five fork/exec fixup loops > (`set_file_pointers_for_exec`, `fixup_after_exec`, `fixup_after_fork`, > `fixup_before_fork`, and `fixup_before_exec`). If a fork or exec runs on > another thread while a FIFO open is still holding the reservation, those > loops would dereference the marker. > > To keep that safe, I would encode the marker as a single distinguished > pointer value stored in the slot itself, rather than as a separate flag. > Then the lock-free `cygheap_fdget` keeps working with one added > comparison, and there is no risk of a torn read. > > > > > I tried it as quick experiment. > > > > The result of the first reproducer is: > > baseline close(-1): result=-1 errno=9 > > descriptor allocation stopped after 3197 opens: errno=24 > > second-thread close(-1): result=-1 errno=9 > > The decisive line here is the second thread's `close(-1)` returning > `EBADF` (errno 9) instead of blocking. That is the proof that the table > lock is no longer held when no descriptor is available; before the fix, > the second thread would have waited forever on the process-wide lock. The > allocation stopping after 3197 opens with `EMFILE` (errno 24) is right as > well: that is `OPEN_MAX` (3200, spelled `__OPEN_MAX` in the headers) minus > the three standard descriptors, so the ceiling is reached cleanly. > > > The result of the second reproducer is: > > expected FIFO writer descriptor: 4 > > provisional descriptor query: result=-1 writer_done=0 > > This one is exactly right too. The writer taking descriptor 4 is just the > expected setup; the line that matters is the provisional-descriptor query, > which returns -1 (`EBADF`) with `writer_done` still 0 while the FIFO open > is still blocked waiting for the other end. The reserved descriptor now > reports a bad descriptor instead of triggering the access violation > (status 0xC0000005) I saw before, where `cygheap_fdget` raised the > reference count from zero to one and its destructor then lowered it back > to zero and deleted the handler while `open()` was still blocked inside > `open_with_arch()`. > > > Are these results as you expected? > > Yes, as detailed above. The second caveat is about coverage: the two > reproducers drive `close()` and the descriptor lookup on a single known > number, but not the paths that walk the whole table. `close_range` is the > clearest of those. It iterates every descriptor in the range and looks > each one up, so it reaches a reserved slot without ever being told its > number. That, incidentally, answers the question you raised earlier about > how the racing thread could reference a descriptor that `open()` has not > returned yet: it does not _have_ to. The internal iterators reach the slot > regardless. A `close_range` call and a fork or exec, each racing an > in-flight open, would exercise the paths the current reproducers miss. > > Independently of all that, the lock-leak fix stands on its own and is > trivial, namely releasing the table lock when no descriptor is available, > so it could land first as a separate commit while we work out the marker > design. I implemented two different ways, one uses independent reserved flag, the other uses special value ((fhandler_base *) -1) as a reservation marker. I'll submit both patches, so please review the one you think is the better approach. -- Takashi Yano <[email protected]>