先看结论与判断条件

  • 信号处理器安装是进程级共享状态,后安装组件可能覆盖业务库、诊断 SDK 或运行时先前登记的 action。
  • 链式转交不能只保存函数地址,还要保留 sigaction 的 handler 形态、mask、flags 与 SIG_DFL、SIG_IGN 语义。
  • 处理器只执行预分配、无锁、异步信号安全的最小操作,格式化日志、堆分配、JNI 和普通锁都移到信号上下文外。
  • 重入保护必须区分首次进入和同线程或跨线程再次触发,递归时直接进入预先定义的最小终态。
  • 诊断采集、业务恢复和进程终止是不同目标;致命同步信号通常不能被当作普通异常后安全继续。
  • 加固、SDK 升级、装载顺序或链接变化后,要重新核对实际安装链、同候选符号和受控故障回执。

一个信号只有一个当前 action,所有组件共享

Native 库调用 sigaction 安装处理器时,修改的是进程对该信号的当前处置,而不是为自己增加一个独立监听器。后安装的组件如果没有保存旧 action,就会让之前的诊断、运行时或业务处理器失去入口。问题常在装载顺序变化后出现:调试包正常,发布包因加固、懒加载或 SDK 初始化时机不同而只剩最后一个处理器。

治理第一步是建立 signal registry。每个组件声明 ownerId、信号集合、installPhase、expectedPreviousOwner、chainPolicy 和 uninstallPolicy。registry 是发布期契约,不依赖运行日志猜顺序;实际安装后再写入 receipt,记录观察到的前任 action 摘要和当前 action 摘要。声明与观察不一致时阻止完成。

系统默认、忽略和自定义 handler 是不同前任类型。SIG_DFL 不能当作空指针跳过,SIG_IGN 也不能被无条件改成默认终止。自定义 action 还可能使用 sa_handler 或 sa_sigaction 两种入口。链式代码必须根据 flags 选择正确签名,不可把三参数函数按一参数调用。

信号登记的进程级角色
角色安装目的前任处理退出责任
运行时或系统平台运行与默认处置由后续组件识别按平台语义
业务 Native 库受控状态或诊断保存并按策略转交不得假设可恢复
诊断 SDK最小崩溃记录链到前任或默认采集后交还终态
加固运行层完整性或诊断不能静默覆盖声明所有权边界
测试探针验证安装链仅测试候选启用测试结束恢复

保存完整 sigaction,而不是一个旧函数指针

完整前任信息包括入口联合体、sa_mask 和 sa_flags。mask 决定处理期间额外阻塞哪些信号,flags 影响三参数入口、自动重置、重启系统调用和替代信号栈等行为。只保存 oldHandler 地址,再用当前组件自己的 mask 与 flags 调用,会改变前任原有语义,产生重复进入或参数不匹配。

安装过程应先构造不可变新 action,再用 sigaction 原子取得旧 action。旧 action 保存到预分配表,表项在处理器可见前已经完整发布。不能在信号处理器第一次执行时才分配链节点或查全局容器;故障可能发生在初始化中途,半构造链比没有诊断更难排查。

卸载同样需要所有权校验。只有当前 action 仍然属于本组件时,才可恢复安装时保存的前任;若另一个组件已经接管,盲目恢复会覆盖新处理器。移动 App 中 Native 库通常随进程常驻,减少运行时卸载能缩小竞态,但测试和动态模块仍需显式卸载契约。

sigaction 字段的链式含义
字段作用链式要求遗漏后果
sa_handler单参数入口按 handler 形态调用签名不匹配
sa_sigaction三参数入口仅对应 flag 启用时调用上下文丢失
sa_mask处理期间阻塞集保留前任语义递归或顺序漂移
sa_flags行为选择逐位审计与保存重置或重启变化
previousKind默认、忽略、自定义分别决策错误终态
ownerGeneration安装代际卸载前核对覆盖后来处理器

处理器内部只做最小异步信号安全工作

信号可能打断任意线程在任意函数中的执行。被打断代码也许正持有 malloc、日志、JNI、C++ 运行库或业务锁;处理器再次调用这些设施会死锁、破坏内部状态或触发另一个信号。普通应用代码中安全的操作,在异步信号上下文中不自动安全,必须依据目标平台和函数契约建立 allowlist。

常见安全设计是预分配固定大小记录区,处理器只写入信号号、有限上下文标识和原子重入状态,再通过受控文件描述符发出最小通知。符号化、JSON 序列化、网络上传、类查找和 UI 提示都在进程外或恢复到普通执行上下文后完成。记录区满时丢弃附加信息,也不在处理器中扩容。

公开登记应列出 declaredOperations,而不是贴完整实现或地址。门禁只允许如 write_fixed_record、set_atomic_flag、chain_previous 和 restore_default 等抽象操作;出现 allocate、format_log、jni_call、lock 或 network_send 就拒绝。抽象清单不能证明机器码合规,仍需代码审查和同候选测试。

信号上下文中的操作分类
操作类别处理器内策略替代位置失败处理
固定记录写入按平台 allowlist处理器内最小执行记录丢失不递归
原子重入标记预先验证实现处理器内进入最小终态
格式化日志禁止普通线程或进程外只保留原始字段
内存分配禁止初始化阶段预分配容量不足降级
JNI 调用禁止恢复后的普通路径不触达 Java
锁与网络禁止后台采集器不等待

重入保护要覆盖递归、并发与链式回环

处理器自身发生故障、前任处理器再次触发同一信号,或两个组件错误地互相链回,都可能形成递归。单纯依赖默认 mask 不够,因为 flags、不同信号和跨线程并发会改变行为。每个信号表项需要预分配 reentry state,首次进入记录 owner 和 generation,再次进入不执行复杂采集,直接采用最小终态。

链式回环来自登记错误:A 保存 B 为前任,而 B 的当前配置又把 A 当作前任。registry 门禁应构建每个信号的有向链,验证没有重复 owner、没有循环、最终落在 default、ignore 或显式 terminal owner。运行时 receipt 再核对实际 previousOwner,不能只相信静态声明。

跨线程同时触发时,一个全局布尔值可能让第二个线程误走恢复路径,也可能让首次线程退出后标记永不清除。对于致命故障,目标通常是确保采集有限且终态确定,而不是继续业务;状态机要定义 first_entered、reentered 和 terminal 三种状态,具体原子实现需按平台审查。

重入状态机的决定
状态允许动作链式行为终态
idle登记首次故障按策略转交由链决定
first_entered写最小记录至多一次转交等待明确终态
reentered跳过复杂采集不回到已访问 owner立即最小终态
terminal不再业务处理不继续链默认或受控退出
unknown_generation拒绝使用旧表恢复已知默认记录配置失配

诊断采集不能伪装成可恢复业务异常

同步致命信号通常意味着当前指令、内存访问或执行状态已经不可安全继续。处理器可以保存最小证据并把处置交给前任或系统默认,但不应通过简单修改返回地址、清除标志后继续业务。恢复策略必须由明确的平台和业务契约证明,不能把一次未立即崩溃当作恢复成功。

诊断 SDK 的职责是采集有限上下文,不拥有最终业务语义。若前任是默认处置,SDK 采集后需要恢复或重新触发明确定义的终态;若前任是业务处理器,则按保存的完整 action 转交。链中每个 owner 只执行一次,不能因为都想上传日志而相互调用。

信号事件与 ANR 也要分开。ANR 多由主线程阻塞、锁、Binder、I/O 或组件超时形成,不等于 Native 致命信号。崩溃处理器里做阻塞 I/O 反而可能制造卡顿或丢失系统记录。退出证据、ANR trace 和业务时间线分别采集,再在普通分析环境关联。

同候选符号和退出记录决定证据能否复核

Native 崩溃地址只有匹配正确架构、同一构建符号和目标候选时才可可靠还原。处理器链变化常伴随库装载与链接变化,版本名相同也可能包含不同 SO。每份受控故障回执要记录 App 候选、SO 哈希、ABI、signal registry 摘要和符号包身份。

ApplicationExitInfo 可在相应平台版本提供退出原因和诊断数据入口,但仍需按候选、时间窗与测试路径关联。若自定义处理器改变默认终态或进程快速重启,系统记录可能与预期不同。缺少退出记录时标记证据缺口,不用自定义日志中的“caught signal”替代系统事实。

ndk-stack 一类符号化工具可把地址映射到同构建符号,但符号化成功不自动证明处理器覆盖、递归或某个 SDK 是根因。需要结合安装 receipt、重入状态、链式访问顺序和退出信息判断。公开报告保留摘要和判定,不披露私有符号、内存地址或故障复现链。

受控故障回执的证据层
证据回答问题绑定身份不能证明
install receipt实际安装顺序与前任SO 与 registry处理器代码安全
signal record哪个 owner 首次进入候选与 generation故障根因
reentry state是否发生再次进入signal 与 owner为何再次触发
system exit系统观察的终态版本与时间窗具体责任函数
symbolization地址对应符号同构建符号包链式策略正确
device test目标环境行为设备与候选未覆盖矩阵通过

用登记门禁检查重复占用、回环和不安全声明

signal registry 可以使用公开安全 JSON,每个 signal 包含 handlers 数组,按安装顺序列出 owner、previousOwner、chainPolicy、declaredOperations 和 terminal。门禁不安装真实处理器,也不触发故障,只验证静态契约:owner 唯一、前任连接连续、操作都在 allowlist、链无回环且最终终止。

下面的 Python 校验器从命令行读取 registry。输入缺失时直接非零退出;随后拒绝空信号表、重复 owner、前任不等于上一节点、不安全操作、递归 owner 和没有 terminal 的链。它使用抽象 owner 和操作名,不包含内部地址、客户数据或可运行攻击代码。

静态门禁通过后还要在目标候选读取实际 action 摘要,与 registry 对比。某些 SDK 可能在运行后重新安装处理器,生命周期测试需覆盖初始化、登录、模型加载、后台再前台和动态模块启用。实际链与声明不同即阻止完成,不通过修改报告让它看起来一致。

Native 信号处理器登记校验器
import json
import sys
from pathlib import Path

SAFE_OPERATIONS = {"write_fixed_record", "set_atomic_flag", "chain_previous", "restore_default"}

def stop(message):
    print(f"signal registry rejected: {message}", file=sys.stderr)
    raise SystemExit(2)

def load_registry(path_text):
    path = Path(path_text)
    if not path.is_file():
        print("signal registry rejected: registry 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 registry: {exc}")

def validate(data):
    signals = data.get("signals")
    if not isinstance(signals, list) or not signals:
        stop("signals must be a non-empty list")
    signal_ids = set()
    for entry in signals:
        signal_id = entry.get("signal")
        if not signal_id or signal_id in signal_ids:
            stop("signal ids must be present and unique")
        signal_ids.add(signal_id)
        handlers = entry.get("handlers")
        if not isinstance(handlers, list) or not handlers:
            stop(f"{signal_id} has no handler chain")
        owners = set()
        previous = "system"
        for index, handler in enumerate(handlers):
            owner = handler.get("owner")
            if not owner or owner in owners:
                stop(f"{signal_id} handler {index} repeats an owner")
            owners.add(owner)
            if handler.get("previousOwner") != previous:
                stop(f"{signal_id} handler {index} breaks the previous-owner chain")
            operations = set(handler.get("declaredOperations", []))
            if not operations or not operations.issubset(SAFE_OPERATIONS):
                stop(f"{signal_id} handler {index} declares unsafe operations")
            previous = owner
        if handlers[-1].get("terminal") not in {"default", "ignore", "exit"}:
            stop(f"{signal_id} has no explicit terminal policy")
    print(f"signal registry accepted: {len(signals)} signals")

if __name__ == "__main__":
    if len(sys.argv) != 2:
        stop("usage: validate_signal_registry.py registry.json")
    validate(load_registry(sys.argv[1]))

发布门禁按安装链、故障回执和恢复行为验收

发布前固定 App、SO、registry 与符号包身份,先验证静态链,再在隔离设备用公开安全的受控测试接口触发预定义故障类别。测试不暴露生产攻击入口,只在测试候选启用。回执核对安装顺序、每个 owner 至多进入一次、重入走最小终态、系统退出与符号化能够关联。

加固、SDK、装载顺序、链接参数或运行时版本变化后,要重新读取实际 action 并重跑关键矩阵。测试包括冷启动、懒加载前后、后台恢复、动态模块启用和组件卸载尝试。旧候选的链式结果不能替新 SO 签字,单台设备通过也不能代表全部 ABI 和厂商环境。

站内的 Native 崩溃符号化文章可继续检查地址与同构建符号绑定;准备评估商业 App 的 SO 加固和信号处理器兼容时,可通过页面行动按钮进入御盾中央平台提交候选包、SDK 清单和 registry。提交只启动评估,链式安全与退出结论仍以同一候选回执为准。

  • 每个信号记录所有者、安装阶段和完整前任 action。
  • 异步信号上下文操作使用经过平台审查的最小 allowlist。
  • 链条没有重复 owner、回环或缺失 terminal。
  • 重入时跳过复杂采集并进入明确最小终态。
  • 系统退出、符号包和安装 receipt 绑定同一候选。
  • SDK、加固或装载顺序变化后重新验证实际 action。

事实依据与适用边界

以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。

本文判断事实或工程依据适用限制
Native 调试和崩溃还原需要正确架构、候选与符号。Debug Android native code 说明 Native 调试所需构建和符号条件。可调试不证明处理器链正确,也不能公开形成故障攻击链。
Native 地址符号化必须使用同一构建的未剥离符号。ndk-stack 说明崩溃地址与符号目录的还原要求。符号化成功不证明覆盖、递归或某个 owner 是根因。
退出记录需要与候选版本、时间窗和测试路径关联。Android ApplicationExitInfo 提供退出原因及相应平台诊断数据入口。系统记录不自动说明链中哪个处理器首先破坏状态。
ANR 与 Native 致命信号需要分别归因。Diagnose Android ANRs 按主线程、锁、Binder、I/O 和组件超时分层诊断。trace 表象帧不一定是初始阻塞源,也不证明信号处理器参与。
实际安装链和生命周期行为应在 Android 设备回归。Android instrumented tests 说明设备端测试可访问应用上下文和系统 API。单一设备通过不能覆盖全部 API、ABI、厂商和 SDK 组合。
处理器变更需要保留来源、构建、验证和变更证据。NIST SP 800-218 SSDF 提供安全开发与供应链治理实践。组织级框架不定义具体信号链,也不证明某候选通过。
重复 owner、链式回环或不安全操作声明必须阻止发布。工程判断:这些配置会破坏单一当前 action 与异步信号安全边界。静态登记仍需与目标候选实际 action 和机器码核对。
当前不能声称目标 App 的信号处理器链已经安全。项目证据尚未接入;需要候选 SO、registry、设备故障和系统退出回执。文章只提供方法和门禁代码,不替代实际验收。

工程常见问题

多个 SDK 都调用 sigaction,系统会自动依次通知吗?

不会。进程对一个信号只有当前 action,后安装者必须保存并按契约转交前任,否则之前处理器会被覆盖。

只保存旧 handler 函数地址为什么不够?

还需要区分 handler 签名,并保存 sa_mask、sa_flags、默认与忽略语义;遗漏这些字段会改变前任执行条件。

信号处理器里能不能直接写完整 JSON 日志?

不应。格式化、分配、锁和普通日志设施可能不具备异步信号安全性,应只写预分配固定记录,复杂工作移到信号上下文外。

捕获 SIGSEGV 后跳过故障指令继续运行可行吗?

不能作为通用恢复策略。执行状态和内存可能已经不可靠,处理器应最小采集并进入前任或默认终态,除非有严格平台契约证明可恢复。

有 tombstone 是否就能证明处理器递归?

不能。还需安装顺序、重入状态、链式访问记录和同构建符号,tombstone 只提供崩溃上下文的一部分。

SO 加固后为什么要重新检查信号链?

加固可能改变装载顺序、初始化入口、符号和运行层处理器。候选哈希变化后,旧 action 与退出回执不能证明新包行为。

想用自己的 App 验证?

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

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