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)