Rust in Everything
基于 bytes 1.12.1

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 <= lenrange 的第二个 assert!调用 range 的那一行:Bytes::slice 里是 bytes.rs:374,extend_from_within 里是 bytes_mut.rs:923
range start must not be greater than end: b <= erange 的第一个 assert!同上
split_to out of bounds: at <= lenBytes::split_to、BytesMut::split_to方法内部的 assert!
split_off out of bounds: at <= lenBytes::split_off同上
split_off out of bounds: at <= capacityBytesMut::split_off同上
range end index e out of range for slice of length len标准库的切片下标(Bytes / BytesMut 经 Deref 成 [u8])你的那一行

同一个越界,换成 &b[..20] 这样的下标写法,信息变成标准库的措辞,位置变成 oob.rs:30,正是写下标的那一行。

📝NOTE
没有蠢问题

问: 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 的上限是容量,不是长度。

💬 关联讨论

发起讨论
加载中...