attempt to subtract with overflow,或者 [],1,2]
主线位置:序列化主线上手写
Serialize的那一步。类型调serialize_seq(len)时给出的len是对格式的承诺,格式信任它,serde 不核对。
flowchart TB s["serialize_seq Some 0"] --> w["写 [ 和 ],状态 Empty"] w --> e["serialize_element 两次"] e --> c["写 ,1 和 ,2"] c --> endnode["end 再写 ]"] endnode --> r1["to_string 得到 [],1,2]"] endnode --> r2["pretty 缩进计数减到负"] r2 --> dbg["debug:panic"] r2 --> rel["release:无限写缩进"]
len: Option<usize> 在各个格式里起什么作用、serde_json 为什么特判 Some(0),在 Serializer 里讲过,那一页的动脑筋就是这个程序。这一页讲它在真实代码里长什么样、怎么修、怎么查。
现象
src/bin/liar.rs,一个声称有 0 个元素、实际写了两个的 Serialize:
use serde::Serialize;
use serde::ser::{SerializeSeq, Serializer};
// Announces an empty sequence, then pushes two elements anyway.
struct Liar;
impl Serialize for Liar {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
let mut seq = s.serialize_seq(Some(0))?;
seq.serialize_element(&1)?;
seq.serialize_element(&2)?;
seq.end()
}
}
fn main() {
let s = serde_json::to_string(&Liar);
println!("to_string : {s:?}");
println!("from_str back : {:?}", serde_json::from_str::<serde_json::Value>(&s.unwrap()).map_err(|e| e.to_string()));
println!("to_value : {:?}", serde_json::to_value(&Liar).map(|v| v.to_string()));
println!("to_string_pretty : {:?}", serde_json::to_string_pretty(&Liar));
}
M2 Max、macOS、rustc 1.99.0,debug 构建,退出码 101:
to_string : Ok("[],1,2]")
from_str back : Err("trailing characters at line 1 column 3")
to_value : Ok("[1,2]")
thread 'main' (2735112) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_json-1.0.154/src/ser.rs:1989:9:
attempt to subtract with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
三种表现,没有一种指向真正的原因:
to_string返回Ok,内容却是非法 JSON;错误要等到读的一方才报出来,而且报的是trailing characters。to_value正常:value::Serializer只把len当容量提示,同样的代码在测试里用to_value比较就测不出来。to_string_pretty的 panic 报在 serde_json 内部,看起来像 serde_json 的 bug。
release 构建下溢出不检查,to_string_pretty 不返回。--release 构建后直接跑二进制,每秒看一次进程的 RSS,9 秒时 kill(路径缩写了):
(target/release/liar > out.txt 2>&1 & pid=$!
for t in 1 2 3 4 5 6 7 8 9; do
perl -e 'select(undef,undef,undef,1)'; echo "t=${t}s $(ps -o rss=,vsz=,%cpu= -p $pid)"
done
kill $pid; wait $pid; echo "killed exit=$?"); cat out.txt
t=1s 1063888 490879248 98.4
t=2s 2119792 492976400 100.0
t=3s 3183584 492976400 100.0
t=4s 4236624 497170704 99.0
t=5s 5277008 497170704 99.7
t=6s 6333552 497170704 100.0
t=7s 7379088 497170704 100.0
t=8s 8430304 505559312 98.3
t=9s 9477392 505559312 100.0
killed exit=143
to_string : Ok("[],1,2]")
from_str back : Err("trailing characters at line 1 column 3")
to_value : Ok("[1,2]")
RSS 的单位是 KB:每秒涨约 1 GB,一个核占满,最后一行永远不出来。放在服务里就是一个请求把进程内存吃光。
从哪里来
serialize_seq 看到 Some(0) 就立刻写完 [ 和 ],状态设成 Empty:
fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq> {
tri!(self
.formatter
.begin_array(&mut self.writer)
.map_err(Error::io));
if len == Some(0) {
tri!(self
.formatter
.end_array(&mut self.writer)
.map_err(Error::io));
Ok(Compound::Map {
ser: self,
state: State::Empty,
})
} else {
// ...
}
}
之后 serialize_element 不看 Empty,只看是不是 First,于是写 ,1、,2 并把状态改成 Rest;end 看到不是 Empty,又调一次 end_array。end_array 一共被调了两次,begin_array 只有一次。
紧凑格式下多一次 end_array 只是多写一个 ]。PrettyFormatter 的 end_array 还要维护缩进深度,第二次从 0 往下减:
fn end_array<W>(&mut self, writer: &mut W) -> io::Result<()>
where
W: ?Sized + io::Write,
{
self.current_indent -= 1;
if self.has_value {
tri!(writer.write_all(b"\n"));
tri!(indent(writer, self.current_indent, self.indent));
}
// ...
}
第 1989 行就是 self.current_indent -= 1;。debug 构建里 usize 减到负数 panic;release 构建里回绕成 usize::MAX,接着 indent 用 for _ in 0..n 往 Vec<u8> 里写 usize::MAX 次两个空格,这就是那条每秒 1 GB 的曲线。
问: serde_json 明明知道 len 是 Some(0),为什么不在 serialize_element 里返回一个错误,而是写出非法 JSON 或者 panic?
答: 这个程序几乎原样被报告过(issue #706,2020 年,用 #[serde(with)] 序列化一个迭代器,写了 serialize_seq(Some(0))),报告里提的就是「serialize_element should return error rather than panics」。得到的回复是「Panics are supposed to be used for signalling a bug in the program, so I think a panic is appropriate in this case. Errors are supposed to be used for expected failure conditions in a correct program.」issue 当天关闭,代码没有改。len 给错是调用方的 bug,不是输入数据的问题,serde_json 不为它付核对的代价。
修法
len 的含义写在 Serializer::serialize_seq 的文档里:「The argument is the number of elements in the sequence, which may or may not be computable before the sequence is iterated. Some serializers only support sequences whose length is known up front.」所以只有两种正确的写法:不知道就给 None,给就给准确的数。
真实代码里出错,多半是先拿了容器的 len(),再过滤掉一部分。src/bin/honest.rs 只序列化偶数,比较三种写法,每种同时试 serde_json 和 bincode:
use serde::Serialize;
use serde::ser::{SerializeSeq, Serializer};
// Only the even numbers are serialized, so items.len() would be a lie.
struct Evens(Vec<u32>);
// Fix 1: say None when the count is not known up front.
struct EvensNone<'a>(&'a Evens);
impl Serialize for EvensNone<'_> {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
let mut seq = s.serialize_seq(None)?;
for x in self.0.0.iter().filter(|x| *x % 2 == 0) {
seq.serialize_element(x)?;
}
seq.end()
}
}
// Fix 2: count first, then announce the exact number.
struct EvensExact<'a>(&'a Evens);
impl Serialize for EvensExact<'_> {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
let n = self.0.0.iter().filter(|x| *x % 2 == 0).count();
let mut seq = s.serialize_seq(Some(n))?;
for x in self.0.0.iter().filter(|x| *x % 2 == 0) {
seq.serialize_element(x)?;
}
seq.end()
}
}
// Fix 3: hand the iterator to collect_seq; it computes len from size_hint.
struct EvensCollect<'a>(&'a Evens);
impl Serialize for EvensCollect<'_> {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.collect_seq(self.0.0.iter().filter(|x| *x % 2 == 0))
}
}
fn show<T: Serialize>(label: &str, v: &T) {
let pretty = serde_json::to_string_pretty(v);
let bin = bincode::serialize(v).map_err(|e| e.to_string());
println!("{label:<8} to_string={:?} pretty={pretty:?}", serde_json::to_string(v));
println!("{:<8} bincode={bin:?}", "");
}
fn main() {
for items in [vec![1, 3], vec![1, 2, 3, 4]] {
let e = Evens(items);
println!("items = {:?}", e.0);
show("None", &EvensNone(&e));
show("exact", &EvensExact(&e));
show("collect", &EvensCollect(&e));
}
}
bincode = "=1.3.3",debug 构建:
items = [1, 3]
None to_string=Ok("[]") pretty=Ok("[]")
bincode=Err("Bincode can only encode sequences and maps that have a knowable size ahead of time")
exact to_string=Ok("[]") pretty=Ok("[]")
bincode=Ok([0, 0, 0, 0, 0, 0, 0, 0])
collect to_string=Ok("[]") pretty=Ok("[]")
bincode=Err("Bincode can only encode sequences and maps that have a knowable size ahead of time")
items = [1, 2, 3, 4]
None to_string=Ok("[2,4]") pretty=Ok("[\n 2,\n 4\n]")
bincode=Err("Bincode can only encode sequences and maps that have a knowable size ahead of time")
exact to_string=Ok("[2,4]") pretty=Ok("[\n 2,\n 4\n]")
bincode=Ok([2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 4, 0, 0, 0])
collect to_string=Ok("[2,4]") pretty=Ok("[\n 2,\n 4\n]")
bincode=Err("Bincode can only encode sequences and maps that have a knowable size ahead of time")
- 三种写法在 serde_json 里都对,包括全被过滤掉的
[1, 3]。 - 只有给准确的数在 bincode 里也能用:它要在第一个元素前写长度前缀(
[2, 0, 0, 0, 0, 0, 0, 0, ...]的前 8 个字节)。 collect_seq不会说谎,但对filter之后的迭代器它给的就是None。iterator_len_hint只在size_hint()的上下界相等时返回Some,filter的下界是 0,于是 bincode 照样报错。要所有格式都能用,就先数一遍(或先collect成Vec),再给准确的数。
怎么确认没中招
- 搜
serialize_seq(Some(、serialize_map(Some(、serialize_tuple(,看给的数和后面实际调serialize_element/serialize_entry的次数是不是同一个来源。用了filter、take_while、skip_serializing式的条件跳过时,最容易对不上。 - 测试里用
to_string/to_string_pretty而不是to_value比较,并且在 debug 构建下跑:Some(0)却有元素的情况会 panic 或写出非法 JSON。其他数目不对时 serde_json 什么也不报,要用 bincode 往返一次才看得出来(Serializer 里Some(1)写三个元素,读回来只剩一个)。 - 能用
collect_seq/collect_map(或者直接让 derive 处理Vec)的地方就别手写serialize_seq。
小结
serialize_seq/serialize_map的len是声明,没有格式核对它。- serde_json 只特判
Some(0):声称为空却写元素,to_string返回Ok("[],1,2]"),to_value不受影响,to_string_pretty在 debug 下于ser.rs:1989减法溢出 panic,release 下indent写usize::MAX次,RSS 每秒涨约 1 GB。 - 只有两种正确写法:
None,或准确的数。collect_seq在size_hint不精确时给None,bincode 这类要长度前缀的格式仍然不收。 - serde_json 认为这是调用方的 bug,panic 是预期行为(issue #706)。