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 regex_lite::Regex;
67use std::convert::Infallible;
68use std::fmt::{Debug, Display, Formatter};
69use std::str::FromStr;
70use std::sync::LazyLock;
71use std::{fmt, fs, io};
72use thiserror::Error;
73
74/// A region in physical memory as indicated in the memory map.
75#[derive(Debug, PartialEq)]
76pub struct MemoryRegion {
77    pub start_address: usize,
78    pub end_address: usize,
79    pub region_type: MemoryRegionType,
80}
81
82impl MemoryRegion {
83    /// Return the length of this memory region in bytes.
84    #[must_use]
85    pub fn size(&self) -> usize {
86        self.end_address - self.start_address
87    }
88}
89
90impl Display for MemoryRegion {
91    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
92        write!(
93            f,
94            "[{:#018x}-{:#018x}] ({:?})",
95            self.start_address, self.end_address, self.region_type
96        )
97    }
98}
99
100/// The types of memory address ranges distinguished by the kernel.
101///
102/// These largely correspond to the _ACPI Address Range Types_ as defined in
103/// the kernel's [`e820_type`] enum (with some minor changes).
104///
105/// UEFI provides even more fine-grained _Memory Types_, but the kernel maps
106/// those to the basic ACPI types in [`do_add_efi_memmap`] (according to the
107/// specified [UEFI–ACPI Mapping]).
108///
109/// [`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
110/// [`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
111/// [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
112#[derive(Debug, PartialEq)]
113pub enum MemoryRegionType {
114    Usable,
115    Reserved,
116    SoftReserved,
117    AcpiData,
118    AcpiNvs,
119    Unusable,
120    Persistent,
121    /// An E820 type not known to the kernel.
122    Unknown,
123
124    /// Any type not part of the ACPI specification, such as an MMIO range.
125    NonAcpi(String),
126}
127
128impl FromStr for MemoryRegionType {
129    type Err = Infallible;
130
131    /// Return the enum variant for a given address range type as printed
132    /// by the kernel.
133    ///
134    /// This mapping is derived from the [`e820_type_to_string`] function.
135    ///
136    /// [`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
137    fn from_str(s: &str) -> Result<Self, Self::Err> {
138        match s {
139            "System RAM" => Ok(Self::Usable),
140            "Reserved" => Ok(Self::Reserved),
141            "Soft Reserved" => Ok(Self::SoftReserved),
142            "ACPI Tables" => Ok(Self::AcpiData),
143            "ACPI Non-volatile Storage" => Ok(Self::AcpiNvs),
144            "Unusable memory" => Ok(Self::Unusable),
145            "Persistent Memory" | "Persistent Memory (legacy)" => Ok(Self::Persistent),
146            "Unknown E820 type" => Ok(Self::Unknown),
147
148            _ => Ok(Self::NonAcpi(s.into())),
149        }
150    }
151}
152
153impl Display for MemoryRegionType {
154    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
155        write!(f, "{self:?}")
156    }
157}
158
159#[derive(Error, Debug, PartialEq)]
160pub enum ParseError {
161    #[error("line of memory map is not in iomem format: '{0}'")]
162    InvalidFormat(String),
163    #[error("line of memory map has invalid start address: {0}")]
164    InvalidStartAddress(String),
165    #[error("line of memory map has invalid end address: {0}")]
166    InvalidEndAddress(String),
167}
168
169/// Directly read and parse `/proc/iomem` to a vector of [`MemoryRegion`]s.
170///
171/// # Errors
172///
173/// This function will return an error if `/proc/iomem` could not be read.
174///
175/// # Panics
176///
177/// This function panics if the file contains unexpected lines or ones with
178/// invalid memory addresses. As this should **never** happen on a standard
179/// Linux kernel, it may indicate a somewhat corrupt system state.
180#[allow(clippy::module_name_repetitions)]
181pub fn parse_proc_iomem() -> io::Result<Vec<MemoryRegion>> {
182    let contents = fs::read_to_string("/proc/iomem")?;
183    let memory_regions =
184        parse_iomem_map(&contents).expect("/proc/iomem should contain only valid lines");
185    Ok(memory_regions)
186}
187
188/// Parse the given `iomem`-style memory map to a vector of [`MemoryRegion`]s.
189///
190/// # Errors
191///
192/// This function will return an error if the memory map could not be parsed.
193pub fn parse_iomem_map(content: &str) -> Result<Vec<MemoryRegion>, ParseError> {
194    let mut memory_regions = Vec::new();
195
196    for line in content.lines().filter(|l| !l.is_empty()) {
197        // This parsing cannot yet represent the resource hierarchy, so
198        // ignore sub-resources rather than potentially misrepresenting
199        // the memory map (e.g. for a "Reserved" within an MMIO region)
200        if line.starts_with(' ') {
201            continue;
202        }
203
204        let region = parse_iomem_map_line(line)?;
205        memory_regions.push(region);
206    }
207
208    Ok(memory_regions)
209}
210
211/// A regex for lines of an `iomem`-style memory map.
212static IOMEM_MAP_LINE_REGEX: LazyLock<Regex> =
213    LazyLock::new(|| Regex::new(r"(\w+)-(\w+) : (.+)").unwrap());
214
215/// Parse the given line of an `iomem`-style memory map to a [`MemoryRegion`].
216fn parse_iomem_map_line(line: &str) -> Result<MemoryRegion, ParseError> {
217    let Some((_full, [start_address, end_address, region_type])) = IOMEM_MAP_LINE_REGEX
218        .captures(line)
219        .map(|caps| caps.extract())
220    else {
221        return Err(ParseError::InvalidFormat(line.into()));
222    };
223
224    let Ok(start_address) = usize::from_str_radix(start_address, 16) else {
225        return Err(ParseError::InvalidStartAddress(start_address.into()));
226    };
227    let Ok(end_address) = usize::from_str_radix(end_address, 16) else {
228        return Err(ParseError::InvalidEndAddress(end_address.into()));
229    };
230
231    let memory_region = MemoryRegion {
232        start_address,
233        end_address,
234        region_type: MemoryRegionType::from_str(region_type)
235            .expect("undefined names should be mapped to a `NonAcpi` variant"),
236    };
237
238    Ok(memory_region)
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244    use indoc::indoc;
245
246    mod memory_region {
247        use super::*;
248
249        #[test]
250        fn size_returns_correct_size_of_region_in_bytes() {
251            let region = MemoryRegion {
252                start_address: 0x00100000,
253                end_address: 0x09bfffff,
254                region_type: MemoryRegionType::Usable,
255            };
256            assert_eq!(region.size(), 162529279)
257        }
258
259        #[test]
260        fn is_formatted_as_expected_human_readable_string() {
261            let region = MemoryRegion {
262                start_address: 0x00100000,
263                end_address: 0x09bfffff,
264                region_type: MemoryRegionType::Usable,
265            };
266            assert_eq!(
267                region.to_string(),
268                "[0x0000000000100000-0x0000000009bfffff] (Usable)"
269            )
270        }
271    }
272
273    mod parse_iomem_map {
274        use super::*;
275
276        /// A dummy `iomem`-style memory map comprising all ACPI types.
277        const PROC_IOMEM_MAP: &str = indoc! {"
278            00000080-000000ff : System RAM
279            00000100-000001ff : Reserved
280              00000180-000001bf : PCI Bus 0000:00
281              000001c0-000001ff : System ROM
282            00000200-000003ff : Soft Reserved
283            00000400-000007ff : ACPI Tables
284            00000800-00000fff : ACPI Non-volatile Storage
285            00001000-00001fff : Unusable memory
286            00002000-00003fff : Persistent Memory
287            00004000-00007fff : Persistent Memory (legacy)
288            00008000-0000ffff : Unknown E820 type
289            00010000-0001ffff : PCI ECAM 0000
290              00010000-0001ffff : Reserved
291                00010000-0001ffff : pnp 00:00
292        "};
293
294        #[test]
295        fn returns_vector_of_expected_memory_regions() {
296            let regions = parse_iomem_map(PROC_IOMEM_MAP);
297            let expected_regions = vec![
298                (0x000080, MemoryRegionType::Usable),
299                (0x000100, MemoryRegionType::Reserved),
300                (0x000200, MemoryRegionType::SoftReserved),
301                (0x000400, MemoryRegionType::AcpiData),
302                (0x000800, MemoryRegionType::AcpiNvs),
303                (0x001000, MemoryRegionType::Unusable),
304                (0x002000, MemoryRegionType::Persistent),
305                (0x004000, MemoryRegionType::Persistent),
306                (0x008000, MemoryRegionType::Unknown),
307                (0x010000, MemoryRegionType::NonAcpi("PCI ECAM 0000".into())),
308            ]
309            .into_iter()
310            .map(|(start_address, region_type)| MemoryRegion {
311                start_address,
312                end_address: start_address * 2 - 1,
313                region_type,
314            })
315            .collect();
316
317            assert_eq!(regions, Ok(expected_regions));
318        }
319    }
320
321    mod parse_iomem_map_line {
322        use super::*;
323
324        #[test]
325        fn returns_expected_memory_region_for_line_with_acpi_type() {
326            let result = parse_iomem_map_line("00100000-09bfffff : System RAM");
327            assert_eq!(
328                result,
329                Ok(MemoryRegion {
330                    start_address: 0x00100000,
331                    end_address: 0x09bfffff,
332                    region_type: MemoryRegionType::Usable,
333                })
334            );
335        }
336
337        #[test]
338        fn returns_expected_memory_region_for_line_with_non_acpi_type() {
339            let result = parse_iomem_map_line("d0000000-f7ffffff : PCI Bus 0000:00");
340            assert_eq!(
341                result,
342                Ok(MemoryRegion {
343                    start_address: 0xd0000000,
344                    end_address: 0xf7ffffff,
345                    region_type: MemoryRegionType::NonAcpi("PCI Bus 0000:00".into()),
346                })
347            );
348        }
349
350        #[test]
351        fn returns_invalid_format_error_if_line_not_in_iomem_format() {
352            let invalid_line = "This is not an iomem memory map line.";
353            assert_eq!(
354                parse_iomem_map_line(invalid_line),
355                Err(ParseError::InvalidFormat(invalid_line.into()))
356            );
357        }
358
359        #[test]
360        fn returns_invalid_start_address_error_if_start_address_invalid() {
361            assert_eq!(
362                parse_iomem_map_line("0000yyyy-00000fff : Reserved"),
363                Err(ParseError::InvalidStartAddress("0000yyyy".into()))
364            );
365        }
366
367        #[test]
368        fn returns_invalid_end_address_error_if_end_address_invalid() {
369            assert_eq!(
370                parse_iomem_map_line("00000000-0000zzzz : Reserved"),
371                Err(ParseError::InvalidEndAddress("0000zzzz".into()))
372            );
373        }
374    }
375}