Skip to content

EBADF error from ioloop poll after close_all() #662

Description

@rgov

It is common to encounter an OSError: [Errno 9] Bad file descriptor on macOS when trying to shut down a server with server.close_all() from another thread. This was previously reported in #430 which was closed without comment by its originator.

I think this could be a race condition where one thread invokes ioloop.close() and closes one of the file descriptors that the server's ioloop.loop() is polling at that moment. _kqueue.control() immediately raises OSError(EBADF) which is not caught.

Consider a simple example:

import threading
import time

from pyftpdlib.handlers import FTPHandler
from pyftpdlib.servers import FTPServer

server = FTPServer(("127.0.0.1", 0), FTPHandler)

t = threading.Thread(target=server.serve_forever, name="pyftpdlib", daemon=True)
t.start()

time.sleep(10)
server.close_all()
t.join(timeout=2.0)

The output on macOS 15.6 and Python 3.9 is:

[I 2025-08-09 19:17:46] concurrency model: async
[I 2025-08-09 19:17:46] masquerade (NAT) address: None
[I 2025-08-09 19:17:46] passive ports: None
[I 2025-08-09 19:17:46] >>> starting FTP server on 127.0.0.1:61530, pid=86473 <<<
Exception in thread pyftpdlib:
Traceback (most recent call last):
  File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/threading.py", line 973, in _bootstrap_inner
    self.run()
  File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/threading.py", line 910, in run
    self._target(*self._args, **self._kwargs)
  File "/private/tmp/testpyftpd/.venv/lib/python3.9/site-packages/pyftpdlib/servers.py", line 251, in serve_forever
    self.ioloop.loop(timeout, blocking)
  File "/private/tmp/testpyftpd/.venv/lib/python3.9/site-packages/pyftpdlib/ioloop.py", line 385, in loop
    poll(soonest_timeout)
  File "/private/tmp/testpyftpd/.venv/lib/python3.9/site-packages/pyftpdlib/ioloop.py", line 745, in poll
    kevents = self._kqueue.control(
OSError: [Errno 9] Bad file descriptor

This affects both the Kqueue and Select IOLoop implementations but Poll seems to work OK.

A workaround with Kqueue might be to subclass it:

# Workaround for kqueue issue on macOS when closing the server.
# See: https://github.com/giampaolo/pyftpdlib/issues/662
if IOLoop.__name__ == "Kqueue":
    class KqueueFix662(IOLoop):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self._closing = False

        def close(self):
            self._closing = True
            super().close()

        def poll(self, *args, **kwargs):
            try:
                return super().poll(*args, **kwargs)
            except OSError as e:
                if self._closing and e.errno == 9:
                    pass
                else:
                    raise

    IOLoop = KqueueFix662

server = FTPServer(("127.0.0.1", 0), FTPHandler, ioloop=IOLoop())

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions