linux_memutils/
iomem.rs

1// SPDX-FileCopyrightText: Benedikt Vollmerhaus <benedikt@vollmerhaus.org>
2// SPDX-License-Identifier: MIT
3/*!
4Parsing of the physical memory map provided by `/proc/iomem`.
5
6The `/proc/iomem` file exposes the kernel's resource tree and thus a map of
7physical memory to user space, making it very useful for gracefully reading
8specific regions of memory from [`/dev/mem`].
9
10# Background
11
12The Linux kernel maintains a _resource tree_ with the memory address ranges
13allocated to every resource (RAM, devices, and so on).
14
15The first additions to this tree are made during early boot when the system
16firmware supplies its initial memory map to the kernel via [E820] (BIOS) or
17[GetMemoryMap()] (UEFI). The kernel will practically always modify this map
18further (based on known quirks or custom `memmap` overrides, for instance)
19before registering its specified memory regions in the tree.
20
21Additional address ranges are allocated for device [MMIO], so the tree will
22contain not just entries for the above memory map (with [ACPI Address Range
23Types] such as `Reserved`) but also more arbitrarily named devices (such as
24`IOAPIC 0` or `PCI Bus <ID>`).
25
26## Excerpt of `/proc/iomem`
27
28> Notice how the address range names include both human-readable ACPI types
29> and MMIO devices.
30
31```text
3200000000-00000fff : Reserved            // Real-Mode Address Space (< 1 MiB)
3300001000-0009efff : System RAM
340009f000-0009ffff : Reserved
35000e0000-000fffff : Reserved
36  000a0000-000effff : PCI Bus 0000:00
37  000f0000-000fffff : System ROM
3800100000-09bfffff : System RAM          // Extended Memory (> 1 MiB)
39[~]
40cad7e000-cbd7dfff : ACPI Non-volatile Storage
41  cbc37000-cbc37fff : USBC000:00
42cbd7e000-cbdfdfff : ACPI Tables
43[~]
44fc000000-fdffffff : PCI Bus 0000:00
45  [~ Other devices on the PCIe bus ~]
46  fd900000-fd9fffff : PCI Bus 0000:01
47    fd900000-fd903fff : 0000:01:00.0
48      fd900000-fd903fff : nvme
49  fdf00000-fdf7ffff : amd_iommu
50feb00000-feb00007 : SB800 TCO
51fec00000-fec003ff : IOAPIC 0
52[~]
53100000000-72e2fffff : System RAM
54  38a400000-38b7fffff : Kernel code
55  38b800000-38c532fff : Kernel rodata
56  38c600000-38c88cf7f : Kernel data
57  38d20e000-38d5fffff : Kernel bss
58```
59
60[`/dev/mem`]: https://man7.org/linux/man-pages/man4/mem.4.html
61[E820]: https://uefi.org/specs/ACPI/6.5/15_System_Address_Map_Interfaces.html#int-15h-e820h-query-system-address-map
62[GetMemoryMap()]: https://uefi.org/specs/ACPI/6.5/15_System_Address_Map_Interfaces.html#uefi-getmemorymap-boot-services-function
63[MMIO]: https://docs.kernel.org/driver-api/device-io.html#memory-mapped-io
64[ACPI Address Range Types]: https://uefi.org/specs/ACPI/6.5/15_System_Address_Map_Interfaces.html#address-range-types
65*/
66use std::convert::Infallible;
67use std::fmt::{self, Debug, Display, Formatter};
68use std::str::FromStr;
69use std::sync::LazyLock;
70use std::{fs, io};
71
72use regex_lite::Regex;
73use thiserror::Error;
74
75/// A region in physical memory as indicated in the memory map.
76#[derive(Debug, PartialEq)]
77pub struct MemoryRegion {
78    pub start_address: usize,
79    pub end_address: usize,
80    pub region_type: MemoryRegionType,
81}
82
83impl MemoryRegion {
84    /// Return the length of this memory region in bytes.
85    #[must_use]
86    pub fn size(&self) -> usize {
87        self.end_address - self.start_address
88    }
89}
90
91impl Display for MemoryRegion {
92    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
93        write!(
94            f,
95            "[{:#018x}-{:#018x}] ({:?})",
96            self.start_address, self.end_address, self.region_type
97        )
98    }
99}
100
101/// The types of memory address ranges distinguished by the kernel.
102///
103/// These largely correspond to the _ACPI Address Range Types_ as defined in
104/// the kernel's [`e820_type`] enum (with some minor changes).
105///
106/// UEFI provides even more fine-grained _Memory Types_, but the kernel maps
107/// those to the basic ACPI types in [`do_add_efi_memmap`] (according to the
108/// specified [UEFI–ACPI Mapping]).
109///
110/// [`e820_type`]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/include/asm/e820/types.h?h=v6.12#n10
111/// [`do_add_efi_memmap`]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/platform/efi/efi.c?h=v6.12#n121
112/// [UEFI–ACPI Mapping]: https://uefi.org/specs/ACPI/6.5/15_System_Address_Map_Interfaces.html#uefi-memory-types-and-mapping-to-acpi-address-range-types
113#[derive(Debug, PartialEq)]
114pub enum MemoryRegionType {
115    Usable,
116    Reserved,
117    SoftReserved,
118    AcpiData,
119    AcpiNvs,
120    Unusable,
121    Persistent,
122    /// An E820 type not known to the kernel.
123    Unknown,
124
125    /// Any type not part of the ACPI specification, such as an MMIO range.
126    NonAcpi(String),
127}
128
129impl FromStr for MemoryRegionType {
130    type Err = Infallible;
131
132    /// Return the enum variant for a given address range type as printed
133    /// by the kernel.
134    ///
135    /// This mapping is derived from the [`e820_type_to_string`] function.
136    ///
137    /// [`e820_type_to_string`]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/kernel/e820.c?h=v6.12#n1063
138    fn from_str(s: &str) -> Result<Self, Self::Err> {
139        match s {
140            "System RAM" => Ok(Self::Usable),
141            "Reserved" => Ok(Self::Reserved),
142            "Soft Reserved" => Ok(Self::SoftReserved),
143            "ACPI Tables" => Ok(Self::AcpiData),
144            "ACPI Non-volatile Storage" => Ok(Self::AcpiNvs),
145            "Unusable memory" => Ok(Self::Unusable),
146            "Persistent Memory" | "Persistent Memory (legacy)" => Ok(Self::Persistent),
147            "Unknown E820 type" => Ok(Self::Unknown),
148
149            _ => Ok(Self::NonAcpi(s.into())),
150        }
151    }
152}
153
154impl Display for MemoryRegionType {
155    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
156        write!(f, "{self:?}")
157    }
158}
159
160#[derive(Error, Debug, PartialEq)]
161pub enum ParseError {
162    #[error("line of memory map is not in iomem format: '{0}'")]
163    InvalidFormat(String),
164    #[error("line of memory map has invalid start address: {0}")]
165    InvalidStartAddress(String),
166    #[error("line of memory map has invalid end address: {0}")]
167    InvalidEndAddress(String),
168}
169
170/// Directly read and parse `/proc/iomem` to a vector of [`MemoryRegion`]s.
171///
172/// # Errors
173///
174/// This function will return an error if `/proc/iomem` could not be read.
175///
176/// # Panics
177///
178/// This function panics if the file contains unexpected lines or ones with
179/// invalid memory addresses. As this should **never** happen on a standard
180/// Linux kernel, it may indicate a somewhat corrupt system state.
181#[allow(clippy::module_name_repetitions)]
182pub fn parse_proc_iomem() -> io::Result<Vec<MemoryRegion>> {
183    let contents = fs::read_to_string("/proc/iomem")?;
184    let memory_regions =
185        parse_iomem_map(&contents).expect("/proc/iomem should contain only valid lines");
186    Ok(memory_regions)
187}
188
189/// Parse the given `iomem`-style memory map to a vector of [`MemoryRegion`]s.
190///
191/// # Errors
192///
193/// This function will return an error if the memory map could not be parsed.
194pub fn parse_iomem_map(content: &str) -> Result<Vec<MemoryRegion>, ParseError> {
195    let mut memory_regions = Vec::new();
196
197    for line in content.lines().filter(|l| !l.is_empty()) {
198        // This parsing cannot yet represent the resource hierarchy, so
199        // ignore sub-resources rather than potentially misrepresenting
200        // the memory map (e.g. for a "Reserved" within an MMIO region)
201        if line.starts_with(' ') {
202            continue;
203        }
204
205        let region = parse_iomem_map_line(line)?;
206        memory_regions.push(region);
207    }
208
209    Ok(memory_regions)
210}
211
212/// A regex for lines of an `iomem`-style memory map.
213static IOMEM_MAP_LINE_REGEX: LazyLock<Regex> =
214    LazyLock::new(|| Regex::new(r"(\w+)-(\w+) : (.+)").unwrap());
215
216/// Parse the given line of an `iomem`-style memory map to a [`MemoryRegion`].
217fn parse_iomem_map_line(line: &str) -> Result<MemoryRegion, ParseError> {
218    let Some((_full, [start_address, end_address, region_type])) = IOMEM_MAP_LINE_REGEX
219        .captures(line)
220        .map(|caps| caps.extract())
221    else {
222        return Err(ParseError::InvalidFormat(line.into()));
223    };
224
225    let Ok(start_address) = usize::from_str_radix(start_address, 16) else {
226        return Err(ParseError::InvalidStartAddress(start_address.into()));
227    };
228    let Ok(end_address) = usize::from_str_radix(end_address, 16) else {
229        return Err(ParseError::InvalidEndAddress(end_address.into()));
230    };
231
232    let memory_region = MemoryRegion {
233        start_address,
234        end_address,
235        region_type: MemoryRegionType::from_str(region_type)
236            .expect("undefined names should be mapped to a `NonAcpi` variant"),
237    };
238
239    Ok(memory_region)
240}
241
242#[cfg(test)]
243#[allow(clippy::unreadable_literal)]
244mod tests {
245    use super::*;
246    use indoc::indoc;
247
248    mod memory_region {
249        use super::*;
250
251        #[test]
252        fn size_returns_correct_size_of_region_in_bytes() {
253            let region = MemoryRegion {
254                start_address: 0x00100000,
255                end_address: 0x09bfffff,
256                region_type: MemoryRegionType::Usable,
257            };
258            assert_eq!(region.size(), 162_529_279);
259        }
260
261        #[test]
262        fn is_formatted_as_expected_human_readable_string() {
263            let region = MemoryRegion {
264                start_address: 0x00100000,
265                end_address: 0x09bfffff,
266                region_type: MemoryRegionType::Usable,
267            };
268            assert_eq!(
269                region.to_string(),
270                "[0x0000000000100000-0x0000000009bfffff] (Usable)"
271            );
272        }
273    }
274
275    mod parse_iomem_map {
276        use super::*;
277
278        /// A dummy `iomem`-style memory map comprising all ACPI types.
279        const PROC_IOMEM_MAP: &str = indoc! {"
280            00000080-000000ff : System RAM
281            00000100-000001ff : Reserved
282              00000180-000001bf : PCI Bus 0000:00
283              000001c0-000001ff : System ROM
284            00000200-000003ff : Soft Reserved
285            00000400-000007ff : ACPI Tables
286            00000800-00000fff : ACPI Non-volatile Storage
287            00001000-00001fff : Unusable memory
288            00002000-00003fff : Persistent Memory
289            00004000-00007fff : Persistent Memory (legacy)
290            00008000-0000ffff : Unknown E820 type
291            00010000-0001ffff : PCI ECAM 0000
292              00010000-0001ffff : Reserved
293                00010000-0001ffff : pnp 00:00
294        "};
295
296        #[test]
297        fn returns_vector_of_expected_memory_regions() {
298            let regions = parse_iomem_map(PROC_IOMEM_MAP);
299            let expected_regions = vec![
300                (0x000080, MemoryRegionType::Usable),
301                (0x000100, MemoryRegionType::Reserved),
302                (0x000200, MemoryRegionType::SoftReserved),
303                (0x000400, MemoryRegionType::AcpiData),
304                (0x000800, MemoryRegionType::AcpiNvs),
305                (0x001000, MemoryRegionType::Unusable),
306                (0x002000, MemoryRegionType::Persistent),
307                (0x004000, MemoryRegionType::Persistent),
308                (0x008000, MemoryRegionType::Unknown),
309                (0x010000, MemoryRegionType::NonAcpi("PCI ECAM 0000".into())),
310            ]
311            .into_iter()
312            .map(|(start_address, region_type)| MemoryRegion {
313                start_address,
314                end_address: start_address * 2 - 1,
315                region_type,
316            })
317            .collect();
318
319            assert_eq!(regions, Ok(expected_regions));
320        }
321    }
322
323    mod parse_iomem_map_line {
324        use super::*;
325
326        #[test]
327        fn returns_expected_memory_region_for_line_with_acpi_type() {
328            let result = parse_iomem_map_line("00100000-09bfffff : System RAM");
329            assert_eq!(
330                result,
331                Ok(MemoryRegion {
332                    start_address: 0x00100000,
333                    end_address: 0x09bfffff,
334                    region_type: MemoryRegionType::Usable,
335                })
336            );
337        }
338
339        #[test]
340        fn returns_expected_memory_region_for_line_with_non_acpi_type() {
341            let result = parse_iomem_map_line("d0000000-f7ffffff : PCI Bus 0000:00");
342            assert_eq!(
343                result,
344                Ok(MemoryRegion {
345                    start_address: 0xd0000000,
346                    end_address: 0xf7ffffff,
347                    region_type: MemoryRegionType::NonAcpi("PCI Bus 0000:00".into()),
348                })
349            );
350        }
351
352        #[test]
353        fn returns_invalid_format_error_if_line_is_not_in_iomem_format() {
354            let invalid_line = "This is not an iomem memory map line.";
355            assert_eq!(
356                parse_iomem_map_line(invalid_line),
357                Err(ParseError::InvalidFormat(invalid_line.into()))
358            );
359        }
360
361        #[test]
362        fn returns_invalid_start_address_error_if_start_address_is_invalid() {
363            assert_eq!(
364                parse_iomem_map_line("0000yyyy-00000fff : Reserved"),
365                Err(ParseError::InvalidStartAddress("0000yyyy".into()))
366            );
367        }
368
369        #[test]
370        fn returns_invalid_end_address_error_if_end_address_is_invalid() {
371            assert_eq!(
372                parse_iomem_map_line("00000000-0000zzzz : Reserved"),
373                Err(ParseError::InvalidEndAddress("0000zzzz".into()))
374            );
375        }
376    }
377}