diff --git a/src/elf/mod.rs b/src/elf/mod.rs index c16e443f..d82d464e 100644 --- a/src/elf/mod.rs +++ b/src/elf/mod.rs @@ -11,6 +11,8 @@ pub(crate) mod types; // of concerns that is not a workable location. pub(crate) static DEFAULT_DEBUG_DIRS: &[&str] = &["/usr/lib/debug", "/lib/debug/"]; +#[cfg(test)] +pub(crate) use parser::BackendImpl; pub(crate) use parser::ElfParser; pub(crate) use resolver::ElfResolverData; diff --git a/src/elf/parser.rs b/src/elf/parser.rs index cb9d6a84..e1f4752a 100644 --- a/src/elf/parser.rs +++ b/src/elf/parser.rs @@ -887,9 +887,9 @@ where _backend: B::ObjTy, } +#[cfg(test)] impl ElfParser { - #[cfg(test)] - pub(crate) fn open_file_io

(file: File, path: P) -> Self + fn open_file_io

(file: File, path: P) -> Self where P: Into, { @@ -903,6 +903,23 @@ impl ElfParser { }; parser } + + /// Create an `ElfParser` from an open file. + pub(crate) fn open_non_mmap

(path: P) -> Result + where + P: Into, + { + let path = path.into(); + let file = + File::open(&path).with_context(|| format!("failed to open `{}`", path.display()))?; + let slf = Self::open_file_io(file, path); + Ok(slf) + } + + /// Retrieve a reference to the backend in use. + pub(crate) fn backend(&self) -> &File { + &self._backend + } } impl ElfParser { diff --git a/src/elf/types.rs b/src/elf/types.rs index 90c5c866..d753b950 100644 --- a/src/elf/types.rs +++ b/src/elf/types.rs @@ -216,7 +216,8 @@ impl ElfN_Ehdr<'_> { } -pub(crate) const PT_LOAD: u32 = 1; +pub(crate) const PT_LOAD: u32 = 1; /* Loadable program segment */ +pub(crate) const PT_NOTE: u32 = 4; /* Auxiliary information */ #[derive(Copy, Clone, Debug, Default)] @@ -274,6 +275,32 @@ impl Has32BitTy for Elf64_Phdr { pub(crate) type ElfN_Phdr<'elf> = ElfN<'elf, Elf64_Phdr>; pub(crate) type ElfN_Phdrs<'elf> = ElfNSlice<'elf, Elf64_Phdr>; +impl ElfN_Phdr<'_> { + #[inline] + pub fn type_(&self) -> Elf64_Word { + match self { + ElfN::B32(phdr) => phdr.p_type, + ElfN::B64(phdr) => phdr.p_type, + } + } + + #[inline] + pub fn offset(&self) -> Elf64_Off { + match self { + ElfN::B32(phdr) => phdr.p_offset.into(), + ElfN::B64(phdr) => phdr.p_offset, + } + } + + #[inline] + pub fn file_size(&self) -> Elf64_Xword { + match self { + ElfN::B32(phdr) => phdr.p_filesz.into(), + ElfN::B64(phdr) => phdr.p_filesz, + } + } +} + pub(crate) const PF_X: Elf64_Word = 1; @@ -703,6 +730,14 @@ mod tests { let _val = shdr.addr(); let _val = shdr.link(); + let phdr32 = Cow::Borrowed(&Elf32_Phdr::default()); + let phdr64 = Cow::Borrowed(&Elf64_Phdr::default()); + for phdr in [ElfN_Phdr::B32(&phdr32), ElfN_Phdr::B64(&phdr64)] { + let _val = phdr.type_(); + let _val = phdr.offset(); + let _val = phdr.file_size(); + } + let sym32 = Elf32_Sym::default(); let sym = ElfN_Sym::B32(Cow::Borrowed(&sym32)); let _val = sym.value(); diff --git a/src/normalize/kernel.rs b/src/normalize/kernel.rs new file mode 100644 index 00000000..e49766f2 --- /dev/null +++ b/src/normalize/kernel.rs @@ -0,0 +1,238 @@ +use std::error::Error as StdError; +use std::fs::File; +use std::io; +use std::io::Read as _; +use std::path::Path; +use std::str; +use std::str::FromStr; + +use crate::elf; +use crate::elf::types::Elf64_Nhdr; +use crate::elf::BackendImpl; +use crate::elf::ElfParser; +use crate::util::align_up_u32; +use crate::util::from_radix_16; +use crate::util::split_bytes; +use crate::Addr; +use crate::Error; +use crate::ErrorExt as _; +use crate::IntoError as _; +use crate::Result; + +use super::normalizer::Output; + + +/// The absolute path of the `randomize_va_space` `proc` node. +const PROC_RANDOMIZE_VA_SPACE: &str = "/proc/sys/kernel/randomize_va_space"; +/// The absolute path to the `kcore` `proc` node. +const PROC_KCORE: &str = "/proc/kcore"; +/// The name of the `VMCOREINFO` ELF note. +/// +/// See https://www.kernel.org/doc/html/latest/admin-guide/kdump/vmcoreinfo.html +const VMCOREINFO_NAME: &[u8] = b"VMCOREINFO\0"; + + +/// The kernel address space layout randomization (KASLR) state of the +/// system. +#[derive(Debug)] +enum KaslrState { + /// KASLR is known to be disabled. + Disabled, + /// KASLR is known to be enabled. + Enabled, + /// The state of KASLR on the system could not be determined. + Unknown, +} + +impl FromStr for KaslrState { + type Err = Error; + + fn from_str(s: &str) -> Result { + let value = usize::from_str(s.trim()).map_err(Error::with_invalid_data)?; + match value { + 0 => Ok(KaslrState::Disabled), + 1 | 2 => Ok(KaslrState::Enabled), + // It's unclear whether we should error out here or map anything + // "unknown" to `Unknown`. + x => Err(Error::with_invalid_data(format!( + "{PROC_RANDOMIZE_VA_SPACE} node value {x} is not understood" + ))), + } + } +} + + +/// # Notes +/// Right now this function imposes an arbitrary limit on the maximum +/// node value content size. +fn read_proc_node_value(path: &Path) -> Result> +where + T: FromStr, + T::Err: StdError + Send + Sync + 'static, +{ + let result = File::open(path); + let mut file = match result { + Ok(file) => file, + Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None), + Err(err) => return Err(err.into()), + }; + + // We don't want to blindly use Read::read_to_end or something like + // that if we can avoid it. + let mut buffer = [0; u8::MAX as usize]; + let count = file.read(&mut buffer)?; + if count >= size_of_val(&buffer) { + return Err(Error::with_invalid_data(format!( + "file content is larger than {} bytes", + size_of_val(&buffer) + ))) + } + + let s = str::from_utf8(&buffer[0..count]).map_err(Error::with_invalid_data)?; + let value = T::from_str(s).map_err(Error::with_invalid_data)?; + Ok(Some(value)) +} + + +/// Try to determine the KASLR state of the system. +fn determine_kaslr_state() -> Result { + // https://www.kernel.org/doc/html/latest/admin-guide/sysctl/kernel.html#randomize-va-space + let kaslr = read_proc_node_value::(Path::new(PROC_RANDOMIZE_VA_SPACE)) + .with_context(|| { + format!( + "failed to determine KASLR state from {}", + PROC_RANDOMIZE_VA_SPACE + ) + })? + .unwrap_or(KaslrState::Unknown); + Ok(kaslr) +} + +/// "Parse" the VMCOREINFO descriptor. +/// +/// This underspecified blob roughly has the following format: +/// ``` +/// OSRELEASE=6.2.15-100.fc36.x86_64 +/// BUILD-ID=d3d01c80278f8927486b7f01d0ab6be77784dceb +/// PAGESIZE=4096 +/// SYMBOL(init_uts_ns)=ffffffffb72b8160 +/// OFFSET(uts_namespace.name)=0 +/// [...] +/// ``` +fn parse_vmcoreinfo_desc(desc: &[u8]) -> impl Iterator { + desc.split(|&b| b == b'\n') + .filter_map(|line| split_bytes(line, |b| b == b'=')) +} + +/// Find and read the `KERNELOFFSET` note in a "kcore" file represented by +/// `parser` (i.e., already opened as an ELF). +fn find_kaslr_offset(parser: &ElfParser) -> Result> { + let phdrs = parser.program_headers()?; + for phdr in phdrs.iter(0) { + if phdr.type_() != elf::types::PT_NOTE { + continue + } + + let file = parser.backend(); + let mut offset = phdr.offset(); + + // Iterate through all available notes. See `elf(5)` for + // details. + while offset + (size_of::() as u64) <= phdr.file_size() { + let nhdr = file + .read_pod_obj::(offset) + .context("failed to read kcore note header")?; + offset += size_of::() as u64; + + let name = if nhdr.n_namesz > 0 { + let name = file.read_pod_slice::(offset, nhdr.n_namesz as _)?; + offset += u64::from(align_up_u32(nhdr.n_namesz, 4)); + Some(name) + } else { + None + }; + + // We are looking for the note named `VMCOREINFO`. + if name.as_deref() == Some(VMCOREINFO_NAME) { + if nhdr.n_descsz > 0 { + let desc = file.read_pod_slice::(offset, nhdr.n_descsz as _)?; + let offset = parse_vmcoreinfo_desc(&desc) + .find(|(key, _value)| key == b"KERNELOFFSET") + // The value is in hexadecimal format. Go figure. + .map(|(_key, value)| { + from_radix_16(value).ok_or_invalid_data(|| { + format!("failed to parse KERNELOFFSET value `{value:x?}`") + }) + }) + .transpose(); + return offset + } + + // There shouldn't be multiple notes with that name, + // but I suppose it can't hurt to keep checking...? + } + + offset += u64::from(align_up_u32(nhdr.n_descsz, 4)); + } + } + Ok(None) +} + + +#[cfg(test)] +mod tests { + use super::*; + + use test_log::test; + + use crate::ErrorKind; + + + /// Check that we can parse a dummy VMCOREINFO descriptor. + #[test] + fn vmcoreinfo_desc_parsing() { + let desc = b"OSRELEASE=6.2.15-100.fc36.x86_64 +BUILD-ID=d3d01c80278f8927486b7f01d0ab6be77784dceb +SYMBOL(init_uts_ns)=ffffffffb72b8160 +OFFSET(uts_namespace.name)=0 +PAGESIZE=4096 +"; + + let page_size = parse_vmcoreinfo_desc(desc) + .find(|(key, _value)| key == b"PAGESIZE") + .map(|(_key, value)| value) + .unwrap(); + assert_eq!(page_size, b"4096"); + } + + /// Check that we can determine the system's KASLR state. + #[test] + fn kaslr_detection() { + let state = determine_kaslr_state().unwrap(); + + // Always attempt reading the KASLR to exercise the VMCOREINFO + // parsing path. + // Note that we cannot use the regular mmap based ELF parser + // backend for this file, as it cannot be mmap'ed. We have to + // fall back to using regular I/O instead. + let parser = match ElfParser::open_non_mmap(PROC_KCORE) { + Ok(parser) => parser, + Err(err) if err.kind() == ErrorKind::NotFound => return, + Err(err) => panic!("{err}"), + }; + let offset = find_kaslr_offset(&parser).unwrap(); + + match state { + KaslrState::Enabled => assert_ne!(offset, None), + KaslrState::Disabled => { + assert!( + offset.is_none() || matches!(offset, Some(0)), + "{offset:#x?}" + ); + } + KaslrState::Unknown => { + // Anything is game. + } + } + } +} diff --git a/src/normalize/mod.rs b/src/normalize/mod.rs index 9838ebf9..5dde3000 100644 --- a/src/normalize/mod.rs +++ b/src/normalize/mod.rs @@ -44,6 +44,10 @@ pub(crate) mod buildid; pub(crate) mod ioctl; +// Still work in progress. +#[allow(unused)] +#[cfg(test)] +mod kernel; mod meta; mod normalizer; mod user; diff --git a/src/util.rs b/src/util.rs index fb1017ce..a3a7dde6 100644 --- a/src/util.rs +++ b/src/util.rs @@ -96,6 +96,8 @@ macro_rules! def_align_up { }; } +#[cfg(test)] +def_align_up!(align_up_u32, u32); def_align_up!(align_up_usize, usize);