Skip to main content

andysu_cli/
elf.rs

1//! Minimal ELF shared-object rewriter (little-endian, 32/64-bit) for `andysu pack`.
2//!
3//! Renaming `libfoo.so` to `libfoo.<hash8>.so` makes the string longer, so the dynamic string
4//! table cannot be edited in place. Instead we append, at the end of the file, a new read-only
5//! PT_LOAD segment that holds a copy of the program header table (now one or two entries longer)
6//! followed by the old `.dynstr` plus the new names, and repoint DT_STRTAB/DT_STRSZ, DT_SONAME,
7//! DT_NEEDED, verneed `vn_file`, PT_PHDR and the `.dynstr` section header. Old string offsets
8//! stay valid because the new table starts with the old one.
9
10use std::collections::HashMap;
11
12pub const PT_LOAD: u32 = 1;
13pub const PT_DYNAMIC: u32 = 2;
14pub const PT_PHDR: u32 = 6;
15const DT_NULL: u64 = 0;
16const DT_NEEDED: u64 = 1;
17const DT_STRTAB: u64 = 5;
18const DT_STRSZ: u64 = 10;
19const DT_SONAME: u64 = 14;
20const DT_VERNEED: u64 = 0x6fff_fffe;
21const DT_VERNEEDNUM: u64 = 0x6fff_ffff;
22const SHT_STRTAB: u32 = 3;
23
24#[derive(Debug, Clone, Copy)]
25pub struct Phdr {
26    pub p_type: u32,
27    pub flags: u32,
28    pub offset: u64,
29    pub vaddr: u64,
30    pub paddr: u64,
31    pub filesz: u64,
32    pub memsz: u64,
33    pub align: u64,
34}
35
36#[derive(Debug, Clone)]
37pub struct Info {
38    pub is64: bool,
39    pub machine: u16,
40    pub soname: Option<String>,
41    pub needed: Vec<String>,
42    pub phdrs: Vec<Phdr>,
43}
44
45impl Info {
46    /// Smallest PT_LOAD alignment (16384 or more is required for 16 KB page devices).
47    pub fn min_load_align(&self) -> u64 {
48        self.phdrs.iter().filter(|p| p.p_type == PT_LOAD).map(|p| p.align).min().unwrap_or(0)
49    }
50
51    /// Android ABI name for e_machine.
52    pub fn abi(&self) -> &'static str {
53        match (self.machine, self.is64) {
54            (183, _) => "arm64-v8a",
55            (40, _) => "armeabi-v7a",
56            (62, _) => "x86_64",
57            (3, _) => "x86",
58            (243, _) => "riscv64",
59            _ => "unknown",
60        }
61    }
62}
63
64struct Elf<'a> {
65    d: &'a [u8],
66    is64: bool,
67}
68
69type R<T> = Result<T, String>;
70
71impl<'a> Elf<'a> {
72    fn new(d: &'a [u8]) -> R<Self> {
73        if d.len() < 52 || &d[..4] != b"\x7fELF" {
74            return Err("not an ELF file".into());
75        }
76        if d[5] != 1 {
77            return Err("big-endian ELF is not supported".into());
78        }
79        let is64 = match d[4] {
80            1 => false,
81            2 => true,
82            c => return Err(format!("bad ELF class {c}")),
83        };
84        if is64 && d.len() < 64 {
85            return Err("truncated ELF header".into());
86        }
87        Ok(Elf { d, is64 })
88    }
89    fn u16(&self, o: u64) -> R<u16> {
90        let o = o as usize;
91        self.d
92            .get(o..o + 2)
93            .map(|b| u16::from_le_bytes(b.try_into().unwrap()))
94            .ok_or_else(|| format!("read past end at {o}"))
95    }
96    fn u32(&self, o: u64) -> R<u32> {
97        let o = o as usize;
98        self.d
99            .get(o..o + 4)
100            .map(|b| u32::from_le_bytes(b.try_into().unwrap()))
101            .ok_or_else(|| format!("read past end at {o}"))
102    }
103    fn u64(&self, o: u64) -> R<u64> {
104        let o = o as usize;
105        self.d
106            .get(o..o + 8)
107            .map(|b| u64::from_le_bytes(b.try_into().unwrap()))
108            .ok_or_else(|| format!("read past end at {o}"))
109    }
110    /// Native word (u32 or u64).
111    fn word(&self, o: u64) -> R<u64> {
112        if self.is64 {
113            self.u64(o)
114        } else {
115            self.u32(o).map(u64::from)
116        }
117    }
118    fn e_type(&self) -> R<u16> {
119        self.u16(16)
120    }
121    fn phoff(&self) -> R<u64> {
122        if self.is64 {
123            self.u64(32)
124        } else {
125            self.u32(28).map(u64::from)
126        }
127    }
128    fn shoff(&self) -> R<u64> {
129        if self.is64 {
130            self.u64(40)
131        } else {
132            self.u32(32).map(u64::from)
133        }
134    }
135    fn phentsize(&self) -> R<u16> {
136        self.u16(if self.is64 { 54 } else { 42 })
137    }
138    fn phnum(&self) -> R<u16> {
139        self.u16(if self.is64 { 56 } else { 44 })
140    }
141    fn shentsize(&self) -> R<u16> {
142        self.u16(if self.is64 { 58 } else { 46 })
143    }
144    fn shnum(&self) -> R<u16> {
145        self.u16(if self.is64 { 60 } else { 48 })
146    }
147    fn phdr(&self, i: u64) -> R<Phdr> {
148        let o = self.phoff()? + i * self.phentsize()? as u64;
149        Ok(if self.is64 {
150            Phdr {
151                p_type: self.u32(o)?,
152                flags: self.u32(o + 4)?,
153                offset: self.u64(o + 8)?,
154                vaddr: self.u64(o + 16)?,
155                paddr: self.u64(o + 24)?,
156                filesz: self.u64(o + 32)?,
157                memsz: self.u64(o + 40)?,
158                align: self.u64(o + 48)?,
159            }
160        } else {
161            Phdr {
162                p_type: self.u32(o)?,
163                offset: self.u32(o + 4)? as u64,
164                vaddr: self.u32(o + 8)? as u64,
165                paddr: self.u32(o + 12)? as u64,
166                filesz: self.u32(o + 16)? as u64,
167                memsz: self.u32(o + 20)? as u64,
168                flags: self.u32(o + 24)?,
169                align: self.u32(o + 28)? as u64,
170            }
171        })
172    }
173    fn phdrs(&self) -> R<Vec<Phdr>> {
174        (0..self.phnum()? as u64).map(|i| self.phdr(i)).collect()
175    }
176    fn vaddr_to_off(&self, phdrs: &[Phdr], v: u64) -> R<u64> {
177        phdrs
178            .iter()
179            .find(|p| p.p_type == PT_LOAD && v >= p.vaddr && v < p.vaddr + p.filesz)
180            .map(|p| v - p.vaddr + p.offset)
181            .ok_or_else(|| format!("address {v:#x} is not in a loaded segment"))
182    }
183    fn dyn_entries(&self, phdrs: &[Phdr]) -> R<Vec<(u64, u64, u64)>> {
184        let dynp = phdrs.iter().find(|p| p.p_type == PT_DYNAMIC).ok_or("no PT_DYNAMIC (not a shared object?)")?;
185        let es = if self.is64 { 16 } else { 8 };
186        let mut v = vec![];
187        let mut o = dynp.offset;
188        while o + es <= dynp.offset + dynp.filesz {
189            let (tag, val) = (self.word(o)?, self.word(o + es / 2)?);
190            v.push((o, tag, val));
191            if tag == DT_NULL {
192                break;
193            }
194            o += es;
195        }
196        Ok(v)
197    }
198    fn cstr(&self, o: u64) -> R<String> {
199        let s = &self.d.get(o as usize..).ok_or("string past end")?;
200        let n = s.iter().position(|&c| c == 0).ok_or("unterminated string")?;
201        String::from_utf8(s[..n].to_vec()).map_err(|_| "non-UTF-8 string".into())
202    }
203}
204
205pub fn info(d: &[u8]) -> R<Info> {
206    let e = Elf::new(d)?;
207    let phdrs = e.phdrs()?;
208    let dynv = e.dyn_entries(&phdrs)?;
209    let strtab = dynv.iter().find(|x| x.1 == DT_STRTAB).ok_or("no DT_STRTAB")?.2;
210    let stroff = e.vaddr_to_off(&phdrs, strtab)?;
211    let mut soname = None;
212    let mut needed = vec![];
213    for &(_, tag, val) in &dynv {
214        match tag {
215            DT_SONAME => soname = Some(e.cstr(stroff + val)?),
216            DT_NEEDED => needed.push(e.cstr(stroff + val)?),
217            _ => {}
218        }
219    }
220    Ok(Info { is64: e.is64, machine: e.u16(18)?, soname, needed, phdrs })
221}
222
223fn put(out: &mut [u8], o: u64, v: u64, width: usize) {
224    let o = o as usize;
225    match width {
226        2 => out[o..o + 2].copy_from_slice(&(v as u16).to_le_bytes()),
227        4 => out[o..o + 4].copy_from_slice(&(v as u32).to_le_bytes()),
228        _ => out[o..o + 8].copy_from_slice(&v.to_le_bytes()),
229    }
230}
231
232fn encode_phdr(p: &Phdr, is64: bool) -> Vec<u8> {
233    let mut b = vec![];
234    if is64 {
235        b.extend(p.p_type.to_le_bytes());
236        b.extend(p.flags.to_le_bytes());
237        for v in [p.offset, p.vaddr, p.paddr, p.filesz, p.memsz, p.align] {
238            b.extend(v.to_le_bytes());
239        }
240    } else {
241        b.extend(p.p_type.to_le_bytes());
242        for v in [p.offset, p.vaddr, p.paddr, p.filesz, p.memsz] {
243            b.extend((v as u32).to_le_bytes());
244        }
245        b.extend(p.flags.to_le_bytes());
246        b.extend((p.align as u32).to_le_bytes());
247    }
248    b
249}
250
251fn round_up(v: u64, a: u64) -> u64 {
252    v.div_ceil(a) * a
253}
254
255/// Rewrite SONAME (if `soname` is set) and every DT_NEEDED found in `needed`. Returns the input
256/// unchanged when there is nothing to do.
257pub fn rewrite(d: &[u8], soname: Option<&str>, needed: &HashMap<String, String>) -> R<Vec<u8>> {
258    let e = Elf::new(d)?;
259    if e.e_type()? != 3 {
260        return Err("not a shared object (ET_DYN)".into());
261    }
262    let phdrs = e.phdrs()?;
263    let dynv = e.dyn_entries(&phdrs)?;
264    let strtab = dynv.iter().find(|x| x.1 == DT_STRTAB).ok_or("no DT_STRTAB")?.2;
265    let strsz = dynv.iter().find(|x| x.1 == DT_STRSZ).ok_or("no DT_STRSZ")?.2;
266    let stroff = e.vaddr_to_off(&phdrs, strtab)?;
267    let old_str = d.get(stroff as usize..(stroff + strsz) as usize).ok_or("dynstr out of range")?;
268
269    // Collect edits: (dyn entry offset or verneed field offset) -> new string.
270    let mut new_str = old_str.to_vec();
271    let mut added: HashMap<String, u64> = HashMap::new();
272    let mut intern = |s: &str, buf: &mut Vec<u8>| -> u64 {
273        *added.entry(s.to_string()).or_insert_with(|| {
274            let o = buf.len() as u64;
275            buf.extend_from_slice(s.as_bytes());
276            buf.push(0);
277            o
278        })
279    };
280    let mut dyn_edits: Vec<(u64, u64)> = vec![]; // (offset of d_val, new string offset)
281    let half = if e.is64 { 8 } else { 4 };
282    for &(o, tag, val) in &dynv {
283        if tag != DT_SONAME && tag != DT_NEEDED {
284            continue;
285        }
286        let cur = e.cstr(stroff + val)?;
287        let repl = match tag {
288            DT_SONAME => soname.map(str::to_string),
289            DT_NEEDED => needed.get(&cur).cloned(),
290            _ => None,
291        };
292        if let Some(r) = repl {
293            if r != cur {
294                dyn_edits.push((o + half, intern(&r, &mut new_str)));
295            }
296        }
297    }
298    let has_soname = dynv.iter().any(|x| x.1 == DT_SONAME);
299    if soname.is_some() && !has_soname {
300        return Err("library has no DT_SONAME entry to rewrite (relink with -Wl,-soname)".into());
301    }
302    // verneed: vn_file must keep matching the (renamed) DT_NEEDED it describes.
303    let mut vn_edits: Vec<(u64, u64)> = vec![];
304    if let (Some(vn), Some(cnt)) =
305        (dynv.iter().find(|x| x.1 == DT_VERNEED).map(|x| x.2), dynv.iter().find(|x| x.1 == DT_VERNEEDNUM).map(|x| x.2))
306    {
307        let mut o = e.vaddr_to_off(&phdrs, vn)?;
308        for _ in 0..cnt {
309            let file = e.cstr(stroff + e.u32(o + 4)? as u64)?;
310            if let Some(r) = needed.get(&file) {
311                vn_edits.push((o + 4, intern(r, &mut new_str)));
312            }
313            let next = e.u32(o + 12)? as u64;
314            if next == 0 {
315                break;
316            }
317            o += next;
318        }
319    }
320    if dyn_edits.is_empty() && vn_edits.is_empty() {
321        return Ok(d.to_vec());
322    }
323
324    // New layout: [old file][pad][phdrs'][pad to 8][dynstr'] in a new PT_LOAD.
325    let loads: Vec<&Phdr> = phdrs.iter().filter(|p| p.p_type == PT_LOAD).collect();
326    let align = loads.iter().map(|p| p.align).max().unwrap_or(4096).max(4096);
327    let vend = loads.iter().map(|p| p.vaddr + p.memsz).max().unwrap_or(0);
328    let new_off = round_up(d.len() as u64, align);
329    let new_vaddr = round_up(vend, align);
330    let has_phdr = phdrs.iter().any(|p| p.p_type == PT_PHDR);
331    let entsize = e.phentsize()? as u64;
332    let count = phdrs.len() as u64 + 1 + u64::from(!has_phdr);
333    let ph_size = count * entsize;
334    let str_rel = round_up(ph_size, 8);
335    let seg_size = str_rel + new_str.len() as u64;
336
337    let mut newph: Vec<Phdr> = vec![];
338    let phdr_entry = Phdr {
339        p_type: PT_PHDR,
340        flags: 4,
341        offset: new_off,
342        vaddr: new_vaddr,
343        paddr: new_vaddr,
344        filesz: ph_size,
345        memsz: ph_size,
346        align: 8,
347    };
348    if !has_phdr {
349        newph.push(phdr_entry);
350    }
351    let last_load = phdrs.iter().rposition(|p| p.p_type == PT_LOAD).ok_or("no PT_LOAD")?;
352    for (i, p) in phdrs.iter().enumerate() {
353        newph.push(if p.p_type == PT_PHDR { phdr_entry } else { *p });
354        if i == last_load {
355            newph.push(Phdr {
356                p_type: PT_LOAD,
357                flags: 4,
358                offset: new_off,
359                vaddr: new_vaddr,
360                paddr: new_vaddr,
361                filesz: seg_size,
362                memsz: seg_size,
363                align,
364            });
365        }
366    }
367
368    let mut out = d.to_vec();
369    out.resize(new_off as usize, 0);
370    for p in &newph {
371        out.extend(encode_phdr(p, e.is64));
372    }
373    out.resize((new_off + str_rel) as usize, 0);
374    out.extend_from_slice(&new_str);
375
376    // ELF header: e_phoff, e_phnum.
377    if e.is64 {
378        put(&mut out, 32, new_off, 8);
379        put(&mut out, 56, count, 2);
380    } else {
381        put(&mut out, 28, new_off, 4);
382        put(&mut out, 44, count, 2);
383    }
384    let w = if e.is64 { 8 } else { 4 };
385    for &(o, tag, _) in &dynv {
386        if tag == DT_STRTAB {
387            put(&mut out, o + half, new_vaddr + str_rel, w);
388        } else if tag == DT_STRSZ {
389            put(&mut out, o + half, new_str.len() as u64, w);
390        }
391    }
392    for (o, v) in dyn_edits {
393        put(&mut out, o, v, w);
394    }
395    for (o, v) in vn_edits {
396        put(&mut out, o, v, 4);
397    }
398    // Section header of .dynstr (for readelf/objdump/debuggers; the loader does not need it).
399    let shoff = e.shoff()?;
400    if shoff != 0 {
401        let es = e.shentsize()? as u64;
402        for i in 0..e.shnum()? as u64 {
403            let o = shoff + i * es;
404            let (ty, off) =
405                if e.is64 { (e.u32(o + 4)?, e.u64(o + 24)?) } else { (e.u32(o + 4)?, e.u32(o + 16)? as u64) };
406            if ty == SHT_STRTAB && off == stroff {
407                if e.is64 {
408                    put(&mut out, o + 16, new_vaddr + str_rel, 8);
409                    put(&mut out, o + 24, new_off + str_rel, 8);
410                    put(&mut out, o + 32, new_str.len() as u64, 8);
411                } else {
412                    put(&mut out, o + 12, new_vaddr + str_rel, 4);
413                    put(&mut out, o + 16, new_off + str_rel, 4);
414                    put(&mut out, o + 20, new_str.len() as u64, 4);
415                }
416            }
417        }
418    }
419    Ok(out)
420}
421
422#[cfg(test)]
423mod tests {
424    use super::*;
425
426    #[test]
427    fn rejects_non_elf() {
428        assert!(info(b"hello").is_err());
429        assert!(rewrite(&[0u8; 100], None, &HashMap::new()).is_err());
430    }
431}