linux_memutils/
agesa.rs

1// SPDX-FileCopyrightText: Benedikt Vollmerhaus <benedikt@vollmerhaus.org>
2// SPDX-License-Identifier: MIT
3/*!
4Utilities for finding the [AGESA] version in physical memory (on AMD Zen).
5
6# AGESA
7
8AGESA is a procedure library by AMD embedded into the UEFI firmware of AMD
9platforms up to and including Zen 5. It performs _Platform Initialization_,
10so it is responsible for CPU startup, memory training, IO (including PCIe)
11configuration, and other aspects.
12
13Because of AGESA's importance for stability _and security_, one may want to
14inspect its version, ideally from user space on a running system. Alas, the
15Linux kernel does not provide a straightforward interface for this; however,
16AGESA's version marker is generally located somewhere in extended memory and
17can thus be obtained via a brute-force search as implemented by this module.
18
19<div class="warning">
20
21  [Per coreboot], there are two documented iterations of AGESA:
22
23  * **v5** (or [Arch2008]) for CPU families before Zen (< `17h`)
24  * **v9** for Zen and later (>= `17h`)
25
26  For now, this module only supports Zen platforms (AGESA **v9**).
27
28</div>
29
30[AGESA]: https://en.wikipedia.org/wiki/AGESA
31[Per coreboot]: https://doc.coreboot.org/soc/amd/family17h.html#introduction
32[Arch2008]: https://www.amd.com/content/dam/amd/en/documents/processor-tech-docs/specifications/44065_Arch2008.pdf
33*/
34use crate::iomem::{parse_proc_iomem, MemoryRegion, MemoryRegionType};
35use crate::reader::SkippingBufReader;
36use std::collections::VecDeque;
37use std::fs::File;
38use std::io;
39use std::io::{Read, Seek};
40use thiserror::Error;
41
42#[derive(PartialEq)]
43pub struct FoundVersion {
44    pub agesa_version: String,
45    pub absolute_address: usize,
46    pub surrounding_region: MemoryRegion,
47}
48
49impl FoundVersion {
50    /// Return this version's offset within its surrounding memory region.
51    #[must_use]
52    pub fn offset_in_region(&self) -> usize {
53        self.absolute_address - self.surrounding_region.start_address
54    }
55}
56
57#[derive(Error, Debug)]
58pub enum SearchError {
59    #[error("could not open /dev/mem")]
60    DevMemUnopenable(#[source] io::Error),
61
62    #[error("could not read iomem memory map")]
63    IomemUnreadable(#[source] io::Error),
64
65    #[error("could not read byte in physical memory")]
66    ByteUnreadable(#[source] io::Error),
67}
68
69pub type SearchResult = Result<Option<FoundVersion>, SearchError>;
70
71/// Search for the AGESA version within all `Reserved` memory regions.
72///
73/// # Errors
74///
75/// This function will return an error if no memory map could be obtained.
76/// It will also return errors for reading from physical memory according
77/// to [`find_agesa_version_in_memory_region`].
78pub fn find_agesa_version() -> SearchResult {
79    let possible_regions =
80        get_reserved_regions_in_extended_memory().map_err(SearchError::IomemUnreadable)?;
81
82    for region in possible_regions {
83        log::info!("Searching memory region: {}", region);
84        let search_result = find_agesa_version_in_memory_region(region)?;
85        if search_result.is_some() {
86            return Ok(search_result);
87        }
88    }
89
90    Ok(None)
91}
92
93/// Search for the AGESA version within the given memory region.
94///
95/// # Errors
96///
97/// This function will return an error if `/dev/mem` could not be opened
98/// or an unexpected read error occurred during the search.
99pub fn find_agesa_version_in_memory_region(region: MemoryRegion) -> SearchResult {
100    let file = File::open("/dev/mem").map_err(SearchError::DevMemUnopenable)?;
101    let buf_reader = SkippingBufReader::new(file, region.start_address, Some(region.end_address));
102
103    if let Some((agesa_version, absolute_address)) = find_agesa_version_in_reader(buf_reader)? {
104        return Ok(Some(FoundVersion {
105            agesa_version,
106            absolute_address,
107            surrounding_region: region,
108        }));
109    }
110
111    Ok(None)
112}
113
114/// The possible states of an ongoing search.
115enum SearchState {
116    Searching,
117    PrefixFound,
118    VersionStartFound,
119}
120
121/// The signature prefix indicating the start of an AGESA version in memory.
122const SEARCH_SEQUENCE: &[u8] = b"AGESA!V";
123
124fn find_agesa_version_in_reader<R: Read + Seek>(
125    mut buf_reader: SkippingBufReader<R>,
126) -> Result<Option<(String, usize)>, SearchError> {
127    let mut agesa_version = Vec::new();
128
129    let mut search_state = SearchState::Searching;
130    let mut search_window = VecDeque::with_capacity(SEARCH_SEQUENCE.len());
131
132    for b in (&mut buf_reader).bytes() {
133        let byte = b.map_err(SearchError::ByteUnreadable)?;
134
135        match search_state {
136            SearchState::Searching => {
137                if search_window.len() == SEARCH_SEQUENCE.len() {
138                    search_window.pop_front();
139                }
140                search_window.push_back(byte);
141
142                if search_window.eq(&SEARCH_SEQUENCE) {
143                    // AGESA!V9␀CezannePI-FP6 1.0.0.E␀
144                    //       ^
145                    search_state = SearchState::PrefixFound;
146                }
147            }
148            SearchState::PrefixFound => {
149                if byte == b'\0' {
150                    // AGESA!V9␀CezannePI-FP6 1.0.0.E␀
151                    //         ^
152                    search_state = SearchState::VersionStartFound;
153                }
154            }
155            SearchState::VersionStartFound if byte == b'\0' => {
156                // AGESA!V9␀CezannePI-FP6 1.0.0.E␀
157                //                               ^
158                let absolute_address = buf_reader.position_in_file() - agesa_version.len() - 1;
159                return Ok(Some((
160                    String::from_utf8(agesa_version).unwrap(),
161                    absolute_address,
162                )));
163            }
164            SearchState::VersionStartFound => {
165                agesa_version.push(byte);
166            }
167        }
168    }
169
170    Ok(None)
171}
172
173/// The start address of extended memory.
174const EXTENDED_MEM_START: usize = 0x0000_0000_0010_0000;
175
176/// Find and return all `Reserved` regions in [extended memory] (> 1 MiB).
177///
178/// Testing on a few machines showed that at least one `Reserved` region in
179/// extended memory reliably contains the AGESA version – usually the first
180/// one at that. Even `Usable` regions may occasionally include it, but the
181/// initial (generally small) `Reserved` regions are much faster to search.
182///
183/// [extended memory]: https://wiki.osdev.org/Memory_Map_(x86)#Extended_Memory_(%3E_1_MiB)
184///
185/// # Errors
186///
187/// This function will return an error if `/proc/iomem` could not be read.
188pub fn get_reserved_regions_in_extended_memory() -> io::Result<Vec<MemoryRegion>> {
189    let all_regions = parse_proc_iomem()?;
190    let reserved_high_mem_regions: Vec<MemoryRegion> = all_regions
191        .into_iter()
192        .filter(|r| r.region_type == MemoryRegionType::Reserved)
193        .filter(|r| r.start_address >= EXTENDED_MEM_START)
194        .collect();
195
196    Ok(reserved_high_mem_regions)
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    mod found_version {
204        use super::*;
205
206        #[test]
207        fn offset_in_region_returns_expected_offset() {
208            let version = FoundVersion {
209                agesa_version: "CezannePI-FP6 1.0.0.E".into(),
210                absolute_address: 20,
211                surrounding_region: MemoryRegion {
212                    start_address: 5,
213                    end_address: 100,
214                    region_type: MemoryRegionType::Reserved,
215                },
216            };
217            assert_eq!(version.offset_in_region(), 15);
218        }
219    }
220
221    mod find_agesa_version_in_reader {
222        use super::*;
223        use indoc::indoc;
224        use std::io::Cursor;
225
226        #[test]
227        fn returns_expected_agesa_version_and_absolute_address() {
228            let file = Cursor::new(indoc! {b"
229                PreceedingUnrelated\0Bytes%p
230                AGESA!V9\0CezannePI-FP6 1.0.0.E\0
231                \0SubsequentUnrelatedBytes\0
232            "});
233            let buf_reader = SkippingBufReader::new(file, 0, None);
234
235            let result = find_agesa_version_in_reader(buf_reader).unwrap();
236            assert_eq!(result, Some(("CezannePI-FP6 1.0.0.E".into(), 37)));
237        }
238
239        #[test]
240        fn returns_none_if_no_agesa_signature_prefix_is_present() {
241            let file = Cursor::new(b"AESA!V9\0CezannePI-FP6 1.0.0.E\0");
242            let buf_reader = SkippingBufReader::new(file, 0, None);
243
244            let result = find_agesa_version_in_reader(buf_reader).unwrap();
245            assert_eq!(result, None);
246        }
247
248        #[test]
249        fn returns_none_if_agesa_version_string_never_ends() {
250            let file = Cursor::new(b"AGESA!V9\0CezannePI-FP6 1.0.0.E");
251            let buf_reader = SkippingBufReader::new(file, 0, None);
252
253            let result = find_agesa_version_in_reader(buf_reader).unwrap();
254            assert_eq!(result, None);
255        }
256    }
257}