先看结论与判断条件
- 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 类别,不回显敏感数据。
| 字段语义 | Java 载体 | Native 目标 | 必须检查 |
|---|---|---|---|
| 字节长度 | jlong | size_t | 非负、上限、分配预算 |
| 元素数量 | jlong | size_t | 非负、乘法上界 |
| 文件偏移 | jlong | off_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 |
| 单位未知 | 协议没有 unit | 否 | missing_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 展开成第二套主题。
| 门禁 | 输入 | 检查位置 | 证据 |
|---|---|---|---|
| 目标类型宽度 | 编译目标 | 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 通过。
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,并能继续执行下一条合法调用。
| 层次 | 用例 | 必须观察 | 不能替代 |
|---|---|---|---|
| 纯函数 | 负值与边界值 | 明确错误且无 cast | 设备 JNI |
| 构建产物 | 各声明 ABI | so 存在且哈希正确 | 实际装载 |
| 设备入口 | 真实 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 签名、异常、链接和优化路径。旧产物的边界回执不能证明新候选仍按相同规则拒绝输入。