lightswitch_object/
object.rs

1use std::fs;
2use std::io::Read;
3use std::path::Path;
4
5use anyhow::{anyhow, Result};
6use memmap2::Mmap;
7use ring::digest::{Context, Digest, SHA256};
8
9use crate::BuildId;
10use object::elf::{FileHeader32, FileHeader64, PT_LOAD};
11use object::read::elf::FileHeader;
12use object::read::elf::ProgramHeader;
13use object::Endianness;
14use object::FileKind;
15use object::Object;
16use object::ObjectKind;
17use object::ObjectSection;
18
19/// Compact identifier for executable files.
20///
21/// Compact identifier for executable files derived from the first 8 bytes
22/// of the hash of the code stored in the .text ELF segment. By using this
23/// smaller type for object files less memory is used and also comparison,
24/// and other operations are cheaper.
25pub type ExecutableId = u64;
26
27/// Elf load segments used during address normalization to find the segment
28/// for what an code address falls into.
29#[derive(Debug, Clone)]
30pub struct ElfLoad {
31    pub p_offset: u64,
32    pub p_vaddr: u64,
33    pub p_memsz: u64,
34}
35
36#[derive(Debug)]
37pub struct ObjectFile {
38    /// Warning! `object` must always go above `mmap` to ensure it will be dropped
39    /// before. Rust guarantees that fields are dropped in the order they are defined.
40    object: object::File<'static>, // Its lifetime is tied to the `mmap` below.
41    mmap: Box<Mmap>,
42    code_hash: Digest,
43}
44
45impl ObjectFile {
46    pub fn new(path: &Path) -> Result<Self> {
47        let file = fs::File::open(path)?;
48        // Rust offers no guarantees on whether a "move" is done virtually or by memcpying,
49        // so to ensure that the memory value is valid we store it in the heap.
50        // Safety: Memory mapping files can cause issues if the file is modified or unmapped.
51        let mmap = Box::new(unsafe { Mmap::map(&file) }?);
52        let object = object::File::parse(&**mmap)?;
53        // Safety: The lifetime of `object` will outlive `mmap`'s. We ensure `mmap` lives as long as
54        // `object` by defining `object` before.
55        let object =
56            unsafe { std::mem::transmute::<object::File<'_>, object::File<'static>>(object) };
57        let Some(code_hash) = code_hash(&object) else {
58            return Err(anyhow!("code hash is None"));
59        };
60        Ok(ObjectFile {
61            object,
62            mmap,
63            code_hash,
64        })
65    }
66
67    /// Returns an identifier for the executable using the first 8 bytes of the Sha256 of the code section.
68    pub fn id(&self) -> Result<ExecutableId> {
69        let mut buffer = [0; 8];
70        let _ = self.code_hash.as_ref().read(&mut buffer)?;
71        Ok(u64::from_ne_bytes(buffer))
72    }
73
74    /// Returns the executable build ID if present. If no GNU build ID and no Go build ID
75    /// are found it returns the hash of the text section.
76    pub fn build_id(&self) -> Result<BuildId> {
77        let object = &self.object;
78        let gnu_build_id = object.build_id()?;
79
80        if let Some(data) = gnu_build_id {
81            return Ok(BuildId::gnu_from_bytes(data));
82        }
83
84        // Golang (the Go toolchain does not interpret these bytes as we do).
85        for section in object.sections() {
86            if section.name()? == ".note.go.buildid" {
87                if let Ok(data) = section.data() {
88                    return BuildId::go_from_bytes(data);
89                }
90            }
91        }
92
93        // No build id (Rust, some compilers and Linux distributions).
94        Ok(BuildId::sha256_from_digest(&self.code_hash))
95    }
96
97    pub fn is_dynamic(&self) -> bool {
98        self.object.kind() == ObjectKind::Dynamic
99    }
100
101    pub fn is_go(&self) -> bool {
102        for section in self.object.sections() {
103            if let Ok(section_name) = section.name() {
104                if section_name == ".gosymtab"
105                    || section_name == ".gopclntab"
106                    || section_name == ".note.go.buildid"
107                {
108                    return true;
109                }
110            }
111        }
112        false
113    }
114
115    pub fn elf_load_segments(&self) -> Result<Vec<ElfLoad>> {
116        let mmap = &**self.mmap;
117
118        match FileKind::parse(mmap) {
119            Ok(FileKind::Elf32) => {
120                let header: &FileHeader32<Endianness> = FileHeader32::<Endianness>::parse(mmap)?;
121                let endian = header.endian()?;
122                let segments = header.program_headers(endian, mmap)?;
123
124                let mut elf_loads = Vec::new();
125                for segment in segments {
126                    if segment.p_type(endian) == PT_LOAD {
127                        elf_loads.push(ElfLoad {
128                            p_offset: segment.p_offset(endian) as u64,
129                            p_vaddr: segment.p_vaddr(endian) as u64,
130                            p_memsz: segment.p_memsz(endian) as u64,
131                        });
132                    }
133                }
134                Ok(elf_loads)
135            }
136            Ok(FileKind::Elf64) => {
137                let header: &FileHeader64<Endianness> = FileHeader64::<Endianness>::parse(mmap)?;
138                let endian = header.endian()?;
139                let segments = header.program_headers(endian, mmap)?;
140
141                let mut elf_loads = Vec::new();
142                for segment in segments {
143                    if segment.p_type(endian) == PT_LOAD {
144                        elf_loads.push(ElfLoad {
145                            p_offset: segment.p_offset(endian),
146                            p_vaddr: segment.p_vaddr(endian),
147                            p_memsz: segment.p_memsz(endian),
148                        });
149                    }
150                }
151                Ok(elf_loads)
152            }
153            Ok(other_file_kind) => Err(anyhow!(
154                "object is not an 32 or 64 bits ELF but {:?}",
155                other_file_kind
156            )),
157            Err(e) => Err(anyhow!("FileKind failed with {:?}", e)),
158        }
159    }
160}
161
162pub fn code_hash(object: &object::File) -> Option<Digest> {
163    for section in object.sections() {
164        let Ok(section_name) = section.name() else {
165            continue;
166        };
167
168        if section_name == ".text" {
169            if let Ok(section) = section.data() {
170                return Some(sha256_digest(section));
171            }
172        }
173    }
174
175    None
176}
177
178fn sha256_digest<R: Read>(mut reader: R) -> Digest {
179    let mut context = Context::new(&SHA256);
180    let mut buffer = [0; 1024];
181
182    loop {
183        let count = reader
184            .read(&mut buffer)
185            .expect("reading digest into buffer should not fail");
186        if count == 0 {
187            break;
188        }
189        context.update(&buffer[..count]);
190    }
191
192    context.finish()
193}