先看结论与判断条件
- JavaVM 可作为进程级入口保存,JNIEnv 与当前线程绑定,不能从一个线程复制到另一个线程或长期当作全局句柄使用。
- Java 创建并进入 Native 的线程通常已连接,Native 代码应先 GetEnv 并复用当前 JNIEnv,不能无条件 attach 后再 detach。
- Native 创建的线程在首次 JNI 调用前需要连接;只有 attach 由当前线程责任域成功执行时,退出前才由同一责任域 detach。
- 线程池要按 worker 生命周期管理连接,而不是每个任务盲目 attach/detach;取消、异常和早退必须汇聚到可证明的清理路径。
- 待处理 JNI 异常不能被 detach 当作清理手段,Native 异常也不应越过 JNI 边界;错误转换与 detach 是两项独立责任。
- 事件配对通过只能证明观测到的连接状态机完整,仍需逐 ABI 设备验证真实回调、并发、类加载和进程退出。
先确定谁创建线程,再确定谁负责连接和断开
JNI attach/detach 最常见的错误不是少写一行 API,而是没有定义所有权。Java 线程进入 Native 方法时,与 Native 代码自行创建的 pthread 或 std::thread 起点不同。前者已经处于虚拟机管理的线程上下文,后者必须先取得当前连接状态,才能安全调用 JNI。
契约应把线程分为 java-owned、native-owned 和 external-attached。java-owned 的生命周期由 Java 或运行时管理,Native 层不拥有 detach;native-owned 若由当前责任域 attach 成功,则线程退出前必须 detach;external-attached 表示线程早已由其他框架连接,当前模块只能借用,不能擅自断开。
验收记录至少包含 threadRunId、creator、processId、nativeThreadId、GetEnv 结果、attachAttempt、attachResult、ownedAttach、jniCallCount、pendingExceptionState、detachResult 和 exitReason。系统线程 ID 可能复用,不能单独作为长期身份,因此需要每次线程生命期唯一的 runId。
| 线程来源 | 进入 Native 时状态 | 当前模块动作 | 退出责任 |
|---|---|---|---|
| Java 调用线程 | 通常已连接 | GetEnv 后借用 JNIEnv | 当前模块不 detach |
| Native 新建线程 | 初始可能未连接 | 必要时 attach | 若本域 attach 则 detach |
| Native 线程池 worker | 取决于 worker 生命周期 | worker 入口建立状态 | worker 退出统一清理 |
| 第三方回调线程 | 状态不可假设 | GetEnv 并记录所有者 | 只释放本域拥有连接 |
| 外部已连接线程 | 已由其他模块 attach | 借用当前环境 | 不得替其他模块 detach |
JavaVM 可以共享,JNIEnv 必须留在线程内
Android JNI tips 说明 JNI 的线程、引用、异常与类加载边界直接影响桥接稳定性。JavaVM 是进入虚拟机线程接口的进程级对象,适合在初始化时保存;JNIEnv 则表示当前线程的 JNI 调用环境。把某线程的 JNIEnv 放进全局变量并在其他线程使用,会破坏线程归属。
正确入口不是“如果有全局 env 就调用”,而是每次在线程责任域中通过 JavaVM 获取当前状态。GetEnv 返回已连接时,使用本线程返回的 JNIEnv;返回未连接时,由明确的 native-owned 路径 attach;其他错误立即失败。是否 detach 由 attachedByThisScope 决定,而不是由 env 是否非空决定。
线程内缓存也要限制生命期。worker 可以在其整个生命期保持连接,并在退出清理;任务对象不应把 JNIEnv 传给下一个 worker。异步回调需要传递稳定业务句柄,再由执行回调的线程取得自己的 JNIEnv。本文不展开局部和全局引用管理,但引用生命期仍需单独契约。
| 对象 | 可见范围 | 允许保存 | 禁止用法 |
|---|---|---|---|
| JavaVM | 进程级接口 | 受控全局或服务对象 | 跨进程复用 |
| JNIEnv | 当前线程 | 线程栈或 worker 状态 | 跨线程缓存 |
| threadRunId | 单次线程生命期 | 事件和日志 | 只用可复用系统 ID |
| 业务句柄 | 由契约定义 | 跨任务传递 | 夹带线程专属 env |
| ownedAttach | 当前责任域 | 清理守卫 | 由 env 非空推断 |
GetEnv、attach、JNI 调用和 detach 组成状态机
一个可审计状态机从 thread-start 开始。先记录 get-env-attached 或 get-env-detached;只有 detached 状态可以进入 attach-ok;attached 状态才能执行 jni-call。若 attach 失败,线程必须走失败策略,不得继续使用未初始化的 JNIEnv,也不能伪造 detach 成功。
detach-ok 的前置条件更严格:线程当前已连接、连接由本责任域拥有、没有尚未处理的 JNI 异常,并且不会再执行 JNI。java-owned 或 external-attached 线程即使处于 attached,也不能由当前模块 detach。重复 attach 和重复 detach 都应被状态机阻断。
thread-exit 是最终门禁。native-owned 且 ownedAttach 为真时,必须先完成 detach;借用连接的线程退出当前 Native 调用时只结束本作用域,不改变虚拟机连接。工程判断是将所有 return、取消、错误和 C++ catch 汇聚到同一守卫,减少遗漏分支。
| 当前状态 | 事件 | 允许结果 | 阻断条件 |
|---|---|---|---|
| started | GetEnv | attached 或 detached | 未判断就 JNI 调用 |
| detached | attach | attachedOwned | attach 失败后继续 |
| attachedBorrowed | jni-call | 保持借用 | 当前模块 detach |
| attachedOwned | jni-call | 保持拥有 | 未处理异常继续调用 |
| attachedOwned | detach | detached | 后续再次 JNI 调用 |
线程池、取消和早退要按 worker 生命周期清理
线程池中的 task 与 worker 不是同一生命周期。若每个任务进入时无条件 attach、结束时无条件 detach,后一个任务可能运行在被错误断开的 worker 上;若只 attach 不 detach,worker 销毁时又留下不完整清理。连接策略应绑定 worker 创建与退出,而非业务任务次数。
取消路径最容易漏配。任务可能在等待队列、JNI 调用前、Java 回调后或异常处理中被取消。每个路径都要保持状态机可终结:尚未 attach 就不 detach,已由本域 attach 则在 worker 真正退出时 detach,外部已连接则不改变连接。取消信号不能绕过 pending exception 处理。
工程上可使用 RAII、pthread TLS destructor 或线程池统一 teardown,但机制必须和实际线程模型匹配。若 worker 永久驻留,detach 发生在线程终止而不是任务结束;若每次创建短线程,守卫覆盖整个入口。只看源代码中出现 attach 与 detach 两个词,不能证明所有退出路径配对。
| 模型 | attach 时点 | detach 时点 | 主要风险 |
|---|---|---|---|
| 短生命周期线程 | 线程入口首次 JNI 前 | 线程最终退出前 | 早退遗漏 |
| 固定 worker 池 | worker 启动或首次需要时 | worker teardown | 按任务重复断开 |
| Java executor 回调 | 通常只 GetEnv | 当前模块不执行 | 误判为 native-owned |
| 第三方回调线程 | 先检查当前状态 | 按所有权决定 | 未知框架已连接 |
| 取消中的 worker | 不改变既有策略 | 真实退出时统一清理 | 取消分支绕过守卫 |
JNI 异常、C++ 异常和 detach 是三个独立问题
JNI 调用可能留下待处理 Java 异常。在异常仍 pending 时继续执行多数 JNI 操作会产生新的错误或掩盖原始原因。线程契约应记录 exception-pending 与 exception-cleared 或 translated,确保进入下一次 jni-call、detach 或 thread-exit 前已经按项目策略处理。
Android C++ library support 说明 C++ 异常、RTTI 与 libc++ 链接方式由构建系统和运行库选择决定。Native 线程入口应捕获允许的 C++ 异常并转换为稳定错误,不让异常越过 C ABI 或 JNI 边界。catch 执行后仍要遵守 attachedByThisScope 的清理责任。
detach 不是异常清理 API,也不能修复错误的运行库或 unwind。若清理路径本身抛出、阻塞或访问已销毁状态,线程可能在退出前再次失败。工程判断是让 teardown 尽量无抛出、可重复检查,并把 Java 异常状态、Native 错误和 detach 结果分别记录。
| 问题 | 责任动作 | 失败信号 | 不能替代 |
|---|---|---|---|
| JNI pending exception | 检查、描述或转换并清理 | 后续 JNI 异常 | detach |
| C++ exception | 在线程边界捕获转换 | 跨边界终止 | Java 异常处理 |
| attach 失败 | 停止 JNI 路径 | env 未建立仍调用 | 伪造空环境 |
| detach 失败 | 记录并阻断验收 | 退出责任不完整 | 忽略返回值 |
| teardown 异常 | 无抛出清理与诊断 | 二次故障掩盖原错 | 只保留最终堆栈 |
调试材料必须绑定架构、符号和精确候选
Android NDK common problems 将 API level、缺失符号、STL、异常类型和错误依赖装载列为常见故障。attach/detach 表象也可能来自更早的 SO 加载、运行库或符号问题。例如线程尚未进入 JNI 守卫就因依赖失败退出,不能写成 detach 漏配。
Debug Android native code 说明 Native 调试和 tombstone 还原依赖正确符号、架构与构建产物。每条崩溃证据要绑定 APK SHA-256、SO 摘要、ABI、Build ID、符号包和线程 runId。使用另一构建的符号化结果会把调用点映射到错误位置。
公开报告应保留错误分类、候选身份和修复边界,不提供攻击复现链、内部地址或敏感 tombstone。调试器能停在线程退出处,不代表线上所有时序已覆盖;动态观察必须和自动化事件台账、重复运行和设备矩阵结合。
| 材料 | 绑定字段 | 用途 | 错配后果 |
|---|---|---|---|
| APK | 文件 SHA-256 | 锁定交付候选 | 旧回执误用 |
| SO | 摘要、ABI、Build ID | 定位实际二进制 | 错误符号化 |
| 符号包 | 同 Build ID | 还原 Native 堆栈 | 位置和函数错误 |
| 线程事件 | threadRunId 与 sequence | 重建状态机 | 线程 ID 复用混淆 |
| 设备环境 | 系统、ABI 和进程 | 限定复现范围 | 结论外推 |
逐 ABI 设备测试覆盖回调、并发和进程退出
Android instrumented tests 在真实 Android 环境执行,可访问组件和系统 API。JNI 线程回归应由设备测试触发 Java 线程回调、Native 短线程、固定 worker、第三方线程入口、取消、异常和进程重建,并通过受控事件接口读取状态机结果。
Android ABIs 说明各 ABI 具有独立调用约定、寄存器、对齐和设备支持范围。一个 arm64 设备上的 attach/detach 配对不能证明其他 ABI 的函数边界、运行库和 unwind 相同。每个承诺支持的 ABI 都应绑定实际打包 SO 与代表设备结果。
单一设备通过不能代表完整 API、ABI 和厂商矩阵。先用稳定设备复现状态机,再扩展到最低支持系统、不同 ABI、厂商和负载条件。通过门禁要求事件完整、无未处理异常、所有权正确和业务结果一致,但不把它写成加固保护强度结论。
- Java-owned 和 native-owned 线程分别测试
- GetEnv 的 attached 与 detached 分支都有覆盖
- 短线程与固定 worker 生命周期分别验证
- 早退、取消和异常均进入清理守卫
- pending JNI 异常在后续调用前处理
- 逐 ABI 绑定实际 SO 和设备回执
- 单设备通过不外推完整矩阵
用事件状态机检查 attach 与 detach 所有权配对
下面的 Python 示例读取 Native 线程事件台账和期望候选 SHA-256。每条事件包含 threadRunId、creator、sequence、event 和 candidateSha256。校验器按线程重放 get-env、attach、jni-call、异常、detach 与 thread-exit,检查连接状态和 ownedAttach。
java creator 必须先出现 get-env-attached,native creator 可从 get-env-detached 后 attach,也可借用外部已连接状态。只有 attach-ok 建立 ownedAttach 后才允许 detach-ok;在未连接或存在 pending exception 时执行 jni-call、detach 或退出都会被阻断。脚本不调用设备命令,也不解析敏感内存。
准备 JNI 线程连接评估时,可整理最终 APK、逐 ABI SO、JavaVM 保存点、线程创建者、事件台账、异常策略、符号材料和设备矩阵,再通过御盾中央平台提交申请。事件配对通过只证明观测状态机满足契约,不证明所有隐藏路径或加固兼容完整。
- 线程事件绑定精确候选摘要
- threadRunId 在单次生命期唯一
- creator 在同一线程事件中一致
- 只有本域 attach 才拥有 detach
- pending exception 阻止后续调用和退出
- thread-exit 前 ownedAttach 已清除
- 状态机失败返回非零状态
from pathlib import Path
import json
import re
import sys
if len(sys.argv) != 3:
raise SystemExit(2)
events_path = Path(sys.argv[1])
expected_candidate = sys.argv[2].lower()
if not events_path.is_file() or not re.fullmatch(r"[0-9a-f]{64}", expected_candidate):
raise SystemExit(2)
events = json.loads(events_path.read_text(encoding="utf-8"))
if not isinstance(events, list) or not events:
raise SystemExit(2)
allowed_creators = {"java", "native", "external"}
allowed_events = {"thread-start", "get-env-attached", "get-env-detached", "attach-ok", "jni-call", "jni-exception-pending", "jni-exception-cleared", "detach-ok", "thread-exit"}
groups = {}
for row in events:
run_id = str(row.get("threadRunId", ""))
creator = row.get("creator")
event = row.get("event")
sequence = row.get("sequence")
if not run_id or creator not in allowed_creators or event not in allowed_events or not isinstance(sequence, int):
raise SystemExit(2)
if str(row.get("candidateSha256", "")).lower() != expected_candidate:
raise SystemExit(2)
groups.setdefault(run_id, []).append(row)
violations = []
reports = []
for run_id, rows in sorted(groups.items()):
rows.sort(key=lambda row: row["sequence"])
if len({row["sequence"] for row in rows}) != len(rows):
raise SystemExit(2)
creator = rows[0]["creator"]
if any(row["creator"] != creator for row in rows) or rows[0]["event"] != "thread-start":
raise SystemExit(2)
attached = False
owned_attach = False
pending_exception = False
attach_count = 0
detach_count = 0
jni_calls = 0
exited = False
for row in rows[1:]:
event = row["event"]
if exited:
violations.append({"threadRunId": run_id, "reason": "event-after-exit"})
continue
if event == "get-env-attached":
if attached:
violations.append({"threadRunId": run_id, "reason": "duplicate-attached-state"})
attached = True
elif event == "get-env-detached":
if attached or creator == "java":
violations.append({"threadRunId": run_id, "reason": "invalid-detached-state"})
elif event == "attach-ok":
if attached:
violations.append({"threadRunId": run_id, "reason": "attach-while-attached"})
else:
attached = True
owned_attach = True
attach_count += 1
elif event == "jni-call":
if not attached or pending_exception:
violations.append({"threadRunId": run_id, "reason": "jni-call-invalid-state"})
jni_calls += 1
elif event == "jni-exception-pending":
if not attached or pending_exception:
violations.append({"threadRunId": run_id, "reason": "invalid-exception-pending"})
pending_exception = True
elif event == "jni-exception-cleared":
if not pending_exception:
violations.append({"threadRunId": run_id, "reason": "clear-without-pending"})
pending_exception = False
elif event == "detach-ok":
if not attached or not owned_attach or pending_exception:
violations.append({"threadRunId": run_id, "reason": "detach-without-ownership"})
else:
attached = False
owned_attach = False
detach_count += 1
elif event == "thread-exit":
if pending_exception or owned_attach:
violations.append({"threadRunId": run_id, "reason": "unclean-thread-exit"})
exited = True
if not exited:
violations.append({"threadRunId": run_id, "reason": "missing-thread-exit"})
reports.append({"threadRunId": run_id, "creator": creator, "attachCount": attach_count, "detachCount": detach_count, "jniCalls": jni_calls})
result = {"status": "pass" if not violations else "blocked", "candidateSha256": expected_candidate, "threads": reports, "violations": violations}
print(json.dumps(result, ensure_ascii=False, indent=2))
if violations:
raise SystemExit(3)事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| JNI 的线程、引用、异常与类加载边界会直接影响 Android Native 桥接稳定性。 | Android JNI tips 描述 JavaVM、JNIEnv、线程连接、异常和相关 JNI 实践。 | JNI 建议不能证明某个加固 SO 保持 ABI、线程时序或全部桥接语义。 |
| C++ 异常、RTTI 与 libc++ 链接方式取决于构建系统和运行库选择。 | Android C++ library support 描述 libc++、异常和 RTTI 配置。 | 启用编译选项不证明跨 SO 的 exception type_info、unwind 和线程清理完整。 |
| NDK 兼容故障常涉及 API level、缺失符号、STL、异常类型和依赖装载。 | Android NDK common problems 汇总常见 Native 构建与运行故障。 | 故障列表不能替代目标 ABI 的线程、启动、异常和退出回归。 |
| Native 调试与 tombstone 还原需要匹配的符号、架构和构建产物。 | Debug Android native code 描述 Native 调试与符号化所需材料。 | 调试能力不能作为生产兼容通过,也不应扩展为公开攻击复现链。 |
| 依赖真实 Android 运行时、组件和系统 API 的 JNI 语义可通过设备端测试验证。 | Android instrumented tests 说明 instrumented test 在真实 Android 环境执行。 | 单一设备通过不能代表完整 API、ABI、系统版本和厂商矩阵。 |
| 每个 Android ABI 具有独立调用约定、寄存器、对齐和设备支持范围。 | Android ABIs 描述 NDK 支持 ABI 的平台约定和特征。 | 声明支持某 ABI 不代表对应 SO 已构建、打包并完成线程回归。 |
| 只有当前线程责任域成功 attach 时,才拥有对应 detach 责任。 | 工程判断:所有权标记可以区分本域建立的连接与 Java 或第三方已建立的连接。 | 所有权契约需要真实事件证明,不能由 JNIEnv 非空或代码关键词出现推断。 |
| 线程池应按 worker 生命周期管理 JNI 连接,而不是按任务无条件连接和断开。 | 工程判断:task 与 worker 生命周期不同,按任务 detach 可能破坏后续任务的线程状态。 | 具体策略取决于线程池实现、回调来源和退出模型,仍需项目设备证据。 |
工程常见问题
可以把一个线程获得的 JNIEnv 保存给其他线程使用吗?
不可以。JNIEnv 与当前线程绑定,应保存 JavaVM,并让每个执行 JNI 的线程通过 GetEnv 获取自己的环境。
每次调用 JNI 前都执行 AttachCurrentThread 更安全吗?
不安全。应先 GetEnv;线程已连接时直接借用,只有未连接且由当前责任域 attach 成功时才记录 detach 责任。
Java 创建的线程返回 Native 方法前需要手工 detach 吗?
当前 Native 模块不应断开它没有建立的连接。Java 或运行时管理的线程只借用当前 JNIEnv,detach 所有权不属于该调用。
线程池应该每个任务 attach 和 detach 吗?
通常应按 worker 生命周期设计。任务和线程不是同一对象,无条件按任务断开可能让同一 worker 的后续任务进入错误状态。
DetachCurrentThread 能清除待处理 JNI 异常吗?
不能把 detach 当作异常处理。应先按项目策略检查、转换或清理 pending exception,再完成后续调用和线程清理。
申请 JNI 线程连接评估前需要哪些材料?
准备最终 APK、逐 ABI SO、JavaVM 保存点、线程创建者、状态事件、异常策略、符号材料和设备矩阵,再从御盾中央平台提交申请。