You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用fsanitize时如何捕获崩溃子进程的stdout输出?

解决fsanitize下Rust无法捕获子进程stdout的问题

我之前碰到过几乎一模一样的情况,核心问题出在标准输出的缓冲策略上,和Valgrind与ASAN的运行机制差异直接相关。

问题根源

默认情况下,C程序的stdout在连接终端时是行缓冲(遇到换行就自动刷新),但如果是连接到管道(比如你的Rust程序用Stdio::piped()创建的管道),就会切换为全缓冲——只有当缓冲区被填满、程序主动调用fflush,或者进程正常退出时,缓冲里的内容才会写入管道。

你的C程序在输出output1 from c后立刻触发栈溢出崩溃,这行内容还留在子进程的用户态缓冲里,根本没来得及写入管道。而Valgrind运行时会接管进程的IO流程,可能强制修改了缓冲策略,或者在进程崩溃时帮你自动刷新了缓冲,所以之前用Valgrind能拿到完整输出。

另外要注意:ASAN的错误信息是输出到stderr的,你当前的代码只读取了stdout,所以这部分内容也会丢失,需要一起处理。

解决方案

下面按可行性从高到低给几个方案:

1. 强制子进程stdout无缓冲(无需修改被测C程序)

可以用stdbuf命令包装你的被测程序,强制stdout使用无缓冲模式,这是最省心的方案:

修改Rust里的Command初始化代码:

let mut run_cmd = Command::new("stdbuf")
    .args(["-o0", &format!("./{}", &self.meta.projdata.project_name)]) // -o0 表示stdout无缓冲
    .current_dir(
        &self
            .meta
            .projdata
            .makefile_path
            .as_ref()
            .unwrap_or(&String::from("./"))
    )
    .args([&self.argv].iter().filter(|s| !s.is_empty()))
    .stdin(Stdio::piped())
    .stdout(Stdio::piped())
    .stderr(Stdio::piped())
    .envs(envs)
    .spawn()
    .expect("could not spawn process");

stdbuf是GNU coreutils自带的工具,几乎所有Linux系统都预装了,它能在不修改程序代码的前提下改变标准IO的缓冲策略。

2. 修改被测C程序主动刷新缓冲

如果你有权限修改被测的C代码,在输出后手动刷新stdout即可:

#include <stdio.h>
int main() {
    char array[1];
    puts("output1 from c");
    fflush(stdout); // 强制刷新stdout缓冲,确保内容写入管道
    array[20000] = 'c';
    printf("output2 from c: %llu\n", (long long unsigned) (array[20000]) );
    return 0;
}

这样即使程序立刻崩溃,之前的输出也会被写入管道,Rust程序就能正常读取。

3. 改进Rust的IO读取逻辑(异步读取)

当前你的代码是先wait_timeout再读取stdout,这种方式在进程崩溃时可能错过缓冲数据。更好的做法是在进程运行时,用线程同时读取stdout和stderr,确保所有输出都被及时捕获:

use std::io::{Read, BufRead, BufReader};
use std::thread;

// 可以自定义错误类型,带上超时情况下的已捕获输出
#[derive(Debug)]
enum ExecuteError {
    Timeout,
    TimeoutWithOutput(String, String),
}

fn command_timeout(
    mut cmd: std::process::Child,
    timeout: i32,
    number: i32,
) -> Result<(String, String, i32), ExecuteError> {
    // 启动线程读取stdout
    let mut stdout = cmd.stdout.take().unwrap();
    let stdout_handle = thread::spawn(move || {
        let mut reader = BufReader::new(stdout);
        let mut output = String::new();
        let _ = reader.read_to_string(&mut output);
        output
    });

    // 启动线程读取stderr(捕获ASAN的错误信息)
    let mut stderr = cmd.stderr.take().unwrap();
    let stderr_handle = thread::spawn(move || {
        let mut reader = BufReader::new(stderr);
        let mut output = String::new();
        let _ = reader.read_to_string(&mut output);
        output
    });

    match cmd.wait_timeout(std::time::Duration::from_secs(timeout as u64)).unwrap() {
        Some(expr) => {
            let stdout_output = stdout_handle.join().unwrap();
            let stderr_output = stderr_handle.join().unwrap();
            let retvar = expr.code().unwrap_or(-1);
            Ok((stdout_output, stderr_output, retvar))
        }
        None => {
            println!("killing {} because of timeout", number);
            cmd.kill().expect("Upps, can't kill this one");
            // 即使超时也要读取已经输出的内容
            let stdout_output = stdout_handle.join().unwrap();
            let stderr_output = stderr_handle.join().unwrap();
            Err(ExecuteError::TimeoutWithOutput(stdout_output, stderr_output))
        }
    }
}

这种方式能确保不管进程是正常退出还是崩溃,所有已经写入管道的输出都会被读取,同时也能完整捕获ASAN的stderr错误信息。

额外提示

你的测试用例预期结果包含ASAN的错误信息,所以在最终的测试对比中,需要把stdout和stderr的内容合并后再与预期值对比,或者分别验证两部分内容。

内容的提问来源于stack exchange,提问作者shanks3042

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.11 07:46:24