• docs/v322_new.md src/sbbs3/zmodem.c

    From Rob Swindell (on Debian Linux)@VERT to Git commit to main/sbbs/master on Fri Jul 24 11:16:06 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 Linux)@VERT to Git commit to main/sbbs/master on Tue Aug 25 01:12:30 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/fd038d9dd3cba68b4d95d026
    Modified Files:
    docs/v322_new.md src/sbbs3/zmodem.c
    Log Message:
    zmodem: escape CR unconditionally when ESCCTL is negotiated

    A sender with ESCCTL negotiated escaped every control character
    except carriage return, which it routed to a conditional rule --
    escape only when the previous byte sent was '@' -- and otherwise put
    on the wire raw. A receiver that requested ESCCTL discards unescaped
    control characters by definition, so it discarded those CRs, every
    subpacket carrying one failed its CRC, and the transfer never
    advanced: the sender ZRPOSed to zero until it spent its error budget
    and gave up. With random data that is any subpacket at all. A
    download to any receiver requesting ESCCTL was therefore impossible,
    not merely slow.

    lrzsz settles what the rule should be. In zsendline_init() (zm.c) the
    table entry for 015/0215 is:

    if (Zctlesc) tab = 1; /* always escape */
    else if (!turbo_escape) tab = 2; /* the conditional '@' rule */
    else tab = 0;

    So the conditional rule is the NON-ESCCTL case, and ESCCTL escapes CR
    like any other control character. This engine had the two swapped:
    the conditional rule was reachable only with ESCCTL on, where it is
    wrong, and never reachable with ESCCTL off, where it would be right.
    Since it could not fire correctly in either state, drop it -- fold CR
    into the control class and let the escape mask decide, which is
    exactly lrzsz's behaviour for both states. The '@' rule can be added
    properly later if wanted; the last_sent field it read is left in
    place, being public.

    The unescaped path is provably untouched: without ESCCTL neither the
    old CR class nor the new one is in the active mask, so CR is a normal
    byte either way. Confirmed byte-for-byte -- a 4 MB send still puts
    4,311,816 bytes on the wire, 256 MB still receives at 484.99 MB/s and
    sends at 206.24 MB/s, and the 3e-6 corruption gate still passes 5 of 5 receiving and 3 of 3 sending.

    With the fix, ESCCTL interoperates in every direction: sexyz to lrz,
    lrz to sexyz, and sexyz to itself all verify byte-identical. The
    strongest evidence it is now correct rather than merely accepted is
    the wire size -- sexyz puts 5,245,034 bytes on the wire where lsz puts 5,245,030 for the same file, so the two escape the same byte set.
    Random data exercises every one of the 256 values, so nothing is
    escaped by luck.

    Under injected errors the escaped path is a little less robust than
    the plain one: 4 of 5 at 3e-6 and 5 of 5 at 1e-6, against 5 of 5 for
    the unescaped path at 3e-6. It carries 25 % more bytes and so takes
    25 % more corruption at a given per-byte rate. No lrzsz baseline
    exists for that comparison -- lsz -e to lrz -e does not survive the
    harness's error injection at all, dying inside 0.04 s on every
    attempt.

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