Skip to main content

proot/exec/
elf.rs

1//! Minimal ELF reader, 32- and 64-bit: enough to find `PT_INTERP`.
2
3use crate::sys::Result;
4
5/// Program header type of the dynamic loader path.
6const PT_INTERP: u32 = 3;
7/// Maximum number of program headers we are willing to walk.
8const MAX_PHNUM: usize = 512;
9
10/// What an `execve` needs to know about an ELF file.
11#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct ElfInfo {
13    /// Path of the dynamic loader named by `PT_INTERP`, or `None` for a static executable.
14    pub interp: Option<Vec<u8>>,
15    /// `e_machine`: `EM_X86_64` (62), `EM_386` (3), `EM_AARCH64` (183), `EM_ARM` (40), ...
16    pub machine: u16,
17}
18
19/// Classification of the first bytes of an executable file.
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub enum FileKind {
22    /// A 64-bit ELF file.
23    Elf64(ElfInfo),
24    /// A 32-bit ELF file.
25    Elf32(ElfInfo),
26    /// A `#!` script; the payload is the first line without the `#!`.
27    Shebang(Vec<u8>),
28    /// Anything else; the kernel will refuse it with `ENOEXEC`.
29    Other,
30}
31
32/// Classifies a file from its leading bytes and, for ELF, its program headers.
33///
34/// Both ELF classes are read: the 64-bit layout (`e_phoff` at 32, `Elf64_Phdr`
35/// of 56 bytes) and the 32-bit one (`e_phoff` at 28, `Elf32_Phdr` of 32 bytes).
36///
37/// # Arguments
38/// * `head` - The first bytes of the file (at least 64 for ELF, up to a few KiB).
39/// * `read_at` - Reads `len` bytes at `offset` of the same file, for headers past `head`.
40///
41/// # Returns
42/// The [`FileKind`].  An ELF whose program headers cannot be read is
43/// reported as `Other` so the kernel gives the final verdict.
44pub fn classify(head: &[u8], read_at: &mut dyn FnMut(u64, usize) -> Result<Vec<u8>>) -> FileKind {
45    if head.starts_with(b"#!") {
46        let line = &head[2..];
47        let end = line
48            .iter()
49            .position(|&b| b == b'\n')
50            .unwrap_or(line.len().min(255));
51        return FileKind::Shebang(line[..end.min(255)].to_vec());
52    }
53    if head.len() < 64 || &head[..4] != b"\x7fELF" {
54        return FileKind::Other;
55    }
56    let is64 = match head[4] {
57        1 => false,
58        2 => true,
59        _ => return FileKind::Other,
60    };
61    if head[5] != 1 {
62        // Big-endian ELF on x86_64 cannot run anyway.
63        return FileKind::Other;
64    }
65    let machine = u16::from_le_bytes(head[18..20].try_into().unwrap());
66    let wrap = |info: ElfInfo| {
67        if is64 {
68            FileKind::Elf64(info)
69        } else {
70            FileKind::Elf32(info)
71        }
72    };
73    // Header geometry per class: (phoff, phentsize, phnum, minimum phentsize,
74    // p_offset field, p_filesz field, field width).
75    let (phoff, phentsize, phnum, min_ent, off_field, sz_field, width) = if is64 {
76        (
77            u64::from_le_bytes(head[32..40].try_into().unwrap()),
78            u16::from_le_bytes(head[54..56].try_into().unwrap()) as usize,
79            u16::from_le_bytes(head[56..58].try_into().unwrap()) as usize,
80            56,
81            8,
82            32,
83            8,
84        )
85    } else {
86        (
87            u32::from_le_bytes(head[28..32].try_into().unwrap()) as u64,
88            u16::from_le_bytes(head[42..44].try_into().unwrap()) as usize,
89            u16::from_le_bytes(head[44..46].try_into().unwrap()) as usize,
90            32,
91            4,
92            16,
93            4,
94        )
95    };
96    if phentsize < min_ent || phnum == 0 || phnum > MAX_PHNUM {
97        return wrap(ElfInfo {
98            interp: None,
99            machine,
100        });
101    }
102    let table = match read_at(phoff, phentsize * phnum) {
103        Ok(t) if t.len() == phentsize * phnum => t,
104        _ => return FileKind::Other,
105    };
106    let field = |ph: &[u8], at: usize| -> u64 {
107        if width == 8 {
108            u64::from_le_bytes(ph[at..at + 8].try_into().unwrap())
109        } else {
110            u32::from_le_bytes(ph[at..at + 4].try_into().unwrap()) as u64
111        }
112    };
113    for ph in table.chunks_exact(phentsize) {
114        let p_type = u32::from_le_bytes(ph[0..4].try_into().unwrap());
115        if p_type != PT_INTERP {
116            continue;
117        }
118        let offset = field(ph, off_field);
119        let filesz = field(ph, sz_field) as usize;
120        if filesz == 0 || filesz > 4096 {
121            return FileKind::Other;
122        }
123        let mut interp = match read_at(offset, filesz) {
124            Ok(i) => i,
125            Err(_) => return FileKind::Other,
126        };
127        while interp.last() == Some(&0) {
128            interp.pop();
129        }
130        return wrap(ElfInfo {
131            interp: Some(interp),
132            machine,
133        });
134    }
135    wrap(ElfInfo {
136        interp: None,
137        machine,
138    })
139}
140
141/// Classifies the file at `host_path` by reading it.
142///
143/// # Arguments
144/// * `host_path` - Host path of the candidate executable.
145///
146/// # Errors
147/// Errno from `open`/`read`; `EACCES` for a directory, `ENOENT` for a missing file.
148pub fn classify_file(host_path: &[u8]) -> Result<FileKind> {
149    use std::io::{Read, Seek, SeekFrom};
150    use std::os::unix::ffi::OsStrExt;
151    let path = std::path::Path::new(std::ffi::OsStr::from_bytes(host_path));
152    let mut f = std::fs::File::open(path).map_err(|e| e.raw_os_error().unwrap_or(libc::EIO))?;
153    if f.metadata().map(|m| m.is_dir()).unwrap_or(false) {
154        return Err(libc::EACCES);
155    }
156    let mut head = vec![0u8; 4096];
157    let mut got = 0;
158    while got < head.len() {
159        match f.read(&mut head[got..]) {
160            Ok(0) => break,
161            Ok(n) => got += n,
162            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
163            Err(e) => return Err(e.raw_os_error().unwrap_or(libc::EIO)),
164        }
165    }
166    head.truncate(got);
167    let mut read_at = |off: u64, len: usize| -> Result<Vec<u8>> {
168        f.seek(SeekFrom::Start(off))
169            .map_err(|e| e.raw_os_error().unwrap_or(libc::EIO))?;
170        let mut buf = vec![0u8; len];
171        f.read_exact(&mut buf)
172            .map_err(|e| e.raw_os_error().unwrap_or(libc::EIO))?;
173        Ok(buf)
174    };
175    Ok(classify(&head, &mut read_at))
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    /// Builds a fake ELF64 with one PT_INTERP header pointing at `interp`.
183    fn fake_elf(interp: &[u8]) -> Vec<u8> {
184        let mut f = vec![0u8; 64];
185        f[..4].copy_from_slice(b"\x7fELF");
186        f[4] = 2;
187        f[5] = 1;
188        f[18..20].copy_from_slice(&62u16.to_le_bytes());
189        f[32..40].copy_from_slice(&64u64.to_le_bytes()); // phoff
190        f[54..56].copy_from_slice(&56u16.to_le_bytes()); // phentsize
191        f[56..58].copy_from_slice(&1u16.to_le_bytes()); // phnum
192        let mut ph = vec![0u8; 56];
193        ph[0..4].copy_from_slice(&PT_INTERP.to_le_bytes());
194        ph[8..16].copy_from_slice(&120u64.to_le_bytes()); // offset
195        ph[32..40].copy_from_slice(&((interp.len() + 1) as u64).to_le_bytes());
196        f.extend_from_slice(&ph);
197        f.extend_from_slice(interp);
198        f.push(0);
199        f
200    }
201
202    /// Builds a fake ELF32 with one PT_INTERP header pointing at `interp`.
203    fn fake_elf32(interp: &[u8]) -> Vec<u8> {
204        let mut f = vec![0u8; 52];
205        f[..4].copy_from_slice(b"\x7fELF");
206        f[4] = 1;
207        f[5] = 1;
208        f[28..32].copy_from_slice(&64u32.to_le_bytes()); // phoff
209        f[42..44].copy_from_slice(&32u16.to_le_bytes()); // phentsize
210        f[44..46].copy_from_slice(&1u16.to_le_bytes()); // phnum
211        f.resize(64, 0);
212        let mut ph = vec![0u8; 32];
213        ph[0..4].copy_from_slice(&PT_INTERP.to_le_bytes());
214        ph[4..8].copy_from_slice(&96u32.to_le_bytes()); // offset
215        ph[16..20].copy_from_slice(&((interp.len() + 1) as u32).to_le_bytes());
216        f.extend_from_slice(&ph);
217        f.extend_from_slice(interp);
218        f.push(0);
219        f
220    }
221
222    #[test]
223    fn finds_interp_elf32() {
224        let file = fake_elf32(b"/lib/ld-linux.so.2");
225        let mut read_at = |off: u64, len: usize| Ok(file[off as usize..off as usize + len].to_vec());
226        assert_eq!(
227            classify(&file[..64], &mut read_at),
228            FileKind::Elf32(ElfInfo {
229                interp: Some(b"/lib/ld-linux.so.2".to_vec()),
230                machine: 0,
231            })
232        );
233    }
234
235    #[test]
236    fn finds_interp() {
237        let file = fake_elf(b"/lib64/ld-linux-x86-64.so.2");
238        let mut read_at = |off: u64, len: usize| Ok(file[off as usize..off as usize + len].to_vec());
239        assert_eq!(
240            classify(&file[..64], &mut read_at),
241            FileKind::Elf64(ElfInfo {
242                interp: Some(b"/lib64/ld-linux-x86-64.so.2".to_vec()),
243                machine: 62,
244            })
245        );
246    }
247
248    #[test]
249    fn classifies_others() {
250        let mut nop = |_: u64, _: usize| Err(libc::EIO);
251        assert_eq!(
252            classify(b"#!/bin/sh -e\necho", &mut nop),
253            FileKind::Shebang(b"/bin/sh -e".to_vec())
254        );
255        assert_eq!(classify(b"MZ..", &mut nop), FileKind::Other);
256        let mut e32 = vec![0u8; 64];
257        e32[..4].copy_from_slice(b"\x7fELF");
258        e32[4] = 1;
259        e32[5] = 1;
260        // No program headers: a static 32-bit executable as far as we can tell.
261        assert_eq!(
262            classify(&e32, &mut nop),
263            FileKind::Elf32(ElfInfo {
264                interp: None,
265                machine: 0
266            })
267        );
268    }
269
270    #[test]
271    fn real_binary_has_interp() {
272        if let Ok(FileKind::Elf64(info)) = classify_file(b"/bin/ls") {
273            assert!(info.interp.map(|i| i.contains(&b'/')).unwrap_or(true));
274        }
275    }
276}