mappings/lib.rs
1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License in the LICENSE file at the
6// root of this repository, or online at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Linux-specific process introspection.
17
18//! Utility crate to extract information about the running process.
19//!
20//! Currently only works on Linux.
21use std::path::PathBuf;
22
23use once_cell::sync::Lazy;
24use tracing::error;
25
26use util::{BuildId, Mapping};
27
28#[cfg(target_os = "linux")]
29mod enabled {
30 use std::ffi::{CStr, OsStr};
31 use std::os::unix::ffi::OsStrExt;
32 use std::path::PathBuf;
33 use std::str::FromStr;
34
35 use anyhow::Context;
36 use libc::{
37 c_int, c_void, dl_iterate_phdr, dl_phdr_info, size_t, Elf64_Word, PT_LOAD, PT_NOTE,
38 };
39
40 use util::{BuildId, CastFrom};
41
42 use crate::LoadedSegment;
43
44 use super::SharedObject;
45
46 /// Collects information about all shared objects loaded into the current
47 /// process, including the main program binary as well as all dynamically loaded
48 /// libraries. Intended to be useful for profilers, who can use this information
49 /// to symbolize stack traces offline.
50 ///
51 /// Uses `dl_iterate_phdr` to walk all shared objects and extract the wanted
52 /// information from their program headers.
53 ///
54 /// SAFETY: This function is written in a hilariously unsafe way: it involves
55 /// following pointers to random parts of memory, and then assuming that
56 /// particular structures can be found there. However, it was written by
57 /// carefully reading `man dl_iterate_phdr` and `man elf`, and is thus intended
58 /// to be relatively safe for callers to use. Assuming I haven't written any
59 /// bugs (and that the documentation is correct), the only known safety
60 /// requirements are:
61 ///
62 /// (1) The running binary must be in ELF format and running on Linux.
63 pub unsafe fn collect_shared_objects() -> Result<Vec<SharedObject>, anyhow::Error> {
64 let mut state = CallbackState {
65 result: Ok(Vec::new()),
66 };
67 let state_ptr = std::ptr::addr_of_mut!(state).cast();
68
69 // SAFETY: `dl_iterate_phdr` has no documented restrictions on when
70 // it can be called.
71 unsafe { dl_iterate_phdr(Some(iterate_cb), state_ptr) };
72
73 state.result
74 }
75
76 struct CallbackState {
77 result: Result<Vec<SharedObject>, anyhow::Error>,
78 }
79
80 impl CallbackState {
81 fn is_first(&self) -> bool {
82 match &self.result {
83 Ok(v) => v.is_empty(),
84 Err(_) => false,
85 }
86 }
87 }
88
89 const CB_RESULT_OK: c_int = 0;
90 const CB_RESULT_ERROR: c_int = -1;
91
92 unsafe extern "C" fn iterate_cb(
93 info: *mut dl_phdr_info,
94 _size: size_t,
95 data: *mut c_void,
96 ) -> c_int {
97 let state: *mut CallbackState = data.cast();
98
99 // SAFETY: `data` is a pointer to a `CallbackState`, and no mutable reference
100 // aliases with it in Rust. Furthermore, `dl_iterate_phdr` doesn't do anything
101 // with `data` other than pass it to this callback, so nothing will be mutating
102 // the object it points to while we're inside here.
103 assert_pointer_valid(state);
104 let state = unsafe { state.as_mut() }.expect("pointer is valid");
105
106 // SAFETY: similarly, `dl_iterate_phdr` isn't mutating `info`
107 // while we're here.
108 assert_pointer_valid(info);
109 let info = unsafe { info.as_ref() }.expect("pointer is valid");
110
111 let base_address = usize::cast_from(info.dlpi_addr);
112
113 let path_name = if state.is_first() {
114 // From `man dl_iterate_phdr`:
115 // "The first object visited by callback is the main program. For the main
116 // program, the dlpi_name field will be an empty string."
117 match current_exe().context("failed to read the name of the current executable") {
118 Ok(pb) => pb,
119 Err(e) => {
120 // Profiles will be of dubious usefulness
121 // if we can't get the build ID for the main executable,
122 // so just bail here.
123 state.result = Err(e);
124 return CB_RESULT_ERROR;
125 }
126 }
127 } else if info.dlpi_name.is_null() {
128 // This would be unexpected, but let's handle this case gracefully by skipping this object.
129 return CB_RESULT_OK;
130 } else {
131 // SAFETY: `dl_iterate_phdr` documents this as being a null-terminated string.
132 assert_pointer_valid(info.dlpi_name);
133 let name = unsafe { CStr::from_ptr(info.dlpi_name) };
134
135 OsStr::from_bytes(name.to_bytes()).into()
136 };
137
138 // Walk the headers of this image, looking for `PT_LOAD` and `PT_NOTE` segments.
139 let mut loaded_segments = Vec::new();
140 let mut build_id = None;
141
142 // SAFETY: `dl_iterate_phdr` is documented as setting `dlpi_phnum` to the
143 // length of the array pointed to by `dlpi_phdr`.
144 assert_pointer_valid(info.dlpi_phdr);
145 let program_headers =
146 unsafe { std::slice::from_raw_parts(info.dlpi_phdr, info.dlpi_phnum.into()) };
147
148 for ph in program_headers {
149 if ph.p_type == PT_LOAD {
150 loaded_segments.push(LoadedSegment {
151 file_offset: u64::cast_from(ph.p_offset),
152 memory_offset: usize::cast_from(ph.p_vaddr),
153 memory_size: usize::cast_from(ph.p_memsz),
154 });
155 } else if ph.p_type == PT_NOTE {
156 // From `man elf`:
157 // typedef struct {
158 // Elf64_Word n_namesz;
159 // Elf64_Word n_descsz;
160 // Elf64_Word n_type;
161 // } Elf64_Nhdr;
162 #[repr(C)]
163 struct NoteHeader {
164 n_namesz: Elf64_Word,
165 n_descsz: Elf64_Word,
166 n_type: Elf64_Word,
167 }
168 // This is how `man dl_iterate_phdr` says to find the
169 // segment headers in memory.
170 //
171 // Note - it seems on some old
172 // versions of Linux (I observed it on CentOS 7),
173 // `p_vaddr` can be negative, so we use wrapping add here
174 let mut offset = usize::cast_from(ph.p_vaddr.wrapping_add(info.dlpi_addr));
175 let orig_offset = offset;
176
177 const NT_GNU_BUILD_ID: Elf64_Word = 3;
178 const GNU_NOTE_NAME: &[u8; 4] = b"GNU\0";
179 const ELF_NOTE_STRING_ALIGN: usize = 4;
180
181 while offset + std::mem::size_of::<NoteHeader>() + GNU_NOTE_NAME.len()
182 <= orig_offset + usize::cast_from(ph.p_memsz)
183 {
184 // Justification: Our logic for walking this header
185 // follows exactly the code snippet in the
186 // `Notes (Nhdr)` section of `man elf`,
187 // so `offset` will always point to a `NoteHeader`
188 // (called `Elf64_Nhdr` in that document)
189 #[allow(clippy::as_conversions)]
190 let nh_ptr = offset as *const NoteHeader;
191
192 // SAFETY: Iterating according to the `Notes (Nhdr)`
193 // section of `man elf` ensures that this pointer is
194 // aligned. The offset check above ensures that it
195 // is in-bounds.
196 assert_pointer_valid(nh_ptr);
197 let nh = unsafe { nh_ptr.as_ref() }.expect("pointer is valid");
198
199 // from elf.h
200 if nh.n_type == NT_GNU_BUILD_ID
201 && nh.n_descsz != 0
202 && usize::cast_from(nh.n_namesz) == GNU_NOTE_NAME.len()
203 {
204 // Justification: since `n_namesz` is 4, the name is a four-byte value.
205 #[allow(clippy::as_conversions)]
206 let p_name = (offset + std::mem::size_of::<NoteHeader>()) as *const [u8; 4];
207
208 // SAFETY: since `n_namesz` is 4, the name is a four-byte value.
209 assert_pointer_valid(p_name);
210 let name = unsafe { p_name.as_ref() }.expect("pointer is valid");
211
212 if name == GNU_NOTE_NAME {
213 // We found what we're looking for!
214 // Justification: simple pointer arithmetic
215 #[allow(clippy::as_conversions)]
216 let p_desc = (p_name as usize + 4) as *const u8;
217
218 // SAFETY: This is the documented meaning of `n_descsz`.
219 assert_pointer_valid(p_desc);
220 let desc = unsafe {
221 std::slice::from_raw_parts(p_desc, usize::cast_from(nh.n_descsz))
222 };
223
224 build_id = Some(BuildId(desc.to_vec()));
225 break;
226 }
227 }
228 offset = offset
229 + std::mem::size_of::<NoteHeader>()
230 + align_up::<ELF_NOTE_STRING_ALIGN>(usize::cast_from(nh.n_namesz))
231 + align_up::<ELF_NOTE_STRING_ALIGN>(usize::cast_from(nh.n_descsz));
232 }
233 }
234 }
235
236 let objects = state.result.as_mut().expect("we return early on errors");
237 objects.push(SharedObject {
238 base_address,
239 path_name,
240 build_id,
241 loaded_segments,
242 });
243
244 CB_RESULT_OK
245 }
246
247 /// Increases `p` as little as possible (including possibly 0)
248 /// such that it becomes a multiple of `N`.
249 pub const fn align_up<const N: usize>(p: usize) -> usize {
250 if p % N == 0 {
251 p
252 } else {
253 p + (N - (p % N))
254 }
255 }
256
257 /// Asserts that the given pointer is valid.
258 ///
259 /// # Panics
260 ///
261 /// Panics if the given pointer:
262 /// * is a null pointer
263 /// * is not properly aligned for `T`
264 fn assert_pointer_valid<T>(ptr: *const T) {
265 // No other known way to convert a pointer to `usize`.
266 #[allow(clippy::as_conversions)]
267 let address = ptr as usize;
268 let align = std::mem::align_of::<T>();
269
270 assert!(!ptr.is_null());
271 assert!(address % align == 0, "unaligned pointer");
272 }
273
274 fn current_exe_from_dladdr() -> Result<PathBuf, anyhow::Error> {
275 let progname = unsafe {
276 let mut dlinfo = std::mem::MaybeUninit::uninit();
277
278 // This should set the filepath of the current executable
279 // because it must contain the function pointer of itself.
280 let ret = libc::dladdr(
281 current_exe_from_dladdr as *const libc::c_void,
282 dlinfo.as_mut_ptr(),
283 );
284 if ret == 0 {
285 anyhow::bail!("dladdr failed");
286 }
287 CStr::from_ptr(dlinfo.assume_init().dli_fname).to_str()?
288 };
289
290 Ok(PathBuf::from_str(progname)?)
291 }
292
293 /// Get the name of the current executable by dladdr and fall back to std::env::current_exe
294 /// if it fails. Try dladdr first because it returns the actual exe even when it's invoked
295 /// by ld.so.
296 fn current_exe() -> Result<PathBuf, anyhow::Error> {
297 match current_exe_from_dladdr() {
298 Ok(path) => Ok(path),
299 Err(e) => {
300 // when failed to get current exe from dladdr, fall back to the conventional way
301 std::env::current_exe().context(e)
302 }
303 }
304 }
305}
306
307/// Mappings of the processes' executable and shared libraries.
308#[cfg(target_os = "linux")]
309pub static MAPPINGS: Lazy<Option<Vec<Mapping>>> = Lazy::new(|| {
310 /// Build a list of mappings for the passed shared objects.
311 fn build_mappings(objects: &[SharedObject]) -> Vec<Mapping> {
312 let mut mappings = Vec::new();
313 for object in objects {
314 for segment in &object.loaded_segments {
315 // I have observed that `memory_offset` can be negative on some very old
316 // versions of Linux (e.g. CentOS 7), so use wrapping add here.
317 let memory_start = object.base_address.wrapping_add(segment.memory_offset);
318 mappings.push(Mapping {
319 memory_start,
320 memory_end: memory_start + segment.memory_size,
321 memory_offset: segment.memory_offset,
322 file_offset: segment.file_offset,
323 pathname: object.path_name.clone(),
324 build_id: object.build_id.clone(),
325 });
326 }
327 }
328 mappings
329 }
330
331 // SAFETY: We are on Linux
332 match unsafe { enabled::collect_shared_objects() } {
333 Ok(objects) => Some(build_mappings(&objects)),
334 Err(err) => {
335 error!("build ID fetching failed: {err}");
336 None
337 }
338 }
339});
340
341#[cfg(not(target_os = "linux"))]
342pub static MAPPINGS: Lazy<Option<Vec<Mapping>>> = Lazy::new(|| {
343 error!("build ID fetching is only supported on Linux");
344 None
345});
346
347/// Information about a shared object loaded into the current process.
348#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
349pub struct SharedObject {
350 /// The address at which the object is loaded.
351 pub base_address: usize,
352 /// The path of that file the object was loaded from.
353 pub path_name: PathBuf,
354 /// The build ID of the object, if found.
355 pub build_id: Option<BuildId>,
356 /// Loaded segments of the object.
357 pub loaded_segments: Vec<LoadedSegment>,
358}
359
360/// A segment of a shared object that's loaded into memory.
361#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
362pub struct LoadedSegment {
363 /// Offset of the segment in the source file.
364 pub file_offset: u64,
365 /// Offset to the `SharedObject`'s `base_address`.
366 pub memory_offset: usize,
367 /// Size of the segment in memory.
368 pub memory_size: usize,
369}