Skip to content

Client softlock entering forge when item shop tab has too many rows #190

Description

@Houmgaor

Summary

The MHF client (mhf.exe) crashes when entering the forge if a shop_type=10 (item shop) tab returns too many rows in MSG_MHF_ENUMERATE_SHOP (0x00A5). The server itself does not crash — the TCP connection is forcibly closed by the client and Erupe just logs the disconnect.

Reproduction

  1. Seed shop_items so that one (shop_type=10, shop_id=N) slot has ~420 rows (the "special item" tab in our case).
  2. Walk a character into the forge.
  3. Client sends 9 successive MSG_MHF_ENUMERATE_SHOP requests (ShopID=0..8); the large response for ShopID=8 is delivered, then ~2s later mhf.exe crashes.

A separate test server with that tab trimmed loads the forge normally, which confirms row count (or one bad row) is the trigger.

Server log excerpt

... opcode_name=MSG_MHF_ENUMERATE_SHOP data_bytes=21 ...   (ShopID=8)
... opcode_name=MSG_SYS_ACK data_bytes=15374 ...
WARN  Connection error, exiting recv loop  error="wsarecv: An existing connection was forcibly closed by the remote host."
INFO  Player logout initiated

Suspected cause

The original client almost certainly has a fixed-size internal buffer per item-shop tab. The request advertises Limit=0x0200 (512), but that's an upper bound the client claims to support — the renderer cannot actually handle that many entries. We know 420 rows crashes and a trimmed tab loads; the real ceiling is somewhere in between and not yet bisected.

Relevant code: server/channelserver/handlers_shop.go case 10 (≈ line 225). The handler honors pkt.Limit but applies no server-side sanity cap.

Workarounds (today)

  • Curate the seed. Split oversized item-shop tabs across multiple shop_ids, or remove duplicates. This is the proper fix — no data is lost.
  • Manually trim the offending (shop_type, shop_id) rows in shop_items until the forge loads.

Possible server-side fix

Add a conservative cap in handlers_shop.go for ShopType=10:

const maxItemShopRows = 256 // tentative; needs bisect
if len(items) > maxItemShopRows {
    items = items[:maxItemShopRows]
}

Caveats:

  • 256 is a guess; the true ceiling needs to be bisected (try 384, 320, …) before being committed as a constant.
  • Truncation is lossy — players silently lose access to the trimmed rows. A cap is only a safety net so a bad seed cannot soft-brick the forge; it is not a replacement for fixing the data.
  • Scope only ShopType=10. Gacha (case 2) and exchange shops (cases 3–9) have different size profiles and are not implicated.

Asks

  • If you have hit the forge softlock, please report your shop_items row counts per (shop_type=10, shop_id) so we can narrow the threshold.
  • Help bisecting the exact safe row count would be appreciated.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcriticalSoftlocks, data loss, crashesprotocolPacket parsing, network protocol issues

    Type

    Fields

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions