• docs/v322_new.md src/sbbs

    From Rob Swindell (on Windows@VERT to Git commit to main/sbbs/m on Fri Jul 17 03:05:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/9fc5505cbc4dfc9aeaab00ff
    Modified Files:
    docs/v322_new.md src/sbbs3/ctrl/build.bat src/sbbs3/jsexec.vcxproj ntsvcs.vcxproj sbbscon.vcxproj
    Log Message:
    Windows: build all 32-bit Synchronet hosts /LARGEADDRESSAWARE

    Mark sbbs.exe (sbbscon.vcxproj), sbbsNTsvcs.exe (ntsvcs.vcxproj),
    jsexec.exe (jsexec.vcxproj), and sbbsctrl.exe (ctrl/build.bat) as large-address-aware, raising their usable address space from 2GB to
    ~4GB on 64-bit Windows.

    This is a stopgap for GitLab #1185: an access violation in mozjs185
    seen only on 32-bit Windows under JS heap pressure (a web-scrape
    attack), never on Linux-x64. It raises the ceiling; it does not fix
    the underlying pointer corruption, which the SM128 / 64-bit migration
    addresses properly.

    The MSVC projects get <LargeAddressAware>true</LargeAddressAware> in
    both Debug|Win32 and Release|Win32 link steps. Borland's ilink32 has
    no LAA option, so build.bat flips the PE-header bit post-build
    instead.

    Validated: the 2006-era Borland host runs stably under LAA with
    system-wide top-down allocation (Synchronet DLLs executing above 2GB,
    all servers listening, no errors), and jsexec exercised the mozjs GC
    and trace-JIT with ~3.4GB live above 2GB without failure. Sustained
    behavior under real scrape load is not yet proven.

    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Windows@VERT to Git commit to main/sbbs/m on Sat Jul 18 22:02:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/3a5e9cb3628031f8812840c5
    Modified Files:
    docs/v322_new.md src/sbbs3/scfg/scfg.c scfg.h scfgmsg.c
    Log Message:
    Restore SCFG import/export of subs.txt, alongside subs.ini

    subs.txt import/export was removed in mayor-19-caps (e6acb3269a) and its
    menu slots later repurposed for *subs.ini in reef-31-feed (cfb32e03f5).
    Some sysops still rely on the legacy text format (especially for import),
    so this restores it as an additional format while keeping *subs.ini as
    the preferred format going forward.

    Message Areas > Export/Import Areas list subs.txt again, the -import=
    command line recognizes a subs.txt filename once more, and the
    line-oriented parser/writer are back.

    Rather than the old memcpy-over-the-existing-sub (which zeroed every
    field the format doesn't carry - qwk_conf, print modes, area_tag, ...),
    the import now copies only the fields subs.txt actually provides, leaving
    the rest of an over-written sub intact. ptridx is likewise preserved
    (the exported value is meaningless on the importing system).

    Partially addresses issue #1128.

    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sun Jul 19 15:25:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/8a79c1ff830f811d824cced5
    Modified Files:
    docs/v322_new.md src/sbbs3/nodedefs.h
    Log Message:
    Document node-rerun-on-recycle behavior; fix stale NODE_RRUN comment

    Add a v3.22a release note for the terminal-server change that flags
    active nodes to re-read their configuration when a recycle is signaled,
    so configuration changes reach new callers without waiting for every
    node to disconnect.

    Also correct the NODE_RRUN definition comment. Its only remaining
    effect in the threaded terminal server is to make a node re-read its configuration on the next logon; nothing is "re-run" and there is no logoff-time action (unlike its neighbors NODE_EVENT and NODE_DOWN).
    The old "Re-run this node when log off" wording described the DOS/OS2 separate-process node model that no longer exists.

    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Fri Jul 24 11:16:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/04b47329aa0cfbc839ec1a4c
    Modified Files:
    docs/v322_new.md src/sbbs3/zmodem.c
    Log Message:
    zmodem: fix SIGFPE when the transmit window is under 4 blocks (#1197)

    zmodem_send_from() picks the ack-request interval as

    subpkts_sent % (max_window_size / block_size / 4)

    which is a division by zero whenever the window is narrower than four
    blocks: with -8 -w8192 the divisor is 8192 / 8192 / 4, that is 1 / 4,
    that is 0. sexyz then dies with SIGFPE right after the ZRQINIT/ZRINIT handshake, having put about 3 KB of handshake on the wire and no file
    data at all. Also affected: -8 -w16384, -2 -w4096, and the same window
    sizes set through MaxWindowSize in sexyz.ini.

    Clamp the interval to a minimum of one, so a window narrower than four
    blocks requests an ACK on every subpacket, which is the only sensible
    reading of "every quarter window" at that size.

    Verified against lrz over the bench harness: -8 -w16384 and -2 -w4096
    both used to die on signal 8 and now complete with a matching SHA-256, at
    3.08 and 0.71 MB/s. Windows that already worked are unchanged, -w32768
    at 4.88 MB/s and -w65536 at 5.03, as is streaming without -w at 11.6.

    SyncTERM shares zmodem.c but never sets max_window_size, so it leaves the
    value at zero, short-circuits the branch, and cannot reach this. The
    exposure is sexyz only.

    A window exactly equal to the block size (-8 -w8192) no longer crashes
    but is still pathologically slow, roughly one subpacket per second with a one-second receive timeout each iteration. That is tracked separately in
    the issue; it is not a regression, since it used to be a crash.

    zmodem_ver bumped 2.3 -> 2.4. Release notes updated, including the already-shipped 2 GB windowed fix and Deuce's send-path speedups, which
    had no v322 entry.

    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Fri Jul 24 23:28:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/aaf394a0d833acd89a64d3a9
    Modified Files:
    docs/v322_new.md src/sbbs3/sexyz.c
    Log Message:
    sexyz: keep the transmit window >= 4x the block size, like lsz (#1197)

    sexyz's -w set only the ZMODEM transmit window and left the block size independent (from -2/-4/-8, default 1 KB), so a window at or below the
    block size was configurable -- and it stalled the transfer to roughly one window per second. lsz and Forsberg sz never allow this: when -w is
    given they force the block down to a quarter of the window
    (blkopt = Txwspac = Txwindow/4), guaranteeing window >= 4*block so the ack-every-quarter-window scheme always has several blocks in flight.

    Do the same after argument parsing, so -8 -w8192 and -w8192 -8 both
    resolve the same way: floor the window to 256, round to a multiple of 64,
    and reduce max_block_size to window/4 when it exceeds it (and the start block_size with it). A one-line notice reports the reduction.

    Effect: the formerly-hanging window<=block configurations now complete --
    -8 -w8192 reduces the block to 2048 and transfers, -4 -w4096 to 1024,
    and so on -- while windows that already left four blocks of headroom
    (-w32768 and up with -8) are untouched and unchanged (4.75 and 4.88 MB/s
    here, streaming still 11.3). This also makes the divide-by-zero guarded
    in 04b47329aa unreachable by configuration, since window/block/4 is now
    always >= 1.

    This does not make sexyz's windowed throughput competitive with lsz --
    at the same block and window sexyz is still many times slower, and very
    small windows (which force tiny blocks) remain slow with occasional
    one-second stalls. That is the separate sexyz windowed-send overhead
    tracked in #1195; this change is only about not permitting the degenerate window<=block configuration that lsz forbids by construction.

    sexyz.c version 3.3 -> 3.4. No zmodem.c change, so SyncTERM is
    unaffected (it never sets a transmit window anyway).

    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Windows@VERT to Git commit to main/sbbs/m on Sat Jul 25 22:24:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/b17b1d3e3e45bbbc4df47ae6
    Modified Files:
    docs/v322_new.md src/sbbs3/atcodes.cpp getkey.cpp js_bbs.cpp putmsg.cpp sbbs.h terminal.h
    Log Message:
    Unicode @-codes: return the expansion instead of printing it

    @U+XXXX@, @CHECKMARK@, @ELLIPSIS@, @COPY@, @SOUNDCOPY@, @REGISTERED@, @TRADEMARK@, @DEGREE_C@, @DEGREE_F@ and @WIDE:text@ wrote their glyph
    straight to the terminal from inside atcode() and returned an empty
    string. Format modifiers were therefore applied to "": @CHECKMARK-R10@
    emitted the glyph and then right-aligned nothing in a 10-column field.
    Callers that expand an @-code into a buffer for a non-printing reason -- bbs.atcode(), bbs.expand_atcodes(), format_text(), mnemonics(),
    putnode(), notify() and putmsg()'s @CENTER@ -- got an empty expansion
    plus stray output on the current node's terminal.

    These codes now encode into the caller's buffer and set P_UTF8, so show_atcode()'s existing display-width compensation makes alignment and truncation work. formatted_atcode() and expand_atcodes() take an
    optional pmode out-param so the buffer-expansion callers can learn the
    result is UTF-8; @CENTER@ and mnemonics() use it, the latter needing the
    P_UTF8 branch bputs() already has, since it emits byte-by-byte through outchar(). Terminal::center() measures with bstrlen(str, mode & P_UTF8):
    the other code sets bstrlen() recognizes are translated by putmsg(), not bputs(), and a centered line bypasses putmsg()'s character loop.

    The |W (doubled) modifier prints a UTF-8 expansion as-is rather than
    feeding it to wide(), which would double each byte rather than the glyph. Output buffers grew to fit @WIDE:, which triples byte length.

    Introduced in 9374bfe37e (pill-4-pencil), a9eea57362 (begun-3-ended) and 43c793a14b (hold-20-maps).

    Fixes #1198

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Windows@VERT to Git commit to main/sbbs/m on Sat Jul 25 22:24:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/dbc654505f7d822a8ece7c0d
    Modified Files:
    docs/v322_new.md src/sbbs3/con_out.cpp terminal.cpp
    Log Message:
    Count UTF-8 columns once, in Terminal::utf8_increment()

    outcp() wrote the codepoint with term_out() and then advanced the column itself, but term_out() -> parse_output() -> Terminal::utf8_increment()
    had already counted it -- so every codepoint outcp() emitted to a UTF-8 terminal advanced the column counter by twice the glyph's display width.
    The counter ran ahead of the cursor, making word-wrap, right-margin
    truncation, centering and the auto-pause line counter wrap early on any
    line containing such text. The call was also unconditional while
    term_out() returns early under CON_ECHO_OFF, so the column advanced for suppressed output too.

    utf8_increment() is now the only place that counts, and it passes sbbs->unicode_zerowidth rather than a hardcoded 0, preserving the
    zero-width quirk the ZWNBSP probe detects at logon. That applies the
    detected value to all UTF-8 output, bputs() included, not just outcp().

    outcp()'s inc_column() was correct when written in 9374bfe37e
    (pill-4-pencil): it paired with putcom(), which does no column
    accounting. It became a double-count in 374282bf81 (skins-28-shop),
    which switched the write to term_out() and introduced utf8_increment().

    Fixes #1200

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Windows@VERT to Git commit to main/sbbs/m on Sat Jul 25 23:32:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/005460da2b098c8ec796464a
    Modified Files:
    docs/v322_new.md src/sbbs3/useredit.cpp
    Log Message:
    Uedit: newline before the non-alpha commands that prompt

    getkeys() runs with K_NOCRLF and only alpha commands got a newline
    before their handler, so the '~' (leech), '+' (adjust credits), '*'
    (adjust minutes), '$' (credits) and '/' (AR string) prompts were echoed
    onto the tail of the "User edit (?=Menu) (n of m): " line.

    Present since ca499c9550 (silence-13-chorus) introduced the alpha-only
    newline.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Tue Jul 28 18:06:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/58dc4d2b39ca0c227d5427aa
    Modified Files:
    docs/v322_new.md src/sbbs3/js_global.cpp src/xpdev/ini_file.h
    Log Message:
    Correct the .local rationale: ctrl/ is not overwritten by an upgrade

    The commit that added the .local filename variation justified it as protecting a sysop's edits from being overwritten by an upgrade. That is not true of the ctrl directory, which is where most of the configuration files it applies to live: ctrl is populated when the BBS is installed and an upgrade leaves it alone. The Windows upgrade script copies exactly one file into it, text.dat.

    What an upgrade does refresh is the distributed program directories -- exec, text and docs unconditionally, and xtrn when the sysop accepts the installer's "Upgrade Externals" option -- so the overwrite argument holds for a file we ship in one of those, and for any installation run from a source checkout, where updating means pulling the whole tree.

    The variation is still worth having for a ctrl file; the honest reason is separation rather than rescue. Reworded accordingly on the wiki (config:ini_files, howto:multihost) and in the release notes, where the claim was made. xpdev is a general-purpose library, so its header documents the search order and what .local means and leaves the rationale to those.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Tue Jul 28 19:54:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/d91e670aaee817fea9820e9e
    Modified Files:
    docs/v322_new.md src/sbbs3/con_out.cpp str_util.c str_util.h terminal.h Log Message:
    bprintf: measure %s field widths in columns rather than bytes

    printf counts a field width in bytes, but a terminal renders columns.
    For a UTF-8 argument the two differ, so a byte-counted field prints
    short and whatever follows it slides left. Every columnar format string
    sent to a UTF-8 terminal is affected; message listings are the most
    visible case.

    Rewrite the numbers instead of the formatting. fmt_col_widths() walks
    the conversions in a format string, and for each %s carrying a literal
    width or precision re-states them in bytes such that printf's
    truncate-then-pad arrives at the requested number of columns: the new
    precision is the byte count of the leading N columns, and the new width
    is padded by the byte-minus-column difference. vsnprintf still does all
    the conversion. Sysop-customized text.dat files therefore keep working untouched, and output to a non-UTF-8 terminal is unchanged, the pre-pass running only for P_UTF8.

    A '*' width or precision is left alone: atcode() computes those itself
    and has already accounted for the difference, so rewriting them would compensate twice. An unrecognized conversion, a NULL argument, or a
    rewrite that would overflow the buffer all return the format string
    unmodified.

    Widths are measured with Terminal::bstrlen(), which already knows what
    occupies a column -- Ctrl-A codes, the PCBoard, Wildcat, Renegade,
    Celerity and WWIV sequences, backspace, and UTF-8 code-point widths.
    It gains an optional column limit and a byte-count out-parameter, which
    is what lets a field be truncated on a column boundary without splitting
    a multi-byte sequence.

    Issue #1204

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Fri Jul 31 21:43:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/72303693ef1b8dc2c11ff006
    Modified Files:
    docs/v322_new.md src/sbbs3/websrvr.cpp
    Log Message:
    websrvr: enforce download credits for unauthenticated file requests

    An anonymous request through the file-area virtual path
    (FileVPathPrefix) was authorized by user_can_download() alone, with no
    credit check, so every file in a directory with empty ARS downloaded
    free of charge regardless of its cost. The FTP server applies the
    credit gate to anonymous users, and issue #446 was closed with that
    being the intended policy.

    Both authorization paths now share check_file_download(), which applies
    the credit gate before the ARS check. Putting the two callers on one
    predicate is the point of the helper: the bypass existed because the
    anonymous branch and the authenticated one had drifted apart.

    The gate measures cost against user_available_credits() rather than
    user.cdt, so a user's free credits now count toward a download, as they
    already do on the Terminal and FTP servers, and as subtract_cdt()
    already assumed when charging: it spends free credits first.

    A refused anonymous request is logged at LOG_DEBUG. An insufficient
    credit refusal now reports NotEnoughCredits; both this and the existing authenticated failure log previously reported CantDownloadFromDir for
    it, naming the wrong cause.

    Fixes #1192

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sat Aug 1 20:33:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/4de1032086f038890a75559e
    Modified Files:
    docs/v322_new.md src/sbbs3/websrvr.cpp
    Log Message:
    websrvr: load a file's cost before checking download credits (#1192)

    The credit gate added in 72303693ef (2026-07-31) compared against a
    cost that was always zero. resolve_vpath() loaded the file record at file_detail_index, and smb_getfile() skips smb_getmsghdr() entirely at
    that detail level, while cost is only ever set from the SMB_COST header
    field. Every comparison against it therefore passed: costed files
    downloaded free of charge for authenticated users as well as anonymous
    ones, while user_downloaded_file() still billed the account afterward
    (it reloads the record itself at file_detail_normal), so a user could
    go credit-negative.

    Read the header record when the directory's files can cost something. A
    free directory has none to report, since loadfile() zeroes the cost and download_is_free() short-circuits on the same DIR_FREE flag before any
    credit comparison, so the index record alone still suffices there and loadfile() is not asked to open the base twice.

    Verified against a live file base: an unauthenticated request for a 2,639,434-credit file is refused with 401, a zero-cost file in the same non-free directory returns 200, and a file in a free directory
    transfers in full.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sat Aug 1 23:40:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/7a0dabc5613b3f1a5e4ff8be
    Modified Files:
    docs/v322_new.md src/sbbs3/js_file.cpp
    Log Message:
    Parse an .ini once in File.iniGetAllObjects()

    It asked for each section's keys by name, and that lookup scans the file
    from the top -- so the cost was O(sections * lines). SyncArcade's lobby
    reads games.ini on every run for the cabinet titles, and at 4740 sections
    that read took two seconds on an idle machine; a loaded one paid seven,
    which reads as the lobby rescanning its ROMs when it is doing nothing of
    the kind.

    Parse the file into per-section lists once instead, the way scfglib1.c
    and scfglib2.c already read the configuration: 2005ms -> 66ms on that
    file. The section list still comes from iniGetSectionList(), so a
    duplicate section name is still collapsed to its first occurrence.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net