Re: [PATCH v2] Cygwin: open: Do not set tentative fhandler to fdtab

Takashi Yano <[email protected]>
Newsgroups gmane.os.cygwin.patches
Message-ID <[email protected]>
Hi Johannes,

Thanks for reviewing v2 patch so quickly!

On Wed, 5 Aug 2026 11:29:54 +0200 (CEST)
Johannes Schindelin wrote:
> Hi Takashi,
> 
> This is exactly what I was hoping for. v2 addresses every point from the
> last round.
> 
> On Wed, 5 Aug 2026, Takashi Yano wrote:
> 
> > Tentative assignment of fhandler to fdtab introduced by the commit
> > 524d75ff7398 ("Cygwin: open: Unlock fdtab before open_with_arch()")
> > causes the undesired behaviour. The commit intended that fhandler
> > was just a marker for reservation of fd. However, another cygwin
> > call may assume that the fd is valid and in use, and may operate on
> > it.
> > 
> > This patch introduces a special value ((fhandler_base *) -1) for
> > fdtab that marks the fd as reserved and means it cannot be assigned
> > for another open(), etc.
> > 
> > Fixes: 524d75ff7398 ("Cygwin: open: Unlock fdtab before open_with_arch()")
> > Suggested-by: Johannes Schindelin <[email protected]>
> > Signed-off-by: Takashi Yano <[email protected]>
> > Reviewed-by: Johannes Schindelin <[email protected]>
> > ---
> >  winsup/cygwin/dtable.cc                | 29 ++++++++++++-----------
> >  winsup/cygwin/local_includes/cygheap.h |  6 ++---
> >  winsup/cygwin/local_includes/dtable.h  | 32 ++++++++++++++++++++++++--
> >  winsup/cygwin/syscalls.cc              | 13 +++++++----
> >  4 files changed, 57 insertions(+), 23 deletions(-)
> > 
> > diff --git a/winsup/cygwin/dtable.cc b/winsup/cygwin/dtable.cc
> > index e4d1cdf8f..7a4fd7dca 100644
> > --- a/winsup/cygwin/dtable.cc
> > +++ b/winsup/cygwin/dtable.cc
> > @@ -13,6 +13,7 @@ details. */
> >  #include <stdio.h>
> >  #include <unistd.h>
> >  #include <wchar.h>
> > +#include <assert.h>
> >  
> >  #define USE_SYS_TYPES_FD_SET
> >  #include <winsock.h>
> > @@ -123,7 +124,7 @@ dtable::get_debugger_info ()
> >  	    fhandler_base *fh = build_fh_name (std[i]);
> >  	    if (!fh)
> >  	      continue;
> > -	    fds[i] = fh;
> > +	    fds.set_fhandler (i, fh);
> >  	    if (!fh->open ((i ? (i == 2 ? O_RDWR : O_WRONLY) : O_RDONLY)
> >  			   | O_BINARY, 0777))
> >  	      release (i);
> > @@ -233,7 +234,7 @@ dtable::find_unused_handle (size_t start)
> >    do
> >      {
> >        for (size_t i = start; i < size; i++)
> > -	if (fds[i] == NULL)
> > +	if (fds[i] == NULL && !fds.reserved (i))
> >  	  {
> >  	    res = (int) i;
> >  	    goto out;
> > @@ -249,8 +250,9 @@ dtable::release (int fd)
> >  {
> >    if (fds[fd]->need_fixup_before ())
> >      dec_need_fixup_before ();
> > +  assert (fds[fd]);
> 
> Tiny nit, not a blocker: This assert sits _after_ the first dereference
> (`fds[fd]->need_fixup_before ()`), so it cannot catch a NULL-or-reserved
> slot before that line already faults. It wants to be the first statement
> in the function.
> 
> >    fds[fd]->dec_refcnt ();
> > -  fds[fd] = NULL;
> > +  fds.set_fhandler (fd, NULL);
> >    if (fd <= 2)
> >      set_std_handle (fd);
> >  }
> > @@ -263,11 +265,12 @@ cygwin_attach_handle_to_fd (char *name, int fd, HANDLE handle, mode_t bin,
> >    if (fd == -1)
> >      fd = cygheap->fdtab.find_unused_handle ();
> >    fhandler_base *fh = build_fh_name (name);
> > -  if (!fh)
> > +  if (!fh || cygheap->fdtab.reserved (fd))
> >      fd = -1;
> >    else
> >      {
> > -      cygheap->fdtab[fd] = fh;
> > +      cygheap->fdtab.set_fhandler (fd, fh);
> > +      assert (cygheap->fdtab[fd]);
> >        cygheap->fdtab[fd]->inc_refcnt ();
> >        fh->init (handle, myaccess, bin ?: fh->pc_binmode ());
> >      }
> > @@ -348,7 +351,7 @@ dtable::init_std_file_from_handle (int fd, HANDLE handle)
> >      handle_to_fn (handle, name);
> >  
> >    if (!name[0] && !dev)
> > -    fds[fd] = NULL;
> > +    fds.set_fhandler (fd, NULL);
> >    else
> >      {
> >        fhandler_base *fh;
> > @@ -425,7 +428,8 @@ dtable::init_std_file_from_handle (int fd, HANDLE handle)
> >        if (!fh->open_setup (openflags))
> >  	api_fatal ("open_setup failed, %E");
> >        fh->usecount = 0;
> > -      cygheap->fdtab[fd] = fh;
> > +      cygheap->fdtab.set_fhandler (fd, fh);
> > +      assert (cygheap->fdtab[fd]);
> >        cygheap->fdtab[fd]->inc_refcnt ();
> >        set_std_handle (fd);
> >        paranoid_printf ("fd %d, handle %p", fd, handle);
> > @@ -795,16 +799,15 @@ dtable::dup3 (int oldfd, int newfd, int flags)
> >  
> >    if (!not_open (newfd))
> >      close (newfd);
> > -  else if ((size_t) newfd >= size
> > -	   && find_unused_handle (newfd) < 0)
> > +  else if (((size_t) newfd >= size && find_unused_handle (newfd) < 0)
> > +	   || reserved (newfd))
> 
> With this, `dup3` (hence `dup2`) and `cygwin_attach_handle_to_fd` with an
> explicit fd refuse a reserved target instead of overwriting it. That was
> the third gap from the last round.
> 
> One raw exposure remains, and I want to record that it is fine: `dtable`
> still hands out the underlying array through `operator fhandler_base
> **()`. But with the wrapper in place its only callers are `extend()`'s own
> bookkeeping (the `memcpy`, the `cfree`, `fds = newfds`) and the
> `!cygheap->fdtab` allocated-check in `init_cygheap`, neither of which
> indexes a slot, so the trapdoor is effectively shut. I also checked the
> mechanical consequence of `operator[]` no longer yielding an lvalue: every
> former raw `fds[...] =` assignment moved to `set_fhandler`
> (`get_debugger_info`, `release`, both `init_std_file_from_handle`
> branches, `dup3`, `move_fd`), so nothing writes through the masked
> accessor.
> 
> One more unrelated nit: the new `dup3` branch that rejects a reserved
> `newfd` returns -1 without setting `errno`. The `reserved (newfd)`
> disjunct falls into the "couldn't extend fdtab" arm, which sets none. It
> is only reachable if a `dup2` targets an fd number a concurrent `open()`
> is still reserving (undefined territory), so it is cosmetic, but an
> `EBADF` there would be tidier.
> 
> One pre-existing aside, explicitly out of scope: a multithreaded `fork()`
> while another thread is blocked in a FIFO `open()` still leaves the
> reserved slot set in the child, where no thread survives to clear it, so
> that fd stays unusable there. It no longer crashes (the child's fixup now
> skips the masked slot), and it predates this patch (the same window at
> `0d3ea0ee` inherits a half-open `fhandler`, which is worse), so nothing to
> change here. Just noting it.
> 
> So the Reviewed-by stands as added. Thank you for fixing this!
> 
> Ciao,
> Johannes
> 
> >      /* couldn't extend fdtab */
> >      {
> >        newfh->close ();
> >        res = -1;
> >        goto done;
> >      }
> > -
> > -  fds[newfd] = newfh;
> > +  fds.set_fhandler (newfd, newfh);
> >  
> >    if ((res = newfd) <= 2)
> >      set_std_handle (res);
> > @@ -874,8 +877,8 @@ void
> >  dtable::move_fd (int from, int to)
> >  {
> >    // close (to); /* It is assumed that this is close-on-exec */
> > -  fds[to] = fds[from];
> > -  fds[from] = NULL;
> > +  fds.set_fhandler (to, fds[from]);
> > +  fds.set_fhandler (from, NULL);
> >  }
> >  
> >  void
> > diff --git a/winsup/cygwin/local_includes/cygheap.h b/winsup/cygwin/local_includes/cygheap.h
> > index 74cfff652..db740f03d 100644
> > --- a/winsup/cygwin/local_includes/cygheap.h
> > +++ b/winsup/cygwin/local_includes/cygheap.h
> > @@ -569,10 +569,10 @@ class cygheap_fdmanip
> >    }
> >    virtual void release () { cygheap->fdtab.release (fd); }
> >    operator int &() {return fd;}
> > -  operator fhandler_base* &() {return cygheap->fdtab[fd];}
> > +  operator fhandler_base* () {return cygheap->fdtab[fd];}
> >    operator fhandler_socket* () const {return reinterpret_cast<fhandler_socket *> (cygheap->fdtab[fd]);}
> >    operator fhandler_pipe* () const {return reinterpret_cast<fhandler_pipe *> (cygheap->fdtab[fd]);}
> > -  void operator = (fhandler_base *fh) {cygheap->fdtab[fd] = fh;}
> > +  void operator = (fhandler_base *fh) {cygheap->fdtab.set_fhandler (fd, fh);}
> >    fhandler_base *operator -> () const {return cygheap->fdtab[fd];}
> >    bool isopen () const
> >    {
> > @@ -609,7 +609,7 @@ class cygheap_fdnew : public cygheap_fdmanip
> >      if (cygheap->fdtab[fd])
> >        cygheap->fdtab[fd]->inc_refcnt ();
> >    }
> > -  void operator = (fhandler_base *fh) {cygheap->fdtab[fd] = fh;}
> > +  void operator = (fhandler_base *fh) {cygheap->fdtab.set_fhandler (fd, fh);}
> >  };
> >  
> >  class cygheap_fdget : public cygheap_fdmanip
> > diff --git a/winsup/cygwin/local_includes/dtable.h b/winsup/cygwin/local_includes/dtable.h
> > index 7803fae1b..89c776c2d 100644
> > --- a/winsup/cygwin/local_includes/dtable.h
> > +++ b/winsup/cygwin/local_includes/dtable.h
> > @@ -17,9 +17,30 @@ details. */
> >  class suffix_info;
> >  
> >  #define BFH_OPTS (PC_NULLEMPTY | PC_FULL | PC_POSIX)
> > +#define FDTAB_RESERVED ((fhandler_base *) -1)
> >  class dtable
> >  {
> > -  fhandler_base **fds;
> > +  class dtable_fds
> > +  {
> > +    fhandler_base **fds;
> > +  public:
> > +    inline void set_fhandler (int fd, fhandler_base *fh) {fds[fd] = fh;}
> > +    inline fhandler_base *operator [](int fd) const
> > +    {
> > +      fhandler_base *fh = fds[fd];
> > +      return (fh == FDTAB_RESERVED) ? NULL : fh;
> > +    }
> 
> Moving the masking into the `dtable_fds` accessor is the right move; it is
> precisely the "hide it in the storage layer" I was after. The five
> fork/exec fixup loops (`set_file_pointers_for_exec`, `fixup_before_fork`,
> `fixup_after_fork`, `fixup_before_exec`, `fixup_after_exec`) all read the
> member `fds[i]`, so they now see NULL for a reserved slot with no change
> at any site; `select_read`/`select_write`/`select_except` and the
> free-standing `set_std_handle` likewise. I traced each of them: the
> sentinel can no longer reach a dereference through the member. And the
> single load into `fh` closes the torn-read window I worried about last
> time: one read, compared and returned, so a lock-free `cygheap_fdget` can
> never observe the marker escaping the mask.
> 
> > +    operator fhandler_base **() {return fds;}
> > +    void operator = (fhandler_base **ptr) {fds = ptr;}
> > +    inline void reserve (int fd) { fds[fd] = FDTAB_RESERVED; }
> > +    inline void unreserve (int fd)
> > +    {
> > +      if (fds[fd] == FDTAB_RESERVED)
> > +	fds[fd] = NULL;
> > +    }
> > +    inline bool reserved (int fd) { return fds[fd] == FDTAB_RESERVED; }
> > +  };
> > +  dtable_fds fds;
> >    fhandler_base **archetypes;
> >    unsigned narchetypes;
> >    unsigned farchetype;
> > @@ -61,7 +82,11 @@ public:
> >    void init_std_file_from_handle (int fd, HANDLE handle);
> >    int dup3 (int oldfd, int newfd, int flags);
> >    void fixup_after_exec ();
> > -  inline fhandler_base *&operator [](int fd) const { return fds[fd]; }
> > +  inline void set_fhandler (int fd, fhandler_base *fh)
> > +  {
> > +    fds.set_fhandler (fd, fh);
> > +  }
> > +  inline fhandler_base *operator [](int fd) const { return fds[fd]; }
> >    bool select_read (int fd, select_stuff *);
> >    bool select_write (int fd, select_stuff *);
> >    bool select_except (int fd, select_stuff *);
> > @@ -76,6 +101,9 @@ public:
> >    void fixup_before_fork (DWORD win_proc_id);
> >    void lock () {lock_process::locker.acquire ();}
> >    void unlock () {lock_process::locker.release ();}
> > +  inline void reserve (int fd) { fds.reserve (fd); }
> > +  inline void unreserve (int fd) { fds.unreserve (fd); }
> > +  inline bool reserved (int fd) { return fds.reserved (fd); }
> >  };
> >  
> >  fhandler_base *build_fh_dev (const device&, const char * = NULL);
> > diff --git a/winsup/cygwin/syscalls.cc b/winsup/cygwin/syscalls.cc
> > index 5465d6c09..5ca02c0a1 100644
> > --- a/winsup/cygwin/syscalls.cc
> > +++ b/winsup/cygwin/syscalls.cc
> > @@ -24,6 +24,7 @@ details. */
> >  #include <dirent.h>
> >  #include <ntsecapi.h>
> >  #include <iptypes.h>
> > +#include <assert.h>
> >  #include "ntdll.h"
> >  
> >  #include <cygwin/version.h>
> > @@ -146,7 +147,9 @@ dup_finish (int oldfd, int newfd, int flags)
> >    int res;
> >    if ((res = cygheap->fdtab.dup3 (oldfd, newfd, flags | O_EXCL)) == newfd)
> >      {
> > -      cygheap_fdget (newfd)->inc_refcnt ();
> > +      cygheap_fdget cfd (newfd);
> > +      assert ((fhandler_base *) cfd);
> > +      cfd->inc_refcnt ();
> >        cygheap->fdtab.unlock ();	/* dup3 exits with lock set on success */
> >      }
> >    return res;
> > @@ -1558,8 +1561,8 @@ open (const char *unix_path, int flags, ...)
> >  	  cygheap->fdtab.unlock ();
> >  	  __leave;		/* errno already set */
> >  	}
> > -      cygheap->fdtab[fd] = fh; /* tentative setting to mark as used */
> > -      cygheap->fdtab.unlock();
> > +      cygheap->fdtab.reserve (fd);
> > +      cygheap->fdtab.unlock ();
> >  
> >        if (fh->dev () == FH_PROCESSFD && fh->pc.follow_fd_symlink ())
> >  	{
> > @@ -1588,7 +1591,7 @@ open (const char *unix_path, int flags, ...)
> >  		    FILE_OPEN_FOR_BACKUP_INTENT);
> >  
> >        cygheap->fdtab.lock ();
> > -      cygheap->fdtab[fd] = fh;
> > +      cygheap->fdtab.set_fhandler (fd, fh);
> >        fh->inc_refcnt ();
> >        cygheap->fdtab.unlock ();
> >  
> > @@ -1601,7 +1604,7 @@ open (const char *unix_path, int flags, ...)
> >      if (res < 0 && fd >= 0)
> >        {
> >  	cygheap->fdtab.lock ();
> > -	cygheap->fdtab[fd] = NULL; /* Mark as unused */
> > +	cygheap->fdtab.unreserve (fd);
> >  	cygheap->fdtab.unlock ();
> >        }
> >    if (res < 0 && fh)
> > -- 
> > 2.51.0

I'll push this patch to master and cygwin-3_6-branch with minor fix.

Thank you very much again.

-- 
Takashi Yano <[email protected]>
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.