Skip to main content

running_process_platform_internal/platform_linux/
loaded_images.rs

1//! Loaded-image inventory for the current Linux process (#974).
2//!
3//! Two loader views are joined here: `/proc/self/maps` says which files back
4//! which address ranges, and `dl_iterate_phdr` says which loaded objects carry
5//! which GNU build id. Reading the object files themselves stays with the
6//! caller.
7
8use std::collections::BTreeMap;
9use std::ops::Range;
10
11use crate::platform::process::{LoadedImage, LoadedImageBackingFile, LoadedImageFormat};
12
13struct LoadedElfIdentity {
14    load_bias: u64,
15    mapped_ranges: Vec<Range<u64>>,
16    build_id: Vec<u8>,
17}
18
19fn loaded_elf_identities() -> Vec<LoadedElfIdentity> {
20    unsafe extern "C" fn visit(
21        info: *mut libc::dl_phdr_info,
22        _size: libc::size_t,
23        data: *mut libc::c_void,
24    ) -> libc::c_int {
25        const MAX_NOTE_BYTES: usize = 1024 * 1024;
26        let info = unsafe { &*info };
27        let out = unsafe { &mut *data.cast::<Vec<LoadedElfIdentity>>() };
28        if info.dlpi_phdr.is_null() || info.dlpi_phnum == 0 {
29            return 0;
30        }
31        // libc exposes Elf_Addr as u64 on our 64-bit CI hosts and as a
32        // narrower integer on 32-bit Linux; this widening keeps both valid.
33        #[allow(clippy::unnecessary_cast)]
34        let load_bias = info.dlpi_addr as u64;
35        let headers =
36            unsafe { std::slice::from_raw_parts(info.dlpi_phdr, usize::from(info.dlpi_phnum)) };
37        let mapped_ranges = headers
38            .iter()
39            .filter(|header| header.p_type == libc::PT_LOAD && header.p_memsz > 0)
40            .filter_map(|header| {
41                let start = load_bias.checked_add(header.p_vaddr)?;
42                let end = start.checked_add(header.p_memsz)?;
43                Some(start..end)
44            })
45            .collect::<Vec<_>>();
46        for header in headers {
47            if header.p_type != libc::PT_NOTE {
48                continue;
49            }
50            let Ok(length) = usize::try_from(header.p_memsz) else {
51                continue;
52            };
53            if length == 0 || length > MAX_NOTE_BYTES {
54                continue;
55            }
56            let Some(address) = load_bias.checked_add(header.p_vaddr) else {
57                continue;
58            };
59            let Some(note_end) = address.checked_add(length as u64) else {
60                continue;
61            };
62            let is_mapped = headers.iter().any(|load| {
63                if load.p_type != libc::PT_LOAD || load.p_flags & libc::PF_R == 0 {
64                    return false;
65                }
66                let Some(start) = load_bias.checked_add(load.p_vaddr) else {
67                    return false;
68                };
69                let Some(end) = start.checked_add(load.p_memsz) else {
70                    return false;
71                };
72                address >= start && note_end <= end
73            });
74            if address == 0 || !is_mapped {
75                continue;
76            }
77            let notes = unsafe { std::slice::from_raw_parts(address as *const u8, length) };
78            if let Some(build_id) = super::gnu_build_id_from_notes(notes) {
79                out.push(LoadedElfIdentity {
80                    load_bias,
81                    mapped_ranges,
82                    build_id: build_id.to_vec(),
83                });
84                break;
85            }
86        }
87        0
88    }
89
90    let mut out = Vec::new();
91    unsafe {
92        libc::dl_iterate_phdr(
93            Some(visit),
94            (&mut out as *mut Vec<LoadedElfIdentity>).cast::<libc::c_void>(),
95        );
96    }
97    out
98}
99
100fn next_maps_field(input: &str) -> Option<(&str, &str)> {
101    let input = input.trim_start();
102    let end = input.find(char::is_whitespace).unwrap_or(input.len());
103    (!input.is_empty()).then_some((&input[..end], &input[end..]))
104}
105
106struct LinuxImage {
107    mapped_ranges: Vec<Range<u64>>,
108    executable_ranges: Vec<Range<u64>>,
109    path: String,
110    device_major: u64,
111    device_minor: u64,
112    inode: String,
113}
114
115type LinuxImageKey = (String, String, String, u64);
116
117struct LinuxMapping {
118    range: Range<u64>,
119    executable: bool,
120}
121
122fn linux_images(maps: &str) -> Vec<LinuxImage> {
123    // (path, device, inode, load instance) -> individual mapped ranges.
124    let mut images: BTreeMap<LinuxImageKey, Vec<LinuxMapping>> = BTreeMap::new();
125    for line in maps.lines() {
126        let Some((range, rest)) = next_maps_field(line) else {
127            continue;
128        };
129        let Some((perms, rest)) = next_maps_field(rest) else {
130            continue;
131        };
132        let Some((offset, rest)) = next_maps_field(rest) else {
133            continue;
134        };
135        let Some((dev, rest)) = next_maps_field(rest) else {
136            continue;
137        };
138        let Some((inode, rest)) = next_maps_field(rest) else {
139            continue;
140        };
141        let path = rest.trim_start();
142        if !path.starts_with('/') {
143            continue;
144        }
145        if path.ends_with(" (deleted)") {
146            // Reopening the same pathname could read a replacement build,
147            // producing plausible but wrong unwind rules. A deleted mapping
148            // is safer left raw.
149            continue;
150        }
151        let path = path.to_owned();
152        let Some((start, end)) = range.split_once('-') else {
153            continue;
154        };
155        let (Ok(start), Ok(end), Ok(offset)) = (
156            u64::from_str_radix(start, 16),
157            u64::from_str_radix(end, 16),
158            u64::from_str_radix(offset, 16),
159        ) else {
160            continue;
161        };
162        let candidate_base = start.saturating_sub(offset);
163        images
164            .entry((path, dev.to_owned(), inode.to_owned(), candidate_base))
165            .or_default()
166            .push(LinuxMapping {
167                range: start..end,
168                executable: perms.as_bytes().get(2) == Some(&b'x'),
169            });
170    }
171
172    images
173        .into_iter()
174        .filter_map(|((path, device, inode, _load_bias), mut mappings)| {
175            let (major, minor) = device.split_once(':')?;
176            let device_major = u64::from_str_radix(major, 16).ok()?;
177            let device_minor = u64::from_str_radix(minor, 16).ok()?;
178            mappings.sort_by_key(|mapping| mapping.range.start);
179            Some(LinuxImage {
180                mapped_ranges: mappings
181                    .iter()
182                    .map(|mapping| mapping.range.clone())
183                    .collect(),
184                executable_ranges: mappings
185                    .into_iter()
186                    .filter_map(|mapping| mapping.executable.then_some(mapping.range))
187                    .collect(),
188                path,
189                device_major,
190                device_minor,
191                inode,
192            })
193        })
194        .collect()
195}
196
197/// Enumerate the file-backed ELF images mapped in this process, ordered by
198/// `(path, device, inode, load instance)`.
199pub fn loaded_images() -> std::io::Result<Vec<LoadedImage>> {
200    let maps = std::fs::read_to_string("/proc/self/maps")?;
201    let identities = loaded_elf_identities();
202    Ok(linux_images(&maps)
203        .into_iter()
204        .map(|image| {
205            let identity = identities.iter().find(|identity| {
206                identity.mapped_ranges.iter().any(|loaded| {
207                    image
208                        .mapped_ranges
209                        .iter()
210                        .any(|mapped| loaded.start < mapped.end && mapped.start < loaded.end)
211                })
212            });
213            LoadedImage {
214                format: LoadedImageFormat::Elf,
215                header_address: 0,
216                image_size: 0,
217                slide: 0,
218                path: Some(image.path),
219                mapped_ranges: image.mapped_ranges,
220                executable_ranges: image.executable_ranges,
221                elf_load_bias: identity.map(|identity| identity.load_bias),
222                build_id: identity.map(|identity| identity.build_id.clone()),
223                backing_file: Some(LoadedImageBackingFile {
224                    device_major: image.device_major,
225                    device_minor: image.device_minor,
226                    inode: image.inode,
227                }),
228            }
229        })
230        .collect())
231}
232
233/// Reopen the file behind `image`, refusing one that no longer has the device
234/// and inode recorded in `/proc/self/maps`.
235pub fn open_loaded_image_file(image: &LoadedImage) -> Option<std::fs::File> {
236    use std::os::unix::fs::MetadataExt as _;
237
238    let file = std::fs::File::open(image.path.as_deref()?).ok()?;
239    let Some(expected) = &image.backing_file else {
240        return Some(file);
241    };
242    let metadata = file.metadata().ok()?;
243    if u64::from(libc::major(metadata.dev())) != expected.device_major
244        || u64::from(libc::minor(metadata.dev())) != expected.device_minor
245        || metadata.ino().to_string() != expected.inode
246    {
247        // The pathname no longer names the object in /proc/self/maps.
248        return None;
249    }
250    Some(file)
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    #[test]
258    fn maps_path_preserves_spaces_and_deleted_suffix() {
259        let line = "1000-2000 r-xp 00000000 08:01 42 /tmp/a file (deleted)";
260        let (_, rest) = next_maps_field(line).unwrap();
261        let (_, rest) = next_maps_field(rest).unwrap();
262        let (_, rest) = next_maps_field(rest).unwrap();
263        let (_, rest) = next_maps_field(rest).unwrap();
264        let (_, rest) = next_maps_field(rest).unwrap();
265        assert_eq!(rest.trim_start(), "/tmp/a file (deleted)");
266        assert!(rest.trim_start().ends_with(" (deleted)"));
267    }
268
269    /// Pins the grouping the probe relied on before this moved (#974):
270    /// mappings of one file at one load instance form one image, anonymous
271    /// and deleted mappings are skipped, and only `x` mappings are executable.
272    #[test]
273    #[allow(clippy::single_range_in_vec_init)] // Lists of ranges, some of one.
274    fn maps_lines_group_into_images_by_file_and_load_instance() {
275        let maps = "\
2767f0000002000-7f0000003000 r--p 00002000 08:01 42 /lib/a b.so
2777f0000000000-7f0000001000 r-xp 00000000 08:01 42 /lib/a b.so
2787f0000010000-7f0000011000 r-xp 00000000 08:01 42 /lib/a b.so
2797f0000020000-7f0000021000 rw-p 00000000 00:00 0
2807f0000030000-7f0000031000 r-xp 00000000 08:01 43 /lib/gone.so (deleted)
2817f0000040000-7f0000041000 r-xp 00000000 fd:0a 44 /usr/bin/app
282not a maps line
283";
284        let images = linux_images(maps);
285        let summary: Vec<_> = images
286            .iter()
287            .map(|image| {
288                (
289                    image.path.as_str(),
290                    image.mapped_ranges.clone(),
291                    image.executable_ranges.clone(),
292                    image.device_major,
293                    image.device_minor,
294                    image.inode.as_str(),
295                )
296            })
297            .collect();
298        assert_eq!(
299            summary,
300            vec![
301                (
302                    "/lib/a b.so",
303                    vec![0x7f00_0000_0000..0x7f00_0000_1000, 0x7f00_0000_2000..0x7f00_0000_3000],
304                    vec![0x7f00_0000_0000..0x7f00_0000_1000],
305                    8,
306                    1,
307                    "42",
308                ),
309                (
310                    "/lib/a b.so",
311                    vec![0x7f00_0001_0000..0x7f00_0001_1000],
312                    vec![0x7f00_0001_0000..0x7f00_0001_1000],
313                    8,
314                    1,
315                    "42",
316                ),
317                (
318                    "/usr/bin/app",
319                    vec![0x7f00_0004_0000..0x7f00_0004_1000],
320                    vec![0x7f00_0004_0000..0x7f00_0004_1000],
321                    0xfd,
322                    0x0a,
323                    "44",
324                ),
325            ]
326        );
327    }
328
329    #[test]
330    fn gnu_note_parser_extracts_the_build_id() {
331        let mut note = Vec::new();
332        note.extend_from_slice(&4u32.to_ne_bytes());
333        note.extend_from_slice(&4u32.to_ne_bytes());
334        note.extend_from_slice(&3u32.to_ne_bytes());
335        note.extend_from_slice(b"GNU\0");
336        note.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
337        assert_eq!(
338            super::super::gnu_build_id_from_notes(&note),
339            Some(&[0xaa, 0xbb, 0xcc, 0xdd][..])
340        );
341    }
342
343    #[test]
344    fn a_path_that_now_names_another_file_is_not_reopened() {
345        let exe = std::env::current_exe().expect("current exe");
346        let image = LoadedImage {
347            format: LoadedImageFormat::Elf,
348            header_address: 0,
349            image_size: 0,
350            slide: 0,
351            path: Some(exe.to_string_lossy().into_owned()),
352            mapped_ranges: Vec::new(),
353            executable_ranges: Vec::new(),
354            elf_load_bias: None,
355            build_id: None,
356            backing_file: Some(LoadedImageBackingFile {
357                device_major: u64::MAX,
358                device_minor: 0,
359                inode: "0".into(),
360            }),
361        };
362        assert!(open_loaded_image_file(&image).is_none());
363    }
364}