whatsapp: fix the real root cause of the pair-success crash (lifetime UB in serialize)

Replaying the captured failing frame proved the std::vector max_size() abort was
never a parser bug. It was an object-lifetime bug in WaDevice::serialize():

    put_blob(b, Bytes((jid ? *jid : "").begin(), (jid ? *jid : "").end()));

(jid ? *jid : "") builds a fresh temporary std::string for each evaluation, so
begin() and end() came from two different, already-destroyed objects; the range
constructor computed a garbage length and vector threw. It only misfired once
jid was set (after the phone scanned), which is why every parser test and the
initial unpaired save passed. Fixed by binding the value to a named const
std::string before taking iterators.

- src/whatsapp/wa_device.cpp: bind jid/lid before iterating; audited the tree
  for the same two-temporaries pattern (no other instances).
- tests/whatsapp_test.cpp: test_device round-trips a PAIRED device (jid+lid set,
  synthetic identifiers), the populated case that triggers the bug; runs under
  tools/san-whatsapp.sh.
- docs/whatsapp.md 13.7: records the real root cause and closes the open item.
- docs/cpp-safety.md: adds the object-lifetime rule (never take iterators from a
  temporary you rebuild) as a distinct bug class from size overflow.

Verified: replaying the captured pair-success frame now serializes, saves, and
encodes the response with no throw; build, whatsapp_test, make lint, and
tools/san-whatsapp.sh all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 19:25:50 +01:00
co-authored by Claude Opus 4.8
parent d34db8f215
commit bfc117b0c5
4 changed files with 71 additions and 3 deletions
+2 -1
View File
@@ -57,7 +57,8 @@ that touches bytes we did not produce (network frames, files, user input, IPC):
on raw offsets. This is the std::span / "parse don't validate" discipline;
note that `std::span::operator[]` and `subspan` do **not** bounds-check in
C++20, so the reader validates, not the view.
7. **RAII and the rule of zero.** Resources are owned by objects that release
7. **Never take iterators from a temporary you rebuild.** `Bytes((c ? *o : "").begin(), (c ? *o : "").end())` evaluates the expression twice, so `begin()` and `end()` come from two different, already-destroyed temporaries: undefined behavior, and the garbage length aborted the paired-device save. Bind the value to a named variable first, then take its iterators. More generally, a range `[a, b)` must come from one live object. This is the object-lifetime class, distinct from size overflow; both are caught by the sanitizer net only if a test exercises the populated path.
8. **RAII and the rule of zero.** Resources are owned by objects that release
them; types that manage no raw resource declare no special members. A bug
that cannot exist needs no test. This is also our object-orientation: small
classes with invariants established in the constructor and preserved by every