先看结论与判断条件
- Local Reference 的有效范围受当前 Native 调用、本地引用帧和线程约束;循环中持续创建而不释放,会把瞬时对象积累成稳定性风险。
- Global Reference 必须拥有明确的业务所有者、创建入口、唯一释放事件和幂等策略,不能把进程退出当作日常资源管理方案。
- Weak Global Reference 只表达弱可达关系,使用前需要在 JNI 语义下确认对象仍可用,不能把缓存命中等同于强引用存活。
- 早退、Java 异常、Native 错误与组件销毁路径都必须经过同一清理责任,成功路径配对并不能覆盖真实泄漏面。
- 静态扫描和事件账本只能发现可见的不配对、跨作用域使用与本地帧失衡,最终结论仍需目标 ABI 设备端的重复调用、回收和退出回归。
- SO 加固验收要绑定候选摘要、ABI、符号、测试路径和退出记录;保护变换不是引用错误的修复器,也不能替代基线归因。
先把引用种类、所有者和失效边界写成契约
JNI 引用审计的起点不是统计 API 次数,而是回答三个问题:谁创建、谁可以使用、什么事件必须释放。Local Reference 通常服务一次 Native 调用或一个显式本地帧,Global Reference 跨调用保存 Java 对象身份,Weak Global Reference 则允许对象被回收。三类引用的寿命和可用性不同,混在一张“句柄列表”里会丢失最重要的边界。
Android JNI tips 把本地引用、全局引用、异常、线程与类加载列为 JNI 正确性的重要边界。可核查事实是 JNI 环境和引用存在明确语义;工程判断是发布团队应把这些语义转换为可审计字段,例如 refId、kind、owner、thread、scope、createSite、releaseEvent 和 candidateSha256。这样的账本能解释某个句柄为什么此刻仍应有效,而不是只证明代码里出现过删除函数。
保护变换前必须先让未加固发布基线通过同一套契约。若基线已经存在未释放全局引用、跨线程复用局部引用或弱引用失效后的继续调用,加固候选出现崩溃时就无法判断是历史缺陷、时序放大还是变换回归。本文提供的是检查和门禁方法,不声称任何具体库、候选包或御盾能力已经通过验证。
| 引用种类 | 典型所有者 | 允许使用范围 | 必须记录的释放或失效事件 |
|---|---|---|---|
| Local | 当前 Native 调用或本地帧 | 创建线程和有效作用域内 | 返回 Java、DeleteLocalRef 或 PopLocalFrame |
| Global | Native 缓存、组件或进程级对象 | 显式删除前可跨调用使用 | 对应所有者终止时 DeleteGlobalRef |
| Weak Global | 允许回收的 Native 索引 | 确认仍存活并取得可用引用后 | DeleteWeakGlobalRef 或对象已不可用 |
| 临时提升引用 | 一次读取或回调 | 当前受控调用范围 | 使用结束立即删除局部强引用 |
Local Reference 要按调用、本地帧与循环压力检查
局部引用通常由 JNI 参数、对象创建、字段读取或方法返回值产生。它们看似会在 Native 方法返回时统一失效,但长循环、批量解析和常驻 Native 线程可能在返回前创建大量对象。只依赖“最终会自动释放”会掩盖峰值压力,因此审计必须定位循环体内的创建点、每轮释放点、退出分支和本地帧边界。
检查循环时,应把每次迭代的新增局部引用数、可到达的最大迭代次数、是否调用 DeleteLocalRef、是否用 PushLocalFrame 与 PopLocalFrame 分段,以及失败分支是否跳过清理写进记录。这里的数字只能来自真实代码路径和观测,不能凭经验给出通用安全上限。门禁关注的是增长是否有界、边界是否可解释,而不是宣称某个固定数量适合所有运行时。
跨调用保存 Local Reference 是更直接的错误。把 jobject 放进静态变量、Native 单例、异步队列或另一个线程的任务结构,并不自动把它变成全局引用。事件审计应在 scope-end 时检查仍被标记为 retained、queued 或 stored 的局部引用;发现后阻断发布,要求调用方明确创建 Global Reference,或把所需数据复制为真正属于 Native 的值对象。
| 路径 | 需要观测 | 阻断信号 | 修正方向 |
|---|---|---|---|
| 短方法 | 入口、创建、删除、返回 | 返回后仍保存 refId | 删除或显式提升 |
| 长循环 | 每轮新增与释放 | 未配对数量随迭代增长 | 逐轮删除或使用本地帧 |
| 早退 | 每个 return 前的清理 | 失败分支残留引用 | 统一清理出口或 RAII |
| 异步任务 | 入队对象的引用种类 | Local 被跨线程保存 | 复制数据或创建受管 Global |
| 本地帧 | push、pop 与保留结果 | 帧深度不平衡 | 成对退出并核对保留对象 |
Global Reference 要绑定业务所有者和唯一释放事件
Global Reference 的危险不只在忘记 DeleteGlobalRef,还在没有人能说清它属于谁。Native 缓存可能跟随 Application、某个 Service、一个引擎实例或一次媒体会话,但这些对象的结束条件完全不同。审计清单必须把 ownerType 与 ownerId 写进 create 事件,并为每种所有者定义可观察的 releaseEvent,禁止使用“以后统一清理”这样的模糊责任。
创建和释放还需要处理幂等与并发。初始化重试可能为同一逻辑对象重复创建 Global Reference,组件销毁与错误回滚也可能同时尝试删除。正确门禁不是简单要求 create 数等于 delete 数,而是核对每个 refId 的状态迁移只能从 absent 到 live,再从 live 到 released;重复创建、未知删除、释放后使用和所有者结束后的残留都要单独报告。
把所有 Global Reference 留到进程退出并不适合可重复创建的组件。Android 进程何时终止不由业务代码完全控制,且用户可能在一个长进程内反复进入和离开同一功能。工程上应优先选择与最窄业务寿命一致的所有者,只有确实服务整个进程且数量有界的对象,才可以采用进程级寿命,并仍要保留初始化失败和测试卸载路径。
| 所有者 | 创建事件 | 释放事件 | 重点失败条件 |
|---|---|---|---|
| 组件实例 | 组件初始化成功 | 组件确定销毁 | 重建后旧引用仍活跃 |
| 会话 | 会话建立并登记 | 结束、取消或错误回滚 | 异常分支未释放 |
| Native 引擎 | 引擎实例完成构造 | 实例 shutdown 完成 | 重复 shutdown 或并发使用 |
| 进程级缓存 | 一次性初始化 | 测试卸载或进程清理 | 条目数量无界增长 |
| 异步回调 | 任务被接受 | 回调完成或取消 | 取消后继续回调 |
Weak Global Reference 必须把存活判断和实际使用分开
Weak Global Reference 适合表达“不应阻止 Java 对象被回收”的关系,但它不是廉价的永久句柄。对象可能在 Native 计划使用前已经不可用,单纯判断 Native 侧地址非空不能建立强可达性。调用路径应先执行符合 JNI 语义的存活确认或取得临时可用引用,再在受控范围内访问对象,最后释放这次临时引用。
弱引用审计要区分 weak-create、weak-promote、weak-use、temporary-release 和 weak-delete。若日志只出现 weak-use 而没有同一 scope 内成功的 promote,就无法证明使用时对象仍存活;若提升成功后把临时 Local Reference 放入队列,又会重新引入跨作用域问题。事件模型因此要同时追踪弱句柄和本次使用产生的临时强引用。
缓存未命中或对象已回收应当是正常分支,而不是崩溃前的异常状态。业务层需要定义降级行为,例如重新解析对象、取消本次回调或返回明确错误,并确保该分支不继续调用实例方法。测试要主动制造组件离开、Java 对象不可达和回收压力,但不能把一次未观察到回收写成弱引用永远安全。
| 阶段 | 需要的证据 | 禁止假设 | 失败处理 |
|---|---|---|---|
| 创建 | owner 与用途已登记 | 弱引用会保持对象存活 | 创建失败立即回滚 |
| 提升 | 当前 scope 取得可用临时引用 | 句柄地址非空等于对象有效 | 返回未命中或取消 |
| 使用 | 仅使用本次提升结果 | 可以跨线程保存 Local | 复制数据或另建 Global |
| 临时释放 | 同一 scope 内完成 | 方法返回前无需关注 | 统一清理出口 |
| 弱引用删除 | 所有者终止并登记 | 对象回收会自动删除 Native 句柄 | 幂等删除并阻止再用 |
异常、早退和回滚路径必须共享清理责任
JNI 调用可能留下待处理的 Java 异常,此时继续执行普通 JNI 操作会使后续行为难以解释。审计不能只在函数尾记录 cleanup,而要在每个可能触发异常的调用后确认控制流:异常是转换为 Native 错误、清除后降级,还是保留并立即返回 Java。无论采用哪种策略,已经创建的 Local、Global 与临时提升引用都必须按所有权规则完成回滚。
C++ RAII 可以把删除动作绑定到对象析构,但包装器自身也需要明确 JNIEnv、线程和移动语义。一个持有 Local Reference 的 RAII 对象若跨线程移动,析构时使用错误环境并不会因为“自动释放”而正确。本文的范围是引用生命周期,因此线程附加细节应由独立的 JNI 线程契约检查覆盖;此处只要求释放发生在合法上下文并留下可关联事件。
Android NDK common problems 汇总的故障还包括 API level、缺失符号、STL、异常类型和依赖装载。它说明 Native 故障有多个相邻原因,所以引用审计通过后不能排除其他兼容问题。工程门禁应保留错误类别:reference-lifecycle、symbol-load、runtime-library、exception-boundary 与 unknown,避免把所有 Native 崩溃强行归入引用泄漏。
| 控制流 | 引用动作 | 异常动作 | 验收证据 |
|---|---|---|---|
| 成功返回 | 释放临时引用并关闭本地帧 | 确认无待处理异常 | scope-end 无残留 |
| Java 异常传播 | 回滚已创建资源 | 保留异常并立即返回 | 异常与清理事件同 scope |
| 转换为错误码 | 回滚已创建资源 | 按策略检查并清除 | 错误码和原始调用点关联 |
| Native 早退 | 执行统一 cleanup | 不掩盖已有异常 | 所有 return 分支被覆盖 |
| 初始化失败 | 删除已建 Global | 保存首个根因 | owner 未进入 live 状态 |
静态清单与事件账本负责发现不同类型的问题
静态检查适合寻找 NewGlobalRef、NewWeakGlobalRef、DeleteGlobalRef、DeleteWeakGlobalRef、PushLocalFrame 和 DeleteLocalRef 的调用点,并标记存入全局容器、静态字段或异步任务的 jobject。它能发现明显缺口,却不能可靠重建宏、模板、回调和异常下的真实控制流,更不能证明运行时对象在某一时刻存活。
事件账本来自调试或测试构建中的公开安全埋点,记录抽象 refId、种类、scope、thread、owner、操作和候选摘要,不记录对象内容、用户数据或真实指针。账本审计可以检查未知删除、重复创建、scope 结束后的 Local 残留、未关闭本地帧、弱引用未提升直接使用,以及 owner 结束后的 Global 残留。它仍然依赖被测试路径确实执行,不能把未出现的事件解释为代码不存在。
两类证据要在同一候选上汇合:静态清单给出理论入口,设备事件证明选定路径被触发。若静态发现十个创建点而测试只覆盖两个,报告应明确剩余覆盖缺口;若事件出现静态清单没有解释的操作,则检查生成代码、第三方库或埋点映射,而不是删除不方便的记录。
| 方法 | 擅长发现 | 不能证明 | 合格输出 |
|---|---|---|---|
| 源码静态清单 | API 点和明显保存路径 | 真实控制流已执行 | 文件、函数、行与所有者 |
| 事件账本 | 已执行路径的状态失衡 | 未覆盖路径不存在问题 | 候选、ABI、scope 与发现项 |
| 内存趋势 | 重复路径后的增长信号 | 增长一定来自 JNI 引用 | 基线、候选和复现步骤 |
| 崩溃与退出记录 | 进程终止时间和原因 | 单凭堆栈确定所有权根因 | 同版本、时间窗、路径关联 |
| 设备端回归 | Android 运行时真实语义 | 全部 API、ABI、厂商均通过 | 设备、系统、ABI 与结果矩阵 |
目标 ABI 设备回归要重复生命周期而非只做一次启动
Android instrumented tests 适合验证依赖真实 Android 运行时、组件与系统 API 的行为。JNI 引用回归应在每个发布 ABI 的代表设备上重复创建和销毁组件、开启和取消会话、触发成功与异常分支、切换前后台,并让长循环达到业务可辩护的批量规模。测试次数和阈值必须来自产品路径与基线测量,不能由文章虚构统一数值。
对比应使用相同业务输入、相同设备状态和明确候选身份。未加固基线先证明引用事件闭合,加固候选再执行同一路径,比较未配对引用、峰值变化、异常类型、退出原因和 Native 堆栈。若只保留“测试通过”而没有 APK 或 AAB 摘要、SO 摘要、ABI、系统版本、路径和时间窗,结果不能进入发布结论。
单一设备通过不代表完整 API、ABI 和厂商矩阵。设备覆盖要以实际发布范围为准,并把未覆盖项保留为限制。某个 x86_64 模拟器上的重复测试能帮助发现控制流问题,却不能替代 arm64-v8a 真机装载和运行证据;同理,一个新系统版本的退出信息丰富,也不能覆盖旧系统上的行为差异。
| 测试维度 | 基线路径 | 候选路径 | 失败判定 |
|---|---|---|---|
| 组件重建 | 进入、离开、再次进入 | 完全相同 | 旧 owner 的 Global 仍存活 |
| 异常回滚 | 触发可控 Java 异常 | 完全相同 | 异常分支遗留引用或帧 |
| 批量循环 | 业务上限内重复处理 | 完全相同 | Local 未配对持续增长 |
| 弱引用失效 | 对象离开并制造回收机会 | 完全相同 | 未提升直接使用或崩溃 |
| 会话取消 | 异步任务建立后取消 | 完全相同 | 取消后回调或 owner 残留 |
| 进程退出 | 记录版本、路径和时间窗 | 完全相同 | 出现不可解释的新退出类别 |
用退出证据、符号和发布记录完成归因
Android ApplicationExitInfo 可以提供进程退出原因、ANR trace,并在较新条件下提供 Native tombstone。使用这些信息时,要把记录与同一包版本、候选摘要、时间窗和用户路径关联。看到 Native 崩溃或低内存退出,只能说明进程如何结束,不能单凭退出类别证明某个 Global Reference 泄漏,更不能把其他进程共享压力排除在外。
Debug Android native code 说明 Native 调试和 tombstone 还原依赖正确符号、架构与构建产物。加固候选的诊断包应保存可追溯的符号映射,并与发布产物摘要一一对应。错误候选的符号可能产生看似合理却完全错误的函数位置,因此任何引用生命周期归因都要先通过 candidate、ABI 和 symbol identity 门禁。
NIST SP 800-218 SSDF 要求在开发与发布过程中保留来源、构建、验证、变更和供应链证据。对本检查而言,最低发布包包括源码清单摘要、事件账本摘要、设备矩阵、异常与退出回执、符号身份和批准人。SSDF 是组织级实践框架,不证明具体加固产品功能;若要提交实际 SO 兼容评估,应先准备这些材料,再通过御盾中央平台提交申请。
| 证据 | 必须绑定 | 可支持的结论 | 不能单独支持 |
|---|---|---|---|
| 引用事件账本 | candidate、ABI、scope、path | 已覆盖路径是否配对 | 未覆盖代码没有缺陷 |
| ApplicationExitInfo | 版本、时间窗、用户路径 | 退出原因与诊断入口 | 引用泄漏是唯一根因 |
| Native tombstone | 同候选符号和架构 | 崩溃位置与线程上下文 | 所有权责任自动成立 |
| 设备矩阵 | 设备、API、ABI、业务输入 | 具体覆盖范围内的结果 | 全部 Android 环境通过 |
| 变更记录 | 源码、构建、审核与批准 | 结论可追溯到候选 | 产品具备未验证能力 |
- 输入只含抽象 refId 和测试元数据,不写对象内容、真实指针、用户数据或凭据
- 每个事件 seq 严格递增,并绑定候选 SHA-256 与实际发布 ABI
- Local、Global、Weak Global 和临时提升引用分别维护状态,不用总数相抵
- scope-end 同时检查局部引用、本地帧、待处理异常和临时提升引用
- owner-end 后仍存活的 Global 与 Weak Global 明确阻断,不用进程退出掩盖
- 静态与事件门禁通过后,仍执行目标设备重复路径和退出证据复核
#!/usr/bin/env python3
import hashlib
import json
import sys
from collections import defaultdict
from pathlib import Path
ALLOWED_ABIS = {"armeabi-v7a", "arm64-v8a", "x86", "x86_64"}
ALLOWED_OPS = {
"frame-push", "frame-pop", "local-create", "local-delete",
"local-retain", "global-create", "global-delete",
"weak-create", "weak-delete", "weak-promote",
"weak-use", "temporary-release", "exception-pending",
"exception-cleared", "owner-end", "scope-end",
}
def fail(message, code=2):
print(message, file=sys.stderr)
raise SystemExit(code)
def sha256_file(path):
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def load_document(path):
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
fail("cannot read event document: " + str(exc))
if not isinstance(value, dict):
fail("event document must be an object")
return value
def require_text(event, field):
value = event.get(field)
if not isinstance(value, str) or not value.strip():
fail("event field is missing: " + field)
return value.strip()
def add_finding(findings, event, kind, detail):
findings.append({
"seq": event.get("seq"),
"kind": kind,
"detail": detail,
})
def audit(document):
artifact = document.get("artifactSha256")
abi = document.get("abi")
events = document.get("events")
if not isinstance(artifact, str) or len(artifact) != 64:
fail("artifactSha256 must contain 64 hexadecimal characters")
try:
int(artifact, 16)
except ValueError:
fail("artifactSha256 is not hexadecimal")
if abi not in ALLOWED_ABIS:
fail("unsupported or missing ABI")
if not isinstance(events, list) or not events:
fail("events must be a non-empty array")
findings = []
last_seq = 0
frames = defaultdict(int)
locals_by_scope = defaultdict(set)
retained_locals = set()
globals_live = {}
weak_live = {}
promoted = defaultdict(set)
pending_exception = set()
ended_owners = set()
for event in events:
if not isinstance(event, dict):
fail("every event must be an object")
seq = event.get("seq")
if not isinstance(seq, int) or seq <= last_seq:
fail("event seq must be a strictly increasing integer")
last_seq = seq
op = require_text(event, "op")
if op not in ALLOWED_OPS:
fail("unsupported event operation: " + op)
thread = require_text(event, "thread")
scope = require_text(event, "scope")
scope_key = (thread, scope)
ref_id = str(event.get("ref", "")).strip()
owner = str(event.get("owner", "")).strip()
if op == "frame-push":
frames[scope_key] += 1
elif op == "frame-pop":
if frames[scope_key] == 0:
add_finding(findings, event, "frame-underflow", str(scope_key))
else:
frames[scope_key] -= 1
locals_by_scope[scope_key].clear()
elif op == "local-create":
if not ref_id:
fail("local-create requires ref")
if ref_id in locals_by_scope[scope_key]:
add_finding(findings, event, "duplicate-local", ref_id)
locals_by_scope[scope_key].add(ref_id)
elif op == "local-delete":
if ref_id not in locals_by_scope[scope_key]:
add_finding(findings, event, "unknown-local-delete", ref_id)
else:
locals_by_scope[scope_key].remove(ref_id)
retained_locals.discard((scope_key, ref_id))
elif op == "local-retain":
if ref_id not in locals_by_scope[scope_key]:
add_finding(findings, event, "retain-unknown-local", ref_id)
else:
retained_locals.add((scope_key, ref_id))
elif op == "global-create":
if not ref_id or not owner:
fail("global-create requires ref and owner")
if ref_id in globals_live:
add_finding(findings, event, "duplicate-global", ref_id)
if owner in ended_owners:
add_finding(findings, event, "create-after-owner-end", owner)
globals_live[ref_id] = {"owner": owner, "seq": seq}
elif op == "global-delete":
if ref_id not in globals_live:
add_finding(findings, event, "unknown-global-delete", ref_id)
else:
del globals_live[ref_id]
elif op == "weak-create":
if not ref_id or not owner:
fail("weak-create requires ref and owner")
if ref_id in weak_live:
add_finding(findings, event, "duplicate-weak", ref_id)
weak_live[ref_id] = {"owner": owner, "seq": seq}
elif op == "weak-promote":
if ref_id not in weak_live:
add_finding(findings, event, "promote-unknown-weak", ref_id)
else:
promoted[scope_key].add(ref_id)
elif op == "weak-use":
if ref_id not in promoted[scope_key]:
add_finding(findings, event, "weak-use-without-promotion", ref_id)
elif op == "temporary-release":
if ref_id not in promoted[scope_key]:
add_finding(findings, event, "unknown-temporary-release", ref_id)
else:
promoted[scope_key].remove(ref_id)
elif op == "weak-delete":
if ref_id not in weak_live:
add_finding(findings, event, "unknown-weak-delete", ref_id)
else:
del weak_live[ref_id]
elif op == "exception-pending":
pending_exception.add(scope_key)
elif op == "exception-cleared":
if scope_key not in pending_exception:
add_finding(findings, event, "clear-without-pending", str(scope_key))
pending_exception.discard(scope_key)
elif op == "owner-end":
if not owner:
fail("owner-end requires owner")
ended_owners.add(owner)
for live_ref, record in sorted(globals_live.items()):
if record["owner"] == owner:
add_finding(findings, event, "global-after-owner-end", live_ref)
for live_ref, record in sorted(weak_live.items()):
if record["owner"] == owner:
add_finding(findings, event, "weak-after-owner-end", live_ref)
elif op == "scope-end":
for live_ref in sorted(locals_by_scope[scope_key]):
add_finding(findings, event, "local-at-scope-end", live_ref)
if frames[scope_key] != 0:
add_finding(findings, event, "unbalanced-local-frame", str(frames[scope_key]))
if promoted[scope_key]:
add_finding(findings, event, "promoted-weak-at-scope-end", ",".join(sorted(promoted[scope_key])))
if scope_key in pending_exception:
add_finding(findings, event, "pending-exception-at-scope-end", str(scope_key))
for retained_scope, live_ref in sorted(retained_locals):
if retained_scope == scope_key:
add_finding(findings, event, "retained-local-crosses-scope", live_ref)
for live_ref, record in sorted(globals_live.items()):
findings.append({"seq": None, "kind": "global-not-released", "detail": live_ref + " owner=" + record["owner"]})
for live_ref, record in sorted(weak_live.items()):
findings.append({"seq": None, "kind": "weak-not-released", "detail": live_ref + " owner=" + record["owner"]})
return {
"status": "blocked" if findings else "runtime-scope-passed",
"artifactSha256": artifact.lower(),
"abi": abi,
"eventCount": len(events),
"findings": findings,
"boundary": "This audit covers only executed and recorded paths.",
}
def main():
if len(sys.argv) != 2:
print("Usage: audit_jni_reference_events.py EVENTS_JSON", file=sys.stderr)
raise SystemExit(2)
input_path = Path(sys.argv[1])
if not input_path.is_file():
print("event document does not exist", file=sys.stderr)
raise SystemExit(2)
document = load_document(input_path)
result = audit(document)
result["inputSha256"] = sha256_file(input_path)
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
if result["status"] == "blocked":
raise SystemExit(20)
if __name__ == "__main__":
main()事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| JNI 的引用、异常、线程和类加载边界会直接影响 Native 桥接稳定性。 | Android JNI tips 说明 JNI 引用及相关运行边界。 | 这些建议不证明某个加固候选保持 ABI,也不代替目标设备验证。 |
| Local、Global 与 Weak Global 需要按不同寿命和可用性建模。 | Android JNI tips 对本地引用、全局引用和弱全局引用给出不同使用语义。 | 文章中的 owner、scope 和事件字段属于工程审计设计,不是 Android 平台自动生成的结论。 |
| Native 兼容故障还可能来自 API level、缺失符号、STL、异常类型和依赖装载。 | Android NDK common problems 汇总常见 NDK 故障类别。 | 故障目录不能替代具体目标 ABI 的启动、加载、异常和业务回归。 |
| Native 调试和 tombstone 还原依赖正确符号、架构与构建产物。 | Debug Android native code 说明 Android Native 调试和符号条件。 | 调试能力不能当作兼容通过,错误符号也不能支持引用所有权归因。 |
| 依赖真实 Android 运行时与组件语义的行为应通过设备端测试验证。 | Android instrumented tests 说明设备端 instrumented test 的适用范围。 | 单一设备通过不能代表完整 API、ABI 和厂商矩阵。 |
| 进程退出记录可提供退出原因、ANR trace,并在部分新版本条件下提供 Native tombstone。 | Android ApplicationExitInfo 描述可查询的进程退出信息。 | 退出记录必须绑定同一版本、时间窗和用户路径,不能单独证明 JNI 引用是根因。 |
| 安全发布应保留来源、构建、验证、变更和供应链证据。 | NIST SP 800-218 SSDF 给出组织级安全软件开发实践。 | SSDF 不定义御盾或任何具体 App 加固产品的功能与效果。 |
| 未知删除、重复创建、Local 跨 scope、弱引用未提升使用和 owner 结束后的残留应阻断发布。 | 工程判断:这些状态使引用所有权或使用有效性无法得到唯一解释。 | 事件审计只覆盖已执行且已记录路径,静态通过或账本通过都不能外推到未覆盖代码。 |
工程常见问题
Native 方法返回时 Local Reference 会释放,为什么还要手动检查?
方法返回前的长循环可能积累大量 Local Reference,异步保存还可能让引用跨越有效 scope。要检查每轮释放、本地帧、早退和保存路径,不能只依赖最终返回。
只要 NewGlobalRef 与 DeleteGlobalRef 数量相等就算通过吗?
不算。总数相等可能掩盖删除了错误 refId、重复创建、释放后使用或 owner 已结束但引用仍存活。门禁要按每个 refId 和所有者检查状态迁移。
Weak Global Reference 非空是否说明 Java 对象仍然可用?
不能这样判断。弱引用不阻止对象回收,使用前应按 JNI 语义取得本次调用可用的临时引用,并在失败时走未命中或取消路径。
使用 C++ RAII 是否能彻底避免 JNI 引用泄漏?
RAII 能集中清理责任,但包装器仍要处理正确线程、JNI 环境、移动语义、异常和 owner 寿命。错误的所有权模型不会因为析构函数存在而自动正确。
事件审计脚本通过后能否直接发布加固候选?
不能。脚本只检查输入账本中已执行路径的状态一致性,还需核对静态入口、目标 ABI 设备回归、候选与符号身份、异常和退出记录。
申请 JNI 引用生命周期兼容评估需要准备哪些材料?
准备未加固基线与加固候选摘要、发布 ABI、源码调用点清单、抽象事件账本、设备矩阵、符号映射、异常和退出回执,再通过御盾中央平台提交申请。