• SMB: votes/polls on a hyperalloc msgbase corrupt existing headers (sto

    From Rob Swindell@1:103/705 to GitLab issue in main/sbbs on Sat Jul 4 17:59:20 2026
    open https://gitlab.synchro.net/main/sbbs/-/issues/1181

    ## Summary

    Casting a **vote** (or adding a **poll** / **poll-closure**) on a message base whose on-disk `status.attr` has **`SMB_HYPERALLOC`** set, but whose sub is configured for **fast** (or self-pack) allocation, writes the new header **on top of existing message headers** — producing *duplicate index offsets* and *overlapping headers*. Because a vote header (message-id + version PID) spans two
    256-byte blocks, it clobbers the collided header **and** overruns into the next one, destroying **two** messages per vote. Readers then fail with `smb_getmsghdr corrupt message header ID ... at offset N` and the sub is unreadable from that message onward.

    Found in the wild: the `DEBATE` sub on Vertrauen — a base whose on-disk attr is
    `SMB_HYPERALLOC` while its `msgs.ini` config selects **fast allocation** — was
    corrupted the first time votes were cast on it. `chksmb` reported
    `Duplicate Index Offsets (!): 2` and `Overlapping Headers (!): 2`; four year-2000 posts (#16–#19) were destroyed. `fixsmb` recovered the other 187.

    Reproduced from source on current `master` (`755471638f`).

    ## Root cause

    `smb_addmsg()` **forces** hyperalloc storage when the base attribute says so, for
    both data and (via the storage arg it threads into `smb_addmsghdr`) the header:

    ```c
    // src/smblib/smbadd.c:139
    if (smb->status.attr & SMB_HYPERALLOC) {
    offset = smb_hallocdat(smb);
    storage = SMB_HYPERALLOC;
    }
    ```

    So normal posts on a hyperalloc base are appended at EOF and the `.sha` / `.sda`
    allocation tables are (correctly) never maintained — **their length has no relationship to the real `.shd` / `.sdt` file size.**

    But **`smb_addvote()` (smbadd.c:315), `smb_addpoll()` (:353) and `smb_addpollclosure()` (:393) have no such override** — they pass the caller's
    `storage` straight into `smb_addmsghdr()`. The application derives that value from the **sub's config flags**, not the base attribute:

    ```c
    // src/sbbs3/load_cfg.c (smb_storage_mode)
    if (cfg->sub[smb->subnum]->misc & SUB_HYPER) { ...; return SMB_HYPERALLOC; }
    if (cfg->sub[smb->subnum]->misc & SUB_FAST) return SMB_FASTALLOC; // <-- DEBATE
    return SMB_SELFPACK;
    ```

    DEBATE is `SUB_FAST`, so votes take the **fast** allocator:

    ```c
    // src/smblib/smblib.c:1714 (smb_new_msghdr)
    if (storage == SMB_HYPERALLOC)
    l = smb_hallochdr(smb); // append at EOF (correct here) else if (storage == SMB_FASTALLOC)
    l = smb_fallochdr(smb, msg->hdr.length); // <-- taken
    else
    l = smb_allochdr(smb, msg->hdr.length);
    ```

    `smb_fallochdr()` computes the new header offset purely from the **`.sha` file length**:

    ```c
    // src/smblib/smballoc.c:431 (smb_fallochdr)
    fseek(smb->sha_fp, 0L, SEEK_END);
    offset = ftell(smb->sha_fp) * SHD_BLOCK_LEN; // .sha_length_bytes * 256
    for (l = 0; l < blocks; l++) fwrite(&c, 1, 1, smb->sha_fp); // grow .sha return offset;
    ```

    On a hyperalloc base the `.sha` was never grown by the (hyper-allocated) posts, so it is tiny and **its length is far below the real header-file EOF** — the returned offset points **into live headers.** The arithmetic matches DEBATE exactly: its `.sha` was 15 bytes, and the two votes landed at `.shd` offsets `0xB20` and `0xD20`:

    ```
    vote #200: .sha length = 11 bytes -> 11*256 = 2816 (+header_offset 32) = 0xB20
    writes 2 blocks -> .sha grows to 13
    vote #201: .sha length = 13 bytes -> 13*256 = 3328 (+32) = 0xD20
    writes 2 blocks -> .sha grows to 15 (== the size seen post-mortem) ```

    Both landed on top of the year-2000 headers at those offsets (#16, #18) and, being
    2 blocks each, overran into the next slot (#17, #19). Normal posts never hit this
    because of the smbadd.c:139 override; **votes/polls are the only way in.**

    ## Reproduction

    Self-contained C program (attached: [vote_repro.c](/uploads/d25b27908f41bfc198da878e46cfc9cc/vote_repro.c)), linked against
    `libsmb`/`libhash`/`libencode`/`libxpdev`. It creates a **hyperalloc** base, adds 6 messages, then casts one upvote whose header (message-id + version PID) spans two blocks — with the vote's `storage` selectable (`fast` = DEBATE's config, `hyper` = the fix).

    ```
    $ gcc -o vote_repro vote_repro.c -Ismblib -Ixpdev -Iencode -Ihash \
    -Wl,--start-group libsmb.a libhash.a libencode.a libxpdev.a -Wl,--end-group -lm -lpthread
    ```

    **A) Bug path — vote with `SMB_FASTALLOC` (DEBATE's config):**

    ```
    Created HYPERALLOC base (status.attr=0x0002). Adding 6 messages:
    add msg #1 rc=0 number=1 offset=0x0020 len=114
    add msg #2 rc=0 number=2 offset=0x0120 len=114
    ... (through #6 at 0x0520)
    Casting an UPVOTE on msg #3 with storage=SMB_FASTALLOC (the bug path):
    smb_addvote rc=0 number=7 offset=0x0020 <-- lands on msg #1, overruns into #2

    # chksmb:
    Active Headers (=): 5 # was 7 — msgs #1 and #2 destroyed Duplicate Index Offsets (!): 1
    Overlapping Headers (!): 1
    ... Message Base has Errors!
    ```

    (The vote lands at `0x0020` here because a fresh base's `.sha` starts empty
    — `0*256`; in the wild DEBATE's `.sha` had drifted to 11 bytes, so the collision
    was mid-file at `0xB20`. Same defect, same `chksmb` signature.)

    **B) Control — same vote with `SMB_HYPERALLOC`:**

    ```
    Casting an UPVOTE on msg #3 with storage=SMB_HYPERALLOC (the fixed path):
    smb_addvote rc=0 number=7 offset=0x0620 <-- clean append at EOF
    # chksmb: 7 total, 7 active headers, no errors.
    ```

    ## Proposed fix

    Make the header allocator honor the base's persisted storage layout, the same way `smb_addmsg()` already does for data. The most robust place is the common path `smb_new_msghdr()` (src/smblib/smblib.c), just before the storage branch at
    line ~1714:

    ```c
    if (smb->status.attr & SMB_HYPERALLOC)
    storage = SMB_HYPERALLOC;
    ```

    That single guard protects **every** header-adding path (votes, polls, poll-closures, and any direct `smb_addmsghdr()` caller) against a `storage` argument that contradicts the on-disk attribute — a hyperalloc base can only be
    allocated hyper. Alternatively (or additionally), mirror the smbadd.c:139 override inside `smb_addvote`/`smb_addpoll`/`smb_addpollclosure`.

    Orthogonally, the config/base mismatch that triggers it (a base carrying the `SMB_HYPERALLOC` attribute under a sub configured `SUB_FAST`/self-pack) is worth
    detecting — e.g. a `chksmb` warning, or having `smb_open_sub()` reconcile the sub
    storage mode with the base attribute — but the library should be defensive regardless, which the guard above accomplishes.

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