• src/sbbs3/websrvr.cpp

    From Rob Swindell (on Debian Linux)@1:103/705 to Git commit to main/sbbs/master on Sat Sep 19 23:53:09 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/f9558d66d5a2d9e7df880e7c
    Modified Files:
    src/sbbs3/websrvr.cpp
    Log Message:
    websrvr: terminate the CGI read loop on a FastCGI socket failure

    fastcgi_read_wait_timeout() reported its recv(), version and session-ID failures by returning ret, which is still 0 at each of those points. Zero
    is the function's "nothing ready yet" value, so do_cgi_stuff() skipped its whole body, including the CGI_PROCESS_TERMINATED check, and polled again immediately. A socket at EOF is always readable, so nothing paced the
    retry: a backend that closed its connection left the session spinning at roughly 1.6 million iterations per second, logging at LOG_ERR on every
    one, until max_cgi_inactivity expired as much as two minutes later.

    Return CGI_PROCESS_TERMINATED from those three paths and from the two fastcgi_read_body() failures below them, so the caller tears the session
    down at once instead of treating a dead socket as a slow one.

    A php-fpm restart is enough to trigger this, which means it fires during routine package upgrades.

    #1246

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Rob Swindell (on Debian Linux)@1:103/705 to Git commit to main/sbbs/master on Wed Sep 30 20:48:07 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/651cbc9886250cb97169edf8
    Modified Files:
    src/sbbs3/websrvr.cpp
    Log Message:
    Web Server: shutdown() lingering session sockets when the client wait expires

    At terminate/recycle the web server thread waits up to MaxInactivity for the active clients to disconnect on their own, then gives up with "!TIMEOUT
    waiting for N active clients" and enters cleanup(), which loops until every child thread has exited. Nothing between those two steps did anything to
    the sessions that were still alive: a session thread blocked in a socket
    wait (the request-line read, the TLS close dance, the output thread's send) stayed blocked, cleanup() has no deadline, and the process never exited.
    On 2026-09-30 one idle HTTPS session on this host held the whole httpd
    service in "Waiting for 2 child threads to terminate" until systemd's 90 s
    stop timeout SIGKILLed it, 60 s of that budget having already gone to the active-client wait.

    Once the active-client wait expires, shutdown(SHUT_RDWR) the socket of
    every session still registered in current_connections. That wakes the
    blocked reader (recv() returns 0, poll() reports HUP) and lets the session thread run its normal teardown and exit; cleanup() then finishes within a second or two. Shutting down rather than closing keeps the descriptor
    owned by the session thread, which still performs the one real close, the
    same way the terminal server handles node sockets at terminate (8101584ded, symbol-19-seek, 2026-07-05).

    To make that safe, the session's current_connections node is now removed
    in close_session_socket(), immediately before the descriptor is closed,
    instead of at session-thread exit. Both the removal and the shutdown pass
    take the list lock, so the list never names a closed (and possibly reused) descriptor and a socket found in it is guaranteed to still be open.

    Validated against a scratch web server (MaxInactivity = 10s) with a plain
    HTTP client that sends a partial request line and then one byte every two seconds, so the session's inactivity timer never fires: before this change
    the server was still "Waiting for 2 child threads" 60 s after SIGTERM and
    had to be SIGKILLed; after it, the session is shut down the same second the client wait expires and the process exits 13 s after SIGTERM.

    Not covered: a connection still inside the TLS handshake is not yet in current_connections (it is added after the handshake, in client_on), so it cannot be kicked; cryptlib bounds that handshake at about 30 s. Which call
    the 2026-09-30 session thread was actually blocked in is not known (the
    SIGKILL left no core), so this is the safety net for that class of hang,
    not a fix for a specific blocking call.

    Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
    --- SBBSecho 3.38-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Rob Swindell (on Debian Linux)@1:103/705 to Git commit to main/sbbs/master on Wed Sep 30 20:48:07 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/a45592ec5bae024adf511679
    Modified Files:
    src/sbbs3/websrvr.cpp
    Log Message:
    Web Server: bound the shutdown wait for child threads, re-kick while waiting

    cleanup() looped on thread_count with no deadline. The previous commit
    (the shutdown() kick of listed session sockets) ends that loop quickly
    whenever the lingering thread is blocked on its client socket, but a thread blocked anywhere else - a script in a native call, a CGI pipe, a mutex -
    is out of its reach, and one such thread still held the whole server (and
    under systemd, the stop timeout) hostage until SIGKILL.

    Give up after TIMEOUT_THREAD_WAIT (60 s), the same bound the terminal, mail
    and FTP servers put on their own thread waits, logging "!TIMEOUT waiting
    for N child thread(s) to terminate" at error level. While waiting, re-run shutdown_sessions() every 5 s: a connection that completes its TLS
    handshake after the first pass is only then registered in
    current_connections, and each pass also logs what is still connected. A
    thread still running at the deadline is abandoned; the counter is left undestroyed (with a "!!!! Terminating with N child thread(s) still running" warning, as active_clients already does) so its eventual thread_down() is harmless. thread_count is initialized once per server start, not per
    recycle, so a thread abandoned by a recycle still decrements the right
    counter when it finally exits.

    Validated against a scratch web server (MaxInactivity = 10s) with an SSJS
    page that calls mswait(120000): before, the kick fired and the server was
    still "Waiting for 2 child threads" when SIGKILLed at 100 s; after, it logs
    the timeout and exits cleanly 73 s after SIGTERM (10 s client wait + 60 s thread wait), exit code 0, no core. The slow-client scenario from the
    previous commit still exits in 13 s.

    Budget note for systemd hosts: the worst case is now MaxInactivity plus
    60 s. With the default TimeoutStopSec of 90 s and MaxInactivity = 1m,
    SIGKILL still arrives first; raise TimeoutStopSec or lower MaxInactivity
    if the timeout diagnostics are wanted in that case.

    Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
    --- SBBSecho 3.38-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From DaiTengu@1:103/705 to Git commit to main/sbbs/master on Wed Oct 14 20:32:26 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/d217da6f41ee2c4e931ecb7f
    Modified Files:
    src/sbbs3/websrvr.cpp
    Log Message:
    Make RemoteIPHeader actually reach the access log
    --- SBBSecho 3.38-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Rob Swindell@1:103/705 to Git commit to main/sbbs/master on Wed Oct 14 20:32:26 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/574a32b0fdcd5033cce7267a
    Modified Files:
    src/sbbs3/websrvr.cpp
    Log Message:
    Merge branch 'remoteipheader-access-log' into 'master'

    Make RemoteIPHeader actually reach the access log

    See merge request main/sbbs!735
    --- SBBSecho 3.38-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Rob Swindell (on Debian Linux)@1:103/705 to Git commit to main/sbbs/master on Thu Oct 22 10:11:17 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/6b8902bab7a555c145012dbc
    Modified Files:
    src/sbbs3/websrvr.cpp
    Log Message:
    Web Server: don't let lingering client sockets stall termination

    close_socket() half-closes the socket and then waits for the peer to
    close its side, for up to max_inactivity seconds in a single blocking
    call that ignored terminate_server. Clients that never close (abandoned
    scraper connections, typically) held each session thread there for up to
    a minute after its inactivity timeout. By then close_session_socket() had already removed the session from current_connections, so
    shutdown_sessions() could not reach it, and a server stop or recycle
    waited on these threads for minutes.

    Poll in one-second slices and stop waiting once terminate_server is set.
    The normal-operation bound is unchanged. This also applies to sockets
    the listener thread rejects (rate-limited, ip-silent.can, max clients),
    which go through the same function.

    Also, after "!TIMEOUT waiting for N active clients to disconnect", no
    longer follow it with "Done waiting for active clients to disconnect".

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    --- SBBSecho 3.38-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Rob Swindell (on Debian Linux)@1:103/705 to Git commit to main/sbbs/master on Thu Oct 22 10:11:17 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/85b2ca4b23bad6e3786ca893
    Modified Files:
    src/sbbs3/websrvr.cpp
    Log Message:
    Web Server: don't keep a stale content handler after PathInfoIndex

    is_dynamic_req() runs once before a directory's webctrl.ini is read and
    again after PathInfoIndex resolves the request to the index script. The
    first pass, seeing the original path's extension (e.g. /robots.txt or
    /x.md), stored the matching web_handler.ini handler in req.xjs_handler,
    and the re-check never cleared it, so the handler (txt_handler.js, md_handler.js, ...) ran against the index script instead: the script's
    source was rendered as a text page, and requests that should reach the
    script never did. Clear the handler on every check.

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    --- SBBSecho 3.38-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Rob Swindell (on Debian Linux)@1:103/705 to Git commit to main/sbbs/master on Thu Oct 22 10:11:17 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/00ce71ebc56082e4af1c90e2
    Modified Files:
    src/sbbs3/websrvr.cpp
    Log Message:
    Web Server: fix doubled byte count for large non-chunked POSTs

    A non-chunked POST body larger than MAX_POST_LEN (4 MiB) is spooled to a
    temp file by post_to_file(), which adds each read to req.post_len. That
    field still held the Content-Length, so it ended at twice the body size
    and every such request logged a false "!ERROR Browser said they sent N
    bytes, but I got 2N". The following clamp to the Content-Length hid the
    error from everything else, so the stored body and the length seen by
    scripts were already correct.

    Zero post_len before spooling, as the chunked path already does.

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    --- SBBSecho 3.38-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)