From d57e65e3b5589dce8eb4f0b36ed8d06d4800de73 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sat, 25 Jul 2026 00:30:47 -0700 Subject: [PATCH] feat: validate dynamic ELF interpreters --- crates/elf/src/lib.rs | 146 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 131 insertions(+), 15 deletions(-) diff --git a/crates/elf/src/lib.rs b/crates/elf/src/lib.rs index 141f854..c2f4ed0 100644 --- a/crates/elf/src/lib.rs +++ b/crates/elf/src/lib.rs @@ -34,7 +34,7 @@ const PF_READ: u32 = 4; pub enum ImageKind { /// A fixed-address `ET_EXEC` image. Executable, - /// An `ET_DYN` image without a dynamic interpreter. + /// An `ET_DYN` image. PositionIndependent, } @@ -128,6 +128,8 @@ pub struct ElfImage { pub kind: ImageKind, pub entry: GuestAddress, pub program_headers: ProgramHeaderTable, + /// Absolute guest path requested by `PT_INTERP`, without its trailing NUL. + pub interpreter: Option>, /// Segments sorted by virtual address. pub load_segments: Vec, } @@ -144,7 +146,12 @@ pub enum ElfError { Parse(String), UnsupportedArchitecture(String), UnsupportedImageKind(String), - DynamicInterpreter, + DuplicateInterpreter, + EmptyInterpreter, + InterpreterFileRangeOutOfBounds, + UnterminatedInterpreter, + InterpreterContainsInteriorNul, + InterpreterNotAbsolute, InvalidProgramHeaderSize(u16), ExtendedProgramHeaderCount, ProgramHeaderTableOutOfBounds, @@ -173,7 +180,10 @@ impl fmt::Display for ElfError { "unsupported ELF data encoding {data}; expected little-endian" ), Self::UnsupportedOsAbi(abi) => { - write!(formatter, "unsupported ELF OS ABI {abi}; expected System V or Linux") + write!( + formatter, + "unsupported ELF OS ABI {abi}; expected System V or Linux" + ) } Self::UnsupportedFileKind => formatter.write_str("input is not an ELF64 object"), Self::Parse(message) => write!(formatter, "invalid ELF: {message}"), @@ -183,11 +193,22 @@ impl fmt::Display for ElfError { ), Self::UnsupportedImageKind(kind) => write!( formatter, - "unsupported ELF image kind {kind}; expected ET_EXEC or interpreter-free ET_DYN" - ), - Self::DynamicInterpreter => formatter.write_str( - "ELF requests a dynamic interpreter through PT_INTERP; only static images are supported", + "unsupported ELF image kind {kind}; expected ET_EXEC or ET_DYN" ), + Self::DuplicateInterpreter => { + formatter.write_str("ELF contains more than one PT_INTERP program header") + } + Self::EmptyInterpreter => formatter.write_str("PT_INTERP contains an empty path"), + Self::InterpreterFileRangeOutOfBounds => { + formatter.write_str("PT_INTERP extends beyond the ELF file") + } + Self::UnterminatedInterpreter => { + formatter.write_str("PT_INTERP path is not NUL-terminated") + } + Self::InterpreterContainsInteriorNul => { + formatter.write_str("PT_INTERP path contains an interior NUL byte") + } + Self::InterpreterNotAbsolute => formatter.write_str("PT_INTERP path is not absolute"), Self::InvalidProgramHeaderSize(size) => write!( formatter, "invalid ELF64 program-header size {size}; expected {ELF64_PROGRAM_HEADER_SIZE}" @@ -210,7 +231,10 @@ impl fmt::Display for ElfError { write!(formatter, "PT_LOAD segment {index} extends beyond the file") } Self::SegmentAddressOverflow { index } => { - write!(formatter, "PT_LOAD segment {index} overflows the guest address space") + write!( + formatter, + "PT_LOAD segment {index} overflows the guest address space" + ) } Self::InvalidSegmentAlignment { index, alignment } => write!( formatter, @@ -229,7 +253,10 @@ impl fmt::Display for ElfError { "PT_LOAD segments {first} and {second} overlap in guest memory" ), Self::EntryPointNotExecutable(entry) => { - write!(formatter, "entry point {entry} is not in an executable PT_LOAD segment") + write!( + formatter, + "entry point {entry} is not in an executable PT_LOAD segment" + ) } } } @@ -237,7 +264,7 @@ impl fmt::Display for ElfError { impl std::error::Error for ElfError {} -/// Parse and validate the initial static `AArch64` ELF profile. +/// Parse and validate the supported `AArch64` ELF profile. /// /// # Errors /// @@ -250,7 +277,7 @@ pub fn inspect(bytes: &[u8]) -> Result { if file_kind != FileKind::Elf64 { return Err(ElfError::UnsupportedFileKind); } - let (program_headers, mut load_segments) = parse_program_headers(bytes)?; + let (program_headers, interpreter, mut load_segments) = parse_program_headers(bytes)?; let file = object::File::parse(bytes).map_err(|error| ElfError::Parse(error.to_string()))?; if file.format() != BinaryFormat::Elf || !file.is_64() || !file.is_little_endian() { @@ -276,6 +303,7 @@ pub fn inspect(bytes: &[u8]) -> Result { kind, entry, program_headers, + interpreter, load_segments, }) } @@ -300,7 +328,9 @@ fn validate_identification(bytes: &[u8]) -> Result<(), ElfError> { Ok(()) } -fn parse_program_headers(bytes: &[u8]) -> Result<(ProgramHeaderTable, Vec), ElfError> { +fn parse_program_headers( + bytes: &[u8], +) -> Result<(ProgramHeaderTable, Option>, Vec), ElfError> { if bytes.len() < ELF_HEADER_SIZE { return Err(ElfError::ProgramHeaderTableOutOfBounds); } @@ -328,9 +358,15 @@ fn parse_program_headers(bytes: &[u8]) -> Result<(ProgramHeaderTable, Vec return Err(ElfError::DynamicInterpreter), + PT_INTERP => { + if interpreter.is_some() { + return Err(ElfError::DuplicateInterpreter); + } + interpreter = Some(parse_interpreter(bytes, header)?); + } PT_LOAD => load_segments.push(parse_load_segment(bytes, header, index)?), _ => {} } @@ -344,10 +380,39 @@ fn parse_program_headers(bytes: &[u8]) -> Result<(ProgramHeaderTable, Vec Result, ElfError> { + let file_offset = read_u64(header, 8).expect("program header file offset is in bounds"); + let file_size = read_u64(header, 32).expect("program header file size is in bounds"); + let start = + usize::try_from(file_offset).map_err(|_| ElfError::InterpreterFileRangeOutOfBounds)?; + let size = usize::try_from(file_size).map_err(|_| ElfError::InterpreterFileRangeOutOfBounds)?; + let end = start + .checked_add(size) + .ok_or(ElfError::InterpreterFileRangeOutOfBounds)?; + let encoded = bytes + .get(start..end) + .ok_or(ElfError::InterpreterFileRangeOutOfBounds)?; + let (&terminator, path) = encoded.split_last().ok_or(ElfError::EmptyInterpreter)?; + if terminator != 0 { + return Err(ElfError::UnterminatedInterpreter); + } + if path.is_empty() { + return Err(ElfError::EmptyInterpreter); + } + if path.contains(&0) { + return Err(ElfError::InterpreterContainsInteriorNul); + } + if path.first() != Some(&b'/') { + return Err(ElfError::InterpreterNotAbsolute); + } + Ok(path.to_vec()) +} + fn parse_load_segment(bytes: &[u8], header: &[u8], index: usize) -> Result { let flags = read_u32(header, 4).expect("program header flags are in bounds"); let file_offset = read_u64(header, 8).expect("program header file offset is in bounds"); @@ -458,6 +523,7 @@ mod tests { assert_eq!(image.program_headers.file_offset, 64); assert_eq!(image.program_headers.entry_size, 56); assert_eq!(image.program_headers.entry_count, 2); + assert_eq!(image.interpreter, None); assert_eq!(image.load_segments.len(), 2); assert_eq!(image.load_segments[0].permissions.to_string(), "R-X"); assert_eq!(image.load_segments[1].permissions.to_string(), "RW-"); @@ -510,13 +576,63 @@ mod tests { } #[test] - fn rejects_dynamic_interpreter() { + fn exposes_dynamic_interpreter() { + let mut bytes = valid_elf(); + write_u16(&mut bytes, ELF_PROGRAM_HEADER_COUNT_OFFSET, 3); + let third_header = 176; + write_u32(&mut bytes, third_header, PT_INTERP); + write_u64(&mut bytes, third_header + 8, 0x1e0); + write_u64(&mut bytes, third_header + 32, 26); + bytes[0x1e0..0x1fa].copy_from_slice(b"/lib/ld-musl-aarch64.so.1\0"); + + assert_eq!( + inspect(&bytes).unwrap().interpreter, + Some(b"/lib/ld-musl-aarch64.so.1".to_vec()) + ); + } + + #[test] + fn rejects_malformed_dynamic_interpreters() { let mut bytes = valid_elf(); write_u16(&mut bytes, ELF_PROGRAM_HEADER_COUNT_OFFSET, 3); let third_header = 176; write_u32(&mut bytes, third_header, PT_INTERP); + write_u64(&mut bytes, third_header + 8, 0x1e0); + + assert_eq!(inspect(&bytes), Err(ElfError::EmptyInterpreter)); + + write_u64(&mut bytes, third_header + 32, 5); + bytes[0x1e0..0x1e5].copy_from_slice(b"/libx"); + assert_eq!(inspect(&bytes), Err(ElfError::UnterminatedInterpreter)); + + bytes[0x1e0..0x1e5].copy_from_slice(b"ld-x\0"); + assert_eq!(inspect(&bytes), Err(ElfError::InterpreterNotAbsolute)); + + bytes[0x1e0..0x1e5].copy_from_slice(b"/a\0b\0"); + assert_eq!( + inspect(&bytes), + Err(ElfError::InterpreterContainsInteriorNul) + ); + + write_u64(&mut bytes, third_header + 8, u64::MAX); + assert_eq!( + inspect(&bytes), + Err(ElfError::InterpreterFileRangeOutOfBounds) + ); + } + + #[test] + fn rejects_duplicate_dynamic_interpreters() { + let mut bytes = valid_elf(); + write_u16(&mut bytes, ELF_PROGRAM_HEADER_COUNT_OFFSET, 4); + for header in [176, 232] { + write_u32(&mut bytes, header, PT_INTERP); + write_u64(&mut bytes, header + 8, 0x1e0); + write_u64(&mut bytes, header + 32, 4); + } + bytes[0x1e0..0x1e4].copy_from_slice(b"/ld\0"); - assert_eq!(inspect(&bytes), Err(ElfError::DynamicInterpreter)); + assert_eq!(inspect(&bytes), Err(ElfError::DuplicateInterpreter)); } #[test] -- 2.51.2