open
https://gitlab.synchro.net/main/sbbs/-/issues/1196
## Summary
A **ZMODEM send with a transmit window (`-w`) hangs permanently the instant the file offset crosses 2 GiB (2^31)**. The cause is in the shared `src/sbbs3/zmodem.c`: several file positions used in the transmit-window / ACK accounting are narrowed to **signed `int32_t`**, which goes negative above 2 GiB
and corrupts the window arithmetic. Because `zmodem.c` is shared, this affects **both `sexyz` and SyncTERM** (any SyncTERM upload with a window).
The ZMODEM header position field is 32-bit on the wire, so 4 GiB is an inherent protocol ceiling for everyone — but this bug bites at **2 GiB**, needlessly.
## Reproduction
Send a >2 GiB file with a transmit window enabled (`sexyz -8 -w1M sz bigfile`). The transfer streams fine until the offset crosses 2^31, then stalls with this in the sender log, repeating until timeout:
```
!2147744768 Transmit-Window management: 4294975488 >= 1048576
!Receive timeout (1 seconds)
```
The computed window is **4,294,975,488 ≈ 2^32 + 8192** — i.e. `current_file_pos (2,147,744,768) − ack_file_pos`, where `ack_file_pos` has wrapped negative. It permanently exceeds the 1 MB window, so the sender throttles
forever waiting for a ZACK that can never satisfy the corrupt window.
## What works vs. hangs (empirical, 2.2 GB file crossing 2^31)
| Case | Result |
|---|---|
| lrzsz → lrzsz, `-w1M` | OK (full 2.36 GB, integrity MATCH) |
| lsz → sexyz (sexyz **receiver**), `-w1M` | OK — sexyz receiver correct past 2 GB |
| sexyz **sender**, **no window** | OK (full transfer; window check short-circuits when `max_window_size==0`) |
| sexyz **sender**, **`-w1M`** | **HANG at 2^31** |
So sexyz's receive path and its windowless send path are 64-bit clean (data path
uses `fseeko`/`ftello` + `int64_t`); only the windowed send accounting breaks.
## Offending narrowings in `zmodem.c` / `zmodem.h`
- `zmodem.h`: `int32_t ack_file_pos;`, `int32_t crc_request;`
- `zmodem.c`: `zmodem_send_pos_header(..., int32_t pos, ...)`,
`zmodem_send_ack(..., int32_t pos)`
- `zmodem.c`: `zm->ack_file_pos = zm->rxd_header_pos;` (uint32 → int32)
- `zmodem.c`: `zm->current_window_size = zm->current_file_pos - zm->ack_file_pos;`
(`rxd_header_pos` itself is correctly `uint32_t`, good through 4 GB; the loss happens when it is assigned into the signed `int32_t` fields.)
## Suggested fix
Widen `ack_file_pos`, `crc_request`, and the `pos` parameters of `zmodem_send_pos_header` / `zmodem_send_ack` to `uint32_t` (or `int64_t`), and audit `current_window_size` for unsigned correctness. This fixes windowed transfers 2–4 GiB for both sexyz and SyncTERM. (Positions ≥ 4 GiB remain limited
by the 32-bit wire field — a protocol constraint, out of scope here.)
## Note for 32-bit builds
lrzsz/Forsberg rzsz use `long` positions and `fseek()` (no `_FILE_OFFSET_BITS=64`
in lrzsz's build), so on a 32-bit (ILP32) host they cap at 2 GiB too — a different root cause (native `long` width), noted for completeness.
— *Authored by Claude (Claude Code), on behalf of @rswindell*
--- SBBSecho 3.37-Linux
* Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)