先看结论与判断条件
- 依赖声明不是最终输入,必须冻结目标 release configuration 的解析组件、artifact 摘要、variant 属性与选择原因。
- 方法身份使用 owner、name、descriptor、访问标记、代码摘要和关键调用边,不能只比较源码展示名。
- 每条 VMP 规则必须声明期望方法和命中数;工具退出码为零但规则零命中仍然是发布失败。
- 删除、迁移、签名变化、实现替换与新增候选分别处理,自动迁移只能生成待审候选规则。
- 先解决传递依赖冲突和重复类,再评估 VMP 命中;宽泛通配会掩盖来源与扩大兼容风险。
- 静态漂移检查不证明 VMP 已执行或运行兼容,仍需最终候选核对、验签、SDK 独立审计与设备测试。
直接答案:同时比较依赖图、最终方法索引、R8 映射和规则命中结果
依赖升级后的 VMP 规则漂移不能靠构建成功判断。门禁应冻结升级前后同一 release 变体的 resolved dependency graph、最终 APK 方法索引、R8 mapping、VMP 规则展开结果和调用路径,再逐条确认旧规则仍命中同一业务语义;零命中、签名变化、owner 替换或命中数异常都阻断发布。
规则文本没有变化不代表保护对象没有变化。依赖可重命名类、迁移包、改变参数或返回类型、把实现改成生成代码,R8 还可能内联、合并或删除方法。若规则只按模糊名称匹配,它可能继续返回成功,却命中另一个 adapter;若工具忽略未命中规则,则关键方法可能完全失去保护。
审计目标不是让命中数量机械保持不变,而是确认受保护的业务责任仍有唯一、稳定且可回归的实现。依赖升级还会带来行为、权限和数据处理变化,VMP 只能处理选定客户端实现的分析成本,不能替代服务端授权、SDK 安全评审或真实设备兼容测试。
| 信号 | 可能原因 | 默认处置 | 需要证据 |
|---|---|---|---|
| 旧规则零命中 | 类或方法删除 | 阻断 | 最终方法索引 |
| 描述符变化 | 参数或返回类型变更 | 阻断 | 前后签名 |
| owner 变化 | 包迁移或重定位 | 人工确认 | 依赖来源 |
| 命中数增加 | 通配扩张 | 阻断 | 展开清单 |
| 调用目标变化 | adapter 或实现替换 | 回归 | 调用边 |
| 规则成功但对象消失 | 工具忽略未命中 | 阻断 | 逐规则回执 |
先冻结同一 release 变体的解析依赖图
Gradle 解析的是直接与传递依赖组成的图,不是 build.gradle 中几行声明。平台、约束、BOM、严格版本、替换和 conflict resolution 都会改变最终选择。漂移比较必须保存 releaseRuntimeClasspath 等目标 configuration 的 component、版本、variant 属性、artifact 摘要和选择原因。
不能拿 debug 依赖图、IDE External Libraries 或版本目录文本代替 release 解析结果。debug 可能多出调试工具、使用不同 flavor 依赖并跳过发布专属插件。门禁需要与最终候选的 build ID、applicationId、variantName、R8 配置和签名流程绑定,避免证据错配。
依赖锁定与 verification metadata 能提高输入稳定性,但不会自动证明 API 或运行语义未变。升级批准应记录为什么变、更改了哪些 artifact、传递图新增和删除什么、是否出现动态版本或仓库来源变化。来源不明或摘要漂移时先停止,而不是直接调整 VMP 通配规则。
| 证据 | 最小字段 | 漂移用途 | 局限 |
|---|---|---|---|
| 声明依赖 | group/name/request | 解释意图 | 非最终选择 |
| 解析组件 | selected version/reason | 发现版本替换 | 不含字节 |
| artifact 清单 | 文件 SHA-256 | 绑定输入 | 不含方法 |
| variant 属性 | usage/flavor/buildType | 避免错图 | 不含运行结果 |
| 锁文件 | 版本集合 | 可复现 | 不证明兼容 |
| 仓库来源 | repository 与校验 | 供应链审计 | 不证明业务语义 |
最终方法索引要以 owner、name、descriptor 和代码摘要为身份
VMP 规则通常最终落到 DEX 方法。稳定身份至少包含类描述符 owner、方法名、参数与返回类型 descriptor、访问标记和所在 DEX;为识别同签名实现替换,还可保存规范化指令摘要、调用目标集合和来源 artifact。只保存 Java 展示名会混淆重载、桥接与合成方法。
索引必须从加固前的最终发布候选生成,而不是从源码或单个依赖 JAR 推断。R8 会改名、内联、合并、移除和优化控制流;Kotlin、Compose、serialization 与 desugaring 还会生成额外方法。规则若使用 mapping 前身份,应保存到 mapping 后方法的确定映射和未映射原因。
升级前后比较时区分身份变化与语义变化。owner 搬迁但代码和调用责任一致,可能通过规则迁移解决;签名相同但指令摘要与调用目标大变,仍需重新评审。任何自动迁移只能生成候选规则,不能未经人工确认直接进入生产范围。
| 字段 | 回答问题 | 变化含义 | 门禁 |
|---|---|---|---|
| owner | 方法属于哪个类 | 包迁移/重定位 | 确认来源 |
| name | 规则入口名 | 重命名/桥接 | 结合 mapping |
| descriptor | 精确参数返回 | API 改动 | 阻断 |
| access flags | 静态/虚拟/桥接 | 调用约定变化 | 回归 |
| code digest | 实现是否替换 | 语义可能变化 | 人工评审 |
| call targets | 关键下游是谁 | 责任漂移 | 路径回归 |
规则必须逐条展开并规定零命中、单命中和多命中语义
精确规则应绑定最终方法身份或可验证的 mapping 前身份,并声明期望命中数量。零命中不能只是 warning;它表示保护对象消失、规则格式失效或工具链忽略。多命中也不能自动接受,可能是重载新增、bridge 生成、通配范围扩大或同名方法被复制。
宽泛包规则会把依赖升级引入的新类自动纳入,表面上保持成功,实际扩大性能和兼容风险。相反,基于不稳定 synthetic 名称的规则会频繁失效。应优先保护显式业务函数,把反射、JNI、生成 adapter 和平台胶水交给精确 keep 与独立回归,避免一条规则承担两种职责。
每条规则回执至少保存规则 ID、期望方法、实际 owner/name/descriptor、命中数、mapping 关系、方法摘要和决策。工具若无法导出展开结果,不应把退出码零视为完成;需从最终 DEX 另行核对标记或保护清单,并明确证据只能覆盖工具可观察部分。
把漂移分成删除、迁移、签名变化、实现替换和新增候选
删除表示旧方法在候选中不存在,可能是功能移除、内联或依赖替换;迁移表示业务责任仍在但 owner 变化;签名变化包括参数、返回类型或静态与实例调用约定改变;实现替换则是身份不变而方法摘要或关键调用目标变化。四类都需要不同处置。
新增候选不是看到新方法就扩大保护。先确认它是否承担新的关键业务责任、是否可被服务端最终裁决、变化是否稳定、能否建立固定向量和设备回归。生成代码、简单 getter、日志、线程调度和 SDK adapter 通常不是高价值候选,盲目纳入只会放大保护面。
规则迁移应和源码或架构变更一起评审。若旧业务责任被拆成多个函数,先确定哪个函数持有不可替代的决策,再把副作用和平台边界剥离。不能为了保留单命中数字,把多个新函数随意包装进一个人工桥接方法而不验证语义。
| 类型 | 识别条件 | 修复 | 禁止做法 |
|---|---|---|---|
| 删除 | 旧身份不存在 | 确认功能或内联 | 静默忽略 |
| 迁移 | owner 变、责任相同 | 精确迁移规则 | 包通配 |
| 签名变化 | descriptor 改变 | 更新调用与测试 | 只改名称 |
| 实现替换 | 摘要/调用边变化 | 重新评审语义 | 沿用旧回执 |
| 新增候选 | 出现关键责任 | 单独立项 | 自动全选 |
| 命中扩张 | 数量超期望 | 收紧规则 | 接受成功码 |
传递依赖冲突与重复类要先解决,再讨论 VMP 规则
Android 依赖诊断资料要求从变体对应的解析结果处理重复类与版本冲突。一个 SDK 升级可能带来旧模块与新模块并存、传递版本被强制、shade 不完整或 classpath 与最终 DEX 来源不一致。若类身份本身不唯一,VMP 规则命中结果也没有稳定解释。
排查顺序是确认 selected component 与原因、检查重复 artifact 和类、定位约束或排除规则、重建最终方法索引,再比较 VMP 规则。不要先扩大通配让工具重新命中,这会把冲突的两份实现都纳入保护,并可能把类加载问题变成更难诊断的运行异常。
构建冲突消失不等于运行时语义恢复。依赖可以在二进制兼容的情况下改变默认值、线程、回调和数据处理。应保存升级前后关键调用路径、错误类型和副作用次数,在真实 Android 运行时执行回归,静态方法身份只用于决定需要测什么。
SDK 升级评审还要覆盖权限、可靠性和数据处理
Google Play SDK Index 可为部分 SDK 提供版本采用、可靠性、安全和数据处理线索,但不覆盖全部私有或开源依赖,也不能替代成品清单和代码审计。发现已知问题或政策变化时,应把它们纳入升级理由和回归范围,而不是据此直接认定某个版本安全。
依赖升级可能新增权限、ContentProvider、后台任务、native library、网络端点或数据收集。VMP 规则漂移门禁只回答关键方法是否仍被正确选择;Manifest 差分、SBOM、签名、Native ABI、隐私与服务端契约需要独立 gate。把所有风险压进一个命中报告会产生错误完成感。
若 SDK 闭源,类方法索引与运行回归更重要,但仍不得虚构其内部实现或攻击效果。对无法解释的新增组件、权限或高敏调用保持 blocked,向供应商索取一手说明与版本证据。预算不应降低证据门槛,缺少事实就明确标记未接入。
用基线和候选方法清单检查规则是否失效或扩张
下面的 Python 脚本读取公开 JSON:规则定义包含 ruleId、expectedBaselineMethod 和 expectedHits;基线与候选方法索引使用 owner、name、descriptor、codeSha256 和 callTargets。脚本不修改 APK,也不生成 VMP 规则,只把删除、签名变化、实现变化、调用边变化、零命中和多命中列为阻断差异。
示例采用精确匹配,避免把通配表达式当作生产规则语言。真实流水线可以在受控适配器中展开产品规则,再把展开后的精确方法集合交给同类比较。任何输入结构、摘要或期望命中数量不合法都会非零退出,确保失败来自供应的证据文件。
通过结果只说明这三份输入之间没有定义的漂移,不证明索引来自完整最终 APK、VMP 实际执行、运行语义兼容或 SDK 没有其他风险。回执必须绑定候选哈希、解析依赖图、mapping 与规则工具版本,并在设备上覆盖关键调用路径。
import json
import re
import sys
from pathlib import Path
if len(sys.argv) != 4:
raise SystemExit("usage: check_rule_drift.py rules.json baseline.json candidate.json")
def load_array(name):
path = Path(name).resolve()
if not path.is_file():
raise SystemExit(f"input missing: {path.name}")
with path.open("r", encoding="utf-8") as stream:
value = json.load(stream)
if not isinstance(value, list):
raise SystemExit(f"{path.name} root must be an array")
return value
rules = load_array(sys.argv[1])
baseline = load_array(sys.argv[2])
candidate = load_array(sys.argv[3])
digest = re.compile(r"^[0-9a-f]{64}$")
def method_key(item):
for field in ("owner", "name", "descriptor", "codeSha256", "callTargets"):
if field not in item:
raise SystemExit(f"method record missing {field}")
if not digest.fullmatch(str(item["codeSha256"])):
raise SystemExit("method codeSha256 is invalid")
if not isinstance(item["callTargets"], list):
raise SystemExit("callTargets must be an array")
return (item["owner"], item["name"], item["descriptor"])
base_by_key = {method_key(item): item for item in baseline}
candidate_by_key = {method_key(item): item for item in candidate}
errors = []
for rule in rules:
rule_id = rule.get("ruleId")
expected = rule.get("expectedBaselineMethod")
hits = rule.get("expectedHits")
if not rule_id or not isinstance(expected, dict) or hits != 1:
errors.append(f"invalid rule declaration: {rule_id!r}")
continue
identity = (expected.get("owner"), expected.get("name"), expected.get("descriptor"))
before = base_by_key.get(identity)
after = candidate_by_key.get(identity)
if before is None:
errors.append(f"{rule_id}: baseline identity is absent")
continue
if after is None:
same_name = [key for key in candidate_by_key if key[:2] == identity[:2]]
if same_name:
errors.append(f"{rule_id}: descriptor changed")
else:
errors.append(f"{rule_id}: zero candidate hits")
continue
if after["codeSha256"] != before["codeSha256"]:
errors.append(f"{rule_id}: implementation digest changed")
if sorted(after["callTargets"]) != sorted(before["callTargets"]):
errors.append(f"{rule_id}: call target set changed")
if errors:
print(json.dumps({"status": "failed", "differences": sorted(errors)}, indent=2))
raise SystemExit("VMP rule drift requires review")
print(json.dumps({
"status": "passed",
"rules_checked": len(rules),
"boundary": "identity evidence passed; actual VMP execution and runtime are separate gates"
}, indent=2))发布前完成规则迁移、候选验证和设备回归
门禁顺序建议为:冻结升级理由与依赖锁、生成同一 release 变体解析图、建立最终方法索引、关联 R8 mapping、展开规则、比较命中与调用边、执行 VMP、重新核对最终候选,再做验签与设备回归。任一输入或候选变化都使后续回执失效。
设备测试覆盖正常、拒绝、异常、取消、反射、JNI 和关键系统组件路径,记录保护前后输出、副作用次数、线程与错误类型。Android instrumented tests 适合验证依赖框架的语义,但单一设备通过不能代表全部 API、ABI 和厂商矩阵。报告必须写明真实覆盖。
关于范围改动的整体影响可继续阅读本站 /zh-cn/articles/vmp-scope-change-impact-analysis/;需要评审具体依赖升级后的保护规则,可在御盾中央平台 https://www.leonadev.com/console/ 提交脱敏的依赖图、候选摘要、mapping 和规则展开清单。
- 只使用目标 release 变体的解析依赖图。
- 从最终候选建立精确 owner/name/descriptor 索引。
- 逐条记录零命中、单命中和多命中。
- 把方法摘要与关键调用边纳入比较。
- 先解决重复类和依赖冲突再迁移规则。
- 候选变化后重新执行 VMP、验签与设备回归。
- 单独审计 SDK 权限、组件、Native 与数据处理。
- 内链和御盾中央行动入口保持可访问。
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| 依赖升级影响的是直接与传递依赖解析后的实际组件图。 | Android Gradle dependency resolution 说明版本解析、依赖图和运行时兼容背景,支持保存 release 变体的最终选择。 | 依赖树只能缩小范围,不能证明某个 SDK 是唯一根因或运行行为保持不变。 |
| 重复类和版本冲突必须从目标变体的解析结果处理。 | Debug Android dependency resolution 描述重复类与依赖版本冲突的诊断路径,支持在规则迁移前解决类身份问题。 | 构建冲突消失不等于加固后运行时冲突、回调或业务语义已通过。 |
| SDK 升级评审应考虑可靠性、安全和数据处理线索。 | Google Play SDK Index 提供部分 SDK 的版本采用、可靠性、安全与数据处理信息,可作为版本评审输入。 | SDK Index 不覆盖所有私有或开源 SDK,也不替代二进制清单、供应商证据和项目测试。 |
| 反射、JNI 和间接入口应使用精确 keep 规则。 | R8 keep rules best practices 说明宽泛 keep 会阻止优化并掩盖边界,支持把可达性规则与 VMP 选择分工。 | keep 规则只描述 R8 可达性,不定义业务价值、VMP 适用性或实际保护强度。 |
| 升级和规则迁移应保留来源、构建、验证与变更证据。 | NIST SP 800-218 SSDF 将软件供应链风险和安全开发、验证、发布证据纳入组织实践。 | SSDF 不定义 Android 方法身份、VMP 规则语言或御盾具体项目结论。 |
| 关键运行语义需要 Android 设备端 instrumentation 验证。 | Android instrumented tests 说明依赖真实 Android 运行时、组件和系统 API 的行为应在设备或模拟器执行。 | 单一设备通过不能代表全部 API、ABI、厂商系统和生产使用路径。 |
| 规则文本未变化不能证明命中的业务语义未变化。 | 工程判断:依赖、R8 和代码生成可改变 owner、descriptor、实现摘要与调用目标,而工具仍可能接受原规则。 | 代码摘要和调用边只是语义线索,仍需源码评审与运行向量确认业务责任。 |
| 零命中和多命中都应默认阻断,而不是只记录 warning。 | 工程判断:零命中意味着目标消失或规则失效,多命中意味着边界扩张或身份歧义,二者都无法证明最小保护范围。 | 明确设计为多方法集合的规则必须逐项列出身份和期望数量,不能使用未展开通配。 |
| 升级前后方法索引必须来自最终 release 候选。 | 工程判断:源码和依赖 JAR 尚未反映 R8、desugaring、生成代码、合并与删除,无法代表实际 DEX 方法集合。 | 索引工具自身需要固定版本和完整性验证,静态观察仍不等于 VMP 实际执行。 |
| 本文没有提供任何项目命中率、性能、兼容、攻击阻断或客户结论。 | 项目证据尚未接入:缺少目标依赖图、APK、mapping、规则展开回执与设备矩阵。 | 文章只提供漂移分类、比较代码和可复核的发布门禁设计。 |
工程常见问题
为什么 VMP 工具退出码为零仍可能规则失效?
部分工具把未命中规则当作警告或静默忽略。必须导出逐规则展开结果并核对期望 owner、name、descriptor 与命中数量。
只比较 Gradle 版本目录能发现传递依赖变化吗?
不能。版本目录是声明输入,平台、约束、BOM 与冲突解析会改变最终选择;应保存目标 release configuration 的解析图。
方法名相同是否表示规则仍保护同一逻辑?
不表示。参数和返回类型、owner、访问标记、代码摘要与关键调用目标都可能变化,需要联合比较并做业务语义回归。
能否在升级后把整个 SDK 包纳入 VMP 避免漏保护?
不建议。包通配会纳入生成代码和平台胶水,扩大性能与兼容面,并掩盖具体业务责任是否仍存在。
R8 keep 规则能代替 VMP 漂移门禁吗?
不能。keep 负责可达性与优化边界,VMP 门禁还要确认保护对象的业务价值、精确身份、命中结果和运行语义。
发现 owner 搬迁后可以自动改规则并发布吗?
不可以。自动迁移只能提出候选,还需确认业务责任、descriptor、实现摘要和调用路径,再用最终候选和设备回归取证。
SDK Index 显示版本正常是否代表升级安全?
不代表。它只覆盖部分公开线索,仍需审计权限、组件、Native、数据处理、依赖图、最终候选与项目运行行为。
静态漂移检查通过后还要测什么?
重新执行 VMP 并核对最终方法,验签,再覆盖正常、拒绝、异常、反射、JNI 与系统组件路径,记录设备、API、ABI 和候选哈希。