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

    From Rob Swindell@1:103/705 to GitLab note in main/sbbs on Fri Mar 13 18:52:45 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1099#note_8576

    Tried to create a small C++ reproduction case for this issue, unsuccessfully.

    Created xpdev/strlisttest.cpp:
    ```
    #include "str_list.h"
    #include "threadwrap.h"
    #include "genwrap.h"
    #include "dirwrap.h"

    #include <atomic>
    #include <mutex>
    #include <time.h>

    static char* process_findstr_item(size_t index, char *str, void* cbdata)
    {
    SKIP_WHITESPACE(str);
    truncnl(str);
    return c_unescape_str(str);
    }

    str_list_t findstr_list(const char* fname)
    {
    FILE* fp;
    str_list_t list;

    if ((fp = fopen(fname, "r")) == NULL)
    return NULL;

    list = strListReadFile(fp, NULL, 1000);
    strListModifyEach(list, process_findstr_item, /* cbdata: */ NULL);
    printf("Read %s\n", fname);

    fclose(fp);

    return list;
    }

    class filterFile {
    public:
    filterFile() {
    pthread_mutex_init(&mutex, nullptr);
    }
    filterFile(const char* fname) : filterFile() {
    init(fname);
    }
    void init(const char* fname) {
    strlcpy(this->fname, fname, sizeof this->fname);
    }
    filterFile(const filterFile&) = delete;
    filterFile& operator=(const filterFile&) = delete;
    ~filterFile() {
    strListFree(&list);
    pthread_mutex_destroy(&mutex);
    }
    void reset() {
    fread_count = 0;
    total_found = 0;
    timestamp = 0;
    lastftime_check = 0;
    strListFree(&list);
    }
    std::atomic<uint> fread_count{};
    std::atomic<uint> total_found{};
    time_t fchk_interval{1}; // seconds
    char fname[MAX_PATH + 1]{};
    bool listed(const char* str1, const char* str2 = nullptr, struct trash* details = nullptr) {
    bool result;
    time_t now = time(nullptr);
    if (fchk_interval) {
    pthread_mutex_lock(&mutex);
    if ((now - lastftime_check) >= fchk_interval) {
    lastftime_check = now;
    time_t latest = fdate(fname);
    if (latest > timestamp) {
    strListFree(&list);
    list = findstr_list(fname);
    timestamp = latest;
    ++fread_count;
    }
    }
    result = false; //trash_in_list(str1, str2, list, details);
    pthread_mutex_unlock(&mutex);
    }
    if (result)
    ++total_found;
    return result;
    }
    private:
    str_list_t list{};
    pthread_mutex_t mutex{};
    time_t lastftime_check{};
    time_t timestamp{};

    };

    filterFile filter;

    void thread(void* arg) {
    filter.listed("");
    }

    int main(int argc, char ** argv) {

    for (int i = 1; i < argc; ++i) {
    filter.init(argv[i]);
    _beginthread(thread, 0, nullptr);
    SLEEP(2000);
    printf("Freeing list\n");
    filter.reset();
    printf("Done freeing list\n");
    }

    return 0;
    }
    ```
    Build it with this command-line replicating all the options to be sbbs.dll Win32-debug and adding address-sanitizer:
    ```
    cl /GS /analyze- /W3 /Zc:wchar_t /Zi /Gm- /Od /Zc:inline /fp:precise /D "_DEBUG" /D "WIN32" /D "_LIB" /D "LINK_LIST_THREADSAFE" /D "WINVER=0x600" /D "_WIN32_WINNT=0x600" /D "HAS_INTTYPES_H" /D "HAS_STDINT_H" /D "XPDEV_THREAD_SAFE" /D "_CRT_SECURE_NO_DEPRECATE" /D "_CRT_NONSTDC_NO_DEPRECATE" /D "_VC80_UPGRADE=0x0600" /D "_MBCS" /errorReport:prompt /WX- /Zc:forScope /RTC1 /std:c17 /arch:IA32 /Gd /Oy- /MTd /FC /EHsc /nologo /diagnostics:column /fsanitize=address -DHAS_STDINT_H strlisttest.cpp str_list.c genwrap.c xpprintf.c dirwrap.c threadwrap.c
    ```

    The resulting executable runs just fine:
    ```
    C:\sbbs\src\xpdev>strlisttest s:\sbbs\text\ip.can s:\sbbs\text\host.can s:\sbbs\text\ip.can s:\sbbs\text\host.can s:\sbbs\text\ip-silent.can
    Read s:\sbbs\text\ip.can
    Freeing list
    Done freeing list
    Read s:\sbbs\text\host.can
    Freeing list
    Done freeing list
    Read s:\sbbs\text\ip.can
    Freeing list
    Done freeing list
    Read s:\sbbs\text\host.can
    Freeing list
    Done freeing list
    Read s:\sbbs\text\ip-silent.can
    Freeing list
    Done freeing list
    ```

    I'm out of ideas for now.
    --- 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 Fri Mar 13 19:47:36 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1099#note_8577

    I tried a dynamic allocation version of this C++ test file as well, with no reproduction:
    ...
    ```
    filterFile* filter;

    void thread(void* arg) {
    filter->listed("");
    }

    int main(int argc, char ** argv) {

    for (int i = 1; i < argc; ++i) {
    filter = new filterFile(argv[i]);
    _beginthread(thread, 0, nullptr);
    SLEEP(2000);
    printf("Freeing list\n");
    delete filter;
    printf("Done freeing list\n");
    }

    return 0;
    }
    ```
    --- 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 Mon Mar 16 02:16:44 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1099#note_8600

    Here's another clue:

    without changing `filterfile.cpp` (so its `reset()` method did *not* call `strListFree()`), I was able to trigger the assertion (still, only on MSVC builds for Win32-debug) by adding the following lines right after `ip_can.init(&scfg, "ip");` in `mail_server()` - before *any* child threads are spawned!
    ```
    ip_can.listed("");
    ip_can.reset();
    ip_can.listed("");
    ip_can.reset();
    ip_can.listed("");
    ip_can.reset();
    ip_can.listed("");
    ip_can.reset();
    ip_can.listed("");
    ip_can.reset();
    ip_can.listed("");
    ip_can.reset();
    ip_can.listed("");
    ip_can.reset();
    ip_can.listed("");
    ip_can.reset();
    ```
    It would crash in one of the calls to `ip_can.listed()`, when it calls `strListFree()`.

    Another thing I noticed that I think could be relevant is at the time of the crash there was another mailsrvr.dll thread (!) running:
    ```
    ntdll.dll!_NtWaitForAlertByThreadId@8() Unknown Non-user code. Symbols loaded without source information.
    ntdll.dll!@RtlpWaitOnAddressWithTimeout@20() Unknown Non-user code. Symbols loaded without source information.
    ntdll.dll!_RtlpWaitOnCriticalSection@8() Unknown Non-user code. Symbols loaded without source information.
    ntdll.dll!_RtlpEnterCriticalSectionContended@8() Unknown Non-user code. Symbols loaded without source information.
    ntdll.dll!_RtlEnterCriticalSection@4() Unknown Non-user code. Symbols loaded without source information.
    mailsrvr.dll!__acrt_lock(__acrt_lock_id _Lock) Line 55 C++ Symbols loaded.
    mailsrvr.dll!heap_alloc_dbg_internal(const unsigned int size, const int block_use, const char * const file_name, const int line_number) Line 309 C++ Symbols loaded.
    mailsrvr.dll!heap_alloc_dbg(const unsigned int size, const int block_use, const char * const file_name, const int line_number) Line 450 C++ Symbols loaded.
    mailsrvr.dll!_calloc_dbg(unsigned int count, unsigned int element_size, int block_use, const char * file_name, int line_number) Line 518 C++ Symbols loaded.
    mailsrvr.dll!__vcrt_getptd_noexit() Line 128 C++ Non-user code. Symbols loaded.
    mailsrvr.dll!__vcrt_thread_attach() Line 155 C++ Non-user code. Symbols loaded.
    mailsrvr.dll!__scrt_dllmain_crt_thread_attach() Line 436 C++ Non-user code. Symbols loaded.
    mailsrvr.dll!dllmain_crt_dispatch(HINSTANCE__ * const instance, const unsigned long reason, void * const reserved) Line 221 C++ Non-user code. Symbols loaded.
    mailsrvr.dll!dllmain_dispatch(HINSTANCE__ * const instance, const unsigned long reason, void * const reserved) Line 276 C++ Non-user code. Symbols loaded.
    mailsrvr.dll!_DllMainCRTStartup(HINSTANCE__ * const instance, const unsigned long reason, void * const reserved) Line 334 C++ Non-user code. Symbols loaded.
    ntdll.dll!_LdrxCallInitRoutine@16() Unknown Non-user code. Symbols loaded without source information.
    ntdll.dll!_LdrpCallInitRoutineInternal@16() Unknown Non-user code. Symbols loaded without source information.
    ntdll.dll!_LdrpCallInitRoutine@16() Unknown Non-user code. Symbols loaded without source information.
    ntdll.dll!_LdrpInitializeThread@4() Unknown Non-user code. Symbols loaded without source information.
    ntdll.dll!__LdrpInitialize@8() Unknown Non-user code. Symbols loaded without source information.
    ntdll.dll!_LdrpInitializeInternal@8() Unknown Non-user code. Symbols loaded without source information.
    ntdll.dll!LdrInitializeThunk() Unknown Non-user code. Symbols loaded without source information.
    ```
    Apparently trying to allocate 40 bytes for what reason I do not know. but this is consistent. Crashes with either sbbs.exe or sbbsctrl.exe, so we know this is not about Borland C++ being involved (its not used when building/running sbbs.exe) and mixed runtime libraries/heaps.

    It would more reliably crash here (usually in the second call to `listed()`) if the mail server was the only server set to run automatically. This also reduced the total number of threads/noise and allowed me to notice the "extra" mail server thread running in (apparently) `calloc()`) at the time of the crash.

    Microsoft's AppVerifier doesn't catch anything (heap page or otherwise) before the assertion.
    --- 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 Mon Mar 16 04:47:53 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1099#note_8601

    More experimentation, I just placed this code at the beginning of `mail_server()` (though this could be any of the servers) and it reliably reproduces the crash. Playing with the `SLEEP()` duration changes to the crash point:
    ```
    startup = (mail_startup_t*)arg;

    SLEEP(200);
    scfg.cache_filter_files = 1;
    SAFECOPY(scfg.ctrl_dir, startup->ctrl_dir);
    SAFECOPY(scfg.text_dir, "../text/");
    ip_can.init(&scfg, "ip");
    ip_can.listed("");
    ip_can.reset();
    ip_can.listed("");
    ip_can.reset();
    ip_can.listed("");
    ip_can.reset();
    ip_can.listed("");
    ip_can.reset();
    ip_can.listed("");
    ip_can.reset();
    ip_can.listed("");
    ip_can.reset();
    ip_can.listed("");
    ip_can.reset();
    ip_can.listed("");
    ip_can.reset();
    ```

    It crashes in one of the `.listed()` calls (in `strListFree()->free`), just like always.

    So it doesn't appear to be caused by any code in the servers.
    --- 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 Mon Mar 16 16:46:37 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1099#note_8602

    Directly calling `mail_server()` from `sbbscon.c` `main()` (not using `_beginthread()`), the exception still happens. So it doesn't have to be a thread.

    Replacing the use of `ip_can` with direct calls to `strListReadFile()` and `strListFree()` does *not* reproduce the issue:
    ```
    str_list_t list = strListReadFile(fp, NULL, 1000);
    strListFree(&list);
    list = strListReadFile(fp, NULL, 1000);
    strListFree(&list);
    list = strListReadFile(fp, NULL, 1000);
    strListFree(&list);
    list = strListReadFile(fp, NULL, 1000);
    strListFree(&list);
    list = strListReadFile(fp, NULL, 1000);
    strListFree(&list);
    list = strListReadFile(fp, NULL, 1000);
    strListFree(&list);
    list = strListReadFile(fp, NULL, 1000);
    strListFree(&list);
    list = strListReadFile(fp, NULL, 1000);
    strListFree(&list);
    list = strListReadFile(fp, NULL, 1000);
    strListFree(&list);
    list = strListReadFile(fp, NULL, 1000);
    strListFree(&list);
    list = strListReadFile(fp, NULL, 1000);
    strListFree(&list);
    list = strListReadFile(fp, NULL, 1000);
    strListFree(&list);
    list = strListReadFile(fp, NULL, 1000);
    strListFree(&list);
    list = strListReadFile(fp, NULL, 1000);
    strListFree(&list);
    list = strListReadFile(fp, NULL, 1000);
    strListFree(&list);
    list = strListReadFile(fp, NULL, 1000);
    strListFree(&list);
    list = strListReadFile(fp, NULL, 1000);
    strListFree(&list);
    list = strListReadFile(fp, NULL, 1000);
    strListFree(&list);
    list = strListReadFile(fp, NULL, 1000);
    strListFree(&list);
    list = strListReadFile(fp, NULL, 1000);
    strListFree(&list);
    list = strListReadFile(fp, NULL, 1000);
    strListFree(&list);
    ```
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)