{"slug": "whatsapp-business-for-ios-crashes-after-16-seconds-when-companion-is-connected", "title": "WhatsApp Business for iOS Crashes After 16 Seconds When Companion Is Connected", "summary": "WhatsApp Business for iOS crashes 13 to 46 seconds after launch when a companion device is linked, due to an unbounded receipt-tracking table with 872,787 rows that causes the app to exceed the 3,376 MB per-process memory limit. The issue affects versions 26.22.76 through 26.24.73 on iOS 26.5.1, and logging out all companion devices restores stability.", "body_md": "# WhatsApp Business for iOS dies after 16 seconds when a companion device is linked\n\n*\n*\n\nThis is a writeup, mostly generated by AI and follows the writing style of good old [MSDN](https://en.wikipedia.org/wiki/Microsoft_Developer_Network) forums and announcements.\n\nI've been blocked from using WhatsApp web for weeks now because of this, so I decided to release the writeup that somewhat unblocked me, if someone else is facing this too.\n\nIt has been lightly edited, but mostly is the result of two independent investigation sessions conducted using [Fable 5](https://platform.claude.com/docs/en/about-claude/models/introducing-claude-fable-5-and-claude-mythos-5).\n\n## Summary\n\nWhatsApp Business for iOS on my device is killed by the operating system 13 to 46 seconds after every launch, but only while a companion device (WhatsApp Web or Desktop) is linked to the account. With no linked devices, the application is stable indefinitely.\n\nOver two investigations spanning June and July 2026, three distinct defects were identified in the application's local data, two of which were confirmed and eliminated.\n\nThe third and currently active cause is an unbounded per-device receipt-tracking table (`receipt_device`\n\nin `MessagingInfraDatabase.sqlite`\n\n) containing 872,787 rows. Evidence indicates that a launch-time reconciliation routine, active only when a companion device is registered, materializes this table in memory in full.\n\nAt approximately 4 KB per materialized row, the resulting allocation reaches the 3,376 MB per-process memory limit, and iOS terminates the process.\n\nThe failure is not caused by database corruption, media volume, or account state on WhatsApp's servers. All analysis was performed read-only against decrypted copies of encrypted local iPhone backups. No jailbreak was used and no data was modified on the device.\n\n## Environment\n\n| Item | Value |\n|---|---|\n| Device | iPhone18,3 |\n| OS | iOS 26.5.1 (build 23F81) |\n| Application | WhatsApp Business (`net.whatsapp.WhatsAppSMB` ), versions 26.22.76 through 26.24.73 |\n| Account scale | ~1.95M messages, ~6,700 chats, ~960k media item records |\n| Primary database | `ChatStorage.sqlite` , 1.6 GB |\n| Analysis host | macOS, libimobiledevice, `iphone_backup_decrypt` , `apsw` (SQLite) |\n\n## Symptom\n\nWhen a companion device is linked, the phone application exits to the home screen shortly after launch. The interval between launch and termination was measured at 13, 16, 25, 29, 36, and 46 seconds across six reproductions on July 7. The companion session itself remains connected throughout, because the multi-device architecture serves companions from WhatsApp's servers rather than from the phone. Logging out all companion devices restores full stability; relinking any companion reintroduces the failure on the next launch.\n\nTwo secondary kill modes were observed in the same period and are attributed to the same underlying workload:\n\n`0xDEAD10CC`\n\nterminations: the process was suspended while holding a file lock (the SQLite database in the shared app-group container).`diskwrites_resource`\n\nviolations: sustained SQLite write rates of 65 to 93 KB/s against an iOS budget of 12.43 KB/s, exhausting the 1,073.74 MB daily write allowance in three to four hours.\n\n## Methodology\n\nAll conclusions derive from three data sources.\n\n**1. iOS crash and resource reports** (`.ips`\n\nfiles), retrieved with `idevicecrashreport`\n\n. These identify the kill reason (jetsam per-process limit, watchdog, suspension-with-lock, disk-write budget) and, for resource violations, include microstackshot samples of the hot path.\n\n**2. Encrypted local backups**, taken with `idevicebackup2`\n\nand decrypted selectively with `iphone_backup_decrypt`\n\n. The backup manifest allows individual files to be located and extracted by domain and path without decrypting the full 80 GB archive. Every extracted database was opened immutable to make writes impossible at the API level:\n\n```\ncon = apsw.Connection(\n    \"file:ChatStorage.sqlite?mode=ro&immutable=1\",\n    flags=apsw.SQLITE_OPEN_READONLY | apsw.SQLITE_OPEN_URI)\n```\n\n**3. Live syslog capture** during reproduction, via `idevicesyslog`\n\n, with the phone connected over USB while the failure was triggered on demand.\n\n## Finding 1 (June): a counter at INT32_MAX poisoned the history exporter\n\nThe June failure mode was a crash during companion history transfer, with `mach_vm_allocate`\n\nfailures at modest process footprint and a database that passed `PRAGMA integrity_check`\n\n. That combination (failed allocation, low usage, structurally healthy data) suggested the exporter was sizing a single allocation from a pathological value rather than running out of memory gradually.\n\nA two-stage scan confirmed this. The first stage measured `MAX(LENGTH(...))`\n\nfor every TEXT and BLOB column in the database and ruled out oversized fields; the largest single field in 1.6 GB of data was 130 KB. The second stage took the minimum and maximum of every ID, counter, and sort column:\n\n```\nfor c in (\"Z_PK\", \"ZSORT\", \"ZMESSAGECOUNTER\", ...):\n    cur.execute(f\"SELECT MIN({c}), MAX({c}), COUNT(*) FROM {t}\")\n```\n\nTwo anomalies emerged, both parked at 32-bit boundaries:\n\n| Field | Observed value | Organic maximum |\n|---|---|---|\n`ZWACHATSESSION.ZMESSAGECOUNTER` (1 row) |\n2,147,473,649 = INT32_MAX − 9,998 | 162,718 |\n`ZWAMESSAGE.ZSORT` (149 rows) |\n214,748,3xx ≈ INT32_MAX / 10 | 162,718 |\n\nThe value distribution was diagnostic: nothing existed between the organic maximum and the sentinel cluster, indicating the application itself wrote these values as overflow sentinels rather than accumulating them organically.\n\nThe poisoned counter resided on the database's only WhatsApp Channel row. Notably, unfollowing a channel does not delete its row; it sets a hidden flag (`ZHIDDEN=1`\n\n) while the row and its counter remain in the database, unreachable from the UI but still visited by the exporter.\n\nRemediation used WhatsApp's own backup-and-restore path (iCloud backup, delete, reinstall, restore), on the theory that restore regenerates local metadata rather than copying it. Post-restore verification confirmed the counter returned at a sane 162,773, the channel row was gone entirely, and no messages were lost. A companion link then completed normally in approximately 19 minutes. The restore had significant operational cost: roughly 21 GB of media re-download and a multi-day episode in which a first-launch cleanup routine attempting to unlink 200,000 files was repeatedly killed by the 20-second launch watchdog (`0x8BADF00D`\n\n) before it could complete.\n\n## Finding 2 (July): the sentinel writer is still active, but sentinels were not the cause\n\nThe failure returned within two weeks. A fresh extraction showed the June repair had held (sane counters, no channel rows), but one new `ZSORT`\n\nsentinel had appeared, written on June 30 to the newest message of a small archived group. An incidental complication: the account contained two groups with identical display names, and the poisoned one was in the chat archive, so the first clearing attempt targeted the wrong group.\n\nDeleting the affected group removed the sentinel. The crashes continued unchanged. A full value scan of the database after deletion found nothing anomalous, which eliminated `ChatStorage.sqlite`\n\nas the cause of the ongoing failure and forced the investigation wider.\n\n## Finding 3 (July, active): an unbounded receipt table and a launch-time reconciliation loop\n\n### Live capture of the termination\n\nWith syslog streaming, the kill sequence is fully visible. From application launch to termination is 16 seconds in the first captured cycle:\n\n```\n15:33:03  runningboardd: [app<net.whatsapp.WhatsAppSMB>:11429]\n          Set jetsam priority to 100 (foreground)\n15:33:19  kernel: memorystatus: WhatsApp [11429] exceeded mem limit:\n          ActiveHard 3376 MB (fatal)\n15:33:19  kernel: killing_specific_process pid 11429 [WhatsApp]\n          (per-process-limit) 3457027KB\n15:33:19  SpringBoard: Process exited:\n          domain:jetsam(1) code:per-process-limit(7)\n```\n\nDuring those 16 seconds, the process read a single feature-flag preference key (`aber`\n\n, in the `group.net.whatsapp.WhatsAppSMB.shared`\n\ndomain) 50,026 times, a rate above 3,000 reads per second. This is the signature of a per-item processing loop consulting a flag for each item while allocating memory it does not release. An annotated capture of one full launch-to-kill cycle is available [here](/files/whatsapp-kill-cycle.txt).\n\nThe controlling experiment: with all companion devices logged out and the application relaunched, the loop does not run (the same flag was read 2,514 times in several minutes of normal use) and the process remains alive indefinitely. Relinking a companion reintroduces the termination on the next launch. The workload is therefore gated exclusively on companion-device registration.\n\n### Locating the data\n\n`ChatStorage.sqlite`\n\nis one of fourteen SQLite databases in the application's container. A value scan across all fourteen found nothing pathological. A row-count scan did:\n\n| Database | Size | Notable content |\n|---|---|---|\n`MessagingInfraDatabase.sqlite` |\n143 MB | `receipt_device` : 872,787 rows (table + unique index account for the entire file); all other sync queues: 0 rows |\n\nThe `receipt_device`\n\nschema tracks delivery/read/played receipts per message, per participant, per participant device:\n\n```\nCREATE TABLE receipt_device(\n  _id INTEGER PRIMARY KEY AUTOINCREMENT,\n  stanza_id TEXT NOT NULL, chat_jid TEXT NOT NULL,\n  user_jid TEXT NOT NULL, device_id INTEGER NOT NULL,\n  send_timestamp LONG_INT, delivered_timestamp LONG_INT,\n  read_timestamp LONG_INT, played_timestamp LONG_INT,\n  device_version INTEGER)\n```\n\nThe rows span April 20 to July 7 (79 days), an average inflow of roughly 11,000 rows per day. Six high-traffic groups account for more than half the volume. No pruning behavior is evident in the data.\n\n### The allocation arithmetic\n\nThe memory limit divided by the row count yields the per-row cost required for the table to be the allocation:\n\n```\n3,376 MB / 872,787 rows ≈ 4.05 KB per row\n```\n\nFour kilobytes per row is consistent with materializing each row as a full Objective-C object graph rather than streaming it. The observed termination sizes (3,454,851 KB to 3,458,595 KB across reproductions) are consistent with the table being loaded in full, plus baseline application footprint.\n\nReconstructing the table's growth from its own timestamps and plotting it against the ceiling produces the following:\n\nThe growth curve crosses the computed ceiling in the first week of July, which is when per-launch termination began.\n\n### Consistency with the secondary evidence\n\nThis mechanism accounts for the remaining observations:\n\n- The disk-write violations worsened from 65 KB/s (June 28) to 93 KB/s (July 2) as the table and its index grew, increasing WAL churn per operation. Stack samples in the disk-write reports show the time concentrated in direct\n`libsqlite3`\n\nwrite paths. - The June restore appeared curative because it reset the container while the table held roughly 600,000 rows, below the ceiling. The table continued to grow; the failure \"returned\" when the row count crossed approximately 850,000. The recurrence interval was a property of the growth rate, not of the earlier fix.\n- The\n`0xDEAD10CC`\n\nsuspension kills occur when the same long-running database workload is backgrounded mid-transaction.\n\n## Timeline\n\n| Date | Event |\n|---|---|\n| ~Apr 20 | `receipt_device` begins accumulating (earliest timestamps in table) |\n| Jun 12–15 | Investigation 1: INT32_MAX counter found on channel row; restore performed; companion link succeeds |\n| Jun 16–28 | `diskwrites_resource` violations resume and escalate |\n| Jun 30 | New `ZSORT` sentinel written to an archived group |\n| Jul 2 | Disk-write violation at 93.57 KB/s, daily budget exhausted in 3.2 hours |\n| Jul 7 | Investigation 2: sentinel eliminated; failure persists; live capture; companion-link gating established; `receipt_device` identified at 872,787 rows |\n\n## Current status and workaround\n\nThe application is fully stable with zero linked devices. That is the workaround, and it is an unsatisfying one, since linked devices are awesome.\n\nRemaining paths, in rough order of preference:\n\n- Application updates, in case the reconciliation path regressed recently and is fixed upstream.\n- Determining whether an extended period with no linked devices causes the table to be pruned.\n- Reducing inflow by leaving the highest-traffic groups.\n- A report to WhatsApp with this analysis attached.\n- Repeating the container reset, which the growth data suggests buys approximately ten weeks per iteration at current inflow.\n- Banging head against wall.\n\n## Notes on reproduction\n\nThe diagnostic queries require only a decrypted copy of the application's databases from a local encrypted backup. The two checks that matter, in order of cost:\n\n- Min/max of every numeric ID, counter, and sort column, flagging values near ±2³¹ and 2³¹/10. Values hugging type boundaries are written by code, not accumulated by use.\n- Row counts of every table in every database, not just the primary one. A table whose count is wildly out of proportion to the others is a candidate allocation bomb even when every individual value in it is sane.\n\nThe general lesson from the pair of investigations: a crash with clean data and a failed large allocation points to one absurd value; a crash with clean values points to an absurd count; and a fix that works and then \"wears off\" on a schedule is telling you the real variable is something that grows.", "url": "https://wpnews.pro/news/whatsapp-business-for-ios-crashes-after-16-seconds-when-companion-is-connected", "canonical_source": "https://granot.io/whatsapp-business-for-ios-sucks/", "published_at": "2026-07-07 15:48:04+00:00", "updated_at": "2026-07-07 15:59:55.552080+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["WhatsApp Business", "iOS", "Apple", "iPhone", "SQLite", "Fable 5", "Claude"], "alternates": {"html": "https://wpnews.pro/news/whatsapp-business-for-ios-crashes-after-16-seconds-when-companion-is-connected", "markdown": "https://wpnews.pro/news/whatsapp-business-for-ios-crashes-after-16-seconds-when-companion-is-connected.md", "text": "https://wpnews.pro/news/whatsapp-business-for-ios-crashes-after-16-seconds-when-companion-is-connected.txt", "jsonld": "https://wpnews.pro/news/whatsapp-business-for-ios-crashes-after-16-seconds-when-companion-is-connected.jsonld"}}