CVE-2026-49932 AI-generated analysis
platform/frameworks/base, telephony/common MMS PDU parser (com.google.android.mms.pdu.PduParser)
Hardens PduParser.parseParts in frameworks/base telephony/common so malformed MMS multipart PDUs are rejected (parse returns null) instead of causing OutOfMemoryError, StackOverflowError, NullPointerException or ArrayIndexOutOfBoundsException. It rejects a declared part dataLength larger than the bytes left in the stream, caps recursion into multipart/vnd.wap.multipart.alternative parts at depth 30, and checks that the nested result is non-null and non-empty before taking its first part.
Root cause
In PduParser.parseParts (telephony/common/com/google/android/mms/pdu/PduParser.java), when a part's dataLength was > 0 the parser immediately did `new byte[dataLength]` without comparing it to pduDataStream.available(). A hostile length could therefore cause an oversized allocation (OutOfMemoryError, per the commit message). When the part content type was ContentType.MULTIPART_ALTERNATIVE, parseParts recursed on the part data with no depth limit, so deeply nested parts could exhaust the stack (StackOverflowError). The recursive result childBody was then used as childBody.getPart(0) without checking that childBody was non-null (NullPointerException) or had at least one part (ArrayIndexOutOfBoundsException). The new tests feed short PDUs, labelled as NPE, AIOOBE and OOM triggers, through PduParser.parse() and expect null.
How the fix works
- PduParser.java: adds the constant MAX_PARSING_DEPTH = 30, with a comment explaining the value (real-world nesting below 5, background thread stacks 256KB-512KB).
- PduParser.java:parseParts(ByteArrayInputStream): the existing protected method becomes a thin wrapper that calls a new private parseParts(ByteArrayInputStream, int depth) with depth 0. The external signature is unchanged.
- PduParser.java:parseParts(stream, depth): right after the existing null-stream check, logs with Log.e and returns null if depth > MAX_PARSING_DEPTH, which bounds the recursion.
- PduParser.java:parseParts(stream, depth): inside `if (dataLength > 0)`, before `new byte[dataLength]`, logs with Log.e and returns null if dataLength > pduDataStream.available(). The allocation and the following read are now bounded by the input actually remaining.
- PduParser.java:parseParts(stream, depth): the multipart/alternative branch now recurses with depth + 1, and logs with Log.e and returns null if childBody == null || childBody.getPartsNum() == 0. childBody.getPart(0) is therefore only reached on a non-empty body.
- tests/TelephonyCommonTests/.../PduParserTest.java (new): three tests (testAioobePayload, testNpePayload, testOomPayload) build PduParser(payload, true) and assert that parse() returns null for the AIOOBE, NPE and OOM trigger inputs. Run with atest TelephonyCommonTests:PduParserTest.
Completeness
- All three guards are confined to PduParser.parseParts. The depth limit applies only to the self-recursion through multipart/vnd.wap.multipart.alternative, which is the only recursive call visible in the diff.
- The dataLength bound only covers the part-data allocation in parseParts. headerLength and other lengths parsed elsewhere in the file are not visible in the diff and are not changed.
- The commit message lists a fourth item ('Optimize skipWapValue and parameter skipping to use InputStream.skip instead of allocating temporary byte arrays'). The provided diff has no changes to skipWapValue or parameter skipping: the 32 changed lines in PduParser.java are all in the constants block and parseParts.
- On any of the new failure conditions the whole body parse fails (return null); the offending part is not skipped.
- Only the frameworks/base copy is fixed. Per index search, separate PduParser copies exist in packages/apps/Messaging and development/samples/ApiDemos, and this commit does not touch them (see potential_variants).
Behavior changes
- PduParser.parse() now returns null, instead of throwing or exhausting memory or stack, for PDUs whose multipart/alternative nesting exceeds the depth limit, whose declared part dataLength exceeds the remaining stream bytes, or whose nested multipart/alternative body fails to parse or has zero parts.
- Three new Log.e messages under LOG_TAG: 'parseParts: recursion depth limit exceeded', 'parseParts: dataLength ... exceeds available stream bytes', 'parseParts: nested multipart alternative parsing failed or empty'.
- Stricter validation can reject PDUs the old code accepted, for example a part whose declared length slightly exceeds the remaining bytes.
- New test class PduParserTest added to the TelephonyCommonTests module.
Per major
- Android 14: same fix: RESTRICT AUTOMERGE cherry-pick 8325de4e11e9 in android-security-14.0.0_r29 (Change-Id If97a6540b86380a01700fca887813ff117922e58); 8/8 patched lines present.
- Android 15: same fix: RESTRICT AUTOMERGE commit b86eef8c2ffe in android-security-15.0.0_r17; identical diff and identical pre/post blob hashes (62eac7ac942a..ecab9562f72e) to Android 14; 8/8 lines present.
- Android 16: same fix: RESTRICT AUTOMERGE commit a28fb127a57b in android-security-16.0.0_r8; identical diff and blob hashes to 14/15; 8/8 lines present.
- Android 17: same fix: RESTRICT AUTOMERGE commit 1c37e9ef69ea in android-security-17.0.0_r1; same hunks at the same line offsets but a different base blob (ffd2a8b419bb..15448a6a77f4); 8/8 lines present.
Unknowns
- Which process the bulletin's Critical DoS rating refers to; the material does not say.
- Whether the 'skipWapValue / parameter skipping' optimization named in the commit message landed in a separate change; it is not in any of the provided diffs.
- The full set of callers of PduParser.parse() in each release, and the source of the data passed to MmsService.parsePduForAnyCarrier.
- Whether the TelephonyCommonTests module exists and builds on every security branch the change was applied to.
- Whether the Messaging and ApiDemos PduParser copies contain the same unguarded allocation and recursion; the index snippets are cut off.