先看结论与判断条件
- 问题单必须以实际 APK 或 AAB 的 SHA-256 固定候选,并同时记录加固配置摘要;版本号、文件名、截图和口头描述都不能唯一识别诊断对象。
- 最小复现要写清前置状态、逐步操作、首次异常点、期望行为、实际行为和复现频率,并提供未加固对照结果,不能只提交一句“加固后闪退”。
- 设备信息应覆盖型号、系统版本、ABI、locale、方向、安装来源和账号或权限前置状态,矩阵中的每个结果都要绑定同一候选与同一场景。
- 进程退出、ANR、Java 崩溃和 Native 崩溃需要不同诊断材料;ApplicationExitInfo、mapping、tombstone 与未剥离符号不能互相替代。
- Java mapping 和 Native 符号必须来自同一构建并与候选摘要关联;符号化成功只是提高堆栈可读性,不证明根因或修复方案正确。
- 提交前应使用白名单信息包清单,限制文件类型和大小,拒绝私钥、令牌、账号会话、客户数据与可重放材料,并由材料所有者做人工脱敏复核。
一份可处理的兼容问题单必须回答八个具体问题
高质量问题单不是堆满日志,而是让接收方能够确认诊断对象、复现异常并排除候选错配。提交前应回答:哪个候选出问题、对应哪份加固配置、未加固对照是否正常、怎样稳定复现、期望与实际差异是什么、在哪个环境发生、进程怎样退出,以及有哪些与同一构建对应的诊断材料。缺少其中一项,就应明确标为待补,而不是猜测。
问题标题应包含可搜索的症状、发生阶段和最小环境,例如“Android 14 arm64 冷启动后进入支付页触发 Native 退出”,比“加固失败”“兼容性不好”更能帮助分流。正文第一屏写候选摘要、首次异常步骤、复现频率和业务影响;完整环境、退出记录、堆栈、附件摘要和脱敏说明放在结构化字段中,避免关键信息埋在聊天记录。
材料齐全仍不等于根因已经确定。兼容异常可能来自应用原有缺陷、加固配置、构建漂移、第三方 SDK、系统行为、设备厂商差异、网络服务或账号状态。问题单的目标是建立可证伪的调查起点,让每个判断都能回到同一候选和同一复现路径,而不是在收到附件时承诺一定修复。
| 信息项 | 最低内容 | 证据形式 | 缺失后的影响 |
|---|---|---|---|
| 候选身份 | 文件 SHA-256 与构建标识 | 本地重算记录 | 无法确认诊断对象 |
| 加固配置 | 工具版本与配置摘要 | 配置导出或规范化摘要 | 无法复现转换条件 |
| 对照基线 | 未加固包同场景结果 | 同设备测试回执 | 无法判断是否由加固引入 |
| 复现步骤 | 前置状态与逐步操作 | 文字步骤或脱敏录屏 | 无法稳定触发 |
| 环境 | 型号、OS、ABI 与 locale | 设备信息记录 | 无法建立覆盖范围 |
| 退出与堆栈 | 原因、时间窗和相应材料 | 退出记录、trace 或 tombstone | 无法选择诊断路径 |
| 期望和实际 | 首个差异点 | 可观察结果 | 无法判断通过标准 |
| 脱敏声明 | 已排除凭据和客户数据 | 材料负责人复核 | 附件不能安全流转 |
先固定候选身份,再比较加固前后的对照结果
每个问题候选都应在本地重新计算 SHA-256,并记录包类型、应用 ID、版本、构建号、ABI 范围、签名证书公开指纹、生成时间和加固配置摘要。SHA-256 是文件身份主键,其他字段用于解释它。若重新签名、重新打包、替换资源或重新运行加固,即使文件名和版本不变,也必须建立新候选记录。
对照实验至少包含同一设备、同一账号状态、同一数据准备和同一操作路径下的未加固包与问题包。若未加固包也失败,问题仍值得调查,但不能先归因于加固;若只有加固包失败,也只能说明差异与转换相关,不能直接断言某项保护规则就是根因。进一步定位需要逐项缩小配置、依赖和运行条件。
不要在问题单中上传签名私钥、证书密码、生产令牌或可重放账号会话。需要确认签名差异时,可提供公开证书信息、签名检查输出和候选摘要;需要确认配置差异时,可提供去除秘密后的规范化配置或其摘要。调查人员若无法用这些材料区分候选,应先补身份信息,而不是索取整套生产凭据。
| 字段 | 问题包 | 对照包 | 比较规则 | 边界 |
|---|---|---|---|---|
| SHA-256 | 实际文件重算 | 实际文件重算 | 必须不同且各自固定 | 只证明字节身份 |
| 版本与构建号 | 清单实际值 | 清单实际值 | 预期应一致 | 不能替代摘要 |
| 签名公开信息 | 证书指纹与方案 | 证书指纹与方案 | 记录预期差异 | 不提交私钥 |
| 配置摘要 | 加固配置规范化摘要 | 未加固标记 | 绑定到问题候选 | 不证明配置正确 |
| 测试前置状态 | 设备、账号与数据 | 同一条件 | 逐项对齐 | 外部服务仍可能漂移 |
把“闪退”改写成可以重复执行的最小复现
最小复现从前置状态开始:应用是全新安装还是覆盖安装,是否首次启动,是否已授予权限,是否登录,是否有历史缓存,网络是否可用,服务端开关是什么。随后按编号记录每一次点击、输入、页面跳转和等待条件,并指出首次偏离期望的步骤。视频可以辅助理解,但不能替代文字步骤和候选摘要。
期望与实际必须是可观察行为。期望可以是“支付页完成初始化并显示订单确认按钮”,实际可以是“进入页面后 800 毫秒内进程退出,系统记录 REASON_CRASH_NATIVE”;不要写“应该正常”“感觉卡顿”。若问题是性能或无响应,应给出测量窗口、采样方法和阈值来源,不能凭主观感受下结论。
复现频率要写分子和分母,并说明是否在清理数据、重启设备或切换网络后变化。例如同一候选连续十次冷启动中三次失败,比“偶现”更可复核。小样本不能推导总体故障率,但能帮助选择日志窗口和缩小前置条件。若没有稳定复现,应保留失败发生时的精确时间和环境,而不是补写不存在的步骤。
| 阶段 | 应记录内容 | 合格示例 | 常见无效写法 |
|---|---|---|---|
| 前置状态 | 安装、权限、账号、缓存与网络 | 全新安装且未登录 | 环境正常 |
| 操作步骤 | 按时间顺序编号 | 启动后进入订单并点击确认 | 随便操作就闪退 |
| 首次差异 | 首个可观察异常 | 按钮点击后进程退出 | 加固不兼容 |
| 期望结果 | 业务可验收状态 | 确认页显示订单号 | 应该没问题 |
| 复现频率 | 失败次数与总次数 | 冷启动 3/10 | 偶尔发生 |
设备矩阵要服务真实覆盖范围,不是随意罗列机型
设备环境至少包含制造商和型号、系统版本与安全补丁、ABI、屏幕方向、locale、安装来源以及是否存在厂商定制行为。对于仅在覆盖安装、低存储、特定权限或后台恢复时出现的问题,还要记录这些状态。只写“Android 14”不足以解释不同 ABI、设备实现和应用状态带来的差异。
Firebase Test Lab Android matrices 将一次矩阵执行拆成设备型号、系统版本、方向和 locale 等维度,任一执行失败会影响对矩阵的判断。设计问题复现矩阵时应先覆盖真实用户集中环境、最低支持版本、已知高风险 ABI 和问题发生设备,再加入少量对照点;扩大数量不能弥补候选错配或场景不一致。
矩阵报告必须给每个单元格绑定候选摘要、场景版本、开始时间、结果类别和可用附件。云真机通过不表示所有实体设备兼容,单台真机失败也不能代表整个系统版本失败。遇到差异时,应优先比较 ABI、厂商系统、权限状态、安装方式和退出原因,形成下一轮最小矩阵,而不是无边界地增加机型。
| 矩阵维度 | 首轮选择依据 | 结果记录 | 不能推出的结论 |
|---|---|---|---|
| 设备型号 | 真实发生设备与代表性对照 | 制造商、型号和硬件信息 | 一个型号代表全部厂商 |
| 系统版本 | 最低支持、主流和问题版本 | 版本与安全补丁 | 同大版本行为完全一致 |
| ABI | 包内实际支持和故障 ABI | 运行 ABI 与库清单 | Java 正常即可排除 Native |
| locale 与方向 | 真实场景和资源风险 | 具体值与切换步骤 | 默认语言通过即可全覆盖 |
| 安装与状态 | 新装、覆盖、恢复或后台路径 | 前置状态和数据策略 | 单一路径代表全部生命周期 |
根据退出类型选择 ApplicationExitInfo、ANR trace 或崩溃材料
Android ApplicationExitInfo 可以提供历史进程退出原因,并在相应平台版本中关联 ANR trace,新版本还可返回 Native tombstone。反馈时应记录异常发生的设备时间、应用进程、候选版本和用户操作,再导出与该时间窗对应的退出记录。只提交一段无时间信息的 logcat,容易把旧进程、其他包或后续重启混入调查。
退出原因用于选择调查分支,不是根因结论。Java 未捕获异常应配合同构建 mapping 处理混淆堆栈;Native 崩溃需要 tombstone、ABI、加载库和未剥离符号;ANR 需要 trace、主线程状态、锁等待和用户路径;低内存、权限或系统终止则要结合系统状态。不要把所有退出统一写成“闪退”。
采集命令和系统接口受 Android 版本、权限、设备策略与日志保留窗口影响。拿不到某项材料时应明确写“未获得”、说明尝试方式和替代证据,不能生成占位堆栈。若日志包含账号、请求参数、设备标识或业务数据,先由材料所有者脱敏并保留原始材料的受控位置,普通问题包只放必要片段。
| 症状类型 | 首要材料 | 关联字段 | 常见误判 |
|---|---|---|---|
| Java 崩溃 | 异常堆栈与同构建 mapping | 候选摘要、版本和线程 | 把混淆名称当根因 |
| Native 崩溃 | tombstone 与未剥离符号 | ABI、Build ID 和加载库 | 符号化后即宣称修复 |
| ANR | ANR trace 与用户路径 | 发生时间、进程和主线程状态 | 只截取最后一行日志 |
| 系统终止 | ApplicationExitInfo 与系统状态 | 退出原因、重要性和时间窗 | 统一归为应用崩溃 |
| 功能异常未退出 | 业务状态与受控日志 | 期望、实际和前置数据 | 用崩溃工具替代业务复现 |
Java mapping 与 Native 符号必须来自同一构建并单独归档
R8 retrace 要使用产生问题构建的 mapping 文件还原混淆后的 Java 或 Kotlin 堆栈。mapping 来自另一个构建时,即使版本号相同,也可能得到错误名称或无法还原。问题包应记录 mapping 文件摘要、对应候选摘要、构建变体和工具版本;文件本身若受访问控制,可先提交元数据并通过授权渠道提供。
ndk-stack 处理 Native 崩溃时需要未剥离符号目录与同一构建地址信息。ABI、共享库名称、Build ID 和 tombstone 地址必须能够对应。Include native symbols 说明发布构建可生成独立 Native 调试符号文件并用于平台诊断,但归档责任仍在项目:每个符号包要绑定版本、ABI、候选摘要和保留周期。
符号化只是把地址转换成更可读的函数和源码位置,不证明该行就是根因,也不排除内存破坏在更早位置发生。加固可能改变代码布局,因此尤其不能拿未加固构建符号处理加固后地址。调查结论应区分“堆栈已还原”“可疑调用路径”“根因已由实验验证”和“修复已在同一范围回归”四个证据等级。
| 材料 | 必须绑定 | 适用问题 | 不适用范围 | 验收动作 |
|---|---|---|---|---|
| R8 mapping | 同构建候选摘要与变体 | Java/Kotlin 混淆堆栈 | Native 地址 | 保存摘要并执行 retrace |
| Native symbols | ABI、SO、Build ID 与候选摘要 | Native 地址符号化 | Java 混淆名称 | 校验身份后执行 ndk-stack |
| tombstone | 设备、时间、进程和 ABI | Native 崩溃现场 | 一般业务逻辑异常 | 与符号和候选配对 |
| 符号化输出 | 工具版本与输入摘要 | 提高堆栈可读性 | 自动证明根因 | 保留命令和非零错误 |
用白名单、大小限制和人工复核保护客户秘密
问题包只应包含诊断所需材料。优先使用白名单标签,例如已脱敏日志、退出信息、Java 堆栈、Native tombstone、复现步骤和映射符号元数据;明确拒绝 `.env`、私钥、证书容器、keystore、数据库导出、生产配置、完整网络抓包和账号会话。文件名检查只能挡住明显错误,内容仍需负责人逐项复核。
脱敏不能破坏诊断关联。可以将用户 ID、订单号、设备标识和 URL 参数替换成稳定占位符,使同一实体在一份材料中保持一致;不能删掉时间戳、线程、异常类型、库名、ABI 或关键调用顺序。原始材料若因审计需要保留,应放在授权存储中并限制访问,普通问题单只引用受控记录号,不复制原文。
NIST SP 800-218 SSDF 支持在安全开发流程中保留来源、构建、验证与变更证据,并处理供应链风险。对应到问题反馈,应指定材料所有者、脱敏复核者、访问范围、保留期限和删除责任。组织级框架不替某个项目决定哪些字段属于秘密,最终仍要遵守合同、隐私政策和内部数据分级。
| 材料类别 | 默认处理 | 允许内容 | 必须排除 | 复核责任 |
|---|---|---|---|---|
| 日志与 trace | 脱敏后纳入 | 时间、线程、异常和必要上下文 | 令牌、账号和业务载荷 | 日志所有者 |
| 候选元数据 | 纳入 | 摘要、版本、ABI 和公开签名信息 | 私钥与密码 | 发布负责人 |
| mapping 与 symbols | 授权提供 | 摘要、Build ID 和必要文件 | 无关源码与秘密配置 | 构建负责人 |
| 复现材料 | 纳入 | 步骤、期望、实际和脱敏录屏 | 真实客户数据 | 问题提交人 |
| 原始现场 | 受控留存 | 内部记录号 | 直接复制到普通工单 | 数据负责人 |
用只读校验器生成安全的兼容问题信息包清单
下面的 Python 示例读取归档根目录和 `compatibility-report.json`。清单声明候选、对照候选、环境、复现步骤和附件。脚本限制附件标签、扩展名、单文件大小与总大小,拒绝绝对路径、目录逃逸、符号链接和常见敏感文件名,重新计算所有附件与候选 SHA-256,然后输出可提交文件清单。
脚本不会自动上传材料,也不会读取签名私钥或调用调试接口。文件名和扩展名过滤不能证明内容已经脱敏,因此清单还要求 `humanRedactionReviewed` 为真,并把其视为人工责任声明。真实提交前仍应打开每个文件复核账号、令牌、客户数据、内网地址和可重放请求,必要时仅提交受控记录号。
向御盾反馈兼容问题时,可准备候选与对照包摘要、加固配置摘要、最小复现、期望与实际结果、设备矩阵、ApplicationExitInfo、同构建 mapping 或 Native 符号元数据和脱敏附件,再通过御盾中央平台提交申请。完整信息包能缩短确认往返,但不构成定位、修复或兼容通过承诺。
- 问题候选与未加固对照候选分别重算 SHA-256
- 设备型号、系统版本、ABI 和 locale 均已填写
- 至少三步可执行复现且包含前置状态
- 附件只使用明确白名单标签
- 敏感文件名、危险扩展名和符号链接被拒绝
- 单文件与信息包总大小受到限制
- 人工脱敏复核声明为真且仍需打开文件复查
- 清单通过不冒充根因、修复或兼容结论
from pathlib import Path
import hashlib
import json
import os
import re
import sys
if len(sys.argv) != 3:
raise SystemExit(2)
root = Path(sys.argv[1]).resolve()
manifest_path = Path(sys.argv[2]).resolve()
if not root.is_dir() or not manifest_path.is_file() or root not in manifest_path.parents:
raise SystemExit(2)
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
if manifest.get("schema") != "hardening-compatibility-report-v1":
raise SystemExit(2)
def safe_file(relative_path):
if not isinstance(relative_path, str) or not relative_path or os.path.isabs(relative_path):
raise SystemExit(2)
unresolved = root / relative_path
resolved = unresolved.resolve()
if root not in resolved.parents or unresolved.is_symlink() or not resolved.is_file():
raise SystemExit(2)
if re.search(r"(^|[._-])(secret|token|password|private|keystore|credential)([._-]|$)", unresolved.name, re.I):
raise SystemExit(4)
if unresolved.suffix.lower() in {".env", ".key", ".pem", ".p12", ".jks", ".keystore", ".db", ".sqlite"}:
raise SystemExit(4)
return resolved
def digest(path):
value = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
value.update(chunk)
return value.hexdigest()
def verify_record(record, max_bytes):
if not isinstance(record, dict) or not re.fullmatch(r"[0-9a-f]{64}", str(record.get("sha256", ""))):
raise SystemExit(2)
path = safe_file(record.get("path"))
if path.stat().st_size > max_bytes or digest(path) != record["sha256"]:
raise SystemExit(3)
return {"path": record["path"], "sha256": record["sha256"], "bytes": path.stat().st_size}
candidate = verify_record(manifest.get("candidate"), 2 * 1024 * 1024 * 1024)
baseline = verify_record(manifest.get("baselineCandidate"), 2 * 1024 * 1024 * 1024)
if candidate["sha256"] == baseline["sha256"]:
raise SystemExit(2)
environment = manifest.get("environment")
if not isinstance(environment, dict) or not all(str(environment.get(k, "")).strip() for k in ("model", "osVersion", "abi", "locale")):
raise SystemExit(2)
steps = manifest.get("reproductionSteps")
if not isinstance(steps, list) or len(steps) < 3 or any(not isinstance(s, str) or len(s.strip()) < 8 for s in steps):
raise SystemExit(2)
if manifest.get("humanRedactionReviewed") is not True:
raise SystemExit(4)
allowed_labels = {"sanitized_log", "exit_info", "java_stack", "native_tombstone", "reproduction_steps", "symbol_manifest"}
attachments = manifest.get("attachments")
if not isinstance(attachments, list) or not attachments:
raise SystemExit(2)
package = []
total_bytes = 0
for item in attachments:
if not isinstance(item, dict) or item.get("label") not in allowed_labels:
raise SystemExit(2)
checked = verify_record(item, 25 * 1024 * 1024)
checked["label"] = item["label"]
total_bytes += checked["bytes"]
package.append(checked)
if total_bytes > 100 * 1024 * 1024:
raise SystemExit(3)
report = {"status": "compatibility-package-list-valid", "candidateSha256": candidate["sha256"], "baselineSha256": baseline["sha256"], "environment": environment, "attachments": package, "boundary": "name and manifest checks do not prove content redaction, root cause, fix, or compatibility"}
print(json.dumps(report, ensure_ascii=False, indent=2))事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| ApplicationExitInfo 可提供进程退出原因,并在相应平台版本中关联 ANR trace 或 Native tombstone。 | Android ApplicationExitInfo 官方参考定义了历史进程退出信息和可用 trace 接口,可支持按退出类型选择材料。 | 退出记录仍需与同一候选、进程、设备时间窗和用户路径关联,退出原因枚举本身不是根因结论。 |
| 混淆后的 Java 或 Kotlin 堆栈需要与同一构建产生的 mapping 配对还原。 | R8 retrace 说明了使用 mapping 文件对混淆堆栈执行 retrace 的方法,可支持问题包记录 mapping 摘要和构建身份。 | retrace 不处理 Native 地址,不能纠正错误候选身份,也不能因名称还原成功就宣布根因已经定位。 |
| Native 崩溃符号化需要未剥离符号目录和对应构建的地址信息。 | ndk-stack 说明了使用符号目录处理 Native 崩溃输出的方式,可支持 ABI、共享库和符号归档配对要求。 | 符号化成功只提高堆栈可读性,不证明首个可见帧就是根因,也不替代同一候选上的实验验证。 |
| Android 发布构建可以生成独立 Native 调试符号文件并用于平台诊断。 | Include native symbols 说明了为发布构建包含或生成 Native 调试符号的官方能力,可支持预先归档诊断材料。 | 符号文件仍要另外绑定候选摘要、版本、ABI 和 Build ID;生成或上传成功不证明后续问题一定可定位。 |
| Android 云测试矩阵由设备型号、系统版本、方向和 locale 等维度组成,单次执行结果会影响矩阵判断。 | Firebase Test Lab Android matrices 的入门资料说明了 Android 测试矩阵选择与执行,可支持结构化记录环境。 | 云设备矩阵不能替代真实用户分布和最低支持范围,也不能从有限通过样本推导所有设备兼容。 |
| 安全发布与问题处置应保留来源、构建、验证、变更和供应链风险相关证据。 | NIST SP 800-218 SSDF 提供组织级安全软件开发实践,可支持为问题材料指定所有者、复核和保留责任。 | SSDF 不定义御盾产品功能、兼容通过标准或具体数据分级规则,项目结论仍依赖真实候选和授权测试。 |
| 加固兼容问题应同时提供问题候选和未加固对照候选的独立摘要。 | 这是基于控制变量和文件身份的工程判断:没有对照就难以判断异常是否已存在,没有摘要就无法确认比较对象。 | 对照差异只能建立调查方向,不能单独证明加固是根因;服务端、账号、设备状态和依赖仍可能同时变化。 |
| 文件名与扩展名过滤不能保证附件内容已经完成脱敏。 | 这是基于数据泄露面的工程判断:敏感值可以出现在普通文本、堆栈、URL、截图或合法扩展名文件中,因此需要人工复核。 | 示例校验器只拒绝明显危险材料并验证清单,不能识别所有个人信息、商业秘密、内网信息或可重放凭据。 |
工程常见问题
只提供崩溃截图可以反馈加固兼容问题吗?
截图可以说明表面症状,但不能唯一识别候选、退出类型和复现环境。至少还要提供问题包 SHA-256、未加固对照结果、复现步骤、设备与系统信息、发生时间和相应 trace 或堆栈。截图含账号或业务数据时也必须先脱敏。
未加固包也会失败,还需要提交给御盾吗?
可以提交,但要明确对照包在同一设备、状态和步骤下的结果。未加固包也失败意味着不能先归因于加固,却可能帮助识别原有缺陷、依赖问题或环境条件。问题单应提供两个候选摘要和差异,不应隐藏对照失败。
Java 崩溃和 Native 崩溃需要提交同一套材料吗?
不完全相同。Java 或 Kotlin 混淆堆栈需要同一构建的 R8 mapping;Native 崩溃需要 tombstone、ABI、共享库、Build ID 和未剥离符号。两类问题都需要候选摘要、环境、复现步骤和时间窗,但 mapping 与 Native symbols 不能互换。
符号化成功是否表示兼容问题根因已经找到?
不是。符号化把地址或混淆名称转换为更可读位置,帮助形成假设,但内存破坏、竞态或状态错误可能发生得更早。根因需要由控制变量、代码检查或实验验证,修复还要针对同一候选范围完成回归。
问题附件中能否包含生产 token、证书或完整数据库?
不能默认提交。私钥、证书密码、生产 token、账号会话、完整数据库和无关客户数据应排除。需要说明身份或请求时,优先使用公开指纹、稳定占位符、受控记录号和最小日志片段,并由材料所有者完成脱敏与授权复核。
向御盾提交兼容问题前最少应整理什么?
整理问题候选和未加固对照候选摘要、加固配置摘要、三步以上复现、期望与实际、设备环境、发生时间、退出类型、同构建 mapping 或 Native 符号元数据,以及经过人工脱敏的必要附件,再通过御盾中央平台提交申请。