Flutter AvPlayer 外接纹理播放异常分析三

Flutter video_player 在 OpenHarmony 上卡死:从 Binder 29201 到一行 SELinux 修复

目录


术语表

术语 英文 说明
ExternalTexture External Texture Flutter 将平台侧图像 Buffer 接入渲染管线的纹理机制
Release Fence Release Fence 表示消费者已经完成 Buffer 使用的同步对象
ProducerListener Producer Listener BufferQueue 通过 Binder 通知生产端 Buffer 已释放的回调接口
Binder Binder IPC OpenHarmony 中用于跨进程调用和对象传递的 IPC 机制
BR_FAILED_REPLY Binder Failed Reply Binder Driver 返回给用户态的事务失败结果,本问题中表现为 29201
MessageParcel Message Parcel Binder 用户态序列化参数及 Binder Object 的载体
File Descriptor File Descriptor 进程用于引用内核对象的整数句柄,文中简称 FD
sync_file Sync File Linux 用 FD 表示同步 Fence 的内核对象类型
BufferQueue Buffer Queue 管理图形 Buffer 申请、提交、释放及复用的队列
freeList Free List BufferQueue 中用于保存可再次请求 Buffer Sequence 的集合
LSM Linux Security Modules Linux 内核安全钩子框架,SELinux 通过该框架实施访问控制
SELinux Security-Enhanced Linux 基于安全域和对象类型实施强制访问控制的安全机制
fd use File Descriptor Use Permission SELinux 中允许目标域使用由另一安全域创建或持有的 FD 的权限
TRACE3 Third-stage Trace 本次调查在 Binder Driver FD 翻译边界增加的第三阶段诊断探针

注:术语按在本文中出现的先后顺序排列。


1. 问题现象与调查目标

1.1 表面症状

Flutter video_player 在 OpenHarmony 上通过 ExternalTexture 播放视频时,最初可以正常工作,但播放一段时间后画面停止,随后音频和媒体同步链路出现持续异常。日志中的高频症状包括:

IProducerListener SendRequest ret=29201
AudioSink: buffer queue is empty
MediasyncManager: Media progress lag

从用户体验看,这是一个“视频播放一段时间后卡死”的问题;从系统行为看,则是 ProducerListener 的 Buffer Release 回调开始大量失败,并逐步拖垮 Buffer Pool 周转。

调查目标不是让错误暂时消失,而是回答三个问题:

  1. 29201 发生在哪一层;
  2. 哪一个事务属性决定成功或失败;
  3. 如何用最小修改修复根因,而不绕过 Fence、关闭 SELinux 或改变 BufferQueue 语义。

1.2 为什么不能直接修改 BufferQueue

早期日志显示失败后会执行 CancelBufferLocked(),因此很容易把注意力放在 BufferQueue 的失败恢复逻辑上,例如“不再把失败 Buffer 放回 freeList_”或“忽略 ProducerListener 返回值”。

但这些做法只能改变错误后的表现,不能解释 Binder 为什么拒绝事务。更严重的是,它们可能造成 Buffer 泄漏、生产者永久失去 Buffer,或者掩盖跨进程同步语义已经失效的事实。

因此本次调查坚持先定位首个跨层失败点,再决定修复位置。


2. 故障链路与验证方法

2.1 跨层调用链

问题横跨 Flutter Engine、图形 BufferQueue、媒体 Codec、Binder 用户态和 Binder Driver:

Flutter Raster Thread
→ ExternalTexture 释放上一帧
→ 创建 Release Fence
→ NativeImage / ProducerSurface
→ BufferQueue 请求已释放 Buffer
→ ProducerListenerProxy 写入 Sequence 与 Fence
→ MessageParcel 携带一个 FD Binder Object
→ Binder Driver 翻译 FD
→ av_codec_service 中的 Listener Stub
→ Codec 回收 Buffer

这条链路中的任意一层都可能制造相似的“回调失败”表象,因此必须用逐轮排除法,把失败边界从上层一路收窄到内核安全钩子。

2.2 单变量验证原则

每轮只测试一个主要假设,并明确保持不变的变量:

轮次 单一问题 保持不变的主要变量
Trace 1 Listener 是否已销毁或注销 Fence、Parcel、Binder 模式、BufferQueue 行为
Trace 2 FD/Parcel 是否决定事务结果 Listener 生命周期、Binder 异步模式
实验 A TF_ASYNC 是否导致 FD 事务失败 相同 Fence、Parcel 布局、Listener、BufferQueue
Trace 3 Binder FD 翻译在哪个分支失败 恢复原始 TF_ASYNC,不改变用户态行为
最终修复 缺少的 SELinux 权限是否为根因 Flutter、BufferQueue、Binder Driver、Kernel 行为

这种设计保证每次结果都能确认或否定一个清晰假设,避免多个修改叠加后无法知道究竟是哪一项生效。


3. 第一轮:排除 ProducerListener 生命周期问题

3.1 假设与探针

第一轮假设是:ProducerListener Stub 在 29201 出现前已经被 Destroy、Reset 或 Unregister,导致回调对象失效。

为验证该假设,在 Media Codec、ProducerSurface、Listener Stub 和 BufferQueue 中增加统一生命周期标记:

LISTENER_CREATE / RESET / DESTROY
PRODUCER_SURFACE_REGISTER / UNREGISTER / DESTROY
BUFFER_QUEUE_REGISTER / UNREGISTER
HDECODER_REGISTER / CALLBACK / UNINITIALIZED
RELEASE_SURFACE

同时关联 Surface ID、Listener Proxy、远端对象、Stub、Binder Handle 和 Buffer Sequence,避免把不同播放实例的日志错误拼接到一起。

3.2 关键证据

首次失败的 Sequence 为:

seq=36241505
Binder error=29201 / BR_FAILED_REPLY

在该失败之前,没有出现 Listener Reset、Destroy、Unregister、Surface Release 或 Codec Uninitialized。

更关键的是,同一 Listener、同一 Binder Handle 在该失败之后仍成功处理了后续 Sequence:

36241506
36241507
36241508
36241509
36241511
36241512

如果 Listener 对象已经整体死亡,后续事务不可能继续通过同一对象到达 Stub。因此一个失败 Sequence 后仍有成功 Sequence,是反证生命周期假设的决定性证据。

3.3 第一轮结论

29201 是事务级失败,而不是 ProducerListener 对象整体失效。

这一轮排除了:

  • Listener Stub 提前销毁;
  • Listener Reset 或 Unregister;
  • Surface 提前释放;
  • Codec 提前进入 Uninitialized;
  • Binder Handle 整体死亡。

调查方向由“对象是否还活着”转向“失败事务和成功事务的载荷有什么不同”。


4. 第二轮:锁定 Fence FD 与 Parcel 的严格相关性

4.1 观测维度

第二轮同时记录 ProducerListenerProxy、SyncFence 序列化和 BufferQueue 状态,重点字段包括:

Sequence
Fence 对象与原始 FD
fcntl(fd, F_GETFL) 与 errno
WriteFileDescriptor 返回值
Parcel data_size
Parcel offsets 数量
ContainFileDescriptors
SendRequest 返回值
Buffer State
freeList_ 大小
CancelBufferLocked 前后状态

本轮并不是尝试修复,而是比较相邻的成功与失败事务,寻找没有例外的分流条件。

4.2 无 FD 与有 FD 的精确分流

共分析 286 次 ProducerListener 发送:

事务载荷 Parcel 特征 结果 数量
无有效 Fence FD data_size=72offsets=0has_fd=0 成功 34/34
携带有效 Fence FD data_size=96offsets=1has_fd=1 29201 252/252

结果没有例外:

有效 FD 成功次数:0
无 FD 失败次数:0

第一次失败事务中,FD 在用户态仍然有效,序列化也成功:

sequence=33816622
fence_fd=120
fcntl(F_GETFL)=0
errno=0
WriteFileDescriptor=true
Parcel data_size=96
Parcel offsets=1
ContainFileDescriptors=true
SendRequest=29201

紧随其后的无 FD 事务使用同一条 Listener 链路,却成功返回:

sequence=33816623
fence_fd=-1
Parcel data_size=72
Parcel offsets=0
ContainFileDescriptors=false
SendRequest=0

这证明失败条件不是 Sequence、Listener 生命周期、源 FD 已关闭或 Parcel 写入失败,而是“跨进程事务携带一个有效 FD 对象”。

4.3 BufferQueue 如何放大一次 Binder 失败

ProducerListener 发送失败后,BufferQueue 执行 CancelBufferLocked()

失败前:
state=BUFFER_STATE_REQUESTED
free_list_size=0

Cancel 后:
old_state=BUFFER_STATE_REQUESTED
new_state=BUFFER_STATE_RELEASED
free_list_size=1

该 Sequence 被重新放入 freeList_。下一次 Release 时,它又会被 RequestBuffersForListenerLocked() 取出,并携带 Fence FD 再次发送。

于是形成放大循环:

FD 事务失败
→ CancelBufferLocked
→ Sequence 回到 freeList_
→ 下一轮再次 Request
→ 相同类型 FD 事务再次失败
→ 失败 Sequence 和请求数量持续积累
→ Buffer Pool 周转能力下降
→ 视频停止
→ AudioSink 饥饿
→ MediaSync 延迟

BufferQueue 不是首个根因,但它的恢复语义会反复触发同一个安全拒绝,把单次 Binder 失败放大为持续播放故障。


5. 第三轮:用同步事务否定 TF_ASYNC 假设

5.1 单一行为修改

第二轮已经确认“有效 FD”与失败严格相关,但尚不能判断是否只有异步 Binder 事务携带 FD 时才失败。

实验 A 只修改 Sequence+Fence 回调的 MessageOption:

// 原始行为
option.SetFlags(MessageOption::TF_ASYNC);

// 实验 A
option.SetFlags(MessageOption::TF_SYNC);

Fence、FD、Parcel 布局、Listener 对象、BufferQueue 状态机和其他 ProducerListener 方法均保持不变。

5.2 同步实验结果

180 秒运行结果如下:

同步事务载荷 结果 数量
无有效 Fence FD 成功 25/25
携带有效 Fence FD 29201 218/218

附加现象:

有效 FD 成功:0
Listener Callback Enter/End:25/25
SEND_FAIL_STATE:218
CANCEL_BUFFER_DONE:218
AudioSink buffer queue is empty:11811
Media progress lag:1208

只有不携带 FD 的事务到达 Listener Stub;把事务改成同步没有改变 FD 事务的失败结果。

5.3 第三轮结论

TF_ASYNC + FileDescriptor 不是充分根因。同步和异步模式下的分流完全一致:

无 FD → 成功
有 FD → 29201

因此后续不再围绕同步/异步模式试错,而是恢复原始 TF_ASYNC,把诊断边界下沉到 Binder Driver 的 FD 翻译路径。


6. 第四轮:下沉到 Binder Driver 定位失败分支

6.1 binder_translate_fd 的判定树

在 Linux 6.6 Binder Driver 中,FD Binder Object 会进入:

kernel/linux/linux-6.6/drivers/android/binder.c
binder_translate_fd()

该函数的关键失败分支可以归纳为:

target_node->accept_fds == false
→ BINDER_FD_TARGET_REJECT

fget(fd) == nullptr
→ BINDER_FD_INVALID

security_binder_transfer_file() < 0
→ BINDER_FD_SECURITY_REJECT

分配 fd_fixup 失败
→ BINDER_FD_ALLOC_FAIL

全部检查通过
→ BINDER_FD_TRANSLATE_OK

Trace 3 只在这些分支增加诊断日志,不修改 Binder 返回语义。

6.2 第一次 Kernel 构建为何不能作为证据

第一次 Trace 3 顶层 OHOS 构建退出码为 0,镜像也成功生成,但烧录后没有出现任何新增 TRACE3 事件。

进一步核查发现:

源码 binder.c 已修改
但 staged binder.c 仍是旧版本
binder.o 仍是旧产物
vmlinux 与 Kernel Image 不含 TRACE3
boot_linux.img 实际打包了旧 Kernel

原因是 Kernel staged source、OBJ 和 checkpoint 被增量构建错误复用。

因此这轮结果被标记为:

build_success_artifact_invalid

这是一个重要区别:顶层构建成功只证明构建工具正常退出,不证明目标源码进入了最终运行镜像。

6.3 强制重建与二进制门禁

在确认没有其他 rk3568 构建占用输出目录后,定向刷新:

out/kernel/src_tmp/linux-6.6
out/kernel/OBJ/linux-6.6
out/kernel/checkpoint/last_build.info
out/kernel/checkpoint/current_build.info

随后不再只看构建退出码,而是逐层验证 TRACE3:

修改后的 binder.c
→ staged binder.c 含 5 个 TRACE3 事件
→ binder.o LLVM bitcode 含 5 个事件字符串
→ vmlinux 含全部事件
→ Kernel Image 含全部事件
→ boot_linux.img 时间晚于 Kernel Image

其中 binder.o 是 LLVM Bitcode,必须使用仓库内 llvm-dis 反汇编后检查;直接使用 stringsgrep -a 可能得到假阴性。

6.4 SELinux 拒绝的决定性证据

强制重建后的运行统计为:

Hilog 29201:493
BINDER_FD_TRANSLATE_OK:111
BINDER_FD_SECURITY_REJECT:245
BINDER_FD_TARGET_REJECT:0
BINDER_FD_INVALID:0
BINDER_FD_ALLOC_FAIL:0

第一次目标失败由 Kernel 直接记录:

[PRODUCER_LISTENER_TRACE3]
event=BINDER_FD_SECURITY_REJECT
from=2103:2342
to=489
fd=122
security_ret=-13

对应关系为:

源进程:video_player example
源安全域:u:r:debug_hap:s0
目标进程:av_codec_service
目标安全域:u:r:av_codec_service:s0
FD:anon_inode:sync_file
security_binder_transfer_file():-13 / EACCES

随后 Binder 输出:

translate fd failed
transaction async ... failed .../29201/-1
size 96-8

96-8 与 Trace 2 中有 FD Parcel 的 data_size=96、一个 8 字节 Object Offset 完全对应。用户态载荷证据和 Kernel 失败分支在这里闭合。


7. 根因:缺少接收端 fd use 权限

7.1 为什么 Binder 权限存在仍会失败

策略中已经存在:

allow av_codec_service normal_hap_attr:binder { call transfer };

该规则允许 av_codec_service 与普通 HAP 属性域进行 Binder 调用和 Binder 对象传递,但它不等于允许目标进程使用来源域持有的 FD。

debug_hap 的类型声明包含 normal_hap_attr,因此本次传递中的来源类型可以由该属性覆盖。目标 av_codec_service 在接收 sync_file FD 时,还需要独立的:

normal_hap_attr:fd use

缺少该权限时,Binder Driver 已经拿到有效源 FD,也确认目标节点接受 FD,但在 security_binder_transfer_file() 的 LSM 检查处返回 -EACCES

7.2 完整故障链

根因确认后的完整链路为:

Flutter ExternalTexture 释放上一帧
→ 创建 Release Fence
→ Fence 表示为 anon_inode:sync_file FD
→ ProducerListenerProxy 成功写入 MessageParcel
→ Binder Driver 进入 BINDER_TYPE_FD
→ binder_translate_fd()
→ 目标节点允许 FD
→ 源 FD 有效
→ security_binder_transfer_file()
→ SELinux 缺少 av_codec_service 对 normal_hap_attr:fd 的 use 权限
→ 返回 -EACCES
→ Binder 将事务失败映射为 -EPERM
→ 用户态收到 BR_FAILED_REPLY / 29201
→ Listener Stub 未收到该次 Release 回调
→ BufferQueue 执行 CancelBufferLocked
→ Sequence 返回 freeList_
→ 后续再次取出并重传
→ 同一安全拒绝反复发生
→ Buffer Pool 周转被拖垮
→ 视频停止、AudioSink 饥饿、MediaSync 延迟

这也解释了为什么无 FD 事务始终成功:它们不会触发 security_binder_transfer_file(),因而不会碰到缺失的 fd use 权限。


8. 最小修复方案

8.1 一行 Policy 修改

最终只修改一个文件:

base/security/selinux_adapter/sepolicy/ohos_policy/multimedia/av_codec/system/av_codec_service.te

在已有 Binder 权限旁增加一条规则:

 allow av_codec_service normal_hap_attr:binder { call transfer };
+allow av_codec_service normal_hap_attr:fd { use };

8.2 为什么这是最小权限修复

该规则的四个维度均与已确认的失败边界一致:

维度 修复范围 依据
目标域 av_codec_service Kernel 记录的接收进程安全域
来源类型 normal_hap_attr debug_hap 具有该属性,且现有 Binder 规则已按该属性授权
对象类别 fd 失败只发生在携带 File Descriptor 的事务
权限 use security_binder_transfer_file() 检查目标域能否使用来源 FD

修复没有采用以下高风险方案:

  • 不关闭 SELinux;
  • 不把目标域设为 permissive;
  • 不对所有 HAP 或所有服务开放宽泛权限;
  • 不丢弃 Release Fence;
  • 不忽略 ProducerListener 错误;
  • 不修改 BufferQueue 状态机;
  • 不把异步事务永久改为同步。

8.3 构建与镜像验证

最终修复镜像构建结果:

构建命令:./build.sh --product-name rk3568 --ccache --target-cpu arm64
开始:2026-07-17 08:37:00 +08:00
结束:2026-07-17 08:59:48 +08:00
退出码:0
SELinux Check:通过
必需镜像:13/13 通过

镜像包:

C:\tools\dayu200_flash\images\rk3568_OpenHarmony_6.1.0.32_20260717_085852.tar.gz
SHA256:9dc8228d09544f28b3e63ae51513237790a82c3ba6f85d2d6950ec646939d3cb

烧录后的系统保持:

OpenHarmony 6.1.0.32
SELinux Enforcing
Linux 6.6.101

这证明修复是在强制访问控制开启状态下生效,而不是通过弱化系统安全模式绕过问题。


9. 修复后的端到端验证

9.1 首次三分钟运行

应用完成 HAP 构建、安装、EntryAbility 启动、Dart VM Service 连接和 DevFS 同步,并持续运行约三分钟。

统计结果:

BINDER_FD_SECURITY_REJECT:0
Binder 29201:0
BINDER_FD_TRANSLATE_OK:244
ExternalTexture RELEASE_BEGIN:100
ExternalTexture RELEASE_END:100
Release 非零返回:0
AudioSink buffer queue is empty:0
Media progress lag:0
CancelBufferLocked:0

修复后既没有安全拒绝,也没有通过“停止发送 FD”来规避问题,因为 Kernel 明确记录了 244 次 FD 翻译成功。

9.2 五次独立重复运行

为避免单次运行偶然通过,每轮都执行:

  1. 强制停止应用;
  2. 清空 Hilog;
  3. 清空 Kernel dmesg;
  4. 重新启动 EntryAbility;
  5. 运行约 60 秒;
  6. 独立保存 Hilog、dmesg 和进程信息。
Run Security Reject Translate OK 29201 Release Begin Release End 非零 Release Audio Empty Media Lag CancelBuffer
1 0 120 0 102 102 0 0 0 0
2 0 120 0 99 100 0 0 0 0
3 0 130 0 104 104 0 0 0 0
4 0 130 0 104 104 0 0 0 0
5 0 120 0 104 104 0 0 0 0
合计 0 620 0 513 514 0 0 0 0

Run 2 的 Begin/End 相差 1,是 60 秒采集窗口截在一次 Release 过程边界;该轮非零 Release、Security Reject、29201 和 CancelBuffer 均为 0。

9.3 修复前后对比

指标 根因确认镜像 最终修复后
BINDER_FD_SECURITY_REJECT 245 首次运行 0;5 次合计 0
BINDER_FD_TRANSLATE_OK 111 首次运行 244;5 次合计 620
Binder 29201 493 首次运行 0;5 次合计 0
AudioSink buffer queue is empty 5551 首次运行 0;5 次合计 0
Media progress lag 567 首次运行 0;5 次合计 0
Release 非零返回 故障链中持续失败 首次运行 0;5 次合计 0
CancelBufferLocked 故障放大 持续发生 首次运行 0;5 次合计 0

不同运行的时长和日志窗口并不完全相同,因此这里不比较比例,只比较目标错误是否仍然出现,以及成功 FD 翻译是否持续发生。


10. 调试过程中得到的工程经验

10.1 29201 只是结果码,不是根因

BR_FAILED_REPLY / 29201 只能说明 Binder 事务失败。它不能区分对象死亡、Parcel 错误、FD 无效、目标节点拒绝 FD、内存分配失败或 SELinux 拒绝。

有效的调查顺序是:

生命周期
→ 事务载荷
→ 同步/异步控制实验
→ Binder Driver 失败分支
→ LSM/SELinux 具体权限

如果只围绕 29201 猜测,很容易在 Listener 生命周期、Binder 模式或 BufferQueue 上反复修改,却始终触碰不到真正失败的安全边界。

10.2 顶层构建成功不等于目标代码进入镜像

Trace 3 首次构建退出码为 0,但 Kernel 探针没有进入 staged source 和二进制。若直接基于运行时“没有出现新日志”判断代码分支未执行,会得到错误结论。

Kernel 改动至少需要验证:

source
→ staged source
→ object / LLVM bitcode
→ vmlinux
→ Image
→ boot_linux.img

构建成功是必要条件,不是产物有效性的充分条件。

10.3 失败恢复逻辑可能把单次错误放大成系统卡死

CancelBufferLocked() 的本意是让发送失败的 Buffer 回到可复用状态,但当失败条件是稳定、可重复的安全策略拒绝时,这个恢复动作会把同一 Sequence 再次送回失败路径。

因此分析系统性卡死时,不仅要定位首个失败,还要追踪错误后的状态回流:

失败后状态去了哪里
→ 是否重新进入同一路径
→ 重试是否有退出条件
→ 是否逐步耗尽共享资源

10.4 SELinux 修复应放在使用 FD 的目标域

跨进程 FD 传递不是简单的“发送者能否发送”。Binder Driver 会检查目标域是否有权使用来源域持有的 FD。

本问题的授权方向是:

allow av_codec_service normal_hap_attr:fd { use };

而不是反向给 debug_hap 增加对 av_codec_service 的 FD 权限。理解“谁创建或持有 FD、谁接收并使用 FD”是写对规则方向的关键。


11. 总结

阶段 核心问题 关键证据 结论
Trace 1 Listener 是否死亡 29201 后同一 Listener 仍处理后续 Sequence 生命周期假设被否定
Trace 2 什么载荷决定失败 无 FD 34/34 成功,有 FD 252/252 失败 失败与有效 FD 严格相关
实验 A 是否仅异步 FD 事务失败 同步有 FD 218/218 仍失败 TF_ASYNC 不是根因
Trace 3 Binder FD 翻译在哪一分支失败 security_binder_transfer_file=-EACCES SELinux/LSM 拒绝得到确认
最终修复 如何最小授权 增加 av_codec_service normal_hap_attr:fd use 一行 Policy 修复
回归验证 是否稳定解决 三分钟运行 + 5 次独立运行均无 Reject/29201 Bug 已解决

最终结论可以浓缩为一句话:

Release Fence 本身、Parcel 序列化、Listener 生命周期和 Binder 同步模式都没有问题;真正缺失的是 av_codec_service 使用普通 HAP 属性域所传 FD 的 SELinux use 权限,一行定向 Policy 即可在保持 SELinux Enforcing 的前提下恢复 video_player 连续播放。

posted @ 2026-07-17 11:39  getmoon  阅读(11)  评论(0)    收藏  举报