blazesym/normalize/
ioctl.rs

1use std::borrow::Cow;
2use std::ffi::c_int;
3use std::fs::File;
4use std::io;
5use std::mem::size_of;
6use std::mem::size_of_val;
7use std::mem::MaybeUninit;
8
9use libc::ENOENT;
10use libc::ENOTTY;
11
12use crate::maps::MapsEntry;
13use crate::maps::Perm;
14use crate::Addr;
15use crate::Error;
16use crate::ErrorExt as _;
17use crate::ErrorKind;
18use crate::Pid;
19use crate::Result;
20
21// From uapi/linux/fs.h
22const PROCMAP_QUERY: usize = 0xC0686611; // _IOWR(PROCFS_IOCTL_MAGIC, 17, struct procmap_query)
23
24#[allow(non_camel_case_types)]
25type procmap_query_flags = c_int;
26
27const PROCMAP_QUERY_VMA_READABLE: procmap_query_flags = 0x01;
28const PROCMAP_QUERY_VMA_WRITABLE: procmap_query_flags = 0x02;
29const PROCMAP_QUERY_VMA_EXECUTABLE: procmap_query_flags = 0x04;
30#[cfg(test)]
31const PROCMAP_QUERY_VMA_SHARED: procmap_query_flags = 0x08;
32const PROCMAP_QUERY_COVERING_OR_NEXT_VMA: procmap_query_flags = 0x10;
33
34
35#[allow(non_camel_case_types)]
36#[repr(C)]
37#[derive(Clone, Default)]
38struct procmap_query {
39    /* Query struct size, for backwards/forward compatibility */
40    size: u64,
41    /* Query flags, a combination of procmap_query_flags values. Defines
42     * query filtering and behavior, see procmap_query_flags.
43     */
44    query_flags: u64, /* in */
45    /* Query address. By default, VMA that covers this address will be
46     * looked up. PROCMAP_QUERY_* flags above modify this default
47     * behavior further.
48     */
49    query_addr: u64, /* in */
50    /* VMA starting (inclusive) and ending (exclusive) address, if VMA is found. */
51    vma_start: u64, /* out */
52    vma_end: u64,   /* out */
53    /* VMA permissions flags. A combination of PROCMAP_QUERY_VMA_* flags. */
54    vma_flags: u64, /* out */
55    /* VMA backing page size granularity. */
56    vma_page_size: u64, /* out */
57    /* VMA file offset. If VMA has file backing, this specifies offset
58     * within the file that VMA's start address corresponds to. Is set
59     * to zero if VMA has no backing file.
60     */
61    vma_offset: u64, /* out */
62    /* Backing file's inode number, or zero, if VMA has no backing file. */
63    inode: u64, /* out */
64    /* Backing file's device major/minor number, or zero, if VMA has no backing file. */
65    dev_major: u32, /* out */
66    dev_minor: u32, /* out */
67    /* If set to non-zero value, signals the request to return VMA name
68     * (i.e., VMA's backing file's absolute path, with " (deleted)" suffix
69     * appended, if file was unlinked from FS) for matched VMA. VMA name
70     * can also be some special name (e.g., "[heap]", "[stack]") or could
71     * be even user-supplied with prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME).
72     *
73     * Kernel will set this field to zero, if VMA has no associated name.
74     * Otherwise kernel will return actual amount of bytes filled in
75     * user-supplied buffer (see vma_name_addr field below), including the
76     * terminating zero.
77     *
78     * If VMA name is longer that user-supplied maximum buffer size,
79     * -E2BIG error is returned.
80     *
81     * If this field is set to non-zero value, vma_name_addr should point
82     * to valid user space memory buffer of at least vma_name_size bytes.
83     * If set to zero, vma_name_addr should be set to zero as well
84     */
85    vma_name_size: u32, /* in/out */
86    /* If set to non-zero value, signals the request to extract and return
87     * VMA's backing file's build ID, if the backing file is an ELF file
88     * and it contains embedded build ID.
89     *
90     * Kernel will set this field to zero, if VMA has no backing file,
91     * backing file is not an ELF file, or ELF file has no build ID
92     * embedded.
93     *
94     * Build ID is a binary value (not a string). Kernel will set
95     * build_id_size field to exact number of bytes used for build ID.
96     * If build ID is requested and present, but needs more bytes than
97     * user-supplied maximum buffer size (see build_id_addr field below),
98     * -E2BIG error will be returned.
99     *
100     * If this field is set to non-zero value, build_id_addr should point
101     * to valid user space memory buffer of at least build_id_size bytes.
102     * If set to zero, build_id_addr should be set to zero as well
103     */
104    build_id_size: u32, /* in/out */
105    /* User-supplied address of a buffer of at least vma_name_size bytes
106     * for kernel to fill with matched VMA's name (see vma_name_size field
107     * description above for details).
108     *
109     * Should be set to zero if VMA name should not be returned.
110     */
111    vma_name_addr: u64, /* in */
112    /* User-supplied address of a buffer of at least build_id_size bytes
113     * for kernel to fill with matched VMA's ELF build ID, if available
114     * (see build_id_size field description above for details).
115     *
116     * Should be set to zero if build ID should not be returned.
117     */
118    build_id_addr: u64, /* in */
119}
120
121
122fn vma_flags_to_perm(vma_flags: u64) -> Perm {
123    let vma_flags = vma_flags as i32;
124    let mut perm = Perm::default();
125
126    if vma_flags & PROCMAP_QUERY_VMA_READABLE != 0 {
127        perm |= Perm::R;
128    }
129    if vma_flags & PROCMAP_QUERY_VMA_WRITABLE != 0 {
130        perm |= Perm::W;
131    }
132    if vma_flags & PROCMAP_QUERY_VMA_EXECUTABLE != 0 {
133        perm |= Perm::X;
134    }
135    perm
136}
137
138
139/// The caller is responsible for checking that the returned `MapsEntry`
140/// actually covers the provided address. If it does not, it represents
141/// the next known entry.
142#[cfg(linux)]
143pub(crate) fn query_procmap(
144    file: &File,
145    pid: Pid,
146    addr: Addr,
147    build_id: bool,
148) -> Result<Option<MapsEntry>> {
149    use libc::ioctl;
150    use std::os::unix::io::AsFd as _;
151    use std::os::unix::io::AsRawFd as _;
152
153    use crate::maps::parse_path_name;
154
155    let mut path_buf = MaybeUninit::<[u8; 4096]>::uninit();
156    let mut build_id_buf = MaybeUninit::<[u8; 56]>::uninit();
157    let mut query = procmap_query {
158        size: size_of::<procmap_query>() as _,
159        query_flags: (PROCMAP_QUERY_COVERING_OR_NEXT_VMA
160            // NB: Keep filter flags roughly in sync with
161            //     `filter_relevant` function. Note that because the
162            //     ioctl ANDs the conditions, we can't mirror exactly
163            //     what `filter_relevant` does (r OR x). So we just
164            //     filter for readable, pretty much all things
165            //     executable will be readable anyway.
166            | PROCMAP_QUERY_VMA_READABLE) as _,
167        query_addr: addr,
168        vma_name_addr: path_buf.as_mut_ptr() as _,
169        vma_name_size: size_of_val(&path_buf) as _,
170        build_id_addr: if build_id {
171            build_id_buf.as_mut_ptr() as _
172        } else {
173            0
174        },
175        build_id_size: if build_id {
176            size_of_val(&build_id_buf) as _
177        } else {
178            0
179        },
180        ..Default::default()
181    };
182
183    // SAFETY: The `procmap_query` pointer is valid because it comes
184    //         from a reference.
185    let rc = unsafe {
186        ioctl(
187            file.as_fd().as_raw_fd(),
188            PROCMAP_QUERY as _,
189            &mut query as *mut procmap_query,
190        )
191    };
192    if rc < 0 {
193        let err = io::Error::last_os_error();
194        match err.raw_os_error() {
195            Some(e) if e == ENOTTY => {
196                return Err(Error::with_unsupported("PROCMAP_QUERY is not supported"))
197            }
198            Some(e) if e == ENOENT => return Ok(None),
199            _ => (),
200        }
201        return Err(Error::from(err))
202    }
203
204    // SAFETY: The kernel will have set the member to a valid value.
205    let path_buf = unsafe { path_buf.assume_init_ref() };
206    let path = &path_buf[0..query.vma_name_size.saturating_sub(1) as usize];
207    let path_name = parse_path_name(path, pid, query.vma_start, query.vma_end)?;
208
209    let mut entry = MapsEntry {
210        range: query.vma_start..query.vma_end,
211        perm: vma_flags_to_perm(query.vma_flags),
212        offset: query.vma_offset,
213        path_name,
214        build_id: None,
215    };
216
217    if build_id && query.build_id_size > 0 {
218        // SAFETY: The kernel will have set the member to a valid value
219        //         because we asked it to.
220        let build_id_buf = unsafe { build_id_buf.assume_init_ref() };
221        let build_id = build_id_buf[0..query.build_id_size as usize].to_vec();
222        entry.build_id = Some(Cow::Owned(build_id));
223    }
224    Ok(Some(entry))
225}
226
227#[cfg(not(linux))]
228pub(crate) fn query_procmap(
229    _file: &File,
230    _pid: Pid,
231    _addr: Addr,
232    _build_id: bool,
233) -> Result<Option<MapsEntry>> {
234    unimplemented!()
235}
236
237
238/// Check whether the `PROCMAP_QUERY` ioctl is supported by the system.
239pub fn is_procmap_query_supported() -> Result<bool> {
240    let pid = Pid::Slf;
241    let path = format!("/proc/{pid}/maps");
242    let file = File::open(&path).with_context(|| format!("failed to open `{path}` for reading"))?;
243    let addr = 0;
244    let build_ids = false;
245
246    let result = query_procmap(&file, pid, addr, build_ids);
247    match result {
248        Ok(..) => Ok(true),
249        Err(err) if err.kind() == ErrorKind::Unsupported => Ok(false),
250        Err(err) => Err(err),
251    }
252}
253
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    use std::env::current_exe;
260    use std::fs::File;
261    use std::thread::sleep;
262    use std::time::Duration;
263
264    use crate::maps;
265
266    use super::super::buildid::read_elf_build_id;
267
268
269    /// Check that we can convert VMA flags into a [`Perm`].
270    #[test]
271    fn vma_flags_conversion() {
272        let flags = 0;
273        assert_eq!(vma_flags_to_perm(flags as _), Perm::default());
274
275        let flags = PROCMAP_QUERY_VMA_READABLE;
276        assert_eq!(vma_flags_to_perm(flags as _), Perm::R);
277
278        let flags = PROCMAP_QUERY_VMA_READABLE | PROCMAP_QUERY_VMA_WRITABLE;
279        assert_eq!(vma_flags_to_perm(flags as _), Perm::RW);
280
281        let flags = PROCMAP_QUERY_VMA_EXECUTABLE | PROCMAP_QUERY_VMA_SHARED;
282        assert_eq!(vma_flags_to_perm(flags as _), Perm::X);
283
284        let flags = PROCMAP_QUERY_COVERING_OR_NEXT_VMA | PROCMAP_QUERY_VMA_EXECUTABLE;
285        assert_eq!(vma_flags_to_perm(flags as _), Perm::X);
286    }
287
288    /// Test that we can check whether the `PROCMAP_QUERY` ioctl is
289    /// supported.
290    #[test]
291    fn procmap_query_supported() {
292        let _supported = is_procmap_query_supported().unwrap();
293    }
294
295    /// Check that we can query an invalid VMA region using the
296    /// `PROCMAP_QUERY` ioctl.
297    #[test]
298    #[ignore = "test requires PROCMAP_QUERY ioctl kernel support"]
299    fn invalid_vma_querying_ioctl() {
300        let pid = Pid::Slf;
301        let path = format!("/proc/{pid}/maps");
302        let file = File::open(path).unwrap();
303        let addr = 0xfffffffff000;
304        let result = query_procmap(&file, pid, addr, false).unwrap();
305        assert_eq!(result, None);
306    }
307
308    /// Check that we can query a valid VMA region using the
309    /// `PROCMAP_QUERY` ioctl.
310    #[test]
311    #[ignore = "test requires PROCMAP_QUERY ioctl kernel support"]
312    fn valid_vma_querying_ioctl() {
313        fn test(build_ids: bool) {
314            let pid = Pid::Slf;
315            let path = format!("/proc/{pid}/maps");
316            let file = File::open(path).unwrap();
317            let addr = valid_vma_querying_ioctl as Addr;
318            let entry = query_procmap(&file, pid, addr, build_ids).unwrap().unwrap();
319            assert!(
320                entry.range.contains(&addr),
321                "{:#x?} : {addr:#x}",
322                entry.range
323            );
324            // The region should be readable (r---) and executable (--x-), as
325            // it's code.
326            assert_eq!(entry.perm, Perm::RX);
327            let exe = current_exe().unwrap();
328            assert_eq!(
329                entry
330                    .path_name
331                    .as_ref()
332                    .unwrap()
333                    .as_path()
334                    .unwrap()
335                    .symbolic_path,
336                exe
337            );
338
339            if build_ids {
340                let build_id = read_elf_build_id(&exe).unwrap();
341                assert_eq!(entry.build_id, build_id);
342            } else {
343                assert_eq!(entry.build_id, None);
344            }
345        }
346
347        test(false);
348        test(true);
349    }
350
351    /// Check that we see the same number of VMAs reported when using
352    /// `/proc/self/maps` parsing versus the `PROCMAP_QUERY` ioctl.
353    #[test]
354    #[ignore = "test requires PROCMAP_QUERY ioctl kernel support"]
355    fn vma_comparison() {
356        fn parse_maps(pid: Pid, from_text: &mut Vec<MapsEntry>) {
357            let () = from_text.clear();
358
359            let it = maps::parse_filtered(pid).unwrap();
360            for result in it {
361                let vma = result.unwrap();
362                let () = from_text.push(vma);
363            }
364        }
365
366        fn parse_ioctl(pid: Pid, from_ioctl: &mut Vec<MapsEntry>) {
367            let () = from_ioctl.clear();
368
369            let path = format!("/proc/{pid}/maps");
370            let file = File::open(path).unwrap();
371            let build_ids = false;
372            let mut next_addr = 0;
373            while let Some(entry) = query_procmap(&file, pid, next_addr, build_ids).unwrap() {
374                next_addr = entry.range.end;
375                if maps::filter_relevant(&entry) {
376                    let () = from_ioctl.push(entry);
377                }
378            }
379        }
380
381        let pid = Pid::Slf;
382        let mut from_text = Vec::new();
383        let mut from_ioctl = Vec::new();
384
385        // The gathering itself has the potential to affect VMAs (e.g.,
386        // if the heap has to grow), meaning that we could see
387        // mismatches for good reason. So we give it a few attempts.
388        for _ in 0..5 {
389            let () = parse_maps(pid, &mut from_text);
390            let () = parse_ioctl(pid, &mut from_ioctl);
391
392            if from_text == from_ioctl {
393                break
394            }
395
396            sleep(Duration::from_millis(500));
397        }
398
399        assert_eq!(from_text, from_ioctl, "{from_text:#x?}\n{from_ioctl:#x?}");
400    }
401}