此问题的修复以 CVE-2024-49746 的形式出现:公告,补丁
上面的标题是来自 Parcel::continueWrite 方法 的注释,该方法实际上负责调整 Parcel 对象的大小,无论是用户显式请求时(例如通过 setDataSize()),还是在当前数据容量过小时调用某个 write 方法```cpp
status_t Parcel::continueWrite(size_t desired)
{
// SNIP: Validate desired size
// SNIP: Assign kernelFields & rpcFields from variant member of this class
// SNIP: Count number of objects (Binder handles and File Descriptors)
// that will be present after resize and assign to objectsSize
if (mOwner) {
// If the size is going to zero, just release the owner's data.
if (desired == 0) {
freeData();
return NO_ERROR;
}
// If there is a different owner, we need to take
// posession.
uint8_t* data = (uint8_t*)malloc(desired);
// SNIP: Check if malloc succeeded
binder_size_t* objects = nullptr;
if (kernelFields && objectsSize) {
objects = (binder_size_t*)calloc(objectsSize, sizeof(binder_size_t));
// SNIP: Check if calloc succeeded
// Little hack to only acquire references on objects
// we will be keeping.
size_t oldObjectsSize = kernelFields->mObjectsSize;
kernelFields->mObjectsSize = objectsSize;
acquireObjects();
kernelFields->mObjectsSize = oldObjectsSize;
}
// SNIP: rpcFields handling for non-/dev/binder Parcels
if (mData) {
memcpy(data, mData, mDataSize < desired ? mDataSize : desired);
}
if (objects && kernelFields && kernelFields->mObjects) {
memcpy(objects, kernelFields->mObjects, objectsSize * sizeof(binder_size_t));
}
// ALOGI("Freeing data ref of %p (pid=%d)", this, getpid());
if (kernelFields) {
// TODO(b/239222407): This seems wrong. We should only free FDs when
// they are in a truncated section of the parcel.
closeFileDescriptors();
}
mOwner(mData, mDataSize, kernelFields ? kernelFields->mObjects : nullptr,
kernelFields ? kernelFields->mObjectsSize : 0);
mOwner = nullptr;
// SNIP: Allocation count tracking
// SNIP: Assign data and objects to this object
} else if (mData) {
// SNIP: Resize data owned by this instance of Parcel
} else {
// SNIP: Allocate initial data for currently empty Parcel
}
return NO_ERROR;
}
[自引入该注释以来](https://android.googlesource.com/platform/frameworks/native/+/53b6ffe5af3951e8784c451ef8c4ff19f3d6b196%5E!/),`closeFileDescriptors()` 调用从 `IPCThreadState::freeBuffer()`(在上面的代码中通过 `mOwner()` 函数指针调用)移动到了 `continueWrite()` 方法中,但逻辑与之前相同。毕竟,`Parcel` 是 Android IPC 的核心部分,如果核心 IPC 在不应该关闭文件描述符的时候关闭它们,那显然会是个问题。
这就引出了重要部分:上面的代码在什么时候被使用?当 `Parcel` 类移动从 Binder 驱动接收的数据的所有权时(此时这些数据位于 [`/dev/binder` `mmap`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/ProcessState.cpp;l=587-592;drc=187efe18e3de6258af0230198c881915cc695567),并且无法写入(任何写入该内存的尝试都会导致 `SIGSEGV`)),也就是说 `Parcel` 要么是传入的事务数据(作为 `data` 参数传给 [`onTransact()`](https://developer.android.com/reference/android/os/Binder#onTransact(int,%20android.os.Parcel,%20android.os.Parcel,%20int))),要么是传入的回复(即作为 `reply` 参数传给 [`transact()`](https://developer.android.com/reference/android/os/IBinder#transact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)) 调用的 `Parcel` 对象,`transact()` 会在该 `Parcel` 对象内设置引用)。
在实践中,我们唯一会进入 `if (mOwner)` 块的情况是系统 [调用 `setDataSize(0)` 来释放事务数据](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/IPCThreadState.cpp;l=1483-1488;drc=187efe18e3de6258af0230198c881915cc695567),但在那种情况下我们也会进入 `if (desired == 0)`,它会提前返回。在合法的系统使用过程中,没有任何情况下我们会进入“如果存在不同的所有者,我们需要接管”这条路径。
# 触发“接管”路径
在我之前的一个漏洞利用中,我展示了 [`createFromParcel()` 实际上可以在它本应读取的 `Parcel` 上调用 `writeInt(0)` 的情况](https://github.com/michalbednarski/TheLastBundleMismatch#side-effects)。虽然那里的修复阻止了 `AccountManagerService` 中任何非 `Intent` 的 `createFromParcel()` 方法的执行,但从 `createFromParcel()` 到 `writeInt(0)` 的路径被保留了下来。
回顾一下,[在 `PackageParser` 内部有如下代码](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/pm/PackageParser.java;l=7789-7795;drc=7d3ffbae618e9e728644a96647ed709bf39ae759):```java
final Class<T> cls = (Class<T>) Class.forName(componentName);
final Constructor<T> cons = cls.getConstructor(Parcel.class);
intentsList = new ArrayList<>(N);
for (int i = 0; i < N; ++i) {
intentsList.add(cons.newInstance(in));
}
因此,我们可以将传递给 createFromParcel 的 Parcel 对象传递给系统中任何可用的、接受单个 Parcel 参数的 public 构造函数
而在其他地方,我们有以下代码:```java public PooledStringWriter(Parcel out) { mOut = out; mPool = new HashMap<>(); mStart = out.dataPosition(); out.writeInt(0); // reserve space for final pool size. }
因此,为了触发“接管”路径,我们需要在传递给 `onTransact()` 的 `Parcel` 上(作为 `data`)进行任意的 `readParcelable` 调用,在本漏洞利用中,我使用的是 [之前在不同漏洞中使用的同一路径](https://github.com/michalbednarski/LeakValue#putting-parcelables-in-system_server-and-retrieving-them)。我让 `readParcelable` 调用 `PackageParser$Activity.CREATOR.createFromParcel()`,而后者又会读取 `PooledStringWriter` 名称并调用其构造函数,之后 `Parcel` 数据结束,因此 `writeInt()` 需要重新分配 Parcel,从而进入我们的“接管”路径
这里值得注意的是,如果那时 `Parcel` 数据没有结束,`writeInt()` 就会尝试就地覆写数据,而在数据由 `/dev/binder` `mmap` 支持的情况下,这将导致 `SIGSEGV`
# File Descriptor Sanitizer
我最初的想法是让“接管”路径关闭文件描述符,之后在事务结束时同样的描述符会被再次关闭,但在这两件事之间,我会在另一个事务中把其他文件描述符放入 `system_server`,稍后再取回我的文件描述符,因为此时该 FD 指向的是不同的文件
在使用旧版 AOSP 的模拟器上这个方法有效,但当我尝试更新版本时,这个计划被 [File Descriptor Sanitizer (FDSan)](https://android.googlesource.com/platform/bionic/+/refs/heads/main/docs/fdsan.md) 阻止了
具体来说,[在 `android-14.0.0_r29` 中,FDSan 的覆盖范围扩大到涵盖 `Parcel` 内的 FD](https://android.googlesource.com/platform/frameworks/native/+/7772039cc5084247450f6113d9a18eca17f672aa%5E!/)
事实上,在 FDSan 覆盖 Parcel 之后,当 `Parcel` 包含 FD 时,我甚至无法到达 `closeFileDescriptors()` 调用。在该调用之前,会有对 `acquireObjects();` 的调用,它会获取对 `Binder` 句柄的引用(这些引用稍后会在该函数内由 `mOwner()` 调用释放),然而 `acquireObjects()` 还会 [为 FD 设置 FDSan 标签](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/Parcel.cpp;l=175-178;drc=f4c9b48c19f1b040efb35932b322f47e7779cafe):```cpp
case BINDER_TYPE_FD:
if (obj.cookie != 0) { // owned
FdTag(obj.handle, nullptr, who);
}
问题在于我们已经对从内核接收到的 FD 打了标签```cpp // In Parcel::ipcSetDataReference, which assigns this Parcel object to data from kernel (mOwner != null) if (type == BINDER_TYPE_FD) { // FDs from the kernel are always owned FdTag(flat->handle, nullptr, this); }
因此,双重 `FdTag`(在不关闭或更改 tag 的情况下指定预期的旧 tag)会导致 FDSan 错误,从而中止进程,而且我们甚至还没有执行到 `closeFileDescriptors()` 调用。由于“接管”路径在正常使用期间是死代码,此类问题可能不会被注意到
然而,我们可以看到 `if (obj.cookie != 0)` 条件。如果该条件为假,则意味着 `Parcel` 中的 FD 并非真正由该 `Parcel` 拥有,只要该 `Parcel` 存在,保持这些 FD 打开就是 `Parcel` 使用者的责任。但由于这个 `Parcel` 刚从内核传来,`cookie` 值实际上来自原始进程,并且当 `Parcel` 确实具有 `mOwner` 时,这些值被认为无关紧要。但“接管”路径实际上并未考虑这一点,而只是复制 `cookie` 值
综合以上所有内容,通过在发送方将 `cookie` 值设为零,我们可以得到一个引用已关闭文件描述符的 `Parcel`,但它并不认为自己拥有这些描述符,这意味着它不会再次关闭它们。这让我们能够避免触发 FDSan,但也消除了所有双重关闭的利用路径
# Parcel 的 Java 端技巧
这类 FD 仍然可以传递给另一个 `Parcel`(然后再传给另一个进程),然而我们触发这种悬空 FD 创建的方式涉及通过反射构造 `PooledStringWriter`,之后当我们尝试将其 `add()` 到 `ArrayList<IntentInfo>` 时,会抛出 `ClassCastException`
我们需要:
* 位于作为 `data` 参数传递给 `onTransact()` 的 `Parcel` 的末尾
* 构造 `PooledStringWriter`,之后 Parcel 将拥有悬空 FD,但也会抛出 `ClassCastException`
* 等待我们想要泄露的 FD 在 `system_server` 中被分配
* 让来自该 `Parcel` 的 FD 被复制到某个将发送给我们进程的其他 `Parcel` 中
为了满足所有这些要求,我需要同时使用一些旧技巧和新技巧
## 旧技巧
让我们从回顾旧技巧开始,其中大多数已经在我此前发布的 [使用 `LazyValue` 在 `recycle()` 后操作 `Parcel` 的利用](https://github.com/michalbednarski/LeakValue) 中描述过
1. [`RemoteViews` 类在设置了 `Parcel.ReadWriteHelper` 的情况下,对其包含的 `Bundle` 执行反序列化](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/widget/RemoteViews.java;l=2287-2300;drc=9b2e54f25456f2726ab1a15e6b6dc19395a3b5b4)。[当设置 `ReadWriteHelper` 时,`Bundle` 不会被立即反序列化,而是惰性反序列化](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/BaseBundle.java;l=1886-1896;drc=e1841f84f41213879e1f1b45ad4300b96970e545)。另外值得注意的是,在 `RemoteViews` 下,该 `Bundle` 中包含的所有 `Bundle` 也会被立即反序列化,而不仅仅是直接位于 `RemoteViews` 内部的那一个
2. [如果在 `system_server` 内反序列化 `Bundle` 时抛出 `BadParcelableException`,该 `BadParcelableException` 将被静默捕获,并且 `Bundle` 内容将被清除](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/BaseBundle.java;l=479-485;drc=efb735f4d5a2f04550e33e8aa9485f906018fe4e)。不过请注意,我们在 `createFromParcel` 中写入的触发器抛出的是 `ClassCastException`,它不会在这里被捕获
3. [`ParceledListSlice` 类在反序列化期间会对序列化数据中指定的对象进行阻塞的外向 Binder 调用](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/pm/BaseParceledListSlice.java;l=95-102;drc=e220b578ebc0885a28b83c95cb9ec78581bd8364),我们可以利用它来拖延反序列化的执行
现在这些都会用到,但这还不是全部
## 新技巧
[AIDL 是用于生成 RPC 接口实现的工具](https://developer.android.com/guide/components/aidl),但除此之外,它还能够生成 `Parcelable` 结构实现
这些结构带有长度前缀,因此在系统内,同一结构的不同版本是兼容的,只要没有在中间添加字段(也就是说,如果一个版本是另一个版本的前缀,则版本兼容)
让我们看看 AIDL 为 [`ReceiverInfo` 结构](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/ReceiverInfo.aidl) 生成了什么代码:```java
public final void readFromParcel(android.os.Parcel _aidl_parcel)
{
int _aidl_start_pos = _aidl_parcel.dataPosition();
int _aidl_parcelable_size = _aidl_parcel.readInt();
try {
if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
intent = _aidl_parcel.readTypedObject(android.content.Intent.CREATOR);
if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
data = _aidl_parcel.readString();
if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
extras = _aidl_parcel.readTypedObject(android.os.Bundle.CREATOR);
// SNIP: Other fields
} finally {
if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
throw new android.os.BadParcelableException("Overflow in the size of parcelable");
}
_aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
}
}
这段代码让我们能够完成剩余的两个技巧,在触发“take possession”路径后,我们需要这两个技巧来执行后续操作;该路径要求我们位于 Parcel 末尾,并会抛出 ClassCastException。
首先,我们从 Parcel 中读取长度,使用它跳过在写入版本中不存在的字段,最后我们将根据该长度调整 Parcel 内的位置。我们不允许移动到该 AIDL Parcelable 的位置之前(移动到 Parcel 末尾之后是可能的,但实际上不会造成任何危险)。然而,我们可以移动到已读取对象的中间,因此我们可以在第一次读取时到达 Parcel 末尾,让 PooledStringWriter 被构造出来,然后回退到该 ReceiverInfo 对象内部某个数据的中间。
这样就解决了位于 Parcel 末尾的问题,但仍存在 ClassCastException 问题。不过,这里有一个 finally 块,它在调用 setDataPosition() 之前会验证是否没有溢出。如果发生溢出,将抛出 BadParcelableException。那么,当另一个 Exception 处于待处理状态时,如果 finally 块抛出 Exception,会发生什么?在 finally 内部抛出的异常具有优先权,之前的 Exception 会静默消失。现在我们得到的不是 ClassCastException,而是 BadParcelableException,Bundle 会很贴心地忽略它,以避免在 system_server 中出现 Exception。
ParcelableListBinder 补丁的说明我们使用 MediaSession 的 ParcelableListBinder 将任意 Parcelable 放入 system_server,之后再将那个对象取回。
最近有一个 ParcelableListBinder 补丁,它恰恰禁止了这种行为。
从这个漏洞利用的角度来看,这个补丁实际上并不能阻止我,但我需要区分它存在和不存在两种情况来处理。
如果该补丁存在,非 QueueItem 项会从 system_server 接收到的列表中被静默移除(不会引发 Exception)。由于我们需要非 QueueItem 来产生副作用,我们可以将非 QueueItem 放在第一项,后面再跟一个真正的 QueueItem。这样虽然不允许我们执行任意 Parcelable 反序列化,但其中包含的 Bundle 可以包含文件描述符(我们正是希望这些描述符被传递给我们的进程)。
如果该补丁不存在,我们就不会遇到这个障碍,但我们也不能使用同样的流程。因为在上述情况下,列表中会同时包含 RemoteViews 和 QueueItem,因此 ParceledListSlice 会拒绝传输混合类型的列表。在这种情况下,我需要将泄漏的 FD 放入 RemoteViews 实例中。
不过,更有意思的是这个补丁被引入的原因。提交信息提到“允许应用从后台启动”,而且虽然 Android 安全公告没有说明太多,我们可以从 CVE 条目中找到有用信息,它指向 Notification.mAllowlistToken 字段,该字段 在读取时可以从静态字段中获取,而 在 system_server 内部,它是允许后台启动 Activity 的令牌,之后 该令牌会在 Notification.writeToParcel() 中被写入。这是否意味着,现在所有 system_server 反序列化任意 Parcelable 并将其发送回应用的情况都成了漏洞?无论如何,目前这只是一个想法,在这个漏洞利用中我无论如何都想做更多。
我认为这个漏洞利用拥有我迄今为止做过的最复杂的 Parcelable gadget 链:
RemoteViews (1)
ReflectionAction (2)
Bundle
Parcelable[] (3)
ReceiverInfo (用于 seek,4 和 10)
Intent
ComponentName (段 A,5)
ParceledListSlice (11)ParcelableParcel 或 QueueItem (12)Bundle (用于 catch,段 B,6)
ReceiverInfo (用于 rethrow,7)
Bundle (8)
上面列表中的 “segment A” 和 “B” 标注指的是我的 FdLeaker.java 类中 “START A”/“END A”/“START B”/“END B” 注释之间的代码块;数字则指向下面列表中的各个点。
上面的树从发送者的角度描述了层次结构,而从接收者的角度来看,情况略有不同:
ParcelableListBinder 接收数据,最外层的对象是 RemoteViews。RemoteViews 中嵌套着一个 Bundle。RemoteViews 会 设置 Parcel.ReadWriteHelper,因此这个 Bundle 以及其中所有的 Bundle 都会被急切地读取。这是必要的,否则我们将无法从 ReceiverInfo.readFromParcel() 执行任意的 readParcelable。Parcelable[] 只是一个便捷包装,用于把我要放入 RemoteViews 的所有 Parcelable 分组。让我们从高层次来看一下上述攻击:
system_server 内打开一个文件描述符,记住它的引用,然后关闭该 FD。system_server 打开另一个不同的文件描述符。这个攻击有一个重大的局限性:我们无法获取在攻击开始之前就已经打开的文件描述符。
不过,仍然有少数有用的事情可以做。
InputChannel老实说,这是我在没有额外假设的情况下唯一能使其工作的漏洞利用变体。
输入事件,也就是来自触摸屏和键盘的事件,由应用通过来自 system_server 的 UNIX 套接字接收。当 Activity 启动或向系统添加新窗口时,会创建一个新的 InputChannel,这反过来意味着 会创建一个 UNIX 套接字对,并且 其中一端会被发送给应用,另一端则由 system_server 用于发送事件。
通过这些套接字发送的结构布局有明确定义(因为它们必须在 32 位和 64 位进程之间兼容),而且看起来意外的序列号不会引起任何问题。此外,InputChannel 似乎是 startActivity() 调用之后在 system_server 内分配的唯一套接字,因此可以很容易地确定哪个 FD 是 InputChannel 的服务端套接字。

虽然这只是一个玩具示例,但我们也可以批准权限提示或应用安装,启用媒体投影或无障碍服务。
zygote 的连接这个想法基本是理论性的。我能在运行缓慢的模拟器上实施此攻击,但在真机上竞争窗口(race window)太小了。
system_server 只在启动时打开一次到 /dev/socket/zygote 的连接,之后所有请求都通过该连接发送。
在 system_server 启动期间,MediaSessionService(用于与 system_server 之间收发 Parcelable)会在与 zygote 建立连接之前发布到 servicemanager。
因此,理论上应用可以启动一个辅助进程,让 system_server 崩溃,然后从那个后台进程在 system_server 启动期间执行攻击。
SensorService 获取与 zygote 的连接我还发现了另一个 bug,这是 SensorService::createSensorDirectConnection() 方法```cpp
sp SensorService::createSensorDirectConnection(
const String16& opPackageName, int deviceId, uint32_t size, int32_t type, int32_t format,
const native_handle *resource) {
// SNIP: Reject direct connections when sensor privacy is enabled
// SNIP: Irrelevant parameter checks
// check specific to memory type
switch(type) {
case SENSOR_DIRECT_MEM_TYPE_ASHMEM: { // channel backed by ashmem
if (resource->numFds < 1) {
ALOGE("Ashmem direct channel requires a memory region to be supplied");
android_errorWriteLog(0x534e4554, "70986337"); // SafetyNet
return nullptr;
}
// SNIP: Further validation for SENSOR_DIRECT_MEM_TYPE_ASHMEM
}
case SENSOR_DIRECT_MEM_TYPE_GRALLOC:
// no specific checks for gralloc
break;
default:
ALOGE("Unknown direct connection memory type %d", type);
return nullptr;
}
native_handle_t *clone = native_handle_clone(resource);
if (!clone) {
return nullptr;
}
native_handle_set_fdsan_tag(clone);
sp<SensorDirectConnection> conn;
int channelHandle = 0;
if (deviceId == RuntimeSensor::DEFAULT_DEVICE_ID) {
// SNIP: usual case where sensor belong to this device (not app streaming)
} else {
auto runtimeSensorCallback = mRuntimeSensorCallbacks.find(deviceId);
if (runtimeSensorCallback == mRuntimeSensorCallbacks.end()) {
ALOGE("Runtime sensor callback for deviceId %d not found", deviceId);
} else {
int fd = dup(clone->data[0]);
channelHandle = runtimeSensorCallback->second->onDirectChannelCreated(fd);
}
}
// SNIP: Return connection
}
我们有一个 `dup(clone->data[0])` 调用。`clone` 是从远程进程接收到的 `native_handle_t`。native handle 在 data 中包含一定数量的 FD 和一定数量的普通整数,native handle 的使用者在访问 `data` 之前,应通过[查看 `numFds` 和 `numInts`](https://cs.android.com/android/platform/superproject/main/+/main:system/core/libcutils/include/cutils/native_handle.h;l=37-38;drc=efb735f4d5a2f04550e33e8aa9485f906018fe4e)来检查这些数量。
这里甚至还有一个“特定于内存类型的检查”部分,它会检查,对于 `SENSOR_DIRECT_MEM_TYPE_ASHMEM`,这里会进行检查;对于 `SENSOR_DIRECT_MEM_TYPE_GRALLOC`,native handle 的格式是设备特定的,无法在这里验证。问题在于,对于“运行时传感器”,其类型总是被视为 `SENSOR_DIRECT_MEM_TYPE_ASHMEM`,但在这种情况下,我们可以指定 `SENSOR_DIRECT_MEM_TYPE_GRALLOC` 来绕过验证。
然而,这段代码只有在存在“运行时传感器”时才可达。我不确定这具体会发生在什么情况下,我认为是当用户使用“Nearby app streaming”时(?)
不过为了测试,我添加了一个小型类,允许注册 [`VirtualDevice`](https://developer.android.com/reference/android/companion/virtual/package-summary),你可以通过它来```sh
adb shell 'CLASSPATH=$(pm path com.example.thisseemswrong | cut -d: -f2) app_process / com.example.thisseemswrong.VirtualDeviceReg'
此后,就可以在生产设备上以 system uid 执行代码。

PackageParser$Activity (9)
PooledStringWriterReceiverInfo 定义的长度落在其数据中间,不过在读取时这一点还未知,Intent 会正常地从它里面读出。ComponentName.readFromParcel() 发起的 readString 调用读取的 (使用 ComponentName 是因为无论 Android 版本如何,它都使用 UTF-16 的 readString;该 readString 仍会返回 null,因为这个字符串与 Binder 对象重叠,所以尽管 ComponentName 的构造已经跳过了这一轮中隐藏的数据,ComponentName 对象并未被创建)。Bundle,在反序列化这个 Bundle 结束时,会捕获一个 BadParcelableException。ReceiverInfo。这个 ReceiverInfo 的长度被指定为 Integer.MAX_VALUE,因此会在 finally 块中抛出 BadParcelableException,从而静默丢弃 ClassCastException。Bundle。这只是为了能够从 ReceiverInfo 触达任意的 readParcelable。现在我们位于 RemoteViews 内部,所以 Bundle 会被急切地读取,因此可以做到这一点。PackageParser$Activity + PooledStringWriter 组合会在正在被读取的 Parcel 上触发 writeInt(0) 调用。由于我们位于 Parcel 末尾,writeInt() 必须扩展 Parcel 容量,从而触发“take possession”路径。这个组合还会导致 ClassCastException,但正如上面第 7 步和第 6 步所述,它会被吞掉。ReceiverInfo 的末尾,ReceiverInfo 会根据其头部的长度将位置定位到之前 ComponentName 内部的数据中间,这些数据会作为 Parcelable[] 中的下一项被读取。ParceledListSlice,它会向我的进程发起一次阻塞式 Binder 事务。此时这个 Parcel 中定义的文件描述符已经被关闭,但还没有任何有趣的东西取代它们。在此反序列化等待这次调用返回期间,我可以让系统打开一些有趣的文件描述符,然后它们会被发送给我。Parcel 中取出文件描述符并发送给我的地方。ParcelableListBinder 是否过滤条目,处理方式有所不同。
a. 如果 ParcelableListBinder 不过滤条目,FD 会被保存在 ParcelableParcel 中。ParcelableParcel 与 Bundle 类似,使用 Parcel.appendFrom() 逐字复制 Parcel 数据,但它没有特殊的 hasReadWriteHelper() 逻辑,所以即使位于 RemoteViews 的 Bundle 之下,它也会这样做。
b. 如果 ParcelableListBinder 过滤条目,那么这就是 RemoteViews 的结尾,RemoteViews 对象会被 ParcelableListBinder 丢弃,但这对我来说没问题,因为副作用已经发生。ParcelableListBinder 接收到的下一个条目是 QueueItem,它包含 MediaDescription,而 MediaDescription 又 包含 Bundle,我泄漏的 FD 就保存在这个 Bundle 中。