先看结论与判断条件

  • 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线程仍存活
candidateIdApp 与 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 successmoved成为唯一 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 exceptionJNI 返回前最小状态清理复杂 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当前 ownerclosed 或明确失败只有一次 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 没有重复关闭或迟到使用。

想用自己的 App 验证?

提交候选包、目标系统和关键业务路径,申请御盾 PoC 与兼容性评估。

继续阅读: SO 加固与 Native 兼容性检查清单