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())

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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