Perfetto 系统级 Tracing 安全模型深度解析:Android/Linux 下 Producer、Consumer 与可信边界设计
【免费下载链接】perfettoProduction-grade client-side tracing, profiling, and analysis for complex software systems.项目地址: https://gitcode.com/GitHub_Trending/pe/perfetto
本文基于 Perfetto 官方设计文档 docs/design-docs/security-model.md,结合仓库内tracing_service_impl.cc、packet_stream_validator系列源码与perfetto.rc配置,系统讲解 Perfetto 在 Android/Linux 上做 system-wide tracing 时的安全模型:谁可信、谁不可信、数据如何隔离、TracePacket 内容如何防伪造,以及服务自身如何被约束以降低被攻破后的影响面。读完本文,你将理解 Perfetto 服务端安全设计的完整骨架,并能从源码层面印证每一条安全原则的具体落地方式。
一、总体安全模型:两个端点、两种信任级别
Perfetto 的 tracing 服务(即traced守护进程)对外暴露两个端点(endpoint):
- Producer 端点(生产者端点):通常对系统内所有想上报 trace 数据的进程开放,是public的。
- Consumer 端点(消费者端点):仅对受信任的消费者开放,是restricted的。
在 Chromium 嵌入场景中,这两个端点表现为 Mojo service;在 Android/Linux 场景中则是 UNIX socket。从 perfetto.rc 可以看到 Android 上这两个 socket 的实际定义:
service traced /system/bin/traced class late_start disabled socket traced_consumer stream 0666 root root socket traced_producer stream 0666 root root user nobody group nobody task_profiles ProcessCapacityHigh capabilities SYS_NICEtraced_consumer与traced_producer两个 socket 都由 init 创建并传递给traced进程,traced自身不负责创建 socket(这是下面"有限 syscall 面"设计的一部分);同时traced以nobody:nobody身份运行,仅保留SYS_NICEcapability。consumer socket 的访问控制并不依赖 socket 权限位(此处为 0666),而是依靠 SELinux 策略将 consumer socket 锁定为仅shell域可访问——这一点在原文档的 "Consumers" 一节中明确说明,是 Android 上信任路径的建立方式。
整个安全模型的信任分层可以概括为一张表:
| 实体 | 信任级别 | 威胁假设 | 防护手段 |
|---|---|---|---|
| Producer | 永不信任 | 会尽力 DoS / 崩溃 / 攻击 tracing 服务 | 服务端全量输入校验、共享内存点对点隔离、数据包保留字段过滤 |
| Tracing service | 高权限但受限 | 自身 bug 可能被利用 | 最小 syscall 面、最小权限运行、无文件/无 socket 创建能力 |
| Consumer | 始终信任 | 不应能崩溃或利用服务;能轻易 DoS,属预期行为(WAI) | Chromium 用 service manifest、Android 用 SELinux 锁定 socket |
二、Producer:永不信任的输入源
2.1 威胁假设
Producer 是 trace 数据的来源,可能来自任何有权限连接 producer socket 的进程。安全模型默认 producer 是恶意的或存在缺陷的,假设它会尽一切努力去:
- DoS服务(如疯狂灌数据、占用内存/带宽);
- 崩溃服务(如发送畸形数据触发解析错误);
- 利用服务(如通过缓冲区溢出等漏洞实现远程代码执行)。
2.2 防护落点:tracing_service_impl.cc
对 producer 的所有防御都在 src/tracing/service/tracing_service_impl.cc 这一层实现,而不是分散到各个 IPC transport 或 embedder 中。这样设计的目的是:无论 Perfetto 被哪个宿主(Android、Linux、Chromium)嵌入、无论底层 IPC 通道是什么(UNIX socket、Mojo、共享库调用),安全强度与测试覆盖都完全一致,不会因为换了一个传输层就出现防御缺口。
从源码看,TracingServiceImpl::ConnectProducer会校验连接的 uid(lockdown_mode_下非当前用户 uid 的连接被拒绝,见 tracing_service_impl.cc 中client_identity.uid()相关逻辑),并将连接信息记录到日志中;随后每个 producer 的数据在写入 trace buffer 前都会经过PacketStreamValidator校验(见下文第四节)。
三、Tracing service:被攻破后也无利可图的沙箱
原文档对 tracing service 提出了两条硬性要求:
- 必须校验所有输入;
- 在最坏情况下(服务自身存在可被远程利用的代码执行漏洞),服务本身不应该拥有任何有意义的可利用能力。
第二条是纵深防御(defense in depth)的典型体现:即便校验失败、即便漏洞被利用,攻击者从traced进程里也拿不到什么。为此,traced的设计刻意收窄了 syscall 面:
- 不打开、不创建任何文件(唯一的例外是 tmpfs);
- 只向 IPC 通道上传入的 fd 写入数据——也就是说,文件描述符是"外面递进来的",服务自身从不发起
open(); - 不打开、不创建 socket——在 Android 上,IPC 用的 socket 由 init 在启动时创建并通过 service 声明传递给
traced,这一点可以直接在 perfetto.rc 中看到:socket traced_consumer stream 0666 root root与socket traced_producer stream 0666 root root两行由 init 建立 socket 后注入进程; - 在 Android 上以
nobody:nobody运行(见 perfetto.rc 的user nobody/group nobody),并受 SELinux 策略traced.te约束,几乎不允许做任何额外操作,仅保留SYS_NICEcapability(用于设置调度优先级); - 在 Chromium 中应作为 utility process 运行,借助 Chromium 自身的进程沙箱进一步隔离。
这套"无文件、无 socket、最小 capability"的组合,使得即使攻击者完全控制了traced,也无法落地持久化文件、无法外联网络、无法读写系统资源,攻击价值被压到极低。
四、Consumer:受信任但仍需防崩溃
Consumer 在安全模型中被始终信任:
- Chromium:信任路径通过 service manifest 建立,只有 manifest 中声明的服务能作为 consumer 连接;
- Android:信任路径通过 SELinux 将 consumer socket 锁定为仅
shell域可访问。
但"信任"不等于"放任":Consumer 依然不应能够崩溃或利用服务。原文档同时坦率地承认——Consumer 可以轻易对服务发起 DoS,这是预期行为(WAI, Working As Intended),因为 consumer 本身就是有特权的系统组件(如 shell、statsd),防 DoS 的优先级低于防崩溃和防提权。
在 tracing_service_impl.cc 中也可以看到针对 consumer 的配套限制,例如按 uid 统计并发 tracing session 数量:普通 uid 受kMaxConcurrentTracingSessionsPerUid限制,而AID_STATSD有单独更高的上限kMaxConcurrentTracingSessionsForStatsdUid(代码中consumer->uid_ == AID_STATSD分支),这同样体现了"consumer 受信任、但有配额约束"的设计。
五、共享内存隔离:数据只在点对点之间流动
Perfetto 采用共享内存(shared memory buffer)在 producer 与 service 之间传递 trace 数据,隔离原则非常严格:
- 内存只在"单个 producer ↔ tracing service"之间点对点共享;
- 绝不允许在不同 producer 之间共享内存——否则会泄漏属于不同 producer 的 trace 数据(比如恶意 producer 可能读到受保护进程的数据);
- 绝不允许在 producer 与 consumer 之间直接共享内存——那会在"不受信、无特权"的 producer 与"受信、更高特权"的 consumer 之间打开一条难以审计的数据通路。
也就是说,共享内存的拓扑是"星形"的:所有数据先汇聚到 service,由 service 统一仲裁、落盘/转发,producer 之间、producer 与 consumer 之间永远没有直接的内存通道。这保证了 trace 数据的横向隔离,也让 service 成为唯一的审计点。
六、Trace 内容的可信标注(Attestation):防伪造与离线审计
这是安全模型中最精细的部分,解决一个核心问题:trace 文件里到底哪些字段是可信的?答案是:凡是 Service 写入的TracePacket顶层字段,producer 都无法伪造。
6.1 校验器:PacketStreamValidator
Service 保证其写入的可信字段(如trusted_uid、trusted_packet_sequence_id、trusted_pid、machine_id等)不可能被 producer 伪造。机制是:producer 提交的每个TracePacket在进入 trace buffer 前,都要经过 PacketStreamValidator 校验,任何试图定义这些保留字段的数据包都会被拒绝——唯一的例外是 clock snapshot(时钟快照)相关的字段。
从源码看,kReservedFieldIds列出了被禁止的顶层字段 id(packet_stream_validator.cc):
const uint32_t kReservedFieldIds[] = { protos::pbzero::TracePacket::kTrustedUidFieldNumber, protos::pbzero::TracePacket::kTrustedPacketSequenceIdFieldNumber, protos::pbzero::TracePacket::kTraceConfigFieldNumber, protos::pbzero::TracePacket::kTraceStatsFieldNumber, protos::pbzero::TracePacket::kCompressedPacketsFieldNumber, protos::pbzero::TracePacket::kZstdCompressedPacketsFieldNumber, protos::pbzero::TracePacket::kSynchronizationMarkerFieldNumber, protos::pbzero::TracePacket::kTrustedPidFieldNumber, protos::pbzero::TracePacket::kMachineIdFieldNumber, protos::pbzero::TracePacket::kServiceEventFieldNumber, protos::pbzero::TracePacket::kTraceProvenanceFieldNumber, protos::pbzero::TracePacket::kProtovmsFieldNumber, };可以看到,trusted_uid、trusted_packet_sequence_id、trusted_pid、machine_id、service_event、trace_provenance、protovms等"只有 service 才能写"的字段全部在列。
6.2 校验实现:基于有限状态机的零拷贝流式解析
PacketStreamValidator::Validate的校验逻辑基于一个状态机ProtoFieldParserFSM,只解析顶层字段、不递归展开子消息(子消息按 length-delimited 整体跳过),从而在保证安全的前提下把性能开销压到最低。校验器逐字节扫描 producer 提交的切片(Slices,一个包可能横跨多个不连续的内存切片),检查四类问题:
- 数据包是否被截断(truncated);
- 数据包末尾是否有残留的悬空字节(dangling bytes);
- 是否存在被保留的可信字段(reserved / trusted fields);
- 字段是否格式非法(未知 wire type、超长的 length-delimited 消息、超过 64 位的 varint 等)。
FSM 的核心状态机设计(见 packet_stream_validator.cc 中的注释图)包括正常状态kFieldPreamble、kVarIntValue、kLenDelimitedLen与四个持久错误状态kWroteReservedField、kUnknownFieldType、kMessageTooBig、kInvalidVarInt。只有当 FSM 扫描完所有字节后回到kFieldPreamble且 varint 移位归零(valid()返回 true)且没有待跳过的字节时,包才被视为合法。该翻译单元对性能极其敏感,仓库还专门提供了基准测试 packet_stream_validator_benchmark.cc 与模糊测试 packet_stream_validator_fuzzer.cc 持续保障其正确性与吞吐。
6.3 单元测试印证
packet_stream_validator_unittest.cc 用 20 余个用例逐一验证了校验器的行为,是理解安全边界的最佳教材:
- 合法包通过:空包、简单包、复杂嵌套包(
ftrace_events+sched_switch)、跨切片分片的包(FragmentedPacket)均通过; - 可信字段一律拒绝:
trusted_uid、trusted_pid、machine_id(即使值为 0 或 -1)都导致校验失败,SimplePacketWithUid/SimplePacketWithPid等用例EXPECT_FALSE(...); - 服务专用字段拒绝:
service_event(flush_started)、trace_provenance、protovms同样被拒; - 畸形数据拒绝:截断包(
TruncatedPacket)与尾部垃圾字节(TrailingGarbage,如追加字符串"bike is short for bichael")都被判非法。
6.4 追加可信字段:防伪装的第二道保险
校验通过后,service 在 tracing_service_impl.cc 中执行一个关键步骤(ReadBuffers路径,约 L2794-L2833):
if (!PacketStreamValidator::Validate(packet.slices())) { tracing_session->invalid_packets++; PERFETTO_DLOG("Dropping invalid packet"); continue; } // Append a slice with the trusted field data. This can't be spoofed // because above we validated that the existing slices don't contain any // trusted fields. For added safety we append instead of prepending // because according to protobuf semantics, if the same field is // encountered multiple times the last instance takes priority. Slice slice = Slice::Allocate(32); protozero::StaticBuffered<protos::pbzero::TracePacket> trusted_packet(...); trusted_packet->set_trusted_uid(...); trusted_packet->set_trusted_packet_sequence_id(...); if (client_identity_trusted.has_pid()) trusted_packet->set_trusted_pid(...);这段代码体现了两个精细的设计决策:
- 追加而非前置(append not prepend):按照 protobuf 语义,同一字段多次出现时以最后一次为准。由于校验已保证原包不含可信字段,service 把可信字段追加在包尾部,就保证了最终生效的是 service 写入的值,producer 无法通过前置伪造字段来覆盖;
- 截断包也拒绝:producer 无法提交一个"半截字符串"的畸形包,再靠 service 追加可信字段后"拼成"一个看似合法的包——截断的包在校验阶段就被丢弃了。
6.5 局限与离线审计
原文档也坦诚说明了该模型的边界:目前没有任何机制阻止 producer 写入不属于它自身 data source 的TracePacket。例如一个 producer 完全可以伪造一段看起来像 ftrace 数据的包。Service 几乎不可能阻止这一点——要做到就必须让 service 了解所有可能的包类型,这不可扩展。
作为补偿,service 会给每个TracePacket追加POSIX uid(以及 packet sequence id、pid、machine id 等可信字段),从而支持对 trace 内容的离线审计(offline attestation):分析时可以根据trusted_uid判断每一段数据实际来自哪个进程,即使数据内容本身无法完全防伪,也能追溯责任方、识别异常来源。trusted_pid在部分平台不可用(注释明确说明 "Not supported on all platforms"),这也是"信任但验证"原则的体现。
七、从源码看安全模型的完整链路
把以上各部分串起来,一次 trace 数据的完整安全链路是:
- 连接建立:producer 连接
traced_producersocket,service 记录并校验其 uid(ConnectProducer中的 lockdown 检查); - 数据写入:producer 通过点对点共享内存写入自己的 trace 数据,与其它 producer 物理隔离;
- 数据校验:service 在读出数据时逐包运行
PacketStreamValidator,拒绝截断包、垃圾字节、保留字段、畸形字段(packet_stream_validator.cc); - 可信标注:校验通过的包由 service 追加
trusted_uid/trusted_packet_sequence_id/trusted_pid/machine_id等字段(tracing_service_impl.cc); - 消费输出:受信任的 consumer(shell / statsd / Chromium utility)通过 SELinux 或 manifest 锁定的 consumer socket 读取 trace,供分析工具做离线审计。
整个模型的设计哲学可以浓缩为三句话:
- 不信任 producer,所以所有输入在服务端统一校验;
- 约束服务自身,所以 syscall 面最小、权限最小、被攻破也无利可图;
- 信任但要验证,所以 consumer 受信任,但 trace 内容的来源通过服务追加的可信字段得以离线审计。
参考与深入阅读
- 安全模型设计文档:docs/design-docs/security-model.md
- 数据包校验器实现:src/tracing/service/packet_stream_validator.cc 与头文件 src/tracing/service/packet_stream_validator.h
- 校验器单元测试:src/tracing/service/packet_stream_validator_unittest.cc
- 校验器基准与模糊测试:packet_stream_validator_benchmark.cc、packet_stream_validator_fuzzer.cc
- tracing 服务主体实现(连接认证、会话配额、可信字段追加):src/tracing/service/tracing_service_impl.cc
- Android 上
traced的 init 服务声明与 socket 定义:perfetto.rc - 服务端架构总览:docs/design-docs/trace-processor-architecture.md、docs/concepts/service-model.md
【免费下载链接】perfettoProduction-grade client-side tracing, profiling, and analysis for complex software systems.项目地址: https://gitcode.com/GitHub_Trending/pe/perfetto
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考