Rust in Everything
基于 bytes 1.12.1

try_get_int 把 -1 读成 16777215

主线位置:共享主线的下游,Buf 读游标。读一个「n 字节的有符号整数」是协议解析里的常见操作(3 字节的偏移、24 位的采样值),get_int 和 try_get_int 本该只差「数据不够时 panic 还是返回 Err」。

flowchart TB
  gi["get_int n"] --> gu["get_uint n"]
  gu --> se["sign_extend"]
  se --> neg["-1"]
  ti["try_get_int n"] --> mac["buf_try_get_impl be"]
  mac --> pad["拷进全零的 8 字节"]
  pad --> fb["i64::from_be_bytes"]
  fb --> pos["16777215"]

get_int 的符号扩展怎么做、try_get_int 为什么绕过了它、这段代码修过几次,在 Buf 的「get_int:符号扩展」和那里的冷知识里讲过。这一页是它的使用者视角:哪些方法、哪些版本受影响,怎么绕开,怎么知道升级之后修好了。

本页的实验在 M2 Max、rustc 1.99.0、--release 下运行,依赖 bytes = "=1.12.1"。

复现

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

// the workaround: read unsigned, then sign-extend like get_int does
fn try_get_int_signed<B: Buf>(buf: &mut B, nbytes: usize) -> Result<i64, TryGetError> {
  let v = buf.try_get_uint(nbytes)?;
  if nbytes == 0 {
    return Ok(0);
  }
  let shift = (8 - nbytes) * 8;
  Ok(((v << shift) as i64) >> shift)
}

fn main() {
  let cases: [(&[u8], usize); 5] = [
    (&[0xff, 0xff, 0xff], 3),
    (&[0x80, 0x00, 0x00], 3),
    (&[0x7f, 0xff, 0xff], 3),
    (&[0xfe], 1),
    (&[0xff; 8], 8),
  ];
  println!("{:<19} {:>9} {:>12} {:>10} {:>14} {:>10}", "hex, n", "get_int", "try_get_int", "get_int_le", "try_get_int_le", "workaround");
  for (data, n) in cases {
    let be = (&data[..]).get_int(n);
    let try_be = (&data[..]).try_get_int(n).unwrap();
    // reverse so the little-endian readers see the same number
    let rev: Vec<u8> = data.iter().rev().copied().collect();
    let le = (&rev[..]).get_int_le(n);
    let try_le = (&rev[..]).try_get_int_le(n).unwrap();
    let fixed = try_get_int_signed(&mut &data[..], n).unwrap();
    println!("{:<19} {be:>9} {try_be:>12} {le:>10} {try_le:>14} {fixed:>10}", format!("{}, {n}", data.iter().map(|b| format!("{b:02x}")).collect::<String>()));
  }

  // native endian on this machine, and other Buf types
  let ne = [0xffu8, 0xff, 0xff];
  println!("get_int_ne(3) = {}, try_get_int_ne(3) = {:?}", (&ne[..]).get_int_ne(3), (&ne[..]).try_get_int_ne(3));
  let mut b = Bytes::from_static(&[0xff, 0xff, 0xff]);
  println!("Bytes:  try_get_int(3) = {:?}", b.try_get_int(3));
  let mut ch = (&[0xffu8][..]).chain(&[0xffu8, 0xff][..]);
  println!("Chain:  try_get_int(3) = {:?}", ch.try_get_int(3));
  println!("fixed width: try_get_i16 = {:?}, try_get_i32 = {:?}", (&[0xffu8; 2][..]).try_get_i16(), (&[0xffu8; 4][..]).try_get_i32());
  let mut s = &[0xffu8, 0xff][..];
  println!("short:  try_get_int(3) = {:?}, workaround = {:?}", s.try_get_int(3), try_get_int_signed(&mut s, 3));
}

跑 3 次,输出相同:

hex, n                get_int  try_get_int get_int_le try_get_int_le workaround
ffffff, 3                  -1     16777215         -1       16777215         -1
800000, 3            -8388608      8388608   -8388608        8388608   -8388608
7fffff, 3             8388607      8388607    8388607        8388607    8388607
fe, 1                      -2          254         -2            254         -2
ffffffffffffffff, 8        -1           -1         -1             -1         -1
get_int_ne(3) = -1, try_get_int_ne(3) = Ok(16777215)
Bytes:  try_get_int(3) = Ok(16777215)
Chain:  try_get_int(3) = Ok(16777215)
fixed width: try_get_i16 = Ok(-1), try_get_i32 = Ok(-1)
short:  try_get_int(3) = Err(TryGetError { requested: 3, available: 2 }), workaround = Err(TryGetError { requested: 3, available: 2 })

哪些受影响,从表里读出来:

情形结果
try_get_int / try_get_int_le / try_get_int_ne,nbytes 小于 8,最高位为 1错:得到 2^(8·nbytes) 加上正确值,例如 -1 成了 16777215、-8388608 成了 8388608
同上,最高位为 0(非负数)对
同上,nbytes 等于 8对:8 个字节填满 i64,不需要扩展
get_int / get_int_le / get_int_ne对
定长的 try_get_i8 / i16 / i32 / i64(及 _le / _ne)对:数组本身就是该类型的宽度
底层是 &[u8]、Bytes、Chain,或经 &mut B 转发都一样错

只有负数出错、非负数都对,所以拿正数写的测试全部通过。这是它能一直留到现在的原因之一,见下面的问答。

源码

try_get_int 和 try_get_int_le 直接用 buf_try_get_impl!:

    fn try_get_int(&mut self, nbytes: usize) -> Result<i64, TryGetError> {
        buf_try_get_impl!(be => self, i64, nbytes);
    }
    fn try_get_int_le(&mut self, nbytes: usize) -> Result<i64, TryGetError> {
        buf_try_get_impl!(le => self, i64, nbytes);
    }

try_get_int_ne 按目标平台的字节序转给这两个之一。宏的 be 分支 把 nbytes 个字节拷进一个全零的 8 字节数组的末尾,再整体转换:

        let mut buf = [0; SIZE];
        $this.try_copy_to_slice(&mut buf[slice_at..])?;
        return Ok($typ::from_be_bytes(buf));

高位那几个字节是 0,i64::from_be_bytes 得到的就是非负数。le 分支同理,只是拷进数组的开头。这个宏对 try_get_uint 是对的(无符号数高位本来就该补 0),对有符号数少了一步。对照 get_int:

    fn get_int(&mut self, nbytes: usize) -> i64 {
        sign_extend(self.get_uint(nbytes), nbytes)
    }

它先读无符号值,再交给 sign_extend 把符号位复制到高位。

Buf 的实现者里没有谁覆盖这几个方法:在 1.12.1 的 src/ 里,fn try_get_int 只出现在 trait 的默认实现和 deref_forward_buf! 的转发里(原样调用 (**self).try_get_int(nbytes),用于 &mut T 和 Box<T>)。所以 Bytes、BytesMut、Chain、&mut B、Box<B> 全都走同一份代码,上面的输出也是这样。

📝NOTE
没有蠢问题

问: 这么明显的错,文档测试为什么没抓到?

答: 1.12.1 里 try_get_int 和 try_get_int_le 各有两个文档示例:一个读 b"\x01\x02\x03 hello"(小端版是 b"\x03\x02\x01 hello"),断言 Ok(0x010203_i64);另一个测数据不够时的 Err。一个是正数,一个是错误路径,而正数恰好不受影响。PR #839 的说明里写,新增的测试 try_get_int_sign_extends 覆盖了大端、小端、本机字节序的符号扩展,并且「It fails on master (try_get_int(3) returns Ok(16777215)) and passes with this change」。

哪些版本受影响

  • try_get_* 这一族是 1.10.0 加入的(#753,git tag --contains 的第一个 tag 是 v1.10.0),try_get_int 从那时起就是现在这样。所以 1.10.0 到 1.12.1 都受影响;更早的版本没有这些方法。

  • 修复是 PR #839(「fix: try_get_int/try_get_int_le do not sign-extend n-byte integers」),2026-09-03 合并,合并提交 7930d93。它把两个方法改成和 get_int 一样的写法:

    fn try_get_int(&mut self, nbytes: usize) -> Result<i64, TryGetError> {
        Ok(sign_extend(self.try_get_uint(nbytes)?, nbytes))
    }
    

    PR 说明还写道:「try_get_int_ne delegates to these two methods and inherited the bug; it is fixed transitively. No Buf implementation overrides these methods, so all implementors were affected.」

  • 写作时(2026-10-24)没有任何 tag 包含 7930d93:git tag --contains 7930d93 输出为空,GitHub 上最新的 release 仍是 2026-07-08 的 v1.12.1,master 比 v1.12.1 多 4 个提交(见 怎么读这一卷 的「锁定版本」)。

修法

  • 用上面的 try_get_int_signed:先 try_get_uint,再做和 sign_extend 一样的移位。它在数据不够时返回同样的 TryGetError、不动游标(因为失败的是 try_get_uint,见 Buf 的「try_get_*:不想 panic 时」),nbytes == 0 返回 0,nbytes > 8 时由 try_get_uint panic,和原方法的约定一致。小端用 try_get_uint_le。
  • 或者先比较 remaining() >= nbytes,够了再调 get_int。
  • 宽度正好是 1、2、4、8 字节时,直接用定长的 try_get_i8 / try_get_i16 / try_get_i32 / try_get_i64,它们不受影响。

这个替代写法在修复发版之后依然正确:修复后的 try_get_int 做的正是同一件事,到时候换回去即可。

怎么确认自己没踩

  • 在自己的代码和依赖里搜 try_get_int(会同时匹配 _le、_ne)。cargo tree -i bytes 看锁定的版本是否在 1.10.0 到 1.12.1 之间。

  • 写一条现在应该失败、修复发版后会通过的测试,升级 bytes 时它会告诉你修好了没有:
    use bytes::Buf;
    
    // red on bytes 1.12.1; turns green once a release contains #839
    #[test]
    fn try_get_int_sign_extends() {
      assert_eq!((&[0xffu8, 0xff, 0xff][..]).try_get_int(3), Ok(-1));
      assert_eq!((&[0xffu8, 0xff, 0xff][..]).try_get_int_le(3), Ok(-1));
    }
    

    在 1.12.1 上 cargo test --release 跑 3 次,除线程号外相同:

    running 1 test
    try_get_int_sign_extends --- FAILED
    
    failures:
    
    ---- try_get_int_sign_extends stdout ----
    
    thread 'try_get_int_sign_extends' (1942153) panicked at tests/try_get_int_sign.rs:6:3:
    assertion `left == right` failed
      left: Ok(16777215)
     right: Ok(-1)
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
    
    
    failures:
        try_get_int_sign_extends
    
    test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
    

    反过来也验过:把 bytes 换成 7930d93 这个提交的源码(git archive 导出,用 path 依赖),同一条测试通过,test result: ok. 1 passed。所以这条测试确实能区分修没修。

  • 自己的协议测试里,给每个有符号字段至少放一个负数样例(最高位为 1 的值,如 0x800000、0xffffff)。

小结

  • bytes 1.10.0 到 1.12.1 的 try_get_int、try_get_int_le、try_get_int_ne 在 nbytes < 8 时不做符号扩展,负数读成正数(0xffffff 得到 16777215);get_int 系列和定长的 try_get_i16 等都对。
  • 原因是它们直接用 buf_try_get_impl! 把字节补零转换,没有经过 sign_extend;没有任何 Buf 实现覆盖它们,所有类型都受影响。
  • 修复 #839 在 2026-09-03 合进 master,写作时没有 tag 包含它。
  • 绕开:try_get_uint 加手动符号扩展,或先查 remaining() 再 get_int;留一条用负数断言的测试,它在修复发版后才会变绿。

💬 关联讨论

发起讨论
加载中...