range end out of bounds 与 split_to out of bounds
主线位置:共享主线的
slice/split_to/split_off,构建主线的split_to/split_off/extend_from_within。它们在 clone 或提升之前先检查范围,越界就 panic;Buf一侧数据不够时的advance out of bounds另见 advance out of bounds。
flowchart TB user["你的代码"] --> slice["Bytes::slice"] user --> ext["extend_from_within"] user --> split["split_to / split_off"] slice --> range["lib.rs range"] ext --> range range -->|track_caller| loc1["报在 slice 内部"] split --> asrt["方法里的 assert"] asrt --> loc2["报在 split 内部"] user --> idx["下标 b[..20]"] idx -->|标准库| loc3["报在你的那一行"]
每个方法的检查逻辑和完整的 panic 原文表在 切片 的「越界时的 panic 信息」和 split 与 freeze 的「越界时的 panic 信息」里,冒号后面那个「20 <= 11」为什么读起来像写反了,也在切片那一页的问答里。这一页从使用者的角度看:拿到一条这样的 panic,怎么知道是哪一行调的,以及怎么写才不会 panic。
本页的实验在 M2 Max、rustc 1.99.0 下运行,依赖 bytes = "=1.12.1"。
复现
用 panic hook 打出每次 panic 的位置(截掉 cargo 注册表的路径前缀)和信息:
use std::panic;
use bytes::{Bytes, BytesMut};
// no panic: bounds-check through the slice, then map back to a zero-copy Bytes
fn checked_slice(b: &Bytes, r: std::ops::Range<usize>) -> Option<Bytes> {
b.get(r).map(|sub| b.slice_ref(sub))
}
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 b = Bytes::from_static(b"hello world"); // len 11
let m = || BytesMut::from(&b"hello world"[..]); // len 11, cap 11
let cases: Vec<(&str, Box<dyn Fn()>)> = vec![
("Bytes::slice(..20)", Box::new(|| drop(b.slice(..20)))),
("Bytes::slice(5..3)", Box::new(|| drop(b.slice(5..3)))),
("Bytes::split_to(20)", Box::new(|| drop(b.clone().split_to(20)))),
("Bytes::split_off(20)", Box::new(|| drop(b.clone().split_off(20)))),
("BytesMut::split_to(20)", Box::new(move || drop(m().split_to(20)))),
("BytesMut::split_off(20)", Box::new(move || drop(m().split_off(20)))),
("BytesMut::extend_from_within(..20)", Box::new(move || m().extend_from_within(..20))),
("&b[..20] (Bytes, Index)", Box::new(|| { std::hint::black_box(&b[..20]); })),
("&m[..20] (BytesMut, Index)", Box::new(move || { std::hint::black_box(&m()[..20]); })),
("b.get(..20)", Box::new(|| println!(" -> {:?}", b.get(..20)))),
("checked_slice(&b, 6..20)", Box::new(|| println!(" -> {:?}", checked_slice(&b, 6..20)))),
("checked_slice(&b, 6..11)", Box::new(|| println!(" -> {:?}", checked_slice(&b, 6..11)))),
];
for (name, f) in cases {
println!("{name}");
let _ = panic::catch_unwind(panic::AssertUnwindSafe(f));
}
}
--release 跑 3 次,输出相同:
Bytes::slice(..20)
at src/bytes.rs:374: range end out of bounds: 20 <= 11
Bytes::slice(5..3)
at src/bytes.rs:374: range start must not be greater than end: 5 <= 3
Bytes::split_to(20)
at src/bytes.rs:533: split_to out of bounds: 20 <= 11
Bytes::split_off(20)
at src/bytes.rs:481: split_off out of bounds: 20 <= 11
BytesMut::split_to(20)
at src/bytes_mut.rs:408: split_to out of bounds: 20 <= 11
BytesMut::split_off(20)
at src/bytes_mut.rs:329: split_off out of bounds: 20 <= 11
BytesMut::extend_from_within(..20)
at src/bytes_mut.rs:923: range end out of bounds: 20 <= 11
&b[..20] (Bytes, Index)
at src/bin/oob.rs:30: range end index 20 out of range for slice of length 11
&m[..20] (BytesMut, Index)
at src/bin/oob.rs:31: range end index 20 out of range for slice of length 11
b.get(..20)
-> None
checked_slice(&b, 6..20)
-> None
checked_slice(&b, 6..11)
-> Some(b"world")
| 原文 | 来自 | 报告位置 |
|---|---|---|
range end out of bounds: e <= len | range 的第二个 assert! | 调用 range 的那一行:Bytes::slice 里是 bytes.rs:374,extend_from_within 里是 bytes_mut.rs:923 |
range start must not be greater than end: b <= e | range 的第一个 assert! | 同上 |
split_to out of bounds: at <= len | Bytes::split_to、BytesMut::split_to | 方法内部的 assert! |
split_off out of bounds: at <= len | Bytes::split_off | 同上 |
split_off out of bounds: at <= capacity | BytesMut::split_off | 同上 |
range end index e out of range for slice of length len | 标准库的切片下标(Bytes / BytesMut 经 Deref 成 [u8]) | 你的那一行 |
同一个越界,换成 &b[..20] 这样的下标写法,信息变成标准库的措辞,位置变成 oob.rs:30,正是写下标的那一行。
问: split 与 freeze 里 len 11 的 BytesMut 做 split_off(20) 没有 panic,这里同样是 len 11,为什么 panic 了?
答: BytesMut::split_off 的上限是 capacity,不是 len。那一页的缓冲是 with_capacity(64) 再写 11 字节,20 没超过 64;这里是 BytesMut::from(&b"hello world"[..]),容量就是 11。信息里的 20 <= 11 中,11 是容量。切进还没写的容量是合法的,后果见 capacity 不是长度。Bytes 没有容量的概念,它的 split_off 上限是 len。
为什么报在 bytes 内部
panic 的位置是 #[track_caller] 决定的。标准库给切片下标的实现都标了它,位置一路往外传到写下标的地方。bytes 1.12.1 里整个 crate 只有一个函数标了 #[track_caller],就是 lib.rs 的 range:
#[inline(always)]
#[track_caller]
fn range(range: impl core::ops::RangeBounds<usize>, len: usize) -> (usize, usize) {
它把位置往外传了一层,传到调用它的 slice 里(bytes.rs:374);slice 自己没有标,传到这里就停了。split_to、split_off 的 assert! 写在方法体里,方法也没有标,位置就是那个 assert!。
所以从 panic 信息只能知道「某次 slice 越界了」,不知道是哪一次。要找到自己的那一行,看 backtrace。把越界放进一个函数里,debug 构建、RUST_BACKTRACE=1:
use bytes::Bytes;
fn header_value(head: &Bytes, start: usize, end: usize) -> Bytes {
head.slice(start..end)
}
fn main() {
let head = Bytes::from_static(b"hello world");
let v = header_value(&head, 6, 20);
println!("{v:?}");
}
跑 2 次,除线程号外相同:
thread 'main' (1893619) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bytes-1.12.1/src/bytes.rs:374:28:
range end out of bounds: 20 <= 11
stack backtrace:
0: __rustc::rust_begin_unwind
at /rustc/b940084d7eb6a299eb4bfeb8e34901bc051e7ac4/library/std/src/panicking.rs:679:5
1: core::panicking::panic_fmt
at /rustc/b940084d7eb6a299eb4bfeb8e34901bc051e7ac4/library/core/src/panicking.rs:80:14
2: bytes::range::<core::ops::range::Range<usize>>
at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bytes-1.12.1/src/lib.rs:159:5
3: <bytes::bytes::Bytes>::slice::<core::ops::range::Range<usize>>
at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bytes-1.12.1/src/bytes.rs:374:28
4: oob_bt::header_value
at ./src/bin/oob_bt.rs:4:8
5: oob_bt::main
at ./src/bin/oob_bt.rs:9:11
6: <fn() as core::ops::function::FnOnce<()>>::call_once
at ~/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
第 4 帧就是自己的代码:oob_bt.rs:4,head.slice(start..end) 那一行;第 5 帧是传进 20 的调用方。同一个程序用 --release 跑,header_value 被内联,回溯只剩三帧,没有行号:
thread 'main' (1916352) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bytes-1.12.1/src/bytes.rs:374:28:
range end out of bounds: 20 <= 11
stack backtrace:
0: __rustc::rust_begin_unwind
1: core::panicking::panic_fmt
2: oob_bt::main
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
线上的 release 构建碰到这条 panic,信息本身只能告诉你是哪一种操作、实际的两个数;要定位到行,得在 debug 构建里复现,或者给 release 打开调试信息([profile.release] debug = true)。
修法
bytes 1.12.1 没有不 panic 的 slice / split_to / split_off:Bytes 的固有方法里以 try_ 开头的只有 try_into_mut,BytesMut 的是 try_reclaim 和 try_unsplit。下标来自外部输入(长度字段、偏移、解析出来的范围)时,要自己先检查:
| 操作 | 不 panic 的写法 |
|---|---|
b.slice(r) | b.get(r).map(|s| b.slice_ref(s)):get 走 Deref 到 [u8],越界返回 None;slice_ref 再把子切片变回零拷贝的 Bytes(它按地址反推区间,见 切片 的「slice_ref」) |
b.split_to(at) / b.split_off(at) | 先判断 at <= b.len();不够就返回「需要更多数据」或错误 |
m.split_off(at)(BytesMut) | 判断 at <= m.len(),而不是 capacity():切进未写的容量不 panic,但多半不是你想要的 |
| 按字段读整数、跳过若干字节 | 用 Buf 的 try_get_*,或先比较 remaining(),见 advance out of bounds |
上面的 checked_slice 就是第一行的写法:6..20 得到 None,6..11 得到 Some(b"world")。slice_ref 遇到空子切片返回 Bytes::new(),不保留地址,这一点和 slice 不同,靠地址算偏移的代码要注意。
怎么确认自己没踩
- 搜代码里的
.slice(、.split_to(、.split_off(、.extend_from_within(,看参数是不是可能来自外部输入。常量或刚刚len()算出来的值不用管。 - 对解析函数做一个「截断输入」的测试:把一段合法输入从每个位置截断后喂进去,结果只能是「需要更多数据」或错误,不能 panic。
- 碰到这类 panic 时,用 debug 构建加
RUST_BACKTRACE=1复现,找回溯里第一个不在bytes-1.12.1/下的帧。
小结
range end out of bounds、range start must not be greater than end来自lib.rs的range,slice和extend_from_within都用它;split_to out of bounds、split_off out of bounds来自各自方法里的assert!。冒号后面是被断言的条件填上实际值。- 只有
range标了#[track_caller],位置只往外传一层;所以 panic 报在bytes.rs/bytes_mut.rs,不是你的代码。下标写法&b[..20]报在你的那一行。 - 找调用行:debug 构建加
RUST_BACKTRACE=1;release 构建里帧会被内联掉。 - bytes 没有不 panic 的切片方法。外部输入决定下标时,先检查,或用
get加slice_ref。 BytesMut::split_off的上限是容量,不是长度。