use crate::VersionFinder;
use regex::bytes::Regex;
use std::{io::SeekFrom, str};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt};
#[allow(clippy::enum_variant_names, clippy::upper_case_acronyms)]
enum LinuxKernelKind {
ARMzImage,
UImage,
X86bzImage,
X86zImage,
}
const UIMAGE_MAGIC_NUMBER: u32 = 0x2705_1956;
const ARM_ZIMAGE_MAGIC_NUMBER: u32 = 0x016F_2818;
async fn discover_linux_kernel_kind<R: AsyncRead + AsyncSeek + Unpin>(
buf: &mut R,
) -> Option<LinuxKernelKind> {
buf.seek(SeekFrom::Start(0x0000)).await.ok()?;
if buf.read_u32().await.ok()? == UIMAGE_MAGIC_NUMBER {
return Some(LinuxKernelKind::UImage);
}
buf.seek(SeekFrom::Start(0x0024)).await.ok()?;
if buf.read_u32_le().await.ok()? == ARM_ZIMAGE_MAGIC_NUMBER {
return Some(LinuxKernelKind::ARMzImage);
}
buf.seek(SeekFrom::Start(0x01FE)).await.ok()?;
if buf.read_u16_le().await.ok()? != 0xAA55 {
return None;
}
buf.seek(SeekFrom::Start(0x0211)).await.ok()?;
match buf.read_u8().await.ok()? & 0x1 {
0 => Some(LinuxKernelKind::X86zImage),
1 => Some(LinuxKernelKind::X86bzImage),
_ => None,
}
}
const WINDOW: usize = 0x200;
const MAGIC_LEN: usize = 6;
async fn read_filled<R: AsyncRead + Unpin>(rd: &mut R, buf: &mut [u8]) -> Option<usize> {
let mut filled = 0;
while filled < buf.len() {
let n = rd.read(&mut buf[filled..]).await.ok()?;
if n == 0 {
break;
}
filled += n;
}
Some(filled)
}
pub(crate) struct LinuxKernel<'a, R: AsyncRead + AsyncSeek + Unpin> {
buf: &'a mut R,
}
impl<'a, R: AsyncRead + AsyncSeek + Unpin> LinuxKernel<'a, R> {
pub(crate) fn from_reader(buf: &'a mut R) -> Self {
LinuxKernel { buf }
}
}
#[async_trait::async_trait(?Send)]
impl<'a, R: AsyncRead + AsyncSeek + Unpin> VersionFinder for LinuxKernel<'a, R> {
async fn get_version(&mut self) -> Option<String> {
match discover_linux_kernel_kind(self.buf).await? {
LinuxKernelKind::ARMzImage => {
async fn get_version_from_arm<R: AsyncRead + Unpin>(mut rd: R) -> Option<String> {
let mut buffer = Vec::default();
compress_tools::tokio_support::uncompress_data(&mut rd, &mut buffer)
.await
.ok()?;
let re = Regex::new(r"Linux version (?P<version>\S+).*").unwrap();
re.captures(&buffer)
.and_then(|m| m.name("version"))
.and_then(|v| str::from_utf8(v.as_bytes()).ok())
.map(|v| v.to_string())
}
let mut buffer = [0; MAGIC_LEN - 1 + WINDOW];
let mut carried = 0;
loop {
let n = self.buf.read(&mut buffer[carried..]).await.ok()?;
if n == 0 {
return None;
}
let filled = carried + n;
for (offset, window) in buffer[..filled].windows(MAGIC_LEN).enumerate() {
match window {
[0x1f, 0x8b, 0x08, ..] => {} [0xfd, b'7', b'z', b'X', b'Z', 0x00] => {} [b'B', b'Z', b'h', ..] => {} [0x5d, 0x00, 0x00, ..] => {} [0x89, 0x4c, 0x5a, ..] => {} [0x02, b'!', b'L', 0x18, ..] => {} [b'(', 0xb5, b'/', 0xfd, ..] => {} _ => continue,
}
let mut slice = &buffer[offset..filled];
let current = self.buf.seek(SeekFrom::Current(0)).await.ok()?;
let rd = AsyncReadExt::chain(&mut slice, &mut self.buf);
if let Some(version) = get_version_from_arm(rd).await {
return Some(version);
}
self.buf.seek(SeekFrom::Start(current)).await.ok()?;
}
carried = filled.min(MAGIC_LEN - 1);
buffer.copy_within(filled - carried..filled, 0);
}
}
LinuxKernelKind::X86bzImage | LinuxKernelKind::X86zImage => {
self.buf.seek(SeekFrom::Start(0x01F1)).await.ok()?;
let setup_sects = u64::from(self.buf.read_u8().await.ok()?);
self.buf.seek(SeekFrom::Start(0x020E)).await.ok()?;
let kernel_version_ptr = u64::from(self.buf.read_u16_le().await.ok()?);
if kernel_version_ptr >= setup_sects * 0x200 {
return None;
}
self.buf
.seek(SeekFrom::Start(kernel_version_ptr + 0x200))
.await
.ok()?;
let mut buffer = [0; WINDOW];
let filled = read_filled(self.buf, &mut buffer).await?;
let re = Regex::new(r"(?P<version>\d+.?\.[^\s\u{0}]+)").unwrap();
re.captures(&buffer[..filled])
.and_then(|m| m.name("version"))
.and_then(|v| str::from_utf8(v.as_bytes()).ok())
.map(|v| v.to_string())
}
LinuxKernelKind::UImage => {
self.buf.seek(SeekFrom::Start(0)).await.ok()?;
let mut buffer = [0; WINDOW];
let filled = read_filled(self.buf, &mut buffer).await?;
let re = Regex::new(r"(?P<version>\d+.?\.[^\s\u{0}]+)").unwrap();
re.captures(&buffer[..filled])
.and_then(|m| m.name("version"))
.and_then(|v| str::from_utf8(v.as_bytes()).ok())
.map(|v| v.to_string())
}
}
}
}
#[cfg(test)]
mod test {
use super::WINDOW;
use crate::{version, BinaryKind};
use std::{
io::{Cursor, Result, Seek, SeekFrom},
pin::Pin,
task::{Context, Poll},
};
use tokio::io::{AsyncRead, AsyncSeek, ReadBuf};
async fn fixture(name: &str) -> impl AsyncRead + AsyncSeek {
use tokio::{fs::File, io::BufReader};
BufReader::new(
File::open(&format!("tests/fixtures/linuxkernel/{name}"))
.await
.unwrap_or_else(|_| panic!("Couldn't open the fixture {name}")),
)
}
fn fixture_bytes(name: &str) -> Vec<u8> {
std::fs::read(format!("tests/fixtures/linuxkernel/{name}"))
.unwrap_or_else(|_| panic!("Couldn't open the fixture {name}"))
}
struct Trickle {
inner: Cursor<Vec<u8>>,
chunk: usize,
}
impl Trickle {
fn new(data: Vec<u8>, chunk: usize) -> Self {
Trickle {
inner: Cursor::new(data),
chunk,
}
}
}
impl AsyncRead for Trickle {
fn poll_read(
mut self: Pin<&mut Self>,
_: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<Result<()>> {
let n = self.chunk.min(buf.remaining());
let mut chunk = vec![0; n];
let read = std::io::Read::read(&mut self.inner, &mut chunk)?;
buf.put_slice(&chunk[..read]);
Poll::Ready(Ok(()))
}
}
impl AsyncSeek for Trickle {
fn start_seek(mut self: Pin<&mut Self>, pos: SeekFrom) -> Result<()> {
self.inner.seek(pos).map(|_| ())
}
fn poll_complete(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<u64>> {
Poll::Ready(self.inner.stream_position())
}
}
#[tokio::test]
async fn compression_header_split_across_reads() {
const HEADER_AT: usize = 0x28;
let original = fixture_bytes("arm-zImage");
for target in [
HEADER_AT + WINDOW - 2,
HEADER_AT + WINDOW - 1,
HEADER_AT + WINDOW,
HEADER_AT + 2 * WINDOW - 3,
] {
let mut data = original[..HEADER_AT].to_vec();
data.resize(target, 0);
data.extend_from_slice(&original[HEADER_AT..]);
assert_eq!(
version(&mut Cursor::new(data), BinaryKind::LinuxKernel).await,
Some("4.4.1".to_string()),
"payload at offset {target:#x} was not found",
);
}
}
#[tokio::test]
async fn version_from_a_reader_with_short_reads() {
for (f, v) in &[
("arm-uImage", "4.1.15-1.2.0+g274a055"),
("x86-bzImage", "4.1.30-1-MANJARO"),
("x86-zImage", "4.1.30-1-MANJARO"),
] {
for chunk in [1, 7, 64, 333] {
assert_eq!(
version(
&mut Trickle::new(fixture_bytes(f), chunk),
BinaryKind::LinuxKernel
)
.await,
Some(v.to_string()),
"{f} read {chunk} bytes at a time",
);
}
}
}
#[tokio::test]
async fn linux_version() {
for (f, v) in &[
("arm-uImage", "4.1.15-1.2.0+g274a055"),
("arm-zImage", "4.4.1"),
("x86-bzImage", "4.1.30-1-MANJARO"),
("x86-zImage", "4.1.30-1-MANJARO"),
] {
assert_eq!(
version(&mut fixture(f).await, BinaryKind::LinuxKernel).await,
Some(v.to_string())
);
}
}
}