Skip to content

Commit

Permalink
Add logic for reading KASLR offset
Browse files Browse the repository at this point in the history
In order to (eventually...) support normalization of kernel addresses,
we need to take into account whether the running kernel has address
space layout randomization enabled. If that is the case, we will need to
incorporate the randomization offset in the normalization process. This
change introduces the necessary logic for reading said offset, so that
it can be used down the line.
This change builds on all the infrastructure we added for making the ELF
parser optionally work with using regular I/O APIs instead of relying on
memory mapping.

Refs: #950

Signed-off-by: Daniel Müller <[email protected]>
  • Loading branch information
d-e-s-o committed Apr 14, 2023
1 parent c3610d2 commit 866e604
Show file tree
Hide file tree
Showing 6 changed files with 301 additions and 3 deletions.
2 changes: 2 additions & 0 deletions src/elf/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
21 changes: 19 additions & 2 deletions src/elf/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -887,9 +887,9 @@ where
_backend: B::ObjTy,
}

#[cfg(test)]
impl ElfParser<File> {
#[cfg(test)]
pub(crate) fn open_file_io<P>(file: File, path: P) -> Self
fn open_file_io<P>(file: File, path: P) -> Self
where
P: Into<PathBuf>,
{
Expand All @@ -903,6 +903,23 @@ impl ElfParser<File> {
};
parser
}

/// Create an `ElfParser` from an open file.
pub(crate) fn open_non_mmap<P>(path: P) -> Result<Self>
where
P: Into<PathBuf>,
{
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<Mmap> {
Expand Down
37 changes: 36 additions & 1 deletion src/elf/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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();
Expand Down
238 changes: 238 additions & 0 deletions src/normalize/kernel.rs
Original file line number Diff line number Diff line change
@@ -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<Self> {
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<T>(path: &Path) -> Result<Option<T>>
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<KaslrState> {
// https://www.kernel.org/doc/html/latest/admin-guide/sysctl/kernel.html#randomize-va-space
let kaslr = read_proc_node_value::<KaslrState>(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<Item = (&[u8], &[u8])> {
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<File>) -> Result<Option<u64>> {
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::<Elf64_Nhdr>() as u64) <= phdr.file_size() {
let nhdr = file
.read_pod_obj::<Elf64_Nhdr>(offset)
.context("failed to read kcore note header")?;
offset += size_of::<Elf64_Nhdr>() as u64;

let name = if nhdr.n_namesz > 0 {
let name = file.read_pod_slice::<u8>(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::<u8>(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.
}
}
}
}
4 changes: 4 additions & 0 deletions src/normalize/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);


Expand Down

0 comments on commit 866e604

Please sign in to comment.