open
https://gitlab.synchro.net/main/sbbs/-/work_items/1302
`data/qnet/route.dat` (the QWKnet routing table learned from `@VIA` paths and read by `qwk_route()` to route netmail to `user@NODE`) is rewritten without a lock, so concurrent QWK/REP imports can lose route entries. A lost entry makes netmail to that node unroutable until a later message's `@VIA` re-teaches the route, which for a node that rarely posts can take a long time.
Code references are as of master eb6e30af8a (raised-27-cute, 2026-10-25).
## How route.dat is maintained
`sbbs_t::update_qwkroute()` (`src/sbbs3/qwk.cpp:178`):
1. On the first `@VIA` seen during an import, it loads all of `route.dat` into that `sbbs_t`'s own `qwknode[]` array (`qwk.cpp:206-245`).
2. Each further `@VIA` updates the in-memory copy.
3. At the end of the import, `update_qwkroute(NULL)` reopens the file `O_WRONLY | O_CREAT | O_TRUNC` and writes the whole in-memory copy back (`qwk.cpp:186-199`).
Writers: `un_qwk.cpp:368` (hub QWK import) and `un_rep.cpp:529` (REP import from a QWK-node account). Readers: `qwk_route()` (`qwk.cpp:70`), called from `netmail.cpp` and JS `qwk_route()`.
## The race
There is no lock and no re-read before the write, so two imports that overlap lose data in two ways:
- **Lost update:** imports A and B both load the file; A writes its copy (with A's newly learned routes); B then writes its copy, which never had them. A's new routes are gone.
- **Truncated read:** B loads the file while A is between the `O_TRUNC` and the end of its write, so B sees an empty or partial table. When B finishes it writes that partial table back, and every route it did not relearn during its own import is lost at once.
`qwk_route()` reading during a rewrite can also miss an entry transiently (netmail to that node fails to route for that one message).
## When imports overlap
- Today: a QWK-node user's REP upload at a terminal node (`qwk_upload()` -> `unpack_rep()`) runs on a node thread concurrently with the event thread's hub QWK import; and every host sharing `data/` (e.g. a Windows and a Linux host on one share) runs its own imports.
- Work under way to pack/import QWK packets directly in the FTP server, Web server (`qwk.ssjs`) and scripts lets those imports run in parallel too, so overlap becomes more likely.
## Suggested fix
In `update_qwkroute(NULL)`:
1. Take a cross-process lock (an fmutex such as `data/qnet/route.lock`, short bounded wait). A process-local mutex is not enough: the web server, jsexec and other hosts are separate processes.
2. Re-read `route.dat` and merge it with the in-memory entries, newest date winning per node ID.
3. Write to a temp file in `data/qnet/` and replace-rename it over `route.dat` (`MoveFileEx(MOVEFILE_REPLACE_EXISTING)` on Windows), so a concurrent reader always sees a complete file.
4. Release the lock.
The 90-day expiry stays as is.
*Authored by Claude (Claude Code), on behalf of @rswindell*
--- SBBSecho 3.38-Linux
* Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)