use std::io;
struct Phdr {
p_type: u32,
p_offset: u64,
p_filesz: u64,
}
fn u16_at(data: &[u8], at: usize) -> Option<u16> {
data.get(at..at + 2)
.and_then(|b| <[u8; 2]>::try_from(b).ok())
.map(u16::from_le_bytes)
}
fn u32_at(data: &[u8], at: usize) -> Option<u32> {
data.get(at..at + 4)
.and_then(|b| <[u8; 4]>::try_from(b).ok())
.map(u32::from_le_bytes)
}
fn u64_at(data: &[u8], at: usize) -> Option<u64> {
data.get(at..at + 8)
.and_then(|b| <[u8; 8]>::try_from(b).ok())
.map(u64::from_le_bytes)
}
fn phdr_table(data: &[u8]) -> Option<(u8, usize, usize, usize)> {
if data.len() < 64 || data[0..4] != *b"\x7fELF" {
return None;
}
let class = data[4];
let (e_phoff, e_phentsize, e_phnum) = match class {
2 => (
u64_at(data, 32)? as usize,
usize::from(u16_at(data, 54)?),
usize::from(u16_at(data, 56)?),
),
1 => (
u32_at(data, 28)? as usize,
usize::from(u16_at(data, 42)?),
usize::from(u16_at(data, 44)?),
),
_ => return None,
};
Some((class, e_phoff, e_phentsize, e_phnum))
}
fn each_phdr(data: &[u8], mut visit: impl FnMut(&Phdr)) {
let Some((class, e_phoff, e_phentsize, e_phnum)) = phdr_table(data) else {
return;
};
if e_phentsize < if class == 2 { 56 } else { 32 } {
return;
}
for i in 0..e_phnum {
let Some(step) = i.checked_mul(e_phentsize) else {
return;
};
let Some(off) = e_phoff.checked_add(step) else {
return;
};
let Some(end) = off.checked_add(e_phentsize) else {
return;
};
if end > data.len() {
return;
}
let phdr = match class {
2 => Phdr {
p_type: match u32_at(data, off) {
Some(v) => v,
None => return,
},
p_offset: match u64_at(data, off + 8) {
Some(v) => v,
None => return,
},
p_filesz: match u64_at(data, off + 32) {
Some(v) => v,
None => return,
},
},
1 => Phdr {
p_type: match u32_at(data, off) {
Some(v) => v,
None => return,
},
p_offset: match u32_at(data, off + 4) {
Some(v) => u64::from(v),
None => return,
},
p_filesz: match u32_at(data, off + 16) {
Some(v) => u64::from(v),
None => return,
},
},
_ => return,
};
visit(&phdr);
}
}
pub fn pt_interp_slot(data: &[u8]) -> Option<(usize, usize)> {
let mut found: Option<(usize, usize)> = None;
each_phdr(data, |ph| {
if found.is_none() && ph.p_type == PT_INTERP {
found = Some((ph.p_offset as usize, ph.p_filesz as usize));
}
});
found
}
pub fn read_interp(path: &std::path::Path) -> Option<String> {
use std::io::{Read, Seek, SeekFrom};
const MAX_INTERP: usize = 4096;
let mut file = std::fs::File::open(path).ok()?;
let file_len = file.metadata().ok()?.len();
let mut head = vec![0u8; 8192];
let n = file.read(&mut head).ok()?;
head.truncate(n);
let (offset, size) = pt_interp_slot(&head)?;
if size == 0 || size > MAX_INTERP {
return None;
}
let end = offset.checked_add(size)?;
if end as u64 > file_len {
return None;
}
let bytes = match head.get(offset..end) {
Some(s) => s.to_vec(),
None => {
file.seek(SeekFrom::Start(offset as u64)).ok()?;
let mut buf = vec![0u8; size];
file.read_exact(&mut buf).ok()?;
buf
}
};
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
std::str::from_utf8(&bytes[..end]).ok().map(String::from)
}
const PT_INTERP: u32 = 3;
const PT_NOTE: u32 = 4;
pub const NOTE_NAME: &[u8] = b"ONELF";
pub const NOTE_TYPE: u32 = 1;
pub const RUNTIME_METADATA_VERSION: u16 = 1;
pub const CAPABILITY_NOTE_BYTES: usize = 32;
const NOTE_DESC_MAGIC: &[u8; 4] = b"OCAP";
const NOTE_DESC_BYTES: usize = 12;
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RuntimeCapabilities: u32 {
const SELECTION_VIEW = 1 << 0;
const STORE_IMPORT = 1 << 1;
const ANCHORS = 1 << 2;
}
}
impl RuntimeCapabilities {
pub const ALL_KNOWN: Self = Self::SELECTION_VIEW
.union(Self::STORE_IMPORT)
.union(Self::ANCHORS);
pub const fn from_bits(bits: u32) -> Option<Self> {
if bits & !Self::ALL_KNOWN.bits() != 0 {
None
} else {
Some(Self { bits })
}
}
pub const fn is_empty(self) -> bool {
self.bits == 0
}
pub const fn difference(self, other: Self) -> Self {
Self {
bits: self.bits & !other.bits,
}
}
}
impl std::fmt::Display for RuntimeCapabilities {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.is_empty() {
return f.write_str("(none)");
}
let mut first = true;
for (flag, name) in [
(Self::SELECTION_VIEW, "selection-view"),
(Self::STORE_IMPORT, "store-import"),
(Self::ANCHORS, "anchors"),
] {
if self.contains(flag) {
if !first {
f.write_str("+")?;
}
f.write_str(name)?;
first = false;
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CapabilityNoteError(String);
impl CapabilityNoteError {
fn new(reason: &str) -> Self {
Self(reason.chars().take(768).collect())
}
}
impl std::fmt::Display for CapabilityNoteError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for CapabilityNoteError {}
impl From<CapabilityNoteError> for io::Error {
fn from(value: CapabilityNoteError) -> Self {
io::Error::new(io::ErrorKind::InvalidData, value.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RuntimeNote {
Present {
metadata_version: u16,
capabilities: RuntimeCapabilities,
},
Absent,
Incompatible(CapabilityNoteError),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RuntimeRequirements {
pub metadata_version: u16,
pub capabilities: RuntimeCapabilities,
}
impl RuntimeRequirements {
pub const NONE: Self = Self {
metadata_version: 0,
capabilities: RuntimeCapabilities::empty(),
};
pub const fn metadata(version: u16) -> Self {
Self {
metadata_version: version,
capabilities: RuntimeCapabilities::empty(),
}
}
pub const fn is_none(self) -> bool {
self.metadata_version == 0 && self.capabilities.is_empty()
}
}
pub const fn encode_capability_note(
metadata_version: u16,
capabilities: RuntimeCapabilities,
) -> [u8; CAPABILITY_NOTE_BYTES] {
let mut out = [0u8; CAPABILITY_NOTE_BYTES];
let namesz = ((NOTE_NAME.len() + 1) as u32).to_le_bytes();
let descsz = (NOTE_DESC_BYTES as u32).to_le_bytes();
let ntype = NOTE_TYPE.to_le_bytes();
let version = metadata_version.to_le_bytes();
let bits = capabilities.bits().to_le_bytes();
let mut i = 0;
while i < 4 {
out[i] = namesz[i];
out[4 + i] = descsz[i];
out[8 + i] = ntype[i];
out[28 + i] = bits[i];
i += 1;
}
i = 0;
while i < NOTE_NAME.len() {
out[12 + i] = NOTE_NAME[i];
i += 1;
}
i = 0;
while i < NOTE_DESC_MAGIC.len() {
out[20 + i] = NOTE_DESC_MAGIC[i];
i += 1;
}
i = 0;
while i < version.len() {
out[24 + i] = version[i];
i += 1;
}
out
}
fn decode_note_desc(desc: &[u8]) -> Result<(u16, RuntimeCapabilities), CapabilityNoteError> {
if desc.len() != NOTE_DESC_BYTES {
return Err(CapabilityNoteError::new(&format!(
"capability note description is {} bytes, expected {NOTE_DESC_BYTES}",
desc.len()
)));
}
if &desc[0..4] != NOTE_DESC_MAGIC {
return Err(CapabilityNoteError::new(
"capability note description has an unknown magic",
));
}
let metadata_version = u16::from_le_bytes([desc[4], desc[5]]);
let reserved = u16::from_le_bytes([desc[6], desc[7]]);
let bits = u32::from_le_bytes([desc[8], desc[9], desc[10], desc[11]]);
if metadata_version == 0 {
return Err(CapabilityNoteError::new(
"capability note advertises metadata version 0",
));
}
if reserved != 0 {
return Err(CapabilityNoteError::new(
"capability note reserved field is not zero",
));
}
let Some(capabilities) = RuntimeCapabilities::from_bits(bits) else {
return Err(CapabilityNoteError::new(&format!(
"capability note advertises unknown features beyond {}",
RuntimeCapabilities::ALL_KNOWN
)));
};
Ok((metadata_version, capabilities))
}
fn scan_note_segment(data: &[u8], ph: &Phdr, state: &mut RuntimeNote) {
if !matches!(state, RuntimeNote::Absent) {
return;
}
let start = ph.p_offset as usize;
if start > data.len() {
return;
}
let Some(seg_end) = start.checked_add(ph.p_filesz as usize) else {
return;
};
if seg_end > data.len() {
return;
}
let mut pos = start;
while pos + 12 <= seg_end {
let namesz = u32_at(data, pos);
let descsz = u32_at(data, pos + 4);
let ntype = u32_at(data, pos + 8);
let candidate = pos;
pos += 4;
let (Some(namesz), Some(descsz), Some(ntype)) = (namesz, descsz, ntype) else {
return;
};
if namesz as usize != NOTE_NAME.len() + 1
|| ntype != NOTE_TYPE
|| candidate + 12 + NOTE_NAME.len() + 1 > seg_end
|| data[candidate + 12..candidate + 12 + NOTE_NAME.len()] != *NOTE_NAME
|| data[candidate + 12 + NOTE_NAME.len()] != 0
{
continue;
}
if descsz as usize != NOTE_DESC_BYTES {
*state = RuntimeNote::Incompatible(CapabilityNoteError::new(&format!(
"capability note declares a {descsz}-byte description, expected {NOTE_DESC_BYTES}"
)));
return;
}
let mut rejected: Option<CapabilityNoteError> = None;
for desc_pos in [candidate + 20, candidate + 24] {
if desc_pos + NOTE_DESC_BYTES > seg_end {
continue;
}
match decode_note_desc(&data[desc_pos..desc_pos + NOTE_DESC_BYTES]) {
Ok((metadata_version, capabilities)) => {
*state = RuntimeNote::Present {
metadata_version,
capabilities,
};
return;
}
Err(e) => {
rejected.get_or_insert(e);
}
}
}
*state = RuntimeNote::Incompatible(rejected.unwrap_or_else(|| {
CapabilityNoteError::new("capability note description is truncated")
}));
return;
}
}
pub fn probe_capability_note(data: &[u8]) -> Result<RuntimeNote, CapabilityNoteError> {
let Some((class, _, _, _)) = phdr_table(data) else {
return Err(CapabilityNoteError::new(
"runtime is not a little-endian 32- or 64-bit ELF object",
));
};
if class != 1 && class != 2 {
return Err(CapabilityNoteError::new(&format!(
"runtime ELF has unknown class {class}"
)));
}
let mut state = RuntimeNote::Absent;
each_phdr(data, |ph| {
if ph.p_type == PT_NOTE {
scan_note_segment(data, ph, &mut state);
}
});
Ok(state)
}
pub fn check_runtime_support(
note: &RuntimeNote,
required: RuntimeRequirements,
) -> Result<(), CapabilityNoteError> {
match note {
RuntimeNote::Incompatible(e) => Err(e.clone()),
RuntimeNote::Present {
metadata_version,
capabilities,
} => {
if *metadata_version < required.metadata_version {
return Err(CapabilityNoteError::new(&format!(
"runtime advertises payload metadata version {metadata_version}, \
packaging requires {required_value}",
required_value = required.metadata_version
)));
}
let missing = required.capabilities.difference(*capabilities);
if !missing.is_empty() {
return Err(CapabilityNoteError::new(&format!(
"runtime does not advertise the required {missing} support"
)));
}
Ok(())
}
RuntimeNote::Absent => {
if required.is_none() {
return Ok(());
}
let mut what = String::new();
if required.metadata_version != 0 {
what.push_str(&format!("metadata version {}", required.metadata_version));
}
if !required.capabilities.is_empty() {
if !what.is_empty() {
what.push_str(" and ");
}
what.push_str(&required.capabilities.to_string());
}
Err(CapabilityNoteError::new(&format!(
"runtime carries no capability note, so it does not support {what}"
)))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn elf64_with_interp(interp: &str) -> Vec<u8> {
let phoff = 64usize;
let phentsize = 56usize;
let interp_off = phoff + phentsize;
let mut v = vec![0u8; interp_off + interp.len() + 1];
v[0..4].copy_from_slice(b"\x7fELF");
v[4] = 2; v[32..40].copy_from_slice(&(phoff as u64).to_le_bytes());
v[54..56].copy_from_slice(&(phentsize as u16).to_le_bytes());
v[56..58].copy_from_slice(&1u16.to_le_bytes()); v[phoff..phoff + 4].copy_from_slice(&3u32.to_le_bytes()); v[phoff + 8..phoff + 16].copy_from_slice(&(interp_off as u64).to_le_bytes());
v[phoff + 32..phoff + 40].copy_from_slice(&((interp.len() + 1) as u64).to_le_bytes());
v[interp_off..interp_off + interp.len()].copy_from_slice(interp.as_bytes());
v
}
#[test]
fn locates_the_interp_slot() {
let name = "/lib64/ld-linux-x86-64.so.2";
let elf = elf64_with_interp(name);
let (off, sz) = pt_interp_slot(&elf).expect("slot");
assert_eq!(off, 120);
assert_eq!(sz, name.len() + 1);
assert_eq!(&elf[off..off + sz - 1], name.as_bytes());
}
#[test]
fn malformed_returns_none_without_panic() {
assert!(pt_interp_slot(b"not an elf").is_none());
assert!(pt_interp_slot(&[]).is_none());
let mut short = vec![0u8; 64];
short[0..4].copy_from_slice(b"\x7fELF");
short[4] = 2;
short[56..58].copy_from_slice(&9999u16.to_le_bytes());
short[32..40].copy_from_slice(&(1u64 << 40).to_le_bytes());
assert!(pt_interp_slot(&short).is_none());
}
#[test]
fn undersized_phentsize_is_rejected() {
let mut elf = elf64_with_interp("/lib/ld.so");
elf[54..56].copy_from_slice(&8u16.to_le_bytes());
assert!(pt_interp_slot(&elf).is_none());
}
}