running_process_platform_internal/platform_linux/
loaded_images.rs1use 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 #[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 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 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
197pub 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
233pub 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 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 #[test]
273 #[allow(clippy::single_range_in_vec_init)] 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(¬e),
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}