先看结论与判断条件

  • JNI 参数的 Java 类型只给出传输宽度,业务协议还必须定义符号、单位、上界和错误终态。
  • 有符号 jlong 转无符号 size_t 前先拒绝负值,再比较 SIZE_MAX,不能依赖实现相关的隐式转换。
  • 32 位与 64 位 ABI 的 size_t 上限不同,同一个 Java 值可能只在部分 ABI 可表示。
  • count 乘 elementSize 在乘法前检查上界,溢出后的较小结果不能进入分配或缓冲区访问。
  • offset、length、index、timestamp 和 handle 不共享同一转换函数,单位和语义必须跟着字段走。
  • 候选变化后在实际 ABI 和设备上回归边界值、异常映射与调用路径,静态编译不能替代运行证据。

先写数值协议,再写任何 cast

JNI 方法签名里的 jlong 只说明 Java long 通过桥接层传来一个有符号定宽整数,并没有说明它代表字节数、元素个数、文件偏移、时间、句柄还是状态码。Native 目标类型也各有语义:size_t 表达对象大小和计数,off_t 常用于偏移,平台 API 可能接收 int 或其他宽度。若协议缺失,开发者往往把能编译当成能转换。

每个字段应在接口表中固定 name、semanticType、unit、signedness、minimum、maximum、zeroMeaning 和 failureMapping。比如 payloadLength 是非负字节数,zero 表示空输入;startOffset 可以非负但不一定能用 size_t;objectHandle 是不透明身份,禁止参与算术。转换函数按语义命名,避免一个 genericLongToNative 被所有入口滥用。

检查顺序必须是读取原值、验证协议、验证目标范围、执行转换,最后才做分配、加法、指针移动或系统调用。先 static_cast<size_t>(value) 再判断 result > limit 会让负数在无符号域中变成大值,也可能让超宽值先被截断。错误应包含字段名、原因和目标 ABI 类别,不回显敏感数据。

常见 JNI 数值字段的协议差异
字段语义Java 载体Native 目标必须检查
字节长度jlongsize_t非负、上限、分配预算
元素数量jlongsize_t非负、乘法上界
文件偏移jlongoff_t类型上限、API 语义
数组索引jint 或 jlong索引类型非负、容器长度
时间值jlong明确时间类型单位、历元、溢出
对象句柄jlong不透明表项代际和状态,禁止算术

有符号转无符号必须先处理负值

jlong 是有符号整数,size_t 是无符号类型。将负 jlong 直接转换为 size_t 不会得到负数,而会按无符号表示产生一个很大的结果。若该值随后用于 vector.resize、malloc、memcpy 长度或数组边界,错误可能表现为异常分配、拒绝服务、越界检查失真或平台间不一致,而不是一个明显的负数错误。

安全转换首先在 jlong 域判断 value < 0,失败就返回 negative_length。通过后,再把 SIZE_MAX 安全表示为可比较上界。若目标 ABI 的 size_t 不比 jlong 窄,非负 jlong 通常可表示;若更窄,则以不引发截断的方式比较上限,确认后才 static_cast。模板或 checked conversion 库也要审查其失败语义。

零值需要按字段定义。长度为零可能是合法空缓冲区,也可能违反协议;句柄为零通常作为无效值;偏移为零通常合法。不能在通用转换层把零一律拒绝或接受。转换结果应携带语义类型,后续 API 不再把 size_t 零猜成未初始化、空内容或 EOF。

符号转换的输入分类
输入类别转换前判断允许 cast终态
负值value 小于零negative_value
按字段 zeroMeaning条件允许empty 或 invalid
目标范围内不超过目标上限converted
超过目标上限与 ABI 上限比较out_of_range
单位未知协议没有 unitmissing_unit

ABI 决定目标宽度,构建配置不能靠主机推断

Android ABI 不只是库目录名称,还关联调用约定、寄存器、对齐和数据模型。size_t 的宽度由目标 ABI 决定,开发机和 CI 主机的类型宽度不代表 APK 中目标库。代码若只在桌面单元测试编译,可能永远走 64 位分支;到了 32 位设备,同一个合法 jlong 会超过 size_t 上限。

转换边界应由目标编译单元的 sizeof(size_t) 和 numeric_limits 推导,并把 ABI 名称写入测试回执。不能通过 Java 的 Build.SUPPORTED_ABIS 猜测当前 Native 库的数据模型,也不能因为 APK 声明某 ABI 就认为对应 so 已构建、打包并加载。产物检查、安装启动和调用回归是不同证据。

跨 ABI 传输的持久化格式不要直接序列化 size_t 或平台 struct。磁盘和 IPC 协议选择定宽整数与明确字节序,读取后再做目标范围检查。否则 64 位进程写出的计数在 32 位进程读取时可能截断,结构体对齐也可能变化。本文只回答整数转换,不把结构体 ABI 展开成第二套主题。

ABI 相关门禁
门禁输入检查位置证据
目标类型宽度编译目标Native 编译单元sizeof 与 limits
so 打包APK 或 AAB产物检查每个 ABI 文件哈希
库装载设备 ABI 与 minSdk启动测试实际加载回执
边界调用临界 jlong 用例设备端 JNI成功或明确异常
持久化读取定宽字段解析器范围和字节序回执

长度乘法和偏移加法必须在运算前验证

很多漏洞不发生在 jlong 到 size_t 的第一次转换,而发生在后续 totalBytes = count * elementSize。两个操作数都在 size_t 范围内,乘积仍可能溢出并回绕成较小值。随后按小值分配、按大循环写入,就出现缓冲区尺寸与访问次数不一致。检查必须在乘法前完成:elementSize 非零时,count 不得超过 SIZE_MAX 除以 elementSize。

加法同样要预检。计算 end = offset + length 前,确认 offset 不超过 MAX 减 length;指针差值还要符合对象边界,不能只保证整数没有溢出。将字节长度、元素数量和字符数量混用也会制造错误,尤其字符串编码后字节数并不等于 Java 字符数。单位转换应返回新的语义类型,而不是继续用裸 size_t。

业务上限与类型上限是两道门。某个值可被 size_t 表示,不代表应用愿意分配同样大小的内存,也不代表输入文件或协议允许。先做类型安全检查,再做配置的资源预算和对象边界检查。两类失败应分别记录 type_range_exceeded 与 policy_limit_exceeded,便于判断是兼容问题还是输入策略问题。

复合算术的预检规则
运算危险形式预检后续边界
count 乘 size乘积回绕count 不超过 MAX 除 size业务分配预算
offset 加 length和回绕offset 不超过 MAX 减 length对象真实大小
index 加 step索引越界剩余长度足够容器代际
字符转字节单位错配按编码计算字节目标缓冲区
时间单位转换倍率溢出乘法上界历元和 API 单位

异常映射要让 Java 看见参数边界而不是随机失败

转换失败发生在任何资源操作之前,适合映射为稳定的 Java 参数异常或结构化错误码。JNI 入口先验证全部相关字段,确认没有 pending exception,再进入 Native 对象方法。若一半字段已经触发分配,后一个字段才发现越界,回滚路径就会与业务异常交织,增加泄漏和双重释放风险。

错误信息应该包含 parameterName、semanticType、reason、targetAbiClass 和 operationId,不包含原始敏感内容或内部地址。超大数值本身通常可以记录为分类后的 valueClass,例如 negative、fits_signed、exceeds_target,而不是把来自文件或网络的完整输入写入生产日志。日志最小化也有助于跨设备比较同类失败。

C++ 异常、errno、JNI pending exception 和 Java Throwable 的责任要明确。转换助手可以返回 expected 风格结果,由 JNI 边界统一抛出;一旦抛出 Java 异常,后续 JNI 调用受到限制,应尽快返回。本文不扩写完整异常翻译协议,但要求转换失败不能被 catch 后改成零继续执行。

用整数边界测试器验证负值、ABI 上限和乘法溢出

下面的 Python 校验器读取脱敏 JSON 用例,要求明确 abiBits、value、unit、operation 和 elementSize。它用目标位数计算 size_t 上限,而不是采用运行脚本主机的类型宽度;先拒绝负值和未知单位,再检查单值范围,最后用除法上界判断长度乘法。任何缺少 ABI 条件的用例都会失败。

示例的输入派生错误路径清晰可达:若 value 为负、超过目标范围、elementSize 无效或乘积溢出,就以非零状态退出。它不执行内存分配、指针运算或 Native 调用,只用于检查边界测试数据是否完整。生产 C++ 仍应使用目标工具链的 numeric_limits,并保证校验与使用之间不发生语义变化。

测试用例至少包含负数、零、目标上限、刚超过目标上限、乘法恰好可表示和乘法不可表示。不要在文章里宣称某组固定数值覆盖所有平台;ABI、目标类型和业务预算变化时应重新生成边界。用例回执记录候选哈希和目标 ABI,避免把本机脚本通过当成设备 JNI 通过。

JNI 整数转换边界用例校验器
import json
import sys
from pathlib import Path

ALLOWED_UNITS = {"bytes", "elements", "offset_bytes"}
ALLOWED_ABI_BITS = {32, 64}

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

def load_case(path_text):
    path = Path(path_text)
    if not path.is_file():
        stop("case file is missing")
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as exc:
        stop(f"cannot parse case: {exc}")

def checked_size(value, abi_bits, field):
    if not isinstance(value, int):
        raise SystemExit(f"integer case rejected: {field} is not an integer")
    if value < 0:
        stop(f"{field} is negative")
    maximum = (1 << abi_bits) - 1
    if value > maximum:
        stop(f"{field} exceeds target size_t")
    return value, maximum

def validate(case):
    abi_bits = case.get("abiBits")
    if abi_bits not in ALLOWED_ABI_BITS:
        stop("abiBits must identify the target data model")
    unit = case.get("unit")
    if unit not in ALLOWED_UNITS:
        stop("unit is missing or unsupported")
    count, maximum = checked_size(case.get("value"), abi_bits, "value")
    operation = case.get("operation")
    if operation == "multiply":
        element_size, _ = checked_size(
            case.get("elementSize"), abi_bits, "elementSize"
        )
        if element_size == 0:
            stop("elementSize must be positive")
        if count > maximum // element_size:
            stop("count multiplied by elementSize would overflow")
        result = count * element_size
    elif operation == "identity":
        result = count
    else:
        stop("operation is missing or unsupported")
    policy_max = case.get("policyMaximum")
    if not isinstance(policy_max, int) or policy_max < 0:
        stop("policyMaximum is invalid")
    if result > policy_max:
        stop("result exceeds the business policy limit")
    print(json.dumps({"result": result, "unit": unit, "abiBits": abi_bits}))

if __name__ == "__main__":
    if len(sys.argv) != 2:
        stop("usage: validate_integer_case.py case.json")
    validate(load_case(sys.argv[1]))

回归矩阵要同时覆盖编译产物、设备 ABI 和错误路径

主机单元测试先验证 checked conversion 的纯函数边界,分别针对目标数据模型构造上限。随后检查 APK 或 AAB 中每个声明 ABI 的 so 是否存在且哈希对应候选。设备端 instrumented test 通过真实 JNI 入口传入边界用例,观察 Java 异常类型、Native 无副作用和后续正常调用仍可执行。

32 位验证不能用 64 位设备上的理论分支替代。测试回执要记录实际设备 ABI、OS、候选哈希和加载的库路径类别。Firebase Test Lab 的设备目录会变化,适合补充设备覆盖,但可用设备不代表覆盖业务所需全部厂商特性;本地或其他实验室的真机回执仍按风险矩阵安排。

异常路径还要覆盖多个字段组合。比如 count 合法但 elementSize 使乘积溢出,offset 合法但 offset 加 length 超界,Java 传负值,以及业务上限小于类型上限。每次失败后验证没有分配、没有修改输出对象、没有残留 JNI exception,并能继续执行下一条合法调用。

JNI 数值转换回归矩阵
层次用例必须观察不能替代
纯函数负值与边界值明确错误且无 cast设备 JNI
构建产物各声明 ABIso 存在且哈希正确实际装载
设备入口真实 jlong 参数异常映射与无副作用完整厂商矩阵
复合算术乘法和加法上界运算前拒绝业务对象边界
恢复性失败后合法调用无 pending exception长期压力
候选变化重编译或处理 SO新哈希重跑沿用旧回执

发布门禁绑定字段协议、目标 ABI 和候选回执

发布前枚举每个 JNI 整数参数和返回值,填写语义、单位、符号、Java 宽度、Native 目标、零值含义、类型上限、业务上限和异常映射。代码审查搜索 jlong 到 size_t、int、off_t 的直接 cast,以及 count 乘 size、offset 加 length。发现 cast 只是风险入口,最终判断要结合协议与运行路径。

minSdk、Native API 使用、ABI、STL、异常、链接方式、编译选项或加固边界变化后,生成新候选并重跑边界矩阵。Android NDK stable APIs 和 common problems 可帮助核对 API 可用性、符号与依赖问题,不能证明所有整数路径无截断。没有同一候选回执时,明确标记未验证,不填性能或兼容数字。

站内关于 JNI 异常翻译的文章可继续核对 range error 怎样映射回 Java;准备评估商业 App 的 SO 加固和 JNI 数值协议时,可通过页面行动按钮进入御盾中央平台提交候选包、字段清单与 ABI 用例。提交只启动评估,转换是否安全仍以同一候选的设备回执为准。

  • 每个 JNI 数值字段有明确语义、单位、符号和零值定义。
  • 有符号转无符号先拒绝负值,再比较目标 ABI 上限。
  • count 乘 size 和 offset 加 length 都在运算前预检。
  • 类型上限与业务资源上限分别验证并返回不同错误。
  • 持久化和 IPC 使用定宽格式,不直接序列化 size_t。
  • 转换失败发生在任何分配、指针运算和状态修改之前。
  • 32 位与 64 位路径都绑定实际产物和设备回执。
  • 候选变化后重跑边界、异常、恢复性和装载矩阵。

事实依据与适用边界

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

本文判断事实或工程依据适用限制
Android 各 ABI 有独立调用约定、寄存器、对齐和设备支持范围。Android ABIs 说明受支持 ABI 及其平台约定。声明支持 ABI 不证明对应 so 已构建、打包、装载并通过整数边界测试。
JNI 注册、异常与其他桥接边界会影响数值错误的稳定返回。Android JNI tips 提供 JNI 注册、线程、引用、异常和类加载相关实践。JNI 建议不定义本文字段协议,也不证明处理后的候选保持 ABI。
Native API 的可用性要与 minSdk 和链接方式匹配。Android NDK stable APIs 说明高于 minSdk 的 API 不能被直接静态调用及版本化处理边界。API 可用不证明动态链接路径在所有 namespace 可达,也不证明整数参数范围正确。
NDK 兼容故障排查需覆盖 API level、符号、STL、异常和依赖装载。Android NDK common problems 列出这些常见故障类别。故障清单不能替代目标 ABI 上 jlong 转换和异常路径的运行回归。
依赖真实 Android 运行时和 JNI 入口的语义应在设备端验证。Android instrumented tests 说明 instrumented test 在 Android 设备环境运行并可使用框架 API。单台设备或单一边界值通过不能代表完整 API、ABI、芯片和厂商矩阵。
云测试设备目录和可用性会变化,计划应保存实际设备回执。Test Lab available devices 提供当前测试设备目录及相关可用性信息。目录中的设备可用不等于覆盖目标业务全部厂商特性,也不代表测试已执行。
先做符号与范围检查,再做 cast,可以阻止负值翻转和目标宽度截断进入资源操作。工程判断:转换前的原始 jlong 仍保留符号和完整值,转换后可能已经丢失判定所需信息。具体实现仍需目标编译器、ABI 与设备回执,本文不提供未验证的性能或兼容结论。
御盾项目中的整数边界覆盖和候选结果当前不能由通用资料推出。项目证据尚未接入:需要字段协议、候选哈希、ABI 产物、设备用例和异常回执。回执齐备前不得声称已消除截断、通过兼容测试、阻断攻击或取得客户效果。

工程常见问题

jlong 和 size_t 都能装大整数,为什么不能直接 cast?

jlong 有符号且宽度固定,size_t 无符号且随目标数据模型变化。负值会翻转为大无符号数,较窄目标还会截断,所以要在原始域先检查。

只检查 value 不超过 SIZE_MAX 是否足够?

不够。还要先拒绝负值、确认单位和零值语义,并检查 count 乘 elementSize、offset 加 length 以及业务资源上限。

64 位设备通过后还需要测试 32 位路径吗?

如果产品仍声明和分发 32 位 ABI,就需要实际产物与设备回执。同一 jlong 可能在 64 位 size_t 可表示,在 32 位目标越界。

乘法溢出能否在乘完后比较结果发现?

不能可靠发现,因为无符号乘积可能已经回绕成较小值。应在乘法前检查 count 是否超过 maximum 除以 elementSize。

转换失败应该返回零还是抛 Java 异常?

由字段协议决定,但不能把错误零与合法零混淆。推荐使用明确错误码或统一参数异常,并在任何资源和状态修改前返回。

SO 加固或重编译后为什么要重新跑数值边界?

候选变化可能影响 ABI、JNI 签名、异常、链接和优化路径。旧产物的边界回执不能证明新候选仍按相同规则拒绝输入。

想用自己的 App 验证?

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

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