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}