• Win32 debug heap assertion after reading a cached filter filter while

    From Rob Swindell@1:103/705 to GitLab note in main/sbbs on Thu Jun 25 14:41:14 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1099#note_9465

    ## Root cause: cross-CRT-heap free (allocated in `sbbs.dll`, freed in the server module)

    A fresh occurrence of this assertion in **`services.dll`** (Win32 debug, v3.21.4) gave enough to nail it. The crash is **not** heap corruption and **not** a thread race — it's a **cross-module C-runtime heap mismatch**, which is exactly why App Verifier (page-heap) found nothing, why a single-threaded `reset()/listed()` loop reproduces it deterministically, and why raw `strListReadFile()`+`strListFree()` does *not*.

    ### The evidence

    Each Synchronet DLL statically links its own (`/MTd`) CRT, so each has a **private heap** — confirmed in the dump: `services!malloc` and `sbbs!malloc` are distinct functions with distinct `_crtheap`s.

    Disassembling `services!filterFile::listed`, the two adjacent calls for `filterfile.hpp` lines 70-71 are:

    ```
    call services!strListFree ; line 70: FREE -> services.dll's local xpdev, services.dll heap
    call dword ptr [services!_imp__findstr_list] ; line 71: ALLOC -> IMPORTED, resolves to sbbs!findstr_list, sbbs.dll heap
    ```

    So the cached `list` is **allocated by `sbbs.dll`** — `findstr_list()` is `__declspec(dllimport)`, so the call goes through the IAT to `sbbs!findstr_list`, whose internal `strListReadFile`/`strListModifyEach`/`strdup` all run in **`sbbs.dll`'s heap** — but **freed by the server module** — `strListFree()` is xpdev, statically linked into `services.dll`, so it binds to the **local `services!strListFree` -> `services!free`**. The freeing module's debug CRT can't find the block in *its* heap's block list, so `_free_dbg_nolock` asserts at `debug_heap.cpp:996`. No memory is damaged; only heap *ownership* is wrong.

    This is general: in `listed()` the sbbs3 functions (`findstr_list`, `trash_in_list`, `find2strs`, `trash_parse_details`) are all `dllimport` from `sbbs.dll`, while xpdev's `strListFree`/`fdate` link locally — so it affects every filter object (`ip_can`, `ip_silent_can`, `host_can`, `host_exempt`) in every server that includes the header-only `filterFile`. That matches "reproducible on any/all servers." The destructor stack originally reported here is the same mismatch (`~filterFile()` -> local `strListFree` on an `sbbs.dll`-allocated list); the termination timing just makes the reload-then-free path more likely to be live.

    Why the symptoms line up:
    - **Single-threaded repro works** — it's deterministic, not a race; the alloc/free heaps simply never match.
    - **Raw `strListReadFile()`+`strListFree()` doesn't repro** — in that test both bind to the same module, so alloc heap == free heap.
    - **Only the `findstr_list()` path repros** — that's the one that allocates across the DLL boundary.
    - **App Verifier finds no corruption** — correct; the block is intact, just owned by another heap.
    - **Debug-only / Windows-only** — only the debug CRT validates block ownership on free; non-Windows builds share one libc heap across all `.so`s.

    ### Fix

    Pair the allocator with a deallocator in the **same** translation unit so both run in `sbbs.dll`'s heap. Added to `findstr.c`/`findstr.h`:

    ```c
    DLLEXPORT void findstr_list_free(str_list_t* list) { strListFree(list); }
    ```

    …and changed `filterfile.hpp` to call `findstr_list_free(&list)` at all three sites (destructor, `reset()`, and the `listed()` reload). Because `findstr_list_free` is `dllimport`ed from `sbbs.dll` just like `findstr_list`, allocation and free now happen in the same heap. Verified in the rebuilt `services.dll` import table: it now imports both `findstr_list` and `findstr_list_free` from `sbbs.dll`.

    This also lets the `strListFree(&list)` that had to be commented out of `reset()` be restored (now done under the object mutex, since `reset()` runs in the shutdown path where a late client thread may still be in `listed()`).

    Implemented and building clean on Win32/Release; still needs a debug-build run to confirm the assertion is gone in the original repro.

    — *Authored by Claude (Claude Code), on behalf of @rswindell*
    --- SBBSecho 3.37-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 Jun 25 17:54:36 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1099#note_9468

    Fixed in d3706726a (tickets-45-that) — pairs `findstr_list()` with a `findstr_list_free()` exported from the same module (`sbbs.dll`) and calls it from all three `filterFile` free sites, so the cached list is allocated and freed in one heap. The `strListFree()` previously commented out of `reset()` is restored, now under the object mutex.

    Root-cause details in the note above. Closing as resolved; reopen if the assertion recurs in a debug build.

    — *Authored by Claude (Claude Code), on behalf of @rswindell*
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Rob Swindell@1:103/705 to GitLab issue in main/sbbs on Thu Jun 25 17:54:40 2026
    close https://gitlab.synchro.net/main/sbbs/-/issues/1099
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)