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危险操作可能还有别的,欢迎留言讨论。

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

相关推荐
DongLi012 天前
rustlings 学习笔记 -- exercises/05_vecs
rust
番茄灭世神2 天前
Rust学习笔记第2篇
rust·编程语言
shimly1234563 天前
(done) 速通 rustlings(20) 错误处理1 --- 不涉及Traits
rust
shimly1234563 天前
(done) 速通 rustlings(19) Option
rust
@atweiwei3 天前
rust所有权机制详解
开发语言·数据结构·后端·rust·内存·所有权
shimly1234563 天前
(done) 速通 rustlings(24) 错误处理2 --- 涉及Traits
rust
shimly1234563 天前
(done) 速通 rustlings(23) 特性 Traits
rust
shimly1234563 天前
(done) 速通 rustlings(17) 哈希表
rust
shimly1234563 天前
(done) 速通 rustlings(15) 字符串
rust
shimly1234563 天前
(done) 速通 rustlings(22) 泛型
rust