• ZMODEM windowed sends hang at 2 GiB: file positions narrowed to signed

    From Rob Swindell@1:103/705 to GitLab issue in main/sbbs on Thu Jul 23 15:53:19 2026
    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)