open
https://gitlab.synchro.net/main/sbbs/-/issues/1229
Setting `Escape8thBit=true` in `sexyz.ini` makes sexyz advertise `ZF0_ESC8` in its ZRINIT, asking the remote sender to escape every byte with the high bit set. **sexyz cannot decode those escapes**, so a sender that honours the request cannot transfer to it at all. sexyz also ignores the flag when *sending*, so it never escapes on behalf of a peer that asks for it.
In short: the option is advertised on the wire but implemented in neither direction.
## Reproduction
`sexyz.ini`:
```ini
[ZMODEM]
Escape8thBit=true
```
Then receive from a sender that honours ESC8 — `zmtx` 2.04 is the only one I know of (see *History* below):
```
zmtx -8 file -> sexyz -8 rz
```
Result: the transfer never starts.
```
sexyz: !0 Illegal sequence: ZDLE 229 (x6)
sexyz: !0 zmodem_recv_raw TIMEOUT (10 seconds)
sexyz: !zmodem_recv_header TIMEOUT
sexyz: Exiting - Error level: -1
zmtx: zmtx: can't start file transfer: protocol timeout
```
With `Escape8thBit=false` (the default) the same pair transfers normally.
## Root cause
**Receive.** `zmodem_rx()`'s ZDLE branch decodes only `(c & 0x60) == 0x40`, i.e. a control character escaped as `ZDLE, c + 0x40`. An ESC8 escape is `ZDLE, c ^ 0x40` where the original byte has bit 7 set — for example `0xA5` is sent as `ZDLE 0xE5`. `0xE5 & 0x60 == 0x60`, not `0x40`, so it falls through to:
```c
lprintf(zm, LOG_WARNING, "%lu Illegal sequence: ZDLE %s", ...);
```
which is exactly the `Illegal sequence: ZDLE 229` (`0xE5`) above. There is no ESC8 case anywhere in `zmodem_rx()`.
**Send.** `zmodem_tx_active()` builds the transmit escape mask from `escape_ctrl_chars` and `escape_telnet_iac` only:
```c
return ZMODEM_TX_ESCAPE_ALWAYS
| (escape_ctrl * ZMODEM_TX_ESCAPE_CTRL)
| (!!zm->escape_telnet_iac * ZMODEM_TX_ESCAPE_IAC);
```
`escape_8th_bit` is not consulted anywhere in the transmit path. Confirmed by measurement: a sexyz → sexyz transfer with `Escape8thBit=true` at **both** ends succeeds while putting the *unescaped* byte count on the wire (4,311,816 bytes for a 4 MiB random file — identical to a run with the option off).
The flag has exactly two uses in the tree today:
- `zmodem.c` — assigned from the peer's ZRINIT (`zm->escape_8th_bit = INT_TO_BOOL(zm->rxd_header[ZF0] & ZF0_ESC8)`)
- `zmodem.c` — consulted in `zmodem_send_zrinit()` to set the flag we advertise
## History
- `escape_8th_bit` arrived with the original import, e1d0ddb0b5 (2003-09-04). Even then it was only ever assigned from the peer's ZRINIT and printed in a debug line — it never fed an escaping decision.
- 49901d3aca (2005-06-11), "Renamed zm.escape_all_control_chars to just escape_ctrl_chars. send_zrinit() now sets ZF0 flags based on configurable BOOLs", is where sexyz began **advertising** ESC8 from the configurable flag.
- Nothing has touched it since.
So it has never been implemented, and has been advertised for 21 years.
## Why this stayed hidden
No other implementation honoured ESC8 either, so nothing ever escaped on our behalf and the missing decode path was never reached:
- Chuck Forsberg's own **rzsz 3.73** defines `ESC8`/`TESC8` in `zmodem.h` and **never references either in any `.c` file** — he specified the flag and did not implement it. (He does consume the neighbouring ZF0 bits: `Zctlesc |= Rxflags & TESCCTL`, `CANFC32`, `CANFDX` in `sz.c`.)
- **lrzsz 0.12.21rc** inherits the same two dead `#define`s and likewise never references them.
- **zmtx/zmrx 2.04**, released 2026-08-25, is the first implementation in this lineage to actually implement it — `zmrx -b` to request it, `TX_ESCAPE_8TH` in `zmdm.c` to honour it. That is what exposed this.
Verified that zmtx/zmrx's implementation is real and self-consistent: `zmtx → zmrx -b` transfers correctly with genuine escaping, the wire growing from 4,311,816 to **6,360,011** bytes for the same 4 MiB file. By contrast `sexyz → zmrx -b` and `lsz → zmrx -b` both "succeed" only because the sender ignores the request and zmrx tolerates unescaped input.
## Impact
`Escape8thBit` defaults to `FALSE`, so a default install is unaffected and this is not a regression — it has never worked. The exposure is any sysop who sets it, which is precisely the sysop on a link that does not pass 8-bit data cleanly: they get a receiver that cannot receive from a conforming sender, with a misleading `Illegal sequence` / `TIMEOUT` failure rather than a diagnostic.
## Suggested resolution
Either is defensible:
1. **Implement it.** Add an ESC8 decode to `zmodem_rx()` and an `ESCAPE_8TH` class to the transmit mask, mirroring how ESCCTL is handled. There is now exactly one peer to interoperate with and test against.
2. **Stop advertising what is not implemented.** Drop `ZF0_ESC8` from `zmodem_send_zrinit()` and remove or deprecate the `Escape8thBit` key, so sexyz never asks for something it cannot decode.
(2) removes the footgun immediately; (1) is the more useful end state now that a working peer exists. Doing neither leaves a config key whose only effect is to break transfers.
## Notes
- Found while re-measuring `docs/zmodem_comparison.md` against current releases of every implementation; see §7.2 there for the measurements and the ESC8 interoperability table.
- Unrelated to this issue, and not ours: zmtx 2.04's **ESCCTL** sender omits carriage return, so it cannot send to any ESCCTL receiver other than its own `zmrx`. That is filed separately in the comparison document for the zmtx/zmrx maintainer.
— *Authored by Claude (Claude Code), on behalf of @rswindell*
--- SBBSecho 3.37-Linux
* Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)