Re: [PATCH v2] Cygwin: open: Unlock fdtab before open_with_arch()

Takashi Yano <[email protected]> Wed, 22 Jul 2026 20:10:12 +0900
Newsgroups gmane.os.cygwin.patches
Message-ID <[email protected]>
Hi Johannes,

Thanks for reviewing!

On Tue, 21 Jul 2026 19:16:11 +0200 (CEST)
Johannes Schindelin wrote:
> Hi Takashi,
> 
> sorry for getting to reply only after you pushed this to `master`. I
> wanted to take the time to double-check a couple of things, and other
> responsibilities got into the way.
> 
> On Fri, 17 Jul 2026, Takashi Yano wrote:
> 
> > [...]
> > diff --git a/winsup/cygwin/syscalls.cc b/winsup/cygwin/syscalls.cc
> > index 2bea79768..e3ba8c65c 100644
> > --- a/winsup/cygwin/syscalls.cc
> > +++ b/winsup/cygwin/syscalls.cc
> > @@ -1451,6 +1451,7 @@ extern "C" int
> >  open (const char *unix_path, int flags, ...)
> >  {
> >    int res = -1;
> > +  int fd = -1;
> >    va_list ap;
> >    mode_t mode = 0;
> >    fhandler_base *fh = NULL;
> > @@ -1550,9 +1551,12 @@ open (const char *unix_path, int flags, ...)
> >        /* Reserve an fdtable entry here, before calling open_with_arch() below.
> >           Otherwise there's a tiny chance of hitting OPEN_MAX further on which
> >           could create a new file without any way for Cygwin to refer to it. */
> > -      cygheap_fdnew fd;
> > +      cygheap->fdtab.lock();
> > +      fd = cygheap->fdtab.find_unused_handle ();
> >        if (fd < 0)
> > -        __leave;		/* errno already set */
> > +	__leave;		/* errno already set */
> > +      cygheap->fdtab[fd] = fh; /* tentative setting to mark as used */
> > +      cygheap->fdtab.unlock();
> >  
> >        if (fh->dev () == FH_PROCESSFD && fh->pc.follow_fd_symlink ())
> >  	{
> 
> I see three problems here:
> 
> When `fd` is negative, the `unlock()` right below is skipped, and
> `__endtry` only unlocks for non-negative ones. So an `open()` that ran out
> of descriptors returns with the fdtab still locked, and the next fdtab
> operation on any other thread waits for good. `cygheap_fdnew` used to
> release the lock in that case; this one does not. I reproduced [*1*] it
> three times out of three on a local build. It stays invisible while
> single-threaded, because the muto is reentrant -- which is also why the
> OPEN_MAX test still passes.

Ugh, I messed up…

> The tentative assignment puts `fh` into the table with a zero reference
> count, then drops the lock across `open_with_arch()`, which for a FIFO
> waits for the other end. Any lookup in that window -- an `fcntl()`, a
> `close()`, a `close_range()` -- raises the count from zero to one and
> lowers it straight back, freeing `fh` while `open()` is still using it and
> leaving the slot pointing at nothing valid. I reproduced [*2*] it by
> querying the descriptor from a second thread while the open was waiting:
> the process ended with an access violation (status 0xC0000005), and the
> same run without the query was clean.

How do you know the descriptor which open() does not return it yet
in your reproducer?

> 
> The `FH_PROCESSFD` branch shows the same thing with no threads at all: it
> deletes `fh` and repoints the local variable at the reopened handler, but
> the table still refers to the deleted one until the re-assignment further
> down.

The same here. The fd is not returned by open() yet, so the program
cannot think the fd is the valid file descriptor, I think...

> > @@ -1580,13 +1584,23 @@ open (const char *unix_path, int flags, ...)
> >  	try_to_bin (fh->pc, fh->get_handle (), DELETE,
> >  		    FILE_OPEN_FOR_BACKUP_INTENT);
> >  
> > -      fd = fh;
> > +      cygheap->fdtab.lock ();
> > +      cygheap->fdtab[fd] = fh;
> > +      fh->inc_refcnt ();
> > +      cygheap->fdtab.unlock ();
> > +
> >        if (fd <= 2)
> >  	set_std_handle (fd);
> >        res = fd;
> >      }
> >    __except (EFAULT) {}
> >    __endtry
> > +    if (res < 0 && fd >= 0)
> > +      {
> > +	cygheap->fdtab.lock ();
> > +	cygheap->fdtab[fd] = NULL; /* Mark as unused */
> > +	cygheap->fdtab.unlock ();
> > +      }
> >    if (res < 0 && fh)
> >      delete fh;
> >    syscall_printf ("%R = open(%s, %y)", res, unix_path, flags);
> 
> 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?

-- 
Takashi Yano <[email protected]>