• Terminal server leaks JS events registered from a js.on_exit handler

    From Rob Swindell@1:103/705 to GitLab issue in main/sbbs on Mon Nov 9 21:46:04 2026
    open https://gitlab.synchro.net/main/sbbs/-/work_items/1332

    `sbbs_t::js_execfile()` (`src/sbbs3/exec.cpp`) drops, without freeing, any event that a script registers from a `js.on_exit()` handler.

    Line numbers are as of 110dd20487 (2026-11-06).

    ## What the code does

    1. Before running the script it saves the node's `js_callback.events`, `rq_head`, `rq_tail` and `listeners` and sets them to NULL (lines 704-711).
    2. `js_handle_events()` runs the script's event loop and ends by calling `js_clear_events()` (line 714).
    3. `js_EvalOnExit(js_cx, js_scope, &js_callback)` runs the script's exit handlers (line 721).
    4. The saved lists are restored by plain assignment (lines 765-768).

    Anything an exit handler registers in step 3 (`js.setTimeout()`, `js.setInterval()`, `js.setImmediate()`, `socket.on()`, `js.addEventListener()`, `js.dispatchEvent()`) is added to `js_callback` after the loop has already ended and been cleared. Step 4 then overwrites the list heads, so those entries are never run and never freed.

    ## Effect

    Each dropped entry is a small heap allocation that still holds two GC roots (`JS_AddObjectRoot()` on the `this` object and the callback function). The rooted objects, and whatever they reference, stay alive until the node's runtime is destroyed.

    The stale entries are unreachable from the list, so they cannot fire in a later script. This is a leak, not a misfire.

    ## Status

    Found by reading the code; not reproduced at runtime. It needs a terminal-server script that registers an event from a `js.on_exit()` handler, and no stock script has been checked for doing that.

    The same pattern under `jsexec -l` and under static-loop services is worse, because there the list is not saved and restored: the stale entries carry into the next run of the script and keep the previous run's global object (and its open sockets) alive. That case is being fixed separately by clearing events in `js_EvalOnExit()` when the scope is the global object. That fix does not cover `exec.cpp`, where the scope is a per-script object.

    ## Likely fix

    Call `js_clear_events(js_cx, &js_callback)` after `js_EvalOnExit()` and before the restore at lines 765-768. `js_clear_events()` is currently static in `js_internal.cpp`, so it needs a declaration in `sbbs.h`.

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