Android’s Parcel serialized format was changed in Android 13

yukuku
6 min readJun 8, 2023

--

tldr: A parcelable is read from and written into a Parcel’s marshaled byte array in a different format on Android 13 compared to earlier versions. We should not save Parcel data in persistent storage that may last across OS version upgrades.

I started receiving reports of NPE errors on the Alkitab app even though the app had not been updated for almost 2 years.

In Alkitab, there are songs. A song is stored in the Song class (irrelevant parts removed):

public class Song implements Parcelable {
public String code;
public String title;
.... other fields
public List<Lyric> lyrics;
}
public class Lyric implements Parcelable {
public String caption;
public List<Verse> verses;
}

The NPE error was java.lang.NullPointerException: Attempt to read from field ‘java.util.List yuku.kpri.model.Lyric.verses’ on a null object reference

When we constructed a song, we made sure that the lyrics has only non-null elements. Therefore it was not possible to access verses from a null instance of a Lyric.

After examining further, the way the app works is by storing the serialized form of Song instance by utilizing the Parcelable-ness of it to a local database. Here is the code that converts a Song instance to a byte array:

private static byte[] marshallSong(Song song) {
Parcel p = Parcel.obtain();
song.writeToParcel(p, 0);
byte[] buf = p.marshall();
p.recycle();
return buf;
}

And when the app needs a song to display, it will convert back a byte array to an instance of Song:

private static Song unmarshallSong(byte[] buf, int dataFormatVersion) {
Parcel p = Parcel.obtain();
p.unmarshall(buf, 0, buf.length);
p.setDataPosition(0);
Song res = Song.createFromParcel(p);
p.recycle();
return res;
}

After many years passed since the time of writing this code, I understood that Parcels are designed for in-memory transactions only; it is not supposed to be persisted because the serialized format is not guaranteed to be compatible across Android versions.

Parcel is not a general-purpose serialization mechanism. This class (and the corresponding Parcelable API for placing arbitrary objects into a Parcel) is designed as a high-performance IPC transport. As such, it is not appropriate to place any Parcel data in to persistent storage: changes in the underlying implementation of any of the data in the Parcel can render older data unreadable.

However, I need to ensure that was the cause of the issue we had.

I picked a song and dumped the serialized byte array of it on Android 9 and again on Android 13.

Android 9:

0: 02 00 00 00 35 00 35 00 00 00 00 00 1a 00 00 00    ....5.5.........
1: 59 00 61 00 20 00 53 00 75 00 6d 00 62 00 65 00 Y.a. .S.u.m.b.e.
2: 72 00 20 00 4b 00 61 00 73 00 69 00 68 00 2c 00 r. .K.a.s.i.h.,.
3: 20 00 52 00 6f 00 68 00 20 00 4b 00 75 00 64 00 .R.o.h. .K.u.d.
4: 75 00 73 00 00 00 00 00 ff ff ff ff ff ff ff ff u.s.............
5: ff ff ff ff ff ff ff ff 03 00 00 00 31 00 3d 00 ............1.=.
6: 46 00 00 00 03 00 00 00 34 00 2f 00 34 00 00 00 F.......4./.4...
7: 01 00 00 00 04 00 00 00 15 00 00 00 79 00 75 00 ............y.u.
8: 6b 00 75 00 2e 00 6b 00 70 00 72 00 69 00 2e 00 k.u...k.p.r.i...
9: 6d 00 6f 00 64 00 65 00 6c 00 2e 00 4c 00 79 00 m.o.d.e.l...L.y.
10: 72 00 69 00 63 00 00 00 ff ff ff ff 04 00 00 00 r.i.c...........
11: 04 00 00 00 15 00 00 00 79 00 75 00 6b 00 75 00 ........y.u.k.u.
12: 2e 00 6b 00 70 00 72 00 69 00 2e 00 6d 00 6f 00 ..k.p.r.i...m.o.

Android 13:

0: 02 00 00 00 35 00 35 00 00 00 00 00 1a 00 00 00    ....5.5.........
1: 59 00 61 00 20 00 53 00 75 00 6d 00 62 00 65 00 Y.a. .S.u.m.b.e.
2: 72 00 20 00 4b 00 61 00 73 00 69 00 68 00 2c 00 r. .K.a.s.i.h.,.
3: 20 00 52 00 6f 00 68 00 20 00 4b 00 75 00 64 00 .R.o.h. .K.u.d.
4: 75 00 73 00 00 00 00 00 ff ff ff ff ff ff ff ff u.s.............
5: ff ff ff ff ff ff ff ff 03 00 00 00 31 00 3d 00 ............1.=.
6: 46 00 00 00 03 00 00 00 34 00 2f 00 34 00 00 00 F.......4./.4...
7: 01 00 00 00 04 00 00 00 2c 04 00 00 15 00 00 00 ........,.......
8: 79 00 75 00 6b 00 75 00 2e 00 6b 00 70 00 72 00 y.u.k.u...k.p.r.
9: 69 00 2e 00 6d 00 6f 00 64 00 65 00 6c 00 2e 00 i...m.o.d.e.l...
10: 4c 00 79 00 72 00 69 00 63 00 00 00 ff ff ff ff L.y.r.i.c.......
11: 04 00 00 00 04 00 00 00 f4 00 00 00 15 00 00 00 ................
12: 79 00 75 00 6b 00 75 00 2e 00 6b 00 70 00 72 00 y.u.k.u...k.p.r.

We can see that starting from chunk 7 (bytes 112–127), there are differences. Let’s see more in detail by putting that line alone consecutively, the one coming from Android 9 and then Android 13.

7: 01 00 00 00 04 00 00 00 15 00 00 00 79 00 75 00    ............y.u.
7: 01 00 00 00 04 00 00 00 2c 04 00 00 15 00 00 00 ........,.......
^ here the difference starts!

I haven’t shown you what the method writeToParcel contains.

out.writeString(code);
out.writeString(title);
out.writeString(title_original);
out.writeStringList(authors_lyric);
out.writeStringList(authors_music);
out.writeString(tune);
out.writeString(keySignature);
out.writeString(timeSignature);
out.writeList(lyrics);

Let’s display the common byte dump and put comments to indicate which write they are a result of.

0: 02 00 00 00 35 00 35 00 00 00 00 00                ....5.5.
code (string len=2) "55"

0: 1a 00 00 00 ....
1: 59 00 61 00 20 00 53 00 75 00 6d 00 62 00 65 00 Y.a. .S.u.m.b.e.
2: 72 00 20 00 4b 00 61 00 73 00 69 00 68 00 2c 00 r. .K.a.s.i.h.,.
3: 20 00 52 00 6f 00 68 00 20 00 4b 00 75 00 64 00 .R.o.h. .K.u.d.
4: 75 00 73 00 00 00 00 00 u.s.....
title (string len=0x1a) "Ya Sumber Kasih, Roh Kudus"

4: ff ff ff ff ff ff ff ff u.s.............
5: ff ff ff ff ff ff ff ff ........
title_original (null string, indicated by 0xffffffff)
authors_lyric (null string list, indicated by 0xffffffff)
authors_music (null string list, indicated by 0xffffffff)
tune (null string, indicated by 0xffffffff)

5: 03 00 00 00 31 00 3d 00 ....1.=.
keySignature (string len=3) "1=F"

6: 46 00 00 00 03 00 00 00 34 00 2f 00 34 00 00 00 F.......4./.4...
timeSignature (string len=3) "4/4"

Now comes the interesting part (we are almost at "writeList")

7: 01 00 00 00 ....
(list len=1, we only have one Lyric)
7: 04 00 00 00 15 00 00 00 79 00 75 00 ........y.u.

The element of the list is written using writeValue.

How writeValue is implemented differently in Android 9 and Android 13.

Android 9

if (v == null) {
writeInt(VAL_NULL);
} else if (v instanceof String) {
writeInt(VAL_STRING);
writeString((String) v);
...
} else if (v instanceof Parcelable) {
// IMPOTANT: cases for classes that implement Parcelable must
// come before the Parcelable case, so that their specific VAL_*
// types will be written.
writeInt(VAL_PARCELABLE);
writeParcelable((Parcelable) v, 0);
...

It is just by writing an int 0x04 to indicate a “parcelable” type, then writeParcelable is called.

Android 13

int type = getValueType(v);
writeInt(type);
if (isLengthPrefixed(type)) {
// Length
int length = dataPosition();
writeInt(-1); // Placeholder
// Object
int start = dataPosition();
writeValue(type, v);
int end = dataPosition();
// Backpatch length
setDataPosition(length);
writeInt(end - start);
setDataPosition(end);
} else {
writeValue(type, v);
}

It is also writing an int 0x04 to indicate a “parcelable” type (found by getValueType), but before writeParcelable is called a number that indicates the length of the whole parcelable is written.

Now we know that the 2c 04 00 00 (0x042c) is the length of the parcelable, only written/read on Android 13.

7: 01 00 00 00 04 00 00 00 2c 04 00 00 15 00 00 00    ........,.......
^^^^^^^^^^^ Length of the parcelable

Conclusion: A serialized parcelable written on Android 9 will surely fail to be read on Android 13.

A little bit more investigation: I wanted to know when this change was made. I looked up the source from cs.android.com.

Android 12L shows a very similar implementation to Android 13’s. So, the change really did happen at the release of Android 13.

--

--