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 M | panic_advance,由 buf_get_impl!(所有 get_*)、copy_to_slice、&[u8] 的 advance 调用 |
cannot advance past `remaining`: M <= N | Bytes::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
);
}
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 字节一批喂进去。