Rust in Everything
基于 bytes 1.12.1

在本站中

本站是开源的,仓库在 yuxuetr/rustineverything。这一部分的链接都指向提交 89a61b1(2026-10-23),行号以它为准。本站的 Cargo.toml 没有直接依赖 bytes,Cargo.lock 里的 bytes 是 axum、hyper 等依赖带进来的;代码里写到 Bytes 的只有 pay_routes.rs 的两处 axum::body::Bytes。用得最多的反而是看不见的那一处:dioxus 的 server 函数都用 axum 的 Bytes 提取器读请求体。

  1. 原始 body:两个支付回调:微信支付和 Stripe 的回调用 Bytes 拿原始 body,因为签名是对原始字节算的,解析再序列化就对不上。这个 Bytes 来自 hyper 读缓冲 split_to + freeze 出来的数据块,经 http-body-util 的 collect 合成一个:只有一块时不拷贝,多块时拷一次;handler 随后转成 String 又拷一次。
  2. server 函数的请求体:dioxus-fullstack 0.7.9 用 Bytes::from_request(..).await.unwrap() 读 server 函数的 JSON body,超过 axum 默认的 2 MiB 时这个 unwrap panic,本站的图片上传因此在 1.5 MB 左右就回 500。一次图片上传在这条路上的几次大块分配,以及本站只给 /api/upload 放宽上限的修法。
flowchart TB
  sock["socket"] --> rbuf["hyper 读缓冲 BytesMut"]
  rbuf --> chunk["数据块 Bytes"]
  chunk --> col["collect 合成一个 Bytes"]
  col --> pay["支付回调:验签"]
  col --> sfn["server 函数:JSON"]

版本:1.12.0 与 1.12.1

89a61b1 的 Cargo.lock 锁的是 bytes 1.12.0(带 serde feature),比本卷早一个补丁版本。两者的差别只有 #837「Handle unwinding from Box::new」:1.12.0 的 BytesMut::promote_to_shared 和 Bytes 的 shallow_clone_vec 先构造出一个拥有缓冲的值再 Box::new 它,Box::new 如果 panic,栈展开会把同一块缓冲释放两次;1.12.1 改成先分配、后构造(From<Vec<u8>> 也按同样的顺序改写了)。细节见 怎么读这一卷 的「1.12.0 到 1.12.1」一节。

这条路径要求 Box::new panic,也就是分配失败时 panic。稳定版 Rust 上分配失败走的是 handle_alloc_error,默认打印一行然后 abort,不展开栈;要让它 panic,只能用还没稳定的功能(nightly 的 -Zoom=panic,或 std::alloc::set_alloc_error_hook)。本站在 rust:1-trixie 镜像里用稳定版构建,没有自定义 #[global_allocator],也没有设置相关的编译参数,所以 1.12.0 里那段代码在本站走不到。下面的程序用一个分配器让 promote_to_shared 要的那块 40 字节的 Shared 分配失败,看稳定版上会发生什么(bytes = "=1.12.1",M2 Max、rustc 1.99.0、--release):

use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicBool, Ordering::Relaxed};

use bytes::BytesMut;

// 打开开关之后,下一次 40 字节的分配返回 null(模拟分配失败)
struct FailOnce;
static ARMED: AtomicBool = AtomicBool::new(false);

unsafe impl GlobalAlloc for FailOnce {
  unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
    if layout.size() == 40 && ARMED.swap(false, Relaxed) {
      return std::ptr::null_mut();
    }
    unsafe { System.alloc(layout) }
  }
  unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
    unsafe { System.dealloc(ptr, layout) }
  }
}

#[global_allocator]
static GLOBAL: FailOnce = FailOnce;

fn main() {
  let mut buf = BytesMut::with_capacity(1024);
  buf.extend_from_slice(b"hello world");
  let result = std::panic::catch_unwind(move || {
    ARMED.store(true, Relaxed);
    // 第一次 split:promote_to_shared 要 Box 一个 40 字节的 Shared
    let head = buf.split_to(5);
    (head, buf)
  });
  println!("catch_unwind 返回了: is_err={}", result.is_err());
}

连跑 3 次输出相同,退出码都是 134(SIGABRT):

memory allocation of 40 bytes failed
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

catch_unwind 没有返回:进程在分配失败处直接 abort,没有栈展开,也就没有 1.12.0 里的那次重复释放。

更早的一个修复也要核对:1.11.1 修了 BytesMut::reserve 里一个未检查的加法(GHSA-434x-w66g-qw3r,CVE-2026-25541),release 构建下可能把 cap 设得比真实分配还大,见 reserve 的「1.11.1」一节。修复提交 d0293b0 在 v1.12.0 里(git tag --contains d0293b0 列出 v1.11.1、v1.12.0、v1.12.1),本站锁的 1.12.0 不受影响。

💬 关联讨论

发起讨论
加载中...