• JS ListeningSocket can never be closed: do_js_close() returns before c

    From Bob Roberts@1:103/705 to GitLab note in main/sbbs on Thu Nov 5 16:52:37 2026
    https://gitlab.synchro.net/main/sbbs/-/work_items/1296#note_10800

    ### Confirmed on Linux: master `55431cba50` (IRCd 2.11)

    **Setup:** IRC as a `STATIC | LOOP` service under sbbs (not `jsexec -l`), listeners on 6667 and 6697 (TLS).

    **Trigger:** a services recycle (`touch ctrl/recycle.services`). `ircd.js` restarts in the same process, and the new instance can't bind 6697:

    ```
    srvc 0073 !ERROR 98 binding IRCd socket to port 6697: Address already in use srvc -001 IRC !Error Error: Unable to add host to socket set creating listening socket on port 6697
    ```

    **Result:** the orphaned listener from the previous run stays in `LISTEN` until the process exits. It accepts TCP into its backlog but never completes a TLS handshake, so 6697 is dead until a full `systemctl restart sbbs`.

    #### Minimal repro (jsexec, loopback)

    ```js
    load("sockdefs.js");
    var a = new ListeningSocket(["127.0.0.1"], 47699, "Test", {retry_count: 0}); a.close();
    var b = new ListeningSocket(["127.0.0.1"], 47699, "Test", {retry_count: 0}); // throws: errno 98
    ```

    | Case | Rebind same port |
    |---|---|
    | `Socket` + `bind()`/`listen()`, dropped and GC'd | ✅ OK |
    | `ListeningSocket`, explicit `close()` | ❌ errno 98 |
    | `ListeningSocket`, dropped and GC'd | ❌ errno 98 |
    --- SBBSecho 3.38-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Rob Swindell@1:103/705 to GitLab note in main/sbbs on Thu Nov 5 18:23:30 2026
    https://gitlab.synchro.net/main/sbbs/-/work_items/1296#note_10804

    Fixed in 13d94e6451 (fires-13-willow, 2026-11-05).

    ### The fix differs from the one proposed above

    The proposal destroys any socket set in `do_js_close()`. That would also destroy the set behind a static service's `server.socket`: `services.cpp` passes its own listening set to `js_CreateSocketObjectFromSet()`, and destroys that set itself when the service ends. Both kinds of object are marked `external`, so nothing told them apart, and Services would have freed its set a second time.

    The commit adds a `set_owned` flag to `js_socket_private_t`, set only by the `ListeningSocket` constructor. `do_js_close()` destroys the set only when it is owned, after removing its event handlers, so `close()` and finalization both release the listeners and `server.socket` is left alone. The `free(p->set)` in `js_finalize_socket()` is gone; it could never run for a set.

    ### Verified on Linux

    - New regression test `exec/tests/socket/listening-close.js`: the port can be bound again after `close()`, after a second `close()`, after garbage collection, and after `close()` with a read handler installed. It failed before the change and passes after.
    - A script that creates a `ListeningSocket` with a read handler, run under `jsexec -l`: before, the first run bound and every later run failed with errno 98; after, every run binds.
    - The `server.socket` path was checked by reading the code, not by running a static service.
    - Not yet run on Windows.

    ### IRCd

    Keeping the listeners in `Open_PLines()` and closing them in `terminate_everything()` is no longer needed for a restart to work, since tearing down the script's context now releases them. It would still make the release explicit.

    ### Related

    814800d565 (select-19-shower, 2026-11-05) fixes a separate fault found in the same code: when `js_DefineSocketOptionsArray()` failed, the `Socket`, `ConnectedSocket` and `ListeningSocket` constructors freed their private data while the new object still pointed to it, and the finalizer then used and freed it again.

    — *Authored by Claude (Claude Code), on behalf of @rswindell*
    --- SBBSecho 3.38-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)