先看结论与判断条件
- fd 只是进程表中的短整数,同一数值关闭后可能被新资源复用,不能作为跨线程或跨模块的长期身份。
- borrow、duplicate 和 transfer 三种契约必须写进 JNI 方法名、参数对象或返回类型,不能靠注释和调用习惯约定。
- transfer 成功后原持有者立即进入 moved 状态,任何 read、write、close 或再次转移都是协议错误。
- duplicate 产生新的 fd 所有权,但相关文件状态是否共享需要按具体 API 契约判断,不能把复制当作完全独立文件。
- 关闭决策要覆盖正常、异常、取消、超时、JNI pending exception、SO 卸载尝试和进程终态,每条路径只有一个责任方。
- 加固、JNI 签名、异常翻译或库边界变化后,要用同一候选回放生命周期事件,发现借用方关闭、重复关闭和退出泄漏。
fd 数值不是资源身份,关闭后可能被复用
文件描述符是当前进程描述符表的索引。代码看到整数相同,只能说明某个时刻使用了同一槽位,不能证明它仍对应原文件、socket、管道或设备。原 owner 关闭后,系统可能把相同数值分配给新资源;迟到线程若继续使用旧整数,就会操作完全无关的对象。
跨 JNI 传递时应创建逻辑 resourceId,并在每次拥有关系变化时推进 generation。事件记录 resourceId、generation、fdSnapshot、owner、action 和 terminal。fdSnapshot 只用于调试,状态机按 resourceId 与 generation 判定。这样即使整数复用,旧 generation 的迟到 close 也会被门禁拒绝。
Java 对象引用同样不等于 Native 所有权。包装对象可能被关闭、复制或被另一个组件接管,Native 侧只保存整数会失去生命周期信息。桥接接口要把 ownershipMode 和 generation 作为显式参数或由受控句柄表维护,不从对象是否非空推断资源仍有效。
| 身份 | 有效范围 | 用途 | 不能证明 |
|---|---|---|---|
| fdSnapshot | 某一时刻进程内 | 系统调用参数与诊断 | 长期资源身份 |
| resourceId | 逻辑资源生命周期 | 串联跨层事件 | 当前 fd 仍打开 |
| generation | 所有权代际 | 拒绝迟到操作 | 底层资源类型 |
| ownerId | 当前责任组件 | 决定唯一 close | 线程仍存活 |
| candidateId | App 与 SO 候选 | 绑定测试回执 | 其他候选行为 |
borrow、duplicate 和 transfer 必须三选一
borrow 表示调用期间临时使用,所有权仍属于调用方。借用方不得关闭、缓存到调用结束之后或交给异步任务,除非合同另行延长借用期。JNI 方法返回前,借用令牌失效;Java 侧可以在确认无未完成借用后关闭。异常返回不会把 borrow 自动升级为 transfer。
duplicate 表示通过明确复制操作获得新 fd,接收方负责关闭自己的副本。复制失败时所有权不变;成功后两个 owner 各有唯一 close 责任。副本可能共享底层打开文件描述及偏移等状态,具体语义按所用 API 核对,因此并发读写仍需业务协调,不能因为整数不同就假设完全隔离。
transfer 表示接收方在成功点接管原 fd,发送方立即把本地状态改为 moved 并清除可调用入口。成功点必须唯一且可记录,通常是接收方确认登记资源之后。若传递中途失败,契约说明所有权回滚给发送方还是由接收方清理,不能让双方都认为对方负责。
| 模式 | 发送方状态 | 接收方权限 | 关闭责任 |
|---|---|---|---|
| borrow | 继续 owner | 限定期限内使用 | 发送方 |
| duplicate pending | 继续 owner | 尚不可使用 | 发送方 |
| duplicate success | 继续 owner | 拥有新副本 | 双方各关自己的 fd |
| transfer pending | 按协议冻结 | 尚未接管 | 由失败策略决定 |
| transfer success | moved | 成为唯一 owner | 接收方 |
| transfer rollback | 恢复 owner | 不得使用 | 发送方 |
接口要让所有权从类型和名称上可见
JNI 方法若只接收 jint fd,调用点无法看出它会借用还是接管。更清楚的设计是拆成 nativeReadBorrowed、nativeDuplicateForWorker 和 nativeTakeOwnership,或者传递带 ownershipMode、resourceId 和 generation 的不可变请求对象。返回值包含 acceptedGeneration 与 terminal,调用方据此更新状态。
多个 SO 之间的 C 接口也要维持同一语义。函数名、头文件、版本和返回状态明确 transfer 成功点,结构体不放语言运行库所有权对象。若库内部再把资源交给另一个模块,生成新的 owner event;外部调用方不需要知道内部 fd 数值,但要能看到自己的 ownership 已经结束。
日志字符串不能充当协议。诸如“fd accepted”或“already closed”容易随语言与实现变化,也不能驱动 Java 状态。公开结果使用有限枚举 accepted、rejected、moved、closed 和 invalid_generation;诊断文本仅供人工分析,不触发再次 close 或重试。
| 字段 | 发送方用途 | 接收方用途 | 缺失风险 |
|---|---|---|---|
| resourceId | 关联原对象 | 建立本地表项 | 整数复用混淆 |
| generation | 拒绝迟到回调 | 验证当前代际 | 旧 owner 继续使用 |
| ownershipMode | 决定失效时机 | 决定 close 责任 | 双方重复关闭 |
| status | 更新状态机 | 说明接受结果 | 解析日志文字 |
| candidateId | 绑定实现版本 | 绑定事件来源 | 跨构建混算 |
| closeReceiptId | 确认唯一关闭 | 防止重复 close | 退出泄漏不可追踪 |
异常、取消与 pending exception 必须走同一清理状态机
Native 操作可能在登记前、登记后、实际 I/O、回调或清理阶段失败。每个阶段都要说明当前 owner。异常路径不能跳过状态迁移,也不能在 catch 中无条件 close 原始整数。统一出口根据 state 决定:owned 才允许关闭,borrowed 只结束借用,moved 不做 close,closed 仅记录重复请求。
协程或任务取消不是资源关闭的同义词。取消方先标记任务终态并阻止新 I/O,再由当前 owner 执行 close;异步工作线程结束后返回 close receipt。若 Java 页面销毁但 Native 工作仍按合同执行,页面不能直接关闭 borrowed fd,需通过 owner 的取消接口协调。
JNI pending exception 存在时,Native 仍要完成允许的最小资源状态更新,但不要继续复杂 JNI 调用。close 失败保存为 secondary error,不能覆盖原 pending exception。返回 Java 后,上层根据单一公开终态处理,不同时从返回值、异常和日志猜测资源是否已经关闭。
| 当前状态 | 事件 | 允许动作 | 禁止动作 |
|---|---|---|---|
| owned | 正常完成 | owner close | 交给借用方关闭 |
| owned | 取消 | 停止新 I/O 后 close | 并发无序 close |
| borrowed | 调用结束 | 释放借用令牌 | close fd |
| moved | 任意迟到事件 | 拒绝并记录 | read、write 或 close |
| closed | 再次 close | 标记 duplicate_close | 触达复用整数 |
| pending exception | JNI 返回前 | 最小状态清理 | 复杂 JNI 日志 |
跨线程时先停止新使用,再由 owner 关闭
一个线程关闭 fd 时,另一个线程可能正在使用相同整数。系统调用终态与平台行为要按具体 API 核对,应用层不能依赖“close 会自动安全唤醒所有等待者”。资源对象需要 acceptingOperations 标志、活动操作计数和 owner generation。关闭流程先拒绝新操作,再等待或取消已登记操作,最后由 owner 执行一次 close。
线程回调携带 resourceId 与 generation,完成时再次核对。旧 generation 的结果不得写入新资源,即便 fdSnapshot 碰巧相同。若工作池需要长期持有,应使用 duplicate 或 transfer,不让短期 borrow 越过方法返回。引用计数只管理逻辑借用,不取代底层唯一 owner 的 close 责任。
SO 卸载尝试也是生命周期事件。若库中仍有 owner、活动操作或关闭回调,卸载会让函数指针和清理代码失效。移动 App 通常让核心 Native 库随进程常驻更容易证明;需要动态模块时,先完成 drain、验证 close receipts,再允许卸载,未完成资源阻止切换。
测试要覆盖整数复用、失败交接和进程终态
普通成功用例只能证明一条路径。回归矩阵还要覆盖 borrow 方误关、duplicate 失败、transfer 登记前失败、transfer 成功后旧 owner 使用、取消与 close 竞态、close 失败、JNI pending exception、线程池迟到回调和模拟整数复用。每个用例断言状态机终态,而不是只检查没有崩溃。
instrumented test 可以从 Android 应用上下文创建受控资源、触发 JNI、重建页面和取消任务。测试使用临时公开安全数据,不碰真实用户文件。每份回执绑定 App、SO、ABI、设备、resourceId 事件摘要和候选哈希;单一设备通过不代表全部系统与厂商。
进程退出时,操作系统会回收进程资源,但这不能证明应用生命周期正确。退出前泄漏会造成长期运行资源耗尽,也可能掩盖未完成写入。ApplicationExitInfo 用于把异常退出与候选和时间窗关联;正常测试仍要在进程存活时验证所有 owned 资源都有唯一 close receipt。
| 用例 | 预期 owner | 预期终态 | 关键断言 |
|---|---|---|---|
| borrow success | 发送方 | borrow ended | 借用方未 close |
| duplicate success | 双方 | 两个 close receipt | 各关自己的副本 |
| transfer success | 接收方 | 发送方 moved | 旧 generation 被拒绝 |
| transfer failure | 按回滚策略 | 单一 owner | 没有悬空责任 |
| cancel race | 当前 owner | closed 或明确失败 | 只有一次 close |
| fd reuse | 新 resourceId owner | 旧事件拒绝 | 不按整数关联 |
用生命周期事件门禁发现重复关闭和退出泄漏
事件文件可以使用抽象 resourceId、generation、actor、action 和 mode,不记录真实路径或用户内容。检查器为每个资源维护 owner、状态和最后代际,按顺序处理 create、borrow、borrow_end、duplicate、transfer、use、close 和 process_end。任何借用方关闭、moved 后使用、重复 close 或退出仍有 owner 都拒绝。
下面的 Python 代码从命令行读取事件。输入缺失时直接非零退出;它要求 sequence 严格递增、create 唯一、actor 与 owner 匹配,并在 process_end 检查所有资源已关闭或明确 transferred_external。示例不执行真实 I/O,不提供内网或攻击步骤,只验证资源账本。
真实项目要扩展 duplicate 新 resourceId、transfer 回滚、并发操作计数和 close failure,但不能放松单一 owner 原则。门禁通过只证明事件自洽,采集点是否覆盖所有 JNI 与 SO 入口仍需代码审查和设备回执确认。
import json
import sys
from pathlib import Path
def stop(message):
print(f"fd lifecycle rejected: {message}", file=sys.stderr)
raise SystemExit(2)
def load_events(path_text):
path = Path(path_text)
if not path.is_file():
print("fd lifecycle rejected: event file is missing", file=sys.stderr)
raise SystemExit(2)
try:
return json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
stop(f"cannot parse events: {exc}")
def validate(data):
events = data.get("events")
if not isinstance(events, list) or not events:
stop("events must be a non-empty list")
resources = {}
previous_sequence = -1
for event in events:
sequence = event.get("sequence")
if type(sequence) is not int or sequence <= previous_sequence:
stop("event sequence is not strictly increasing")
previous_sequence = sequence
resource = event.get("resourceId")
action = event.get("action")
actor = event.get("actor")
if action == "create":
if not resource or resource in resources or not actor:
stop("create has an invalid resource or owner")
resources[resource] = {"owner": actor, "state": "owned", "borrower": None}
continue
if action == "process_end":
leaked = [key for key, value in resources.items() if value["state"] == "owned"]
if leaked:
stop(f"process ended with owned resources: {sorted(leaked)}")
continue
state = resources.get(resource)
if state is None:
stop("event references an unknown resource")
if action == "borrow":
if state["state"] != "owned" or actor == state["owner"]:
stop("borrow has an invalid owner or borrower")
state["borrower"] = actor
elif action == "borrow_end":
if actor != state["borrower"]:
stop("borrow_end comes from the wrong actor")
state["borrower"] = None
elif action == "transfer":
if actor != state["owner"] or not event.get("newOwner"):
stop("transfer comes from the wrong owner")
state["owner"] = event["newOwner"]
elif action == "use":
if actor not in {state["owner"], state["borrower"]} or state["state"] != "owned":
stop("use comes from an actor without ownership")
elif action == "close":
if actor != state["owner"] or state["state"] != "owned":
stop("close is duplicated or comes from a non-owner")
state["state"] = "closed"
else:
stop(f"unknown action: {action}")
print(f"fd lifecycle accepted: {len(events)} events")
if __name__ == "__main__":
if len(sys.argv) != 2:
stop("usage: check_fd_events.py events.json")
validate(load_events(sys.argv[1]))发布门禁绑定候选、契约和唯一 close 回执
发布前列出所有跨 Java、JNI 和 SO 的 fd 入口,为每个入口指定 ownershipMode、成功点、失败回滚、取消策略和 close owner。静态接口审查后运行事件矩阵,门禁确认每个 create 最终只有一个 close,borrow 方从不 close,transfer 后旧 owner 的任何事件都被拒绝。
加固、JNI 签名、异常翻译、线程模型、动态库边界或 API level 变化后,重新生成候选哈希并重跑关键矩阵。旧事件可以帮助比较,不能替新候选签字。若描述符泄漏只在长时间运行出现,增加受控循环与资源计数,但不虚构泄漏已消失。
站内的 JNI 错误转换文章可继续检查 close 失败和 pending exception 的公开终态;准备评估商业 App 的 SO 加固与跨层资源契约时,可通过页面行动按钮进入御盾中央平台提交候选包和 fd 接口清单。提交只启动评估,关闭责任结论仍以同一候选回执为准。
- 所有 fd 入口显式选择 borrow、duplicate 或 transfer。
- resourceId 与 generation 取代整数作为长期身份。
- 异常、取消和 pending exception 进入同一资源状态机。
- 借用方从不关闭,转移后原 owner 立即失效。
- 正常与失败路径都产生唯一 close receipt。
- 候选变化后重跑复用、竞态、卸载与退出矩阵。
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| JNI 线程、引用、异常和类加载边界会影响跨层资源清理。 | Android JNI tips 说明 JNI 注册、线程、引用和异常实践。 | 官方建议不证明加固后的目标 SO 生命周期完整。 |
| 高于 minSdk 的 Native API 需要独立版本化处理。 | Android NDK stable APIs 说明静态调用与必要动态解析边界。 | API 可用不证明特定 namespace 可达,也不定义 fd 所有权。 |
| API level、符号、运行库和依赖装载问题可能干扰清理路径。 | Android NDK common problems 汇总常见 Native 兼容故障。 | 问题清单不能替代目标 ABI 的异常和资源回归。 |
| 异常退出需要与候选、时间窗和用户路径关联。 | Android ApplicationExitInfo 提供退出原因及相应平台诊断数据入口。 | 进程退出回收资源不证明存活期没有泄漏或重复关闭。 |
| 跨生命周期、线程和 JNI 的资源行为应在 Android 设备验证。 | Android instrumented tests 说明设备端测试可访问应用上下文和平台 API。 | 单一设备通过不能覆盖全部系统、ABI、厂商和竞态。 |
| 资源契约变化应保留来源、构建、验证和变更证据。 | NIST SP 800-218 SSDF 提供安全开发与供应链治理实践。 | 组织级框架不定义 fd 状态机,也不证明候选通过。 |
| 借用方关闭、转移后使用和重复 close 必须阻止发布。 | 工程判断:这些事件违反单一 owner 与代际约束,可能操作复用后的无关资源。 | 事件采集完整性仍需结合代码审查和设备测试确认。 |
| 当前不能声称目标 App 的 fd 关闭责任已完成验证。 | 项目证据尚未接入;需要候选 SO、接口契约、事件矩阵和设备回执。 | 文章只提供方法和检查代码,不替代运行验收。 |
工程常见问题
fd 整数相同是否说明还是原来的文件?
不能。关闭后整数槽位可能被新资源复用,长期身份应使用 resourceId 与 generation,fd 只作当时系统调用参数。
JNI 方法拿到 fd 后默认应该关闭吗?
没有默认答案。接口必须明确 borrow、duplicate 或 transfer;borrow 方不关,duplicate 各关副本,transfer 成功后由接收方关闭。
dup 后两个 fd 是否完全互不影响?
整数和 close 责任独立,但底层打开文件状态可能共享,偏移等语义按具体 API 核对,仍需并发与业务协调。
协程取消时页面直接 close 能否快速停止 Native I/O?
可能造成竞态和错误 owner 关闭。应通过当前 owner 的取消协议停止新操作、处理活动任务,再产生唯一 close receipt。
进程退出会回收 fd,为什么还要查泄漏?
长期运行中泄漏会耗尽资源并影响业务,退出回收也不能证明写入和清理完成。测试要在进程存活时核对所有 owner。
SO 加固后为什么要重跑 fd 生命周期?
加固可能改变 JNI、异常、线程和库边界。候选变化后,旧 close 回执不能证明新 SO 没有重复关闭或迟到使用。