• sexyz: YMODEM-G receive (rg) never falls back when the sender does not

    From Rob Swindell@1:103/705 to GitLab issue in main/sbbs on Sun Aug 30 17:26:42 2026
    open https://gitlab.synchro.net/main/sbbs/-/issues/1232

    A YMODEM-G receive (`sexyz rg`) never falls back when the sender turns out not to support G. It keeps requesting G, exhausts its error budget, and fails with `!Error fetching YMODEM header block`. Forsberg's DSZ, given the identical situation, drops out of G mode and completes the transfer.

    This is user-facing: the stock transfer-protocol table uses `rg` for the YMODEM-G entry —

    ```
    name=YMODEM-G
    ulcmd=%!sexyz %h -%p rg %f
    ```

    — so a user who picks YMODEM-G from the protocol menu with a terminal that does not actually implement G-send gets a failed upload rather than a slower, working one.

    ## Reproduction

    A sender that refuses G, in front of a G-mode receiver:

    ```
    sexyz.ini: [XMODEM]
    SendG=false

    sender: sexyz sY <file>
    receiver: sexyz -g rg
    ```

    Result: `!Error fetching YMODEM header block`, session abandoned after 27 seconds, nothing transferred.

    The same sender in front of DSZ.EXE 1997 under DOSBox (`dsz port 1 … d rb -g`): **DSZ falls back and the file arrives byte-identical.**

    | Receiver | Falls back? |
    |---|---|
    | **sexyz `rg`** | **no** — fails after 27 s |
    | DSZ `rb -g` (1997) | yes — transfers |

    ## Cause

    The fallback machinery already exists, and is explicitly switched off in G mode. In `sexyz.c`, in the YMODEM header-block loop:

    ```c
    if (errors + 1 > xm.max_errors / 3 && mode & CRC && !(mode & GMODE)) {
    lprintf(LOG_NOTICE, "Falling back to 8-bit Checksum mode");
    mode &= ~CRC;
    }
    ```

    The trailing `!(mode & GMODE)` means that when G is requested there is no downgrade path at all — not to CRC, not to checksum. A second, identical guard appears in the data-block loop. Nothing on the receive side ever clears `GMODE`, so `xmodem_put_nak()` keeps choosing `G`:

    ```c
    if (*(xm->mode) & GMODE) { /* G for X/Ymodem-G */
    lprintf(xm, LOG_INFO, "Block %u: Requesting mode: Streaming, 16-bit CRC", block_num);
    result = putcom('G');
    }
    ```

    The guard is understandable in isolation — mid-transfer you cannot switch a streaming G session to an ACKed one, since the sender is already firing blocks without waiting. But at **block 0/1, before any data has moved**, there is nothing to lose by dropping to `C`, which is exactly what DSZ does.

    ## Suggested fix

    Mirror the existing CRC fallback one level up: if repeated `G` requests go unanswered while still on the header block, clear `GMODE` and continue as YMODEM-CRC. The CRC fallback three lines away is the pattern, and `xmodem.c` already contains a `*(xm->mode) &= ~(GMODE | CRC)` on the sender side, so the idiom exists in both files.

    Worth checking at the same time whether XMODEM-G (`rx -g`) has the same gap; only the YMODEM-G path was measured here.

    ## Context

    Found during an audit of every sexyz option — command-line, `sexyz.ini` and protocol-negotiated — prompted by #1229, where an option turned out to have been inert for 21 years. Full results in `docs/xymodem_verification.md`; the ZMODEM half is in `docs/zmodem_comparison.md`.

    The audit's other X/YMODEM findings are deliberately not filed here, because comparing against other implementations showed they are not ours to fix:

    - **CRC fallback we get right.** Against a checksum-only sender, `sexyz rc` falls back and completes in 12 s and DSZ `rx` does the same, while `lrz --xmodem -c` never degrades and times out at 40 s. The deadlock is lrzsz's.
    - **`[XMODEM] MaxBlockSize`** has no counterpart in `lrz` or DSZ to compare against; it is Synchronet-only, does not cap an explicit `sX` send, and on the receive side breaks an otherwise working transfer from a 1K sender. That is a documentation or removal question rather than a protocol defect.

    Interoperability itself is clean: all twelve X/YMODEM sender/receiver combinations against lrzsz and DSZ transfer byte-for-byte.

    — *Authored by Claude (Claude Code), on behalf of @rswindell*
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)