summaryrefslogtreecommitdiff
path: root/tools/kapi/src/extractor/vmlinux/binary_utils.rs
diff options
context:
space:
mode:
Diffstat (limited to 'tools/kapi/src/extractor/vmlinux/binary_utils.rs')
-rw-r--r--tools/kapi/src/extractor/vmlinux/binary_utils.rs461
1 files changed, 461 insertions, 0 deletions
diff --git a/tools/kapi/src/extractor/vmlinux/binary_utils.rs b/tools/kapi/src/extractor/vmlinux/binary_utils.rs
new file mode 100644
index 000000000000..046772ea690c
--- /dev/null
+++ b/tools/kapi/src/extractor/vmlinux/binary_utils.rs
@@ -0,0 +1,461 @@
+// SPDX-License-Identifier: GPL-2.0
+// Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+
+// Array-bound constants matching `include/linux/kernel_api_spec.h`.
+// String fields are `const char *`; call `DataReader::ptr_size()` for
+// the per-target pointer width.
+pub mod sizes {
+ pub const MAX_PARAMS: usize = 16;
+ pub const MAX_ERRORS: usize = 32;
+ pub const MAX_CONSTRAINTS: usize = 32;
+ pub const MAX_LOCKS: usize = 16;
+ pub const MAX_CAPABILITIES: usize = 8;
+ pub const MAX_SIGNALS: usize = 32;
+ pub const MAX_STRUCT_SPECS: usize = 8;
+ pub const MAX_SIDE_EFFECTS: usize = 32;
+ pub const MAX_STATE_TRANS: usize = 8;
+
+ pub const NAME: usize = 0;
+ pub const DESC: usize = 0;
+}
+
+// Section markers of `struct kernel_api_spec`. The kernel only sets a
+// marker when the matching *_COUNT()/KAPI_EXAMPLES() macro is used, so
+// a zero marker is as valid as the expected one.
+pub mod magic {
+ pub const PARAMS: u32 = 0x4B415031; // 'KAP1'
+ pub const RETURN: u32 = 0x4B415232; // 'KAR2'
+ pub const ERRORS: u32 = 0x4B414533; // 'KAE3'
+ pub const LOCKS: u32 = 0x4B414C34; // 'KAL4'
+ pub const CONSTRAINTS: u32 = 0x4B414335; // 'KAC5'
+ pub const INFO: u32 = 0x4B414936; // 'KAI6'
+ pub const SIGNALS: u32 = 0x4B415337; // 'KAS7'
+ pub const SIGMASK: u32 = 0x4B414D38; // 'KAM8'
+ pub const STRUCTS: u32 = 0x4B415439; // 'KAT9'
+ pub const EFFECTS: u32 = 0x4B414641; // 'KAFA'
+ pub const TRANS: u32 = 0x4B415442; // 'KATB'
+ pub const CAPS: u32 = 0x4B414343; // 'KACC'
+}
+
+/// Resolve a virtual-address string pointer against the vmlinux ELF
+/// and return the NUL-terminated C string it points at.
+pub fn resolve_vaddr_string(elf: &goblin::elf::Elf, data: &[u8], vaddr: u64) -> Option<String> {
+ if vaddr == 0 {
+ return None;
+ }
+ for sh in &elf.section_headers {
+ let start = sh.sh_addr;
+ let end = start.checked_add(sh.sh_size)?;
+ if vaddr < start || vaddr >= end {
+ continue;
+ }
+ // File-backed sections only (skip SHT_NOBITS etc.)
+ if sh.sh_type == goblin::elf::section_header::SHT_NOBITS {
+ return None;
+ }
+ let rel = (vaddr - start) as usize;
+ let file_start = sh.sh_offset as usize + rel;
+ if file_start >= data.len() {
+ return None;
+ }
+ let tail = &data[file_start..];
+ let nul = tail.iter().position(|&b| b == 0)?;
+ return std::str::from_utf8(&tail[..nul]).ok().map(str::to_string);
+ }
+ None
+}
+
+/// Endianness of the target ELF binary
+#[derive(Clone, Copy, PartialEq)]
+pub enum Endian {
+ Little,
+ Big,
+}
+
+/// Resolves string pointers read from `.kapi_specs` back to their
+/// underlying C strings in the vmlinux rodata.
+pub struct StringResolver<'a> {
+ pub elf: &'a goblin::elf::Elf<'a>,
+ pub vmlinux: &'a [u8],
+}
+
+// Helper for reading data at specific offsets
+pub struct DataReader<'a> {
+ pub data: &'a [u8],
+ pub pos: usize,
+ pub endian: Endian,
+ /// true for 64-bit ELF, false for 32-bit
+ pub is_64bit: bool,
+ /// Used to follow `const char *` fields into rodata.
+ pub resolver: Option<StringResolver<'a>>,
+}
+
+impl<'a> DataReader<'a> {
+ pub fn new(data: &'a [u8], offset: usize, endian: Endian, is_64bit: bool) -> Self {
+ Self {
+ data,
+ pos: offset,
+ endian,
+ is_64bit,
+ resolver: None,
+ }
+ }
+
+ pub fn with_resolver(mut self, resolver: StringResolver<'a>) -> Self {
+ self.resolver = Some(resolver);
+ self
+ }
+
+ /// Pointer width of the target in bytes (4 or 8).
+ pub fn ptr_size(&self) -> usize {
+ if self.is_64bit {
+ 8
+ } else {
+ 4
+ }
+ }
+
+ /// Advance the read position to the next multiple of `align`.
+ /// Needed before every naturally-aligned field when the containing
+ /// struct is not `__packed`.
+ pub fn align_to(&mut self, align: usize) {
+ if align > 1 {
+ let rem = self.pos % align;
+ if rem != 0 {
+ self.pos = (self.pos + (align - rem)).min(self.data.len());
+ }
+ }
+ }
+
+ /// Read a target-sized pointer slot. Returns the virtual address
+ /// stored in the slot, or `None` if there isn't enough data. The
+ /// caller is expected to align the reader first if the containing
+ /// struct demands natural alignment.
+ pub fn read_ptr(&mut self) -> Option<u64> {
+ self.align_to(self.ptr_size());
+ if self.is_64bit {
+ self.read_u64()
+ } else {
+ self.read_u32().map(|v| v as u64)
+ }
+ }
+
+ pub fn read_bytes(&mut self, len: usize) -> Option<&'a [u8]> {
+ if self.pos + len <= self.data.len() {
+ let bytes = &self.data[self.pos..self.pos + len];
+ self.pos += len;
+ Some(bytes)
+ } else {
+ None
+ }
+ }
+
+ pub fn read_u32(&mut self) -> Option<u32> {
+ self.align_to(4);
+ let b: [u8; 4] = self.read_bytes(4)?.try_into().unwrap();
+ Some(match self.endian {
+ Endian::Little => u32::from_le_bytes(b),
+ Endian::Big => u32::from_be_bytes(b),
+ })
+ }
+
+ pub fn read_u8(&mut self) -> Option<u8> {
+ self.read_bytes(1).map(|b| b[0])
+ }
+
+ pub fn read_i32(&mut self) -> Option<i32> {
+ self.align_to(4);
+ let b: [u8; 4] = self.read_bytes(4)?.try_into().unwrap();
+ Some(match self.endian {
+ Endian::Little => i32::from_le_bytes(b),
+ Endian::Big => i32::from_be_bytes(b),
+ })
+ }
+
+ pub fn read_u64(&mut self) -> Option<u64> {
+ self.align_to(8);
+ let b: [u8; 8] = self.read_bytes(8)?.try_into().unwrap();
+ Some(match self.endian {
+ Endian::Little => u64::from_le_bytes(b),
+ Endian::Big => u64::from_be_bytes(b),
+ })
+ }
+
+ pub fn read_i64(&mut self) -> Option<i64> {
+ self.align_to(8);
+ let b: [u8; 8] = self.read_bytes(8)?.try_into().unwrap();
+ Some(match self.endian {
+ Endian::Little => i64::from_le_bytes(b),
+ Endian::Big => i64::from_be_bytes(b),
+ })
+ }
+
+ /// Read a target-sized unsigned value (4 bytes for 32-bit, 8 bytes for 64-bit).
+ pub fn read_usize(&mut self) -> Option<usize> {
+ self.align_to(self.ptr_size());
+ if self.is_64bit {
+ // No double-align: read_u64 would re-align, but we just
+ // did that with ptr_size() which is 8 on 64-bit.
+ let b: [u8; 8] = self.read_bytes(8)?.try_into().unwrap();
+ Some(match self.endian {
+ Endian::Little => u64::from_le_bytes(b) as usize,
+ Endian::Big => u64::from_be_bytes(b) as usize,
+ })
+ } else {
+ let b: [u8; 4] = self.read_bytes(4)?.try_into().unwrap();
+ Some(match self.endian {
+ Endian::Little => u32::from_le_bytes(b) as usize,
+ Endian::Big => u32::from_be_bytes(b) as usize,
+ })
+ }
+ }
+
+ // Helper methods for common patterns
+ pub fn read_bool(&mut self) -> Option<bool> {
+ self.read_u8().map(|v| v != 0)
+ }
+
+ /// Read a `const char *` slot using the target pointer width
+ /// (4 bytes on 32-bit, 8 bytes on 64-bit) and, if a resolver is
+ /// attached, follow the address into the vmlinux to recover the
+ /// C string. The `_max_len` argument is ignored.
+ pub fn read_optional_string(&mut self, _max_len: usize) -> Option<String> {
+ let vaddr = self.read_ptr()?;
+ let resolver = self.resolver.as_ref()?;
+ resolve_vaddr_string(resolver.elf, resolver.vmlinux, vaddr).filter(|s| !s.is_empty())
+ }
+
+ pub fn read_string_or_default(&mut self, max_len: usize) -> String {
+ self.read_optional_string(max_len).unwrap_or_default()
+ }
+
+ /// Follow a `const s64 *` slot (already read with `read_ptr`) to the
+ /// `count` values it points at. A NULL or unresolvable pointer yields
+ /// an empty list.
+ pub fn resolve_s64_array(&self, vaddr: u64, count: u32) -> Vec<i64> {
+ let Some(resolver) = self.resolver.as_ref() else {
+ return Vec::new();
+ };
+ if vaddr == 0 {
+ return Vec::new();
+ }
+ let Some(start) = super::vaddr_to_file_offset(resolver.elf, vaddr) else {
+ return Vec::new();
+ };
+ let raw = (count as usize)
+ .checked_mul(8)
+ .and_then(|len| start.checked_add(len))
+ .and_then(|end| resolver.vmlinux.get(start..end));
+ raw.map_or_else(Vec::new, |raw| {
+ raw.chunks_exact(8)
+ .map(|chunk| {
+ let b: [u8; 8] = chunk.try_into().unwrap();
+ match self.endian {
+ Endian::Little => i64::from_le_bytes(b),
+ Endian::Big => i64::from_be_bytes(b),
+ }
+ })
+ .collect()
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ // ---- DataReader little-endian tests ----
+
+ #[test]
+ fn read_u32_little_endian() {
+ let data = [0x78, 0x56, 0x34, 0x12];
+ let mut reader = DataReader::new(&data, 0, Endian::Little, true);
+ assert_eq!(reader.read_u32(), Some(0x12345678));
+ }
+
+ #[test]
+ fn read_u32_big_endian() {
+ let data = [0x12, 0x34, 0x56, 0x78];
+ let mut reader = DataReader::new(&data, 0, Endian::Big, true);
+ assert_eq!(reader.read_u32(), Some(0x12345678));
+ }
+
+ #[test]
+ fn read_u64_little_endian() {
+ let data = 0xDEADBEEFCAFEBABEu64.to_le_bytes();
+ let mut reader = DataReader::new(&data, 0, Endian::Little, true);
+ assert_eq!(reader.read_u64(), Some(0xDEADBEEFCAFEBABE));
+ }
+
+ #[test]
+ fn read_u64_big_endian() {
+ let data = 0xDEADBEEFCAFEBABEu64.to_be_bytes();
+ let mut reader = DataReader::new(&data, 0, Endian::Big, true);
+ assert_eq!(reader.read_u64(), Some(0xDEADBEEFCAFEBABE));
+ }
+
+ #[test]
+ fn read_i32_little_endian_negative() {
+ let val: i32 = -42;
+ let data = val.to_le_bytes();
+ let mut reader = DataReader::new(&data, 0, Endian::Little, true);
+ assert_eq!(reader.read_i32(), Some(-42));
+ }
+
+ #[test]
+ fn read_i32_big_endian_negative() {
+ let val: i32 = -1;
+ let data = val.to_be_bytes();
+ let mut reader = DataReader::new(&data, 0, Endian::Big, true);
+ assert_eq!(reader.read_i32(), Some(-1));
+ }
+
+ #[test]
+ fn read_i64_little_endian() {
+ let val: i64 = -9999999999;
+ let data = val.to_le_bytes();
+ let mut reader = DataReader::new(&data, 0, Endian::Little, true);
+ assert_eq!(reader.read_i64(), Some(-9999999999));
+ }
+
+ #[test]
+ fn read_i64_big_endian() {
+ let val: i64 = i64::MIN;
+ let data = val.to_be_bytes();
+ let mut reader = DataReader::new(&data, 0, Endian::Big, true);
+ assert_eq!(reader.read_i64(), Some(i64::MIN));
+ }
+
+ // ---- read_usize tests ----
+
+ #[test]
+ fn read_usize_64bit() {
+ let val: u64 = 0x00000000FFFFFFFF;
+ let data = val.to_le_bytes();
+ let mut reader = DataReader::new(&data, 0, Endian::Little, true);
+ assert_eq!(reader.read_usize(), Some(0xFFFFFFFF));
+ }
+
+ #[test]
+ fn read_usize_32bit() {
+ let val: u32 = 0xABCD1234;
+ let data = val.to_le_bytes();
+ let mut reader = DataReader::new(&data, 0, Endian::Little, false);
+ assert_eq!(reader.read_usize(), Some(0xABCD1234));
+ }
+
+ #[test]
+ fn read_usize_32bit_does_not_consume_8_bytes() {
+ // In 32-bit mode, read_usize should only consume 4 bytes
+ let mut data = [0u8; 8];
+ data[..4].copy_from_slice(&42u32.to_le_bytes());
+ data[4..8].copy_from_slice(&99u32.to_le_bytes());
+ let mut reader = DataReader::new(&data, 0, Endian::Little, false);
+ assert_eq!(reader.read_usize(), Some(42));
+ // After reading 4 bytes, pos should be at 4
+ assert_eq!(reader.pos, 4);
+ assert_eq!(reader.read_usize(), Some(99));
+ }
+
+ // ---- Bounds checking ----
+
+ #[test]
+ fn read_u32_past_end_returns_none() {
+ let data = [0x01, 0x02, 0x03]; // only 3 bytes, need 4
+ let mut reader = DataReader::new(&data, 0, Endian::Little, true);
+ assert_eq!(reader.read_u32(), None);
+ }
+
+ #[test]
+ fn read_u64_past_end_returns_none() {
+ let data = [0u8; 7]; // only 7 bytes, need 8
+ let mut reader = DataReader::new(&data, 0, Endian::Little, true);
+ assert_eq!(reader.read_u64(), None);
+ }
+
+ #[test]
+ fn read_bytes_past_end_returns_none() {
+ let data = [0u8; 4];
+ let mut reader = DataReader::new(&data, 0, Endian::Little, true);
+ assert_eq!(reader.read_bytes(5), None);
+ }
+
+ #[test]
+ fn read_at_offset() {
+ // read_u32 auto-aligns to a 4-byte boundary, so the starting
+ // offset must itself be 4-aligned for the value to be read
+ // from its declared position.
+ let data = [0xFF, 0xFF, 0xFF, 0xFF, 0x78, 0x56, 0x34, 0x12];
+ let mut reader = DataReader::new(&data, 4, Endian::Little, true);
+ assert_eq!(reader.read_u32(), Some(0x12345678));
+ }
+
+ #[test]
+ fn read_u32_auto_aligns() {
+ // Starting mid-word, read_u32 snaps to the next 4-byte boundary.
+ let data = [0xDE, 0xAD, 0xBE, 0xEF, 0x78, 0x56, 0x34, 0x12];
+ let mut reader = DataReader::new(&data, 1, Endian::Little, true);
+ assert_eq!(reader.read_u32(), Some(0x12345678));
+ assert_eq!(reader.pos, 8);
+ }
+
+ #[test]
+ fn read_ptr_32bit_uses_4_bytes() {
+ let data = [0x78, 0x56, 0x34, 0x12];
+ let mut reader = DataReader::new(&data, 0, Endian::Little, false);
+ assert_eq!(reader.read_ptr(), Some(0x12345678));
+ assert_eq!(reader.pos, 4);
+ }
+
+ #[test]
+ fn read_ptr_64bit_uses_8_bytes() {
+ let data = [0x78, 0x56, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00];
+ let mut reader = DataReader::new(&data, 0, Endian::Little, true);
+ assert_eq!(reader.read_ptr(), Some(0x12345678));
+ assert_eq!(reader.pos, 8);
+ }
+
+ #[test]
+ fn read_bool_values() {
+ let data = [0, 1, 255];
+ let mut reader = DataReader::new(&data, 0, Endian::Little, true);
+ assert_eq!(reader.read_bool(), Some(false));
+ assert_eq!(reader.read_bool(), Some(true));
+ assert_eq!(reader.read_bool(), Some(true)); // any non-zero is true
+ }
+
+ #[test]
+ fn sequential_reads_advance_position() {
+ let mut data = [0u8; 12];
+ data[..4].copy_from_slice(&1u32.to_le_bytes());
+ data[4..8].copy_from_slice(&2u32.to_le_bytes());
+ data[8..12].copy_from_slice(&3u32.to_le_bytes());
+ let mut reader = DataReader::new(&data, 0, Endian::Little, true);
+ assert_eq!(reader.read_u32(), Some(1));
+ assert_eq!(reader.read_u32(), Some(2));
+ assert_eq!(reader.read_u32(), Some(3));
+ assert_eq!(reader.pos, 12);
+ }
+
+ #[test]
+ fn read_optional_string_empty_returns_none() {
+ // A string buffer that is just NUL
+ let data = [0u8; 10];
+ let mut reader = DataReader::new(&data, 0, Endian::Little, true);
+ assert_eq!(reader.read_optional_string(10), None);
+ }
+
+ #[test]
+ fn read_string_or_default_with_empty() {
+ let data = [0u8; 10];
+ let mut reader = DataReader::new(&data, 0, Endian::Little, true);
+ assert_eq!(reader.read_string_or_default(10), "");
+ }
+
+ #[test]
+ fn read_u8_value() {
+ let data = [0x42];
+ let mut reader = DataReader::new(&data, 0, Endian::Little, true);
+ assert_eq!(reader.read_u8(), Some(0x42));
+ }
+}