Re: [PATCH v2] Cygwin: open: Unlock fdtab before open_with_arch()
Johannes Schindelin <[email protected]> Tue, 21 Jul 2026 19:16:11 +0200 (CEST)
| Newsgroups | gmane.os.cygwin.patches |
|---|---|
| Message-ID | <[email protected]> |
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 =3D -1;
> + int fd =3D -1;
> va_list ap;
> mode_t mode =3D 0;
> fhandler_base *fh =3D 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 =3D cygheap->fdtab.find_unused_handle ();
> if (fd < 0)
> - __leave; /* errno already set */
> + __leave; /* errno already set */
> + cygheap->fdtab[fd] =3D fh; /* tentative setting to mark as used *=
/
> + cygheap->fdtab.unlock();
> =20
> if (fh->dev () =3D=3D 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.
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.
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.
> @@ -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);
> =20
> - fd =3D fh;
> + cygheap->fdtab.lock ();
> + cygheap->fdtab[fd] =3D fh;
> + fh->inc_refcnt ();
> + cygheap->fdtab.unlock ();
> +
> if (fd <=3D 2)
> set_std_handle (fd);
> res =3D fd;
> }
> __except (EFAULT) {}
> __endtry
> + if (res < 0 && fd >=3D 0)
> + {
> + cygheap->fdtab.lock ();
> + cygheap->fdtab[fd] =3D NULL; /* Mark as unused */
> + cygheap->fdtab.unlock ();
> + }
> if (res < 0 && fh)
> delete fh;
> syscall_printf ("%R =3D 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.
Ciao,
Johannes
Footnotes:
[*1*] I let GPT 5.6 write the code to reproduce this (it's a bit verbose,
as AI-generated code tends to be, but looked correct to me, hopefully you
find it useful; Originally I wanted to condense this into a better form,
but I realized that I simply lack the time to do so):
=2D- snip --
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <pthread.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>
static pthread_mutex_t mutex =3D PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t condition =3D PTHREAD_COND_INITIALIZER;
static int requested_stage;
static int completed_stage;
static int stage_result[3];
static int stage_errno[3];
static void
set_deadline (struct timespec *deadline, int seconds)
{
clock_gettime (CLOCK_REALTIME, deadline);
deadline->tv_sec +=3D seconds;
}
static int
wait_for_stage (int stage, int seconds)
{
struct timespec deadline;
int result =3D 0;
set_deadline (&deadline, seconds);
pthread_mutex_lock (&mutex);
while (completed_stage < stage)
{
int err =3D pthread_cond_timedwait (&condition, &mutex, &deadline);
if (err =3D=3D ETIMEDOUT)
{
result =3D ETIMEDOUT;
break;
}
if (err)
{
result =3D err;
break;
}
}
pthread_mutex_unlock (&mutex);
return result;
}
static void
request_stage (int stage)
{
pthread_mutex_lock (&mutex);
requested_stage =3D stage;
pthread_cond_broadcast (&condition);
pthread_mutex_unlock (&mutex);
}
static void *
descriptor_thread (void *unused)
{
(void) unused;
for (int stage =3D 1; stage <=3D 2; stage++)
{
pthread_mutex_lock (&mutex);
while (requested_stage < stage)
pthread_cond_wait (&condition, &mutex);
pthread_mutex_unlock (&mutex);
errno =3D 0;
int result =3D close (-1);
int saved_errno =3D errno;
pthread_mutex_lock (&mutex);
stage_result[stage] =3D result;
stage_errno[stage] =3D saved_errno;
completed_stage =3D stage;
pthread_cond_broadcast (&condition);
pthread_mutex_unlock (&mutex);
}
return NULL;
}
int
main (void)
{
pthread_t thread;
int opened =3D 0;
int failure_errno =3D 0;
setvbuf (stdout, NULL, _IONBF, 0);
if (pthread_create (&thread, NULL, descriptor_thread, NULL))
{
perror ("pthread_create");
return 1;
}
request_stage (1);
if (wait_for_stage (1, 3))
{
printf ("baseline descriptor operation did not complete\n");
return 1;
}
printf ("baseline close(-1): result=3D%d errno=3D%d\n",
stage_result[1], stage_errno[1]);
if (stage_result[1] !=3D -1 || stage_errno[1] !=3D EBADF)
return 1;
for (; opened < OPEN_MAX + 16; opened++)
{
errno =3D 0;
if (open ("/dev/null", O_RDONLY) < 0)
{
failure_errno =3D errno;
break;
}
}
printf ("descriptor allocation stopped after %d opens: errno=3D%d\n",
opened, failure_errno);
if (failure_errno !=3D EMFILE)
return 1;
request_stage (2);
if (wait_for_stage (2, 3) =3D=3D ETIMEDOUT)
{
printf ("second-thread descriptor operation remained waiting\n");
return 0;
}
printf ("second-thread close(-1): result=3D%d errno=3D%d\n",
stage_result[2], stage_errno[2]);
return 2;
}
=2D- snap --
[*2*] Also here, I let GPT 5.6 write the code for reproducing the issue:
=2D- snip --
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
static const char *fifo_name =3D "fifo-fd-lookup-check.pipe";
static pthread_mutex_t mutex =3D PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t condition =3D PTHREAD_COND_INITIALIZER;
static int start_writer;
static int writer_done;
static int writer_fd =3D -1;
static int writer_errno;
static int
before_deadline (const struct timespec *deadline)
{
struct timespec now;
clock_gettime (CLOCK_MONOTONIC, &now);
return now.tv_sec < deadline->tv_sec
|| (now.tv_sec =3D=3D deadline->tv_sec
&& now.tv_nsec < deadline->tv_nsec);
}
static void *
open_writer (void *unused)
{
(void) unused;
pthread_mutex_lock (&mutex);
while (!start_writer)
pthread_cond_wait (&condition, &mutex);
pthread_mutex_unlock (&mutex);
errno =3D 0;
int fd =3D open (fifo_name, O_WRONLY);
int saved_errno =3D errno;
pthread_mutex_lock (&mutex);
writer_fd =3D fd;
writer_errno =3D saved_errno;
writer_done =3D 1;
pthread_cond_broadcast (&condition);
pthread_mutex_unlock (&mutex);
return NULL;
}
int
main (void)
{
pthread_t writer;
struct timespec poll_deadline;
struct timespec pause =3D { 0, 1000000 };
struct timespec wait_deadline;
int anchor;
int expected_fd;
int query_result =3D -1;
int reader_fd;
setvbuf (stdout, NULL, _IONBF, 0);
unlink (fifo_name);
if (mkfifo (fifo_name, 0600))
{
perror ("mkfifo");
return 1;
}
anchor =3D open ("/dev/null", O_RDONLY);
if (anchor < 0)
{
perror ("open /dev/null");
return 1;
}
if (pthread_create (&writer, NULL, open_writer, NULL))
{
perror ("pthread_create");
return 1;
}
expected_fd =3D dup (anchor);
if (expected_fd < 0)
{
perror ("dup");
return 1;
}
close (expected_fd);
printf ("expected FIFO writer descriptor: %d\n", expected_fd);
pthread_mutex_lock (&mutex);
start_writer =3D 1;
pthread_cond_broadcast (&condition);
pthread_mutex_unlock (&mutex);
clock_gettime (CLOCK_MONOTONIC, &poll_deadline);
poll_deadline.tv_sec +=3D 5;
while (before_deadline (&poll_deadline))
{
errno =3D 0;
query_result =3D fcntl (expected_fd, F_GETFD);
if (query_result >=3D 0)
break;
if (errno !=3D EBADF)
{
printf ("fcntl(%d, F_GETFD): result=3D%d errno=3D%d\n",
expected_fd, query_result, errno);
return 1;
}
nanosleep (&pause, NULL);
}
pthread_mutex_lock (&mutex);
int completed_before_reader =3D writer_done;
pthread_mutex_unlock (&mutex);
printf ("provisional descriptor query: result=3D%d writer_done=3D%d\n",
query_result, completed_before_reader);
if (query_result < 0 || completed_before_reader)
return 1;
reader_fd =3D open (fifo_name, O_RDONLY | O_NONBLOCK);
printf ("reader open: fd=3D%d errno=3D%d\n", reader_fd, errno);
if (reader_fd < 0)
return 1;
clock_gettime (CLOCK_REALTIME, &wait_deadline);
wait_deadline.tv_sec +=3D 5;
pthread_mutex_lock (&mutex);
while (!writer_done)
{
int err =3D pthread_cond_timedwait (&condition, &mutex, &wait_deadli=
ne);
if (err)
break;
}
int completed_after_reader =3D writer_done;
int result_fd =3D writer_fd;
int result_errno =3D writer_errno;
pthread_mutex_unlock (&mutex);
printf ("writer completion: done=3D%d fd=3D%d errno=3D%d\n",
completed_after_reader, result_fd, result_errno);
if (!completed_after_reader)
return 2;
if (result_fd >=3D 0)
{
int close_result =3D close (result_fd);
printf ("writer close: result=3D%d errno=3D%d\n", close_result, errn=
o);
}
close (reader_fd);
close (anchor);
pthread_join (writer, NULL);
unlink (fifo_name);
return 0;
}
=2D- snap --
> --=20
> 2.51.0
>=20
>=20