mipsel 架构上 Rust 标准库与 musl 的时间 ABI 兼容性问题一例

 

本文承接上文最后提及的问题。

问题表征

在交叉编译到 mipsel 架构之后,如下代码的运行结果非常奇怪:

use pcap::Capture;
use std::error;


fn main() -> Result<(), Box<dyn error::Error>> {
    let cap = Capture::from_device("eth0")?.immediate_mode(true).open()?;
    while let Ok(packet) = cap.next_packet() {
        let packet = fix_packet(packet);
        println!("{:?} {:?}", packet.header.caplen, &packet.data.len());
    }
    Ok(())
}

具体地,返回的结构体 PacketHeader 对象定义如下:

#[repr(C)]
pub struct PacketHeader {
    pub ts: timeval,
    pub caplen: u32,
    pub len: u32,
}

其中问题代码读到的 header.caplen 是一个很大的数,而 header.len 始终是 0。 data 是一个胖指针。虽然其地址没有问题,但长度因为也是用 caplen 初始化的,因此也不正确。直接操作这个结构会产生读溢出,从而导致程序崩溃(或者读到垃圾数据)。

为了排除问题,我有以下初步的尝试和分析:

  1. 首先排除 Rust pcap 库的原生代码问题:这么多人用,如果有问题应该早就被发现了;而且在 x86 上的确是好的。
  2. 其次考虑到 Rust 和 musl 工具链的浮点支持不同(前者是硬浮点,后者是软浮点)。我反编译了生成的二进制文件,发现里面完全没有对浮点寄存器的操作。
  3. 再考虑 libpcap 自己的问题,用纯 C 编写了一个小 demo,使用 musl 工具链交叉编译后包长度是正确的。

代码分析

在困惑了一阵子之后,我发现上述程序的循环中,打印出的长度数据似乎是某种递增的序列,并在一定数值后回到 0,如此往复。 回想 PacketHeader 的定义,caplen 前一个字段是 ts,类型是 libc::timeval,也就是是精确到微秒的 UNIX 时间戳。 于是我突然意识到,读到的这个“长度”可能是从前面的时间中溢出的数据。

怎么会产生这样的问题呢?阅读 pcap 这个 Rust 库的代码可知, 包头直接来自于 libpcap 的 C API 返回的结构体 raw::pcap_pkthdr。在 C 和 Rust 中,这个结构体的第一个字段都是 timeval,展开后通常是:

struct timeval {
    time_t tv_sec;
    suseconds_t tv_usec;
};

进一步查阅代码,在 musl 的定义中,time_tsubseconds_t 无论架构都是 64 位的; 而 Rust 标准库的定义中,它们都是 c_long,在 32 位的 mipsel 架构上是 32 位。 也就是说,Rust 中看到的 caplenlen 其实分别是 ts.tv_usec 的低 32 位(范围是 0 - 999999)和 高 32 位(始终为 0),所以会有上述的诡异行为。

问题背景

在找到引发问题的代码后,我一度以为找到了 Rust 标准库的 bug。但在进一步 STFW 之后,我发现这居然是一个 feature:

  • musl 为了缓解 2038 年 32 位 UNIX 时间戳会溢出的问题,从 1.2.0 版开始将所有的时间戳类型修改成了 64 位
  • Rust 标准库实现选择不更改相关定义:#1848,只在相关的类型定义上加了一个弃用警告(但由于标准库是预编译的,如果直接用 timeval,在编译的时候并不能看到这个警告)。

这种不一致就导致了上面描述的 ABI 问题。

解决方案

虽然问题来源比较深,但至少还能(在不修改任何库的前提下)魔改一下绕过去:

use pcap::{Packet, PacketCodec, PacketHeader};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PacketOwned {
    pub header: PacketHeader,
    pub data: Box<[u8]>
}

pub struct FixHeaderCodec;
impl PacketCodec for FixHeaderCodec {
    type Item = PacketOwned;
    #[cfg(all(target_env = "musl", target_arch = "mips", target_os = "linux", target_endian = "little"))]
    fn decode(&mut self, packet: Packet) -> Self::Item {
        struct ActualHeader {
            tv_sec: i64,
            tv_usec: i64,
            caplen: u32,
            len: u32
        }
        let actual_header: &ActualHeader;
        let data: &[u8];
        unsafe {
            actual_header = &*(packet.header as *const _ as *const ActualHeader);
            data = std::slice::from_raw_parts(packet.data as *const _ as *const u8, actual_header.caplen as usize);
        }
        let new_header = PacketHeader {
            ts: libc::timeval {
                tv_sec: actual_header.tv_sec as i32,
                tv_usec: actual_header.tv_usec as i32,
            },
            caplen: actual_header.caplen,
            len: actual_header.len,
        };
        PacketOwned {
            header: new_header,
            data: data.into()
        }
    }
    #[cfg(not(all(target_env = "musl", target_arch = "mips", target_os = "linux", target_endian = "little")))]
    fn decode(&mut self, packet: Packet) -> Self::Item {
        PacketOwned {
            header: *packet.header,
            data: packet.data.into(),
        }
    }
}

而后使用 cap.iter(FixHeaderCodec) 即可获得正确长度的包。注意这里的 unsafe 转换是安全的,因为 header 的长度的确有 24 字节,代码也没有改变其中的内容。