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)