结论:崩溃采集不是「一律上报」,而是一条跨进程链路,且至少被三道门约束:报告门(构建来源为官方 origin、本地已注册、非 PC 三者同时成立)、扫描过滤门(只接受精确 mode 0o100640 的 .crash,超过 100 MB 直接丢弃)、上传门(计费网络暂缓最近 12 小时的 crash/、HTTP 401/403/412 不再重试)。要特别记住两点:100 MB 阈值不是隐私过滤,完整 crash 副本可能含路径、进程环境与内存相关信息;而「已进入本地处理」也不等于云端已保存。
入口:报告门只在进程启动时采样一次
在固定源码快照里,tombstoned 被配置成非 PC 的常驻进程。它的 main() 启动时计算一次 should_report,要求构建元数据的官方 origin、is_registered_device() 与「非 PC」三项都成立。这里的「已注册」只依据本地 DongleId 非空且不为 UnregisteredDevice,不是每次崩溃都向后端重新鉴权。
两个时序后果必须分开看。第一,should_report 不在循环中刷新,运行期间身份或来源发生变化不会立即改变本实例的处理门。第二,启动时 clear_apport_folder() 会遍历 /var/crash/* 尝试删除,之后才建立初始快照——这意味着重启前尚未处理的 crash 文件可能直接丢失,源文档给出的动机是避免重复报告。
扫描与本地处理:三道门各自不管什么
采集循环每五秒用 get_tombstones() 扫描至多 1000 个目录项,用「路径 + ctime」的集合差异找出新条目。下面这张表把各道门的条件和边界摊开——每一道的「不管什么」比它「管什么」更容易被误读。
| 门 / 环节 | 条件 | 边界与后果 |
|---|---|---|
| 报告门 | 官方 origin 且 is_registered_device() 且非 PC,仅在启动时计算一次 |
运行中改变返回值不会改变本实例的删除决策 |
| 文件模式门 | .crash 必须是精确 mode 0o100640 |
名字以 tombstone 开头的条目不受该 mode 门约束,但报告门打开时只记录「unknown crash type」,不进入解析与复制 |
| 大小门 | 超过 100 MB 记错后返回 | 该分支不复制也不删除原文件;大小阈值不是隐私过滤 |
| 目录常量 | TOMBSTONE_DIR="/data/tombstones/" 已定义 |
该常量未被 get_tombstones() 使用,实际扫描的是 /var/crash/;不能据常量名推断 Android tombstone 也被处理 |
解析与落盘阶段的关键行为见下表。摘要与完整副本走的是两条路径,清洗只作用于摘要,不改写完整文件。
| 阶段 | 源码行为 |
|---|---|
| 摘要 | 提取 ExecutablePath、Signal,经 apport-retrace -s 获取栈,优先找 openpilot/selfdrive 栈帧;去掉形似指针的参数并剥除圆括号内容后写 cloudlog.error。外部命令设 30 秒超时 |
| 完整报告 | 用日期、提交哈希与清洗后路径组成文件名,复制完整原始 crash 文件到 Paths.log_root()/crash/,随后尝试删除 /var/crash 原件 |
| 未启用报告 | 对新检测到的条目尝试删除;不会保留「本地可查但不上传」的独立模式 |
| 处理异常 | 异常写 cloudlog,下一轮仍把当前集合设为基线;未变化的同一文件通常不会因本次失败自动重试 |
需要强调的是:摘要去掉指针、剥掉圆括号内容,只是让日志更短,并不等于对 dump 做了脱敏审计。完整 crash 可能包含路径、进程环境和内存相关信息,不能因为 cloudlog 摘要删了指针就把副本视为匿名数据。
上游处理与下游上传是两段独立判断
Uploader 把 crash/ 放进 immediate_folders,next_file_to_upload() 会优先选该目录的未标记文件。但「优先」仍受网络与计费策略约束:在计费连接上,最近 12 小时的 crash/ 与 boot/ 文件会暂缓;无网络时主循环不上传。因此 tombstoned 的「reporting」日志只表示进入本地处理,不代表云端已收到文件。
Uploader.upload() 把 HTTP 200/201/401/403/412 都列为停止重试的状态,并尝试写 user.upload xattr。其中 401/403 或 412 的情形要单独记忆:处理完成不等于服务端保存成功。回收侧 deleter_step() 在空间不足时优先删普通目录,把 boot/crash 列为最后删除,但这不是永久保留——存储紧张时崩溃副本最终仍可能被删掉。
对自制硬件与插件平台的要求
源文档给出的重构要求可以归纳为一组「必须分开授权」的清单:
- 崩溃诊断插件默认只读、驻车或后台运行;原始 dump 的收集、读取和外传分别授权,并提供本地留存但禁止上传的隐私模式,避免把「是否官方 origin」当成唯一隐私开关。
- 在受信任核心记录采集决策与数据流:设备身份状态、用户/车队同意、文件分类、脱敏结果、上传结果与删除原因。不要把文件名清洗或摘要去指针等同于 dump 脱敏。
- 重构时把「启动清空、扫描上限、失败后不自动重试、上报门固定不变」列为可复现的行为差异,并用模拟 crash、权限错误、断电、磁盘满和计费网络来测试新策略。
- 崩溃报告管线不应向
carControl、sendcan或 panda 安全模式写入;诊断过载或上传阻塞不得影响实时控制与驾驶员告警。
此外要承认覆盖局限:system/tombstoned.py 没有就近的专门单元测试,工作区六项隔离探针只覆盖扫描主循环与本地文件处理。1000 条目录项上限、启动残留清理与实际文件竞争仍只有静态证据,具体 crash 内容与服务端保留策略仍需在目标硬件和服务协议上验证。
常见问题
100 MB 的文件大小上限是隐私过滤吗?
不是。源文档明确指出超过 100 MB 的文件只是记错后返回,该分支不复制也不删除原文件,并特别标注「大小阈值不是隐私过滤」。完整 crash 副本可能包含路径、进程环境或内存相关信息,不能因为摘要清洗了指针就把副本当匿名数据。
为什么「本地已上报」不等于云端收到了文件?
因为本地与远端是两段独立判断。tombstoned 的 reporting 日志只表示进入本地处理;上传侧还受网络与计费策略约束,计费连接上最近 12 小时的 crash 文件会暂缓、无网络时不上传,而 401/403 或 412 属于停止重试状态,处理完成不等于服务端保存成功。
重启后没处理的崩溃文件会保留吗?
可能不会。启动时 clear_apport_folder() 会遍历 /var/crash/* 尝试删除,之后才建立初始快照,源文档的动机是避免重复报告。这意味着重启前尚未处理的 crash 文件可能直接丢失。
崩溃报告会影响实时控制或驾驶员告警吗?
按源文档的重构要求不应影响。管线不应向 carControl、sendcan 或 panda 安全模式写入,诊断过载或上传阻塞也不得干扰实时控制与驾驶员告警。但这些是重构要求,不是当前实现已验证的隔离保证。
延伸阅读
- Athena 诊断日志转发:扫描、确认与重发 —— 同属日志外传链,解释本地「标记已发」与远端确认的落差。
- Athena 上传队列的持久化与重试 —— 承接上传队列在网络与中断条件下的行为。
- loggerd 合成回放的隐私门 —— 从录制侧看同一批隐私门如何被固定。
- 上传器的选择结果与退避 —— 解释「优先目录」为何仍受计费与网络约束。
- UDS 诊断工具为什么不能随手调 —— 诊断类工具的只读与授权边界可作对照。
参考资料
- 项目 02 ——
openpilot-research/CRASH_COLLECTION_PRIVACY_DEEP_DIVE.md(取用了崩溃采集的报告门、扫描与大小过滤、完整副本复制、上传与回收语义及插件化要求)。
说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。
更新记录
首次发布 2026-10-11 最近核对:2026-10-11
发表回复