[BUG] CSVWriter 写入CSV数据超过 65536 个字节时报错
问题描述
我们使用 Fastjson2 用于生成 CSV 格式的数据文件。 写入少量数据时,一切表现正常。 然而,当写入超过 65536 个字节的数据时,就会触发如下数组越界异常 。
java.lang.ArrayIndexOutOfBoundsException: Index 65536 out of bounds for length 65536
at com.alibaba.fastjson2.support.csv.CSVWriterUTF8.writeComma(CSVWriterUTF8.java:52)
at com.alibaba.fastjson2.support.csv.CSVWriter.writeLine(CSVWriter.java:149)
通过异常堆栈信息得知报错代码位置如下:
https://github.com/alibaba/fastjson2/blob/a122b8d58133c429f85a954546d12df2eb233265/core/src/main/java/com/alibaba/fastjson2/support/csv/CSVWriterUTF8.java#L48-L53
如果之前 CSVWriterUTF8.off 已经是 65536,本次再调用 writeComma()方法时,由于 bytes.length 固定是 65536,并不满足 if( off + 1 == bytes.length ) 的 if 条件,因此触发越界异常。
此外,我们恰好在该数据量边界写入的是字符串,也得到了一个类似的异常:
java.lang.StringIndexOutOfBoundsException: offset 65532, count 5, length 65536
at java.base/java.lang.String.checkBoundsOffCount(String.java:4591)
at java.base/java.lang.String.getBytes(String.java:1734)
环境信息
请填写以下信息:
- OS信息: CentOS 8.2
- JDK信息: Openjdk 17
- 版本信息:Fastjson2 2.0.52
期待的正确结果
正常写入,不再报错。
堆栈信息可以发一下么?
堆栈信息可以发一下么?
简单的堆栈信息就是我上面发的,线上环境的堆栈信息在记录日志时做了精简,只会显示最近2行 + 本应用包名开头的,所以暂时没有完整的堆栈信息。
我们目前是临时通过 每 write 100 行,就手动调用一次 writer.flush() 的方式来规避该 bug。
通过上述简单关键堆栈信息里的 报错文件名称 和 行数,再结合报错异常分析,也能定位到具体的问题。
在这里,把 if (off + 1 == bytes.length) 改为 if (off >= bytes.length) 应该可以解决该问题。
如果这里有问题,应该是很多个地方都会有问题,应该是某个地方的offset计算错了,你本地方便做调试么?看调用这个之前是什么操作,最后一列是什么类型?
try (CSVWriter writer = CSVWriter.of()) {
writer.writeValue("1".repeat(65531));
writer.writeComma();
writer.writeValue(new BigDecimal("1.00"));
writer.writeComma(); // java.lang.ArrayIndexOutOfBoundsException: Index 65536 out of bounds for length 65536
}
前几天比较忙,今天调试了一下,发现 write BigDecimal 刚好达到 65536 时,内部并没有 flush(),再次调用其他方法就会报错 。
以上是复现该问题的简单模拟代码,CSVWriterUTF8 和 CSVWriterUTF16 类似的其他方法也可以一并检查一下,如果某个 write 方法里面没有 flush() 调用,就可能存在该风险。
https://oss.sonatype.org/content/repositories/snapshots/com/alibaba/fastjson2/fastjson2/2.0.53-SNAPSHOT/ 问题已修复,帮忙用2.0.53-SNAPSHOT帮忙验证,2.0.53版本预计在月底发布
https://github.com/alibaba/fastjson2/releases/tag/2.0.53 问题已修复,请用新版本
@wenshao 用新版本测试了下,writeBigDecimal 的问题已经解决了,不过又发现了一个类似的 bug。
OutputStreamWriter out = new OutputStreamWriter(new ByteArrayOutputStream(), StandardCharsets.UTF_8);
try (CSVWriter writer = CSVWriter.of(out)) {
writer.writeValue("1".repeat(65534));
writer.writeComma();
writer.writeString("123"); // java.lang.StringIndexOutOfBoundsException: offset 65535, count 3, length 65536
}
https://github.com/alibaba/fastjson2/blob/a122b8d58133c429f85a954546d12df2eb233265/core/src/main/java/com/alibaba/fastjson2/support/csv/CSVWriterUTF16.java#L141-L149
定位了下,如上所示,是 CSVWriterUTF16 的 142 行代码处没有 预先检测数组容量 所致。
错误堆栈信息如下:
java.lang.StringIndexOutOfBoundsException: offset 65535, count 3, length 65536
at java.base/java.lang.String.checkBoundsOffCount(String.java:4593)
at java.base/java.lang.String.getChars(String.java:1681)
at com.alibaba.fastjson2.support.csv.CSVWriterUTF16.writeString(CSVWriterUTF16.java:142)
at test.FastjsonTest.test(FastjsonTest.java:56)
发现这些 bug 有些随机,必须要恰好构造出符合特定条件的数据才能出现。
@wenshao 举一反三,我分析了一下,出 bug 的地方一般可能存在两种情况:
- 存量部分 + 增量部分 刚好超过底层数组容量 65536,且没有预检测
- 增量部分 自己就超过了 65536。
我先构造一个 65535 容量的 Writer buffer,然后依次调用每一个 write*() 方法,发现还有如下方法存在类似的问题(此处以 CSVWriterUTF16 为例,CSVWriterUTF8 同理】):
OutputStreamWriter out = new OutputStreamWriter(new ByteArrayOutputStream(), StandardCharsets.UTF_8);
try (CSVWriter writer = CSVWriter.of(out)) {
writer.writeValue("1".repeat(65534));
writer.writeComma();
writer.writeLocalDateTime(LocalDateTime.now()); // java.lang.ArrayIndexOutOfBoundsException: Index 65539 out of bounds for length 65536
// 【writeInstant(Instant instant) 也受此影响】
}
还有
OutputStreamWriter out = new OutputStreamWriter(new ByteArrayOutputStream(), StandardCharsets.UTF_8);
try (CSVWriter writer = CSVWriter.of(out)) {
writer.writeValue("1".repeat(65534));
writer.writeComma();
writer.writeString("2".repeat(65537)); // java.lang.StringIndexOutOfBoundsException: offset 65535, count 65537, length 65536
// 【CSVWriterUTF16.writeString(byte[] utf8) 也受此影响】
}