• src/sbbs3/zmodem.c

    From Rob Swindell (on Debian Linux)@VERT to Git commit to main/sbbs/master on Monday, August 24, 2026 20:32:03
    https://gitlab.synchro.net/main/sbbs/-/commit/decf9d9ef8da1209bf890cee
    Modified Files:
    src/sbbs3/zmodem.c
    Log Message:
    zmodem: don't abort a receive after the tenth error of the whole file

    zmodem_recv_file_data() counted errors in a local that was set to zero once
    per file and only ever incremented, so the tenth error ended the transfer however far apart those errors were and however cleanly each had been recovered. A receive over any link with a non-zero error rate therefore had
    a file size beyond which it could not succeed, while the sender in the same conditions ran indefinitely: zmodem_send_from() counts consecutive_errors and clears it on success.

    Reset the count whenever the file position has advanced past the position the last error was recorded at, which is the receive-side statement of the same rule. A run of errors with no progress between them still stops at
    max_errors, so a wedged transfer ends as before.

    Measured with the bench harness injecting bit errors at 3e-6 per byte into an
    8 MB transfer -- around 25 corrupted subpackets, each individually recovered. Before, the receiver aborted at the tenth every time; after, all three runs complete and verify, with the error counter returning to zero between them. zmtx/zmrx survives the same treatment and lrzsz usually does, which is what prompted looking at this.

    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 Linux)@VERT to Git commit to main/sbbs/master on Tuesday, August 25, 2026 01:12:30
    https://gitlab.synchro.net/main/sbbs/-/commit/d3e4f8094d0255fafaf436f2
    Modified Files:
    src/sbbs3/zmodem.c
    Log Message:
    zmodem: don't drop the CR/LF that terminates a hex header when ESCCTL is on

    zmodem_recv_hex_header() read the CR/LF ending a hex header through zmodem_rx(), which drops unescaped control characters once
    escape_ctrl_chars is set. CR and LF are control characters, so with
    ESCCTL negotiated the terminator was dropped, the header never
    terminated, and the receiver timed out on it:

    !Dropping unescaped ctrl char ... (x20)
    !zmodem_recv_hex_header HEX header not terminated with LF: TIMEOUT
    !zmodem_recv_header detected an INVALID HEADER

    The data itself transfers, since data subpackets use binary headers; it
    is the session teardown that fails. Measured on an 8 MB receive with EscapeCtrlChars=true: 100.17 seconds and exit -1, against 0.07 seconds
    once fixed. The bytes are all there and verify byte-identical -- the
    receiver simply cannot read the ZFIN and sits re-sending ZRINIT until it
    gives up.

    A hex header's terminator is framing, not data. A hex header is built
    from printable characters plus CR/LF precisely so it survives a link
    that mangles control characters, and every implementation sends that
    terminator unescaped whether or not ESCCTL was negotiated -- so it must
    not go through the ESCCTL filter at all. Read it with a small helper
    that calls zmodem_recv_raw() directly and skips flow-control bytes the
    way zmodem_rx() does.

    This is long-standing, not a regression: a sexyz built from the commit
    that moved the sexyz change log out of the packaging directory --
    before the plain-byte table and before the bulk receive callback --
    reproduces it identically, 100.14 seconds and the same 20 dropped
    characters. It went unnoticed because EscapeCtrlChars defaults to
    false and had no command-line equivalent to reach it with.

    Verified unchanged on the normal path, where every hex header also goes
    through the new helper: 256 MB receive 482.7 MB/s and send 206.3 MB/s,
    the 3e-6 corruption gate 5 of 5 receiving and 3 of 3 sending, plus a
    YMODEM round trip.

    NOT fixed here, and separate: sending WITH ESCCTL is also broken --
    a receiver that requests it (lrz -e) rejects sexyz's escaped data and
    ZRPOSes until the error budget is spent. That path is driven by the
    peer's ZRINIT rather than by any local option, reproduces on the same pre-change binary, and needs its own investigation.

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