Skip to content

flm serve: a client that resets before accept stops the server accepting connections (HttpSession ctor throws ENOTCONN) #762

Open

Description

@noamsto

What happens

If a client connects and then resets the connection (RST) before flm serve's accept handler runs, the server stops accepting new connections until it is restarted. The listening socket stays open, so new clients complete the TCP handshake into the backlog and then hang.

Cause

WebServer::do_accept counts the connection, builds the session, and only then re-arms itself (server.cpp:568-590):

active_connections_.fetch_add(1);
auto session = std::make_shared<HttpSession>(std::move(socket), *this);
session->start(cors);
...
if (running) {
    do_accept();
}

The HttpSession constructor calls the throwing overloads socket_.set_option(...) and socket_.remote_endpoint() (server.cpp:175-186). On Linux, accept() still returns a socket that was reset while it sat in the accept queue, but getpeername() on it fails with ENOTCONN, so remote_endpoint() throws. That exception:

  1. escapes the accept completion handler before do_accept() re-arms, so no further connection is ever accepted;
  2. escapes after the fetch_add, so the connection slot is never released (see also Server stops accepting connections after 10 client aborts — connection slot leak ("Connection limit reached (10)") #680);
  3. escapes ioc.run(), ending that I/O thread. The per-thread catch logs Error in WebServer I/O thread: ... (server.cpp:507).

The window is between the connection landing in the accept queue and the handler reaching the constructor. It widens whenever I/O threads are busy, for example during NPU work.

Repro

The socket behaviour, on Linux 7.2:

import socket, struct, time
srv = socket.socket(); srv.bind(("127.0.0.1", 0)); srv.listen(8)
c = socket.create_connection(srv.getsockname())
c.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0)); c.close()
time.sleep(0.2)
a, _ = srv.accept()   # succeeds
a.getpeername()       # OSError: [Errno 107] Transport endpoint is not connected

A deterministic way to hit it in flm serve, with no timing luck needed:

  1. Start flm serve llama3.2:1b and wait for GET /api/version to answer.
  2. kill -STOP <flm pid>. The kernel still completes handshakes into the backlog.
  3. Connect to the port, set SO_LINGER {1, 0}, and close. This queues a reset connection.
  4. kill -CONT <flm pid>.
  5. GET /api/version now hangs, and the log shows Error in WebServer I/O thread: remote_endpoint: Transport endpoint is not connected.

The socket behaviour above was verified on our Strix Halo host. We are measuring the full flm serve sequence now, together with a fix; the code path was read on main at 39ff855.

Suggested fix

  • In the constructor, use the error_code overloads of set_option and remote_endpoint, and log "Remote endpoint unavailable" on error, as close_connection and read_request already do. The session then ends through the normal read-error path.
  • In do_accept, also wrap session creation and start() in a try/catch that gives the slot back and falls through to the re-arm. Then no future throw during session setup can stop the accept loop.

We'll link our patch here once it is tested.

Activity

  1. noamsto commented on Sep 29, 2026

    @noamsto
    Author

    Measured through flm serve now (v1.0.6, llama3.2:1b, Strix Halo), using the SIGSTOP / connect+RST / SIGCONT sequence above:

    • Before the fix: the first cycle hangs. GET /api/version times out, and the log shows Error in WebServer I/O thread: remote_endpoint: Transport endpoint is not connected.
    • After the fix: 15 of 15 cycles answer 200, past the 10-connection cap, with no I/O-thread errors. A counter-logging debug build shows 32 accepts and 32 releases, so each reset connection gives its slot back once.

    Our fix does both parts of the suggestion above:

    • the error_code overloads in the HttpSession constructor;
    • a try/catch in do_accept that releases the slot and always re-arms.

    Patch: https://github.com/noamsto/nix-amd-ai/blob/35a61ad06930603957f9c51ec7bea68f0cc97a5f/pkgs/fastflowlm/patches/accept-loop-rearm.patch

    It is layered on our other server patches (including the slot-release fix for #680), so its context lines won't match upstream exactly.

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions