Cloudflare unwrap崩溃?整理下Rust危险操作

引言

这是Rust九九八十一难第十五篇。之前聊过anyhow,也介绍了thiserror,感觉差不多了,没想爆出了Cloudflare的新闻。猜测很多,据说有一个原因是用了unwrap。这个前车之鉴,给了一个提醒,比如Rust还有哪些危险操作,为什么catch_unwind抓不到崩溃,能否自动化检查等。因此今天梳理下Rust代码的危险操作。

一、危险操作一览

先定个标准,按照受控程度区分。受控程度是指代码不可预测,有安全漏洞,代码可控的停止等。这里先排除带有usafe关键字的,因为关键字本身就说明了问题。梳理了下,从低到高,大体分为下面三类。

1、低危险

1.1、 乱用clone

大部分情况没问题,clone本身不会产生UB,也没有panic。但是乱用可能埋下性能炸弹,而且很隐蔽。

  • 原因

    • 大对象clone:有些类型的clone是深拷贝,比如let b = a.clone();。如果a是Vec<T>(大量数据),Arc<Mutex<T>> 内含大结构,自定义 struct 里包了大对象。clone() 会 重新分配内存 + 复制一整份数据,导致内存不足,程序延迟跳变、吞吐下降,一般的cr,很难识别,明面上看不出来。

    • 高并发场景频繁 clone Arc :多线程下都要原子性地更新同一个内存地址上的计数器,则有线程不得不等待原子操作完成和同步缓存,这个时间本来应该处理业务逻辑。

    • 多线程复制锁:原子操作很昂贵,尤其是锁结构clone,会增加更多内存共享冲突。

  • 乱用的例子

    • 逃避问题
    rust 复制代码
    fn foo(s: &String) {
        let x = s.clone(); // 逃生命周期问题
    }

    说明:一般没问题,但是属于逃避问题,避开rust生命周期检查

    • 循环内clone,造成性能灾难

      rust 复制代码
      for _ in 0..10000 {
          let v2 = v.clone(); // 大对象向量复制一万次
      }

1.2、整数溢出(wrapping)

这个一出,在Debug 模式下会 panic,但是在 Release 模式下,整数算术会环绕 (wrapping) 而不报错,导致结果不正确(逻辑错误)。举个例子:

rust 复制代码
let size = a * b;  // 溢出
let ptr = alloc(size);  // size可能是负数,0等,分配错误大小, 内存被破坏

可以用下面方案替,(也有乘除等类似的api,可以问下ai,很容易查到):

  • checked_add,溢出则返回None,安全地失败

  • overflowing_add,会返回布尔值报告是否有溢出

  • saturating_add:溢出的话会限制到最大或者最小值上,不会painic和环绕

    rust 复制代码
    fn safe_overflow_demo() {
        let max = i32::MAX;
        match max.checked_add(1) {
            Some(v) => println!("v={}", v),
            None => println!("overflow detected"), //overflow detected
        }
    }

2、中危险

运行的时候崩溃等,都是可预期的,不会破坏内存结构和编译器假设的规则。

2.1、expect()

unwrap() 一样会 panic,只是能自定义错误信息。用于用于快速原型和Demo,但线上不应该使用。

可以使用使用 ?者显式错误处理(matchmap_err

  • demo

    rust 复制代码
    fn expect_demo() -> Result<(), String> {
        let num: i32 = "abc".parse().map_err(|e| e.to_string())?; // 正确处理错误
        println!("num = {}", num);
        Ok(())
    }

2.2、 panic!()

调用这个API程序立刻崩溃,生成 unwinding(除非 panic=abort)。一般调试时使用。不适合生产系统流程控制。可以用 Result<T, E> 返回错误,或者使用错误库:thiserroranyhow,之前文章介绍过。

  • Demo
rust 复制代码
fn safe_panic_demo(input: Option<i32>) -> Result<i32, String> {
    match input {
        Some(v) => Ok(v),
        None => Err("input is None".to_string()),
    }
}

2.3、数组越界

这个有两种操作: 使用 [] (Panic)是safe越界 ,使用 get_unchecked()或者*v.as_ptr().add(i) unsafe操作可能导致UB。据说越界访问是Rust最常见的UB来源。

  • Demo

    Rust 复制代码
    fn main() {
        let v: Vec<i32> = vec![10, 20, 30];
        let index_safe: usize = 1;
        let index_oob: usize = 5; // 越界索引,有效范围是 0..3
    
        println!("--- 1. 安全访问方法 (Safe Access) ---");
    
        // 1.1. `v.get(x)` -> 返回 `Option<T>`,安全地处理越界
        match v.get(index_safe) {
            Some(val) => println!("v.get({}) (安全, 有效): {}", index_safe, val),
            None => println!("v.get({}) (安全, 越界): 返回 None", index_safe),
        }
    
        match v.get(index_oob) {
            Some(val) => println!("v.get({}) (安全, 有效): {}", index_oob, val),
            None => println!("v.get({}) (安全, 越界): 返回 None, 程序继续", index_oob),
        }
    
        println!("\n--- 2. 运行时检查访问方法 (Runtime Panic) ---");
    
        // 2.1. `v[x]` -> 越界时触发 `panic!`,中止程序
        println!("v[{}] (安全, 有效): {}", index_safe, v[index_safe]);
    
        // 取消注释下方代码块以观察 panic! 行为
        println!("尝试 v[{}] (越界, panic)...", index_oob);
        let _val_panic = v[index_oob]; //index out of bounds: the len is 3 but the index is 5
        println!("此行不会被执行");
    }

3、极度危险

包含 跳过检查取值,原始指针(*mut T、*const T)操作(unsafe操作就不介绍了)等

3.1、unwrap_unchecked()

这个操作跳过检查 ,直接取值,如果是 None 会导致 UB(未定义行为),不是普通 panic。主要在极少数高性能场景,如编译器内部、手工优化代码。

  • 替代方案

    • 不推荐使用,99.9% 情况不需要。

    • 使用普通 unwrap() 和开发环境 panic 更安全。

  • Demo(仅示意,不要用)

rust 复制代码
unsafe fn unchecked_demo() -> i32 {
    let x: Option<i32> = Some(10);
    x.unwrap_unchecked()
}

3.2、 mem::transmute

mem::transmute 被认为是极度危险(Rust unsafe 中最危险的之一)。它会 完全跳过 Rust 的类型系统,把一个值的 原始内存比特强行解释成另一种类型,而编译器不会检查是否合理。适用于底层优化、ABI 对接。可以用枚举/结构体替代表达,者用 From / TryFrom。

  • Demo(安全替代版)
rust 复制代码
fn safe_transmute_demo() -> Result<u8, String> {
    let x: i32 = 150;
    u8::try_from(x).map_err(|_| "overflow".to_string())
}
  • 替代方案表
想做的事 不要用 应该用
数字 → 字节数组 transmute .to_ne_bytes()
&T → &U transmute reinterpret_cast 方案:ptr.cast()
类型安全转换 transmute From / Into / TryFrom
C ABI struct 转换 transmute #[repr(C)] + 指针转换
Option<Box> 优化大小 transmute Option::takeManuallyDrop
枚举表示(discriminant)操作 transmute std::mem::discriminant

二、catch_unwind为什么有时候捕获不到崩溃

一般使用panic::catch_unwind捕获panic,std::panic::set_hook用于记录日志和堆栈,GDB/LLDB等记录系统外的崩溃。只有catch_unwind会优雅处理错误,保持服务运行,主要说下这个。

1、panic::catch_unwind 是如何工作的?

  • panic 可以理解成是"强制异常 + 栈回退",他会执行 栈展开(stack unwinding),逐层 drop 栈上的变量,若无法继续展开,则进程 abort。

  • Rust是怎么 "展开" 栈的,什么是uwind

    rust 复制代码
    fn a() { b(); }
    fn b() { c(); }
    fn c() { panic!("boom"); }
    
    a();
    markdown 复制代码
    ┌──────────┐
    │  a()     │
    └───▲──────┘
        │ calls
    ┌───┴──────┐
    │  b()     │
    └───▲──────┘
        │ calls
    ┌───┴──────┐
    │  c()     │   ← panic 发生
    └──────────┘

    unwind是这样做的:

    • c() 退出 → drop c 中的变量

    • 回到 b() → drop b 中的变量

    • 回到 a() → drop a 中的变量

    • 若某处有 catch_unwind,停止回退

    • 否则退到线程根并结束线程

    每一步都是 栈帧被弹出(pop stack frame),并执行对应资源释放逻辑。

    这就是 "展开(unwind)"。

  • 增加catch_unwind 后:它捕获当前线程中的 正常 panic 展开(unwind),返回 Result<(), Box<dyn Any + Send>>

rust 复制代码
use std::panic;

let result = panic::catch_unwind(|| {
    panic!("boom");
});

assert!(result.is_err());

2、捕获不到场景

2.1、panic 被设置为 abort

Cargo.toml:

toml 复制代码
[profile.release]
panic = "abort"

这时候代码既不 unwind,也不执行 drop,进程直接终止了,catch_unwind 根本没机会执行。

2.2、跨语言

panic 发生在跨 FFI/外部库边界,尤其是与非-Rust 语言交互(例如 C 或 C++)时 → unwind → abort 行为不确定 → catch_unwind 未必捕获到。doc.rust-lang.org/std/panic/f...

2.3、unsafe 导致的 UB 可能不被 catch_unwind 捕获

当在 unsafe 中触发 UB(例如用裸指针非法读写、悬垂引用、违反借用/别名规则、数据竞争、对齐错误等)------这在语言层面没有定义语义。这可能产生任意行为:程序挂掉、数据破损、继续运行但状态破坏、内存泄漏......这类错误不走 panic/unwind 机制 ,它们不是 "panic! unwind" 的流程。UB本身代表未定义的结果,不好确定代码流程,那么也不一定捕获到,大概率捕获不到。

三、第三方抓取工具

1、FutureExt

相比原生的,它可以直接在 async Future 上捕获 panic,支持链式打印等。

地址:github.com/rust-lang/f...

rust 复制代码
use futures::FutureExt; // 0.3.5

#[tokio::test]
async fn test_async() -> Result<(), Box<dyn std::error::Error>> {
    println!("before catch_unwind");

    let may_panic = async {
        println!("inside async catch_unwind");
        panic!("this is error")
    };

    let async_result = may_panic.catch_unwind().await;

    println!("after catch_unwind");

    assert!(async_result.is_ok());

    Ok(())
}

future 内部 的panic 被 catch_unwind 捕获 转为 Result::Err

2、tower http的catch-panic

中间件来捕获 handler 中 panic,并给客户端返回合适的 HTTP 错误响应(例如 500)

docs.rs/tower-http/...

rust 复制代码
use http::{Request, Response, header::HeaderName};
use std::convert::Infallible;
use tower::{Service, ServiceExt, ServiceBuilder, service_fn};
use tower_http::catch_panic::CatchPanicLayer;
use http_body_util::Full;
use bytes::Bytes;

async fn handle(req: Request<Full<Bytes>>) -> Result<Response<Full<Bytes>>, Infallible> {
    panic!("something went wrong...")
}

let mut svc = ServiceBuilder::new()
    // Catch panics and convert them into responses.
    .layer(CatchPanicLayer::new())
    .service_fn(handle);

// Call the service.
let request = Request::new(Full::default());

let response = svc.ready().await?.call(request).await?;

assert_eq!(response.status(), 500);

axum也可用哈:github.com/tokio-rs/ax...

ini 复制代码
tower-http = { version = "0.5", features = ["catch-panic"] }

代码:

css 复制代码
  use tower_http::catch_panic::CatchPanicLayer;
  ...
   let app = Router::new()
        .route("/", get(ok_handler))
        .route("/panic", get(panic_handler))
        // 加上 CatchPanicLayer
        .layer(CatchPanicLayer::new());
        ...

panic则返回HTTP 500 Internal Server Error,服务不会退出。

3、tokio::spawn自带的工具

rust 复制代码
let handle = tokio::spawn(async {
    panic!("boom!");
});

let result = handle.await;
if let Err(join_err) = result {
    if join_err.is_panic() {
        println!("panic caught!");
    }
}

每个 Tokio 任务都是独立执行,如果任务 panic,返回 JoinError

四、总结

本文总结了Rust代码的危险操作,在提交过程中还可以增加lint,阻止unwrap等的提交或者加白名单,服务本身性能允许的话,还可以兜底抓取崩溃,能抓到90%的panic。另外,如果是后端服务,从经验看,一般还涉及灰度发布,自动回滚,熔断等等保护性操作,服务崩溃估计是各种问题累加,一个unwrap估计没那么大威力。Rust危险操作可能还有别的,欢迎留言讨论。

如果觉得本文有用,辛苦点个关注吧,本人公众号大鱼七成饱

相关推荐
程序员老刘4 小时前
假如Flutter用Rust,你也写不出更快的App
flutter·rust·dart
h***8566 小时前
Rust在Web中的前端开发
开发语言·前端·rust
Rust语言中文社区7 小时前
【Rust日报】 walrus:分布式消息流平台,比 Kafka 快
开发语言·分布式·后端·rust·kafka
武子康8 小时前
AI研究-133 Java vs Kotlin/Go/Rust/Python/Node:2025 详细对比分析 定位与取舍指南
java·javascript·python·golang·rust·kotlin·node
mit6.82410 小时前
C 语言仓库引入 Rust: MCUboot 为例
开发语言·rust
星释10 小时前
Rust 练习册 99:让数字开口说话
开发语言·后端·rust
我发在否10 小时前
Rust > 牛客OJ在线编程常见输入输出练习场
算法·rust
y***548811 小时前
Rust在嵌入式中的实时操作系统
开发语言·后端·rust
苦难之路11 小时前
rCore1
rust