CVE-2025-22429 AI-generated analysis
platform/frameworks/base, core/java/android/os (BaseBundle / Parcel lazy unparceling)
Changes Parcel.readArrayMap to report the number of LazyValue entries through an int[] out-parameter that is updated as each value is read. BaseBundle.initializeFromParcelLocked therefore sees the correct count in its finally block even when readArrayMap throws partway, and no longer recycles a Parcel that LazyValue entries still reference.
Root cause
In BaseBundle.initializeFromParcelLocked, the lazy value count came from the return value of Parcel.readArrayMap: `numLazyValues = parcelledData.readArrayMap(...)`. readArrayMap adds entries to the map one by one and returns the count only at the end. If it threw after some LazyValue objects had already been added to `map`, the assignment never happened and numLazyValues stayed 0. The finally block runs regardless. With ownsParcel true and numLazyValues == 0, it called recycleParcel(parcelledData), set mLazyValues = 0 and installed mMap = map, so the map could keep LazyValue entries tied to the recycled Parcel. In the non-defuse path (sShouldDefuse false) the map is not erased before the exception propagates.
How the fix works
- core/java/android/os/Parcel.java:readArrayMap: the signature changes from `int readArrayMap(map, size, sorted, lazy, loader)` to `void readArrayMap(map, size, sorted, lazy, loader, int[] lazyValueCount)`. The local counter and `return lazyValues` are removed. `lazyValueCount[0]++` now runs inside the loop each time a value is `instanceof LazyValue`, so the caller's array reflects the LazyValue entries added so far even if a later iteration throws.
- core/java/android/os/Parcel.java:readArrayMapInternal: updated to the new signature, passing `null` for lazyValueCount, with lazy=false.
- core/java/android/os/BaseBundle.java:initializeFromParcelLocked: `int numLazyValues = 0` becomes `int[] numLazyValues = new int[]{0}` and is passed as the out-parameter. The finally block tests `numLazyValues[0] == 0` to choose between recycling the Parcel and keeping `mWeakParcelledData = new WeakReference<>(parcelledData)`, and stores `mLazyValues = numLazyValues[0]`.
Completeness
- Covers the lazy call site in BaseBundle.initializeFromParcelLocked (lazy = ownsParcel). The only other caller shown in the diff is readArrayMapInternal, which uses lazy=false and passes a null counter. This is safe only if readValue never returns a LazyValue, which the material does not show.
- The fix does not change which exceptions are caught or how the map is left after an exception. In the non-defuse path the partially read map is still installed as mMap in finally and the exception still propagates. The fix only keeps the backing Parcel (via WeakReference) and makes the lazy count correct.
- Exceptions other than BadParcelableException are not caught, but the finally block still runs, and the count is now updated incrementally for those too.
- Passing null for lazyValueCount would cause a NullPointerException if a LazyValue were produced on that path. Any new lazy caller must pass a non-null array.
Behavior changes
- When unparceling a Bundle throws after at least one LazyValue has been read, the owning Parcel is no longer recycled. It is kept via mWeakParcelledData, and mLazyValues reflects the number of lazy entries read.
- Parcel.readArrayMap (@hide) changes signature: it returns void and takes an int[] out-parameter.
- No change on the success path: the count and the recycle/keep decision are the same when readArrayMap completes normally.
Per major
- Android 14: same fix: platform/frameworks/base ece83fb425b1 in android-security-14.0.0_r19 (cherry-pick, Change-Id Id423936872cbb0e0265ccf2855092357cb175d47). At android-security-14.0.0_r29 the int[] out-parameter logic is present, and the recycle call reads parcelledData.recycle() instead of the diff's recycleParcel(parcelledData). Resolver reports 5/8 lines of the patched region present.
- Android 15: same fix: platform/frameworks/base 567af706fd9d in android-security-15.0.0_r7. The diff is identical to the Android 14 one; only the Parcel.java line offsets differ (5519/5529 vs 5335/5345).
- Android 16: not affected (not listed by the bulletin); at android-security-16.0.0_r8, 5/8 lines of the patched region are present.
- Android 17: not affected (not listed by the bulletin); at android-security-17.0.0_r1, 4/8 lines of the patched region are present.
Unknowns
- The Android 13 fix commit and tag are not shown in the material.
- The reproduction in b/373357090 and the process/IPC path are not in the material.
- The commit message ('considered a success case to be an unsuccessful case') does not spell out the failing scenario; the error-path reading is inferred from the diff.
- Whether readValue (lazy=false) can ever return a LazyValue is not shown.
- What the partial line matches on Android 16/17 correspond to is not shown.