Rust in Everything
基于 serde 1.0.229

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 的曲线。

📝NOTE
没有蠢问题

问: 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)。

💬 关联讨论

发起讨论
加载中...