本文承接上文最后提及的问题。
问题表征
在交叉编译到 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
初始化的,因此也不正确。直接操作这个结构会产生读溢出,从而导致程序崩溃(或者读到垃圾数据)。
为了排除问题,我有以下初步的尝试和分析:
- 首先排除 Rust
pcap
库的原生代码问题:这么多人用,如果有问题应该早就被发现了;而且在 x86 上的确是好的。 - 其次考虑到 Rust 和 musl 工具链的浮点支持不同(前者是硬浮点,后者是软浮点)。我反编译了生成的二进制文件,发现里面完全没有对浮点寄存器的操作。
- 再考虑
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_t
和 subseconds_t
无论架构都是 64 位的;
而 Rust 标准库的定义中,它们都是 c_long
,在 32 位的 mipsel 架构上是 32 位。
也就是说,Rust 中看到的 caplen
和 len
其实分别是 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 字节,代码也没有改变其中的内容。