Rust in Everything
基于 bytes 1.12.1

advance out of bounds: the len is 3 but advancing by 4

主线位置:共享主线的下游,Buf 读游标。hyper 把 body 分块以 Bytes 交给上层,上层的协议解析代码用 get_* 一段段往前读;数据是分批到的,读的时候不一定够。

flowchart TB
  arrive["收到一批字节"] --> dec["decode"]
  dec --> how{怎么读长度}
  how -->|get_u32| boom["不够就 panic"]
  how -->|"缓冲上 try_get_u32"| lost["头读走了 体不够"]
  lost --> desync["下一轮错位"]
  how -->|"副本上 try_get_u32"| enough{整帧到齐}
  enough -->|否| wait["什么都不动 等下一批"]
  enough -->|是| take["advance 4 再 split_to"]

get_* 怎么由宏生成、数据不够时为什么都调用同一个 panic_advance、try_get_* 为什么失败时游标不动,在 Buf 的「数据不够时:panic 的原文」和「try_get_*:不想 panic 时」里讲过。这一页看它在解码器里怎么出事,以及 try_get_* 用错位置时为什么比 panic 还糟。

本页的实验在 M2 Max、rustc 1.99.0、--release 下运行,每个程序跑 3 次,输出相同。依赖 bytes = "=1.12.1"。

现象

use std::panic;

use bytes::{Buf, Bytes, BytesMut};

fn main() {
  panic::set_hook(Box::new(|info| {
    let msg = info.payload().downcast_ref::<String>().cloned()
      .or_else(|| info.payload().downcast_ref::<&str>().map(|s| s.to_string()))
      .unwrap_or_default();
    let loc = info.location().unwrap();
    let file = loc.file().rsplit("bytes-1.12.1/").next().unwrap();
    println!("    at {file}:{}: {msg}", loc.line());
  }));
  let three = || BytesMut::from(&[1u8, 2, 3][..]);
  let cases: Vec<(&str, Box<dyn Fn()>)> = vec![
    ("BytesMut get_u32()", Box::new(move || { three().get_u32(); })),
    ("Bytes get_u16_le() x2", Box::new(|| { let mut b = Bytes::from_static(&[1, 2, 3]); b.get_u16_le(); b.get_u16_le(); })),
    ("&[u8] copy_to_slice(&mut [0; 4])", Box::new(|| (&[1u8, 2, 3][..]).copy_to_slice(&mut [0; 4]))),
    ("BytesMut advance(5)", Box::new(move || three().advance(5))),
    ("Bytes advance(5)", Box::new(|| Bytes::from_static(&[1, 2, 3]).advance(5))),
    ("&[u8] advance(5)", Box::new(|| (&[1u8, 2, 3][..]).advance(5))),
  ];
  for (name, f) in cases {
    println!("{name}");
    let _ = panic::catch_unwind(panic::AssertUnwindSafe(f));
  }
  let mut b = three();
  let e = b.try_get_u32().unwrap_err();
  println!("try_get_u32(): Err({e:?}), remaining = {}", b.remaining());
  println!("Display: {e}");
}
BytesMut get_u32()
    at src/lib.rs:207: advance out of bounds: the len is 3 but advancing by 4
Bytes get_u16_le() x2
    at src/lib.rs:207: advance out of bounds: the len is 1 but advancing by 2
&[u8] copy_to_slice(&mut [0; 4])
    at src/lib.rs:207: advance out of bounds: the len is 3 but advancing by 4
BytesMut advance(5)
    at src/bytes_mut.rs:1246: cannot advance past `remaining`: 5 <= 3
Bytes advance(5)
    at src/bytes.rs:694: cannot advance past `remaining`: 5 <= 3
&[u8] advance(5)
    at src/lib.rs:207: advance out of bounds: the len is 3 but advancing by 5
try_get_u32(): Err(TryGetError { requested: 4, available: 3 }), remaining = 3
Display: Not enough bytes remaining in buffer to read value (requested 4 but only 3 available)
  • 所有 get_* 和 copy_to_slice,不管底层是 BytesMut、Bytes 还是 &[u8],都是同一句:advance out of bounds: the len is <剩余> but advancing by <要读的>。第二行是读完一个 u16 后只剩 1 字节,所以是「len is 1」。
  • 直接调 advance 时,Bytes 和 BytesMut 用自己的 assert!,信息是 cannot advance past `remaining`: 5 <= 3;&[u8] 仍是 advance out of bounds。
  • 位置:get_* 那几行都报在 lib.rs:207,advance 报在 bytes.rs / bytes_mut.rs,没有一行指向调用方。找调用行的办法和 range end out of bounds 与 split_to out of bounds 一样:debug 构建加 RUST_BACKTRACE=1。
  • try_get_u32 返回 Err,remaining 仍是 3。
原文来自
advance out of bounds: the len is N but advancing by Mpanic_advance,由 buf_get_impl!(所有 get_*)、copy_to_slice、&[u8] 的 advance 调用
cannot advance past `remaining`: M <= NBytes::advance、BytesMut::advance 里的 assert!

panic_advance 只标了 #[cold],没有 #[track_caller]:

/// Panic with a nice error message.
#[cold]
fn panic_advance(error_info: &TryGetError) -> ! {
    panic!(
        "advance out of bounds: the len is {} but advancing by {}",
        error_info.available, error_info.requested
    );
}
📝NOTE
冷知识:这句报错是 1.6.0 才有的

1.6.0 之前,get_u32 在当前块不够 4 字节时去调 copy_to_slice,而 copy_to_slice 开头是一句没有自定义信息的 assert!(self.remaining() >= dst.len());,数据不够时 panic 的就是这个表达式本身。PR #635(「Various cleanup」,首次发布于 1.6.0,CHANGELOG 记为「Various cleanup (#635)」)的说明第一条是「Factor a lot of panics into functions to improve error messages and reduce codegen」:新增 panic_advance 和 panic_does_not_fit 两个 #[cold] 函数,get_* 改成先比较 remaining() 再调用它们;get_uint / get_int 的「nbytes 超过 8」原来只有 debug_assert!,也改成了 panic_does_not_fit。同一条说明的第二条原文是「Add missing #[inline] and #[track_caller]」:#[track_caller] 写上又划掉了,所以这些 panic 至今报在 lib.rs 里。之后 1.10.0 的 #753 把 get_* 改成调用 try_get_* 再交给 panic_advance,信息没变。

真实场景:帧解码器碰到半包

一个最常见的协议格式:4 字节大端长度,后面跟这么多字节的内容。字节从 socket 上分批到达,每到一批就追加进 BytesMut,然后尽量多地解出整帧。下面三个解码函数只差在怎么读长度:

use std::panic;

use bytes::{Buf, Bytes, BytesMut};

// frame = u32 big-endian length + payload
fn decode_get(buf: &mut BytesMut) -> Option<Bytes> {
  if buf.is_empty() {
    return None;
  }
  let len = buf.get_u32() as usize;
  if buf.len() < len {
    return None;
  }
  Some(buf.split_to(len).freeze())
}

fn decode_try_consuming(buf: &mut BytesMut) -> Option<Bytes> {
  let len = buf.try_get_u32().ok()? as usize; // header is consumed here
  if buf.len() < len {
    return None;
  }
  Some(buf.split_to(len).freeze())
}

fn decode_peek(buf: &mut BytesMut) -> Option<Bytes> {
  let len = (&buf[..]).try_get_u32().ok()? as usize; // reads a copy of the cursor
  if buf.len() < 4 + len {
    return None;
  }
  buf.advance(4);
  Some(buf.split_to(len).freeze())
}

fn run(name: &str, decode: fn(&mut BytesMut) -> Option<Bytes>, arrivals: &[usize]) {
  let wire: &[u8] = b"\x00\x00\x00\x05hello\x00\x00\x00\x03abc";
  println!("{name}");
  let r = panic::catch_unwind(|| {
    let mut buf = BytesMut::new();
    let mut at = 0;
    for &n in arrivals {
      buf.extend_from_slice(&wire[at..at + n]);
      at += n;
      while let Some(frame) = decode(&mut buf) {
        println!("    frame {frame:?}");
      }
      println!("    after {at:>2} bytes: buffered {buf:?}");
    }
  });
  if r.is_err() {
    println!("    (panicked)");
  }
}

fn main() {
  panic::set_hook(Box::new(|info| {
    let msg = info.payload().downcast_ref::<String>().cloned()
      .or_else(|| info.payload().downcast_ref::<&str>().map(|s| s.to_string()))
      .unwrap_or_default();
    let loc = info.location().unwrap();
    let file = loc.file().rsplit("bytes-1.12.1/").next().unwrap();
    println!("    panicked at {file}:{}: {msg}", loc.line());
  }));
  let split = [2, 5, 4, 5]; // how the bytes come off the socket
  run("get_u32", decode_get, &split);
  run("try_get_u32 on the buffer", decode_try_consuming, &split);
  run("try_get_u32 on the buffer, all at once", decode_try_consuming, &[16]);
  run("try_get_u32 on a peek", decode_peek, &split);
}

线上是两帧 hello、abc,共 16 字节。除了第三组一次到齐,其余都分 2、5、4、5 字节四批到达:

get_u32
    panicked at src/lib.rs:207: advance out of bounds: the len is 2 but advancing by 4
    (panicked)
try_get_u32 on the buffer
    after  2 bytes: buffered b"\0\0"
    after  7 bytes: buffered b"hel"
    after 11 bytes: buffered b"o\0\0"
    after 16 bytes: buffered b"\x03abc"
try_get_u32 on the buffer, all at once
    frame b"hello"
    frame b"abc"
    after 16 bytes: buffered b""
try_get_u32 on a peek
    after  2 bytes: buffered b"\0\0"
    after  7 bytes: buffered b"\0\0\0\x05hel"
    frame b"hello"
    after 11 bytes: buffered b"\0\0"
    frame b"abc"
    after 16 bytes: buffered b""
  • get_u32:第一批只有长度字段的前 2 字节,直接 panic。本地测试时请求往往一次到齐,这一行不会出现;到了真实网络、慢客户端或大包,才第一次碰到。
  • 在缓冲上 try_get_u32:没有 panic,但两帧一帧都没解出来。第一批 2 字节时 try_get_u32 失败、游标不动,这一步没问题。第二批到 7 字节,长度 5 读成功、4 字节被消费掉了,内容只有 hel 不够,返回 None;长度就此丢了。第三批之后缓冲里是 hello\0\0,try_get_u32 把 hell 当长度读走(0x68656c6c),又不够,又返回 None。从这里开始每一轮都在错位地读,不报错,也不会再对齐。同一个函数在一次到齐时(第三组)两帧都对,所以只喂完整输入的测试发现不了它。
  • 在副本上 peek:(&buf[..]) 是一个新的 &[u8] 游标,try_get_u32 移动的是它,buf 不动。整帧到齐之前什么都不消费,到齐后再 advance(4)、split_to(len)。两帧都正确解出,缓冲最后为空。

try_get_* 保证的是「这一次读失败时不动游标」(Buf 里讲的宏结构)。一帧要读好几次(先长度、后内容),前面读成功、后面不够时,前面那次已经动过游标了。它不替你回滚。

修法

情形写法
解码一帧(长度 + 内容),数据可能不全在 &buf[..] 这样的副本上读长度,判断 buf.len() >= 头长 + len,够了再在 buf 上 advance / split_to
读一个独立的定长字段,不够就报错try_get_*,用 ? 转成自己的错误;有 std 时 TryGetError 能 From 成 io::Error
已经确认过 remaining() 足够get_* 没问题,panic 只会出在检查写错时
跳过 n 个字节,n 来自输入先比较 remaining() >= n 再 advance(n);bytes 没有 try_advance

try_get_* 是 1.10.0 加的(#753)。项目锁的 bytes 比它旧时没有这些方法,只能先比较 remaining()。

另一种正确做法是把读到的长度记下来。tokio-util 0.7.19 的 LengthDelimitedCodec 就是这样:头部到齐后在一个 Cursor 上读出长度,再消费头部,把长度存进自己的状态 DecodeState::Data(n),下一轮直接等内容。两种做法的共同点是:消费了的东西,要么属于一整帧,要么被记在别处。

怎么确认自己没踩

  • 搜解码代码里的 get_、advance(、copy_to_slice(、try_get_。对每一处问:调用前有没有确认整帧(不只是这个字段)已经到齐?try_get_ 后面如果还有别的读取,前一个读成功、后一个失败时缓冲处于什么状态?
  • 用本页的办法测:把一段合法的多帧输入按各种切分方式(至少「每次 1 字节」)喂给解码器,解出的帧序列必须和一次喂完时相同,而且不能 panic。上面的 try_get_u32 on the buffer 一次喂完时两帧都对,只有切分后才暴露。

小结

  • get_*、copy_to_slice 数据不够时的 panic 都是 advance out of bounds: the len is N but advancing by M,来自 lib.rs 的 panic_advance(1.6.0 起,#635);Bytes / BytesMut 直接 advance 越界是 cannot advance past `remaining`: M <= N。位置都在 bytes 内部。
  • 帧解码器在半包上用 get_u32 会 panic;本地一次到齐的测试发现不了。
  • try_get_*(1.10.0 起)只保证单次失败不动游标。在缓冲上先读长度、再发现内容不够,长度已经被消费,之后整条流错位且不报错。
  • 正确做法:在副本(&buf[..])上 peek 长度,整帧到齐再消费;测试时把输入按 1 字节一批喂进去。

💬 关联讨论

发起讨论
加载中...