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 more.
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  This module supports both, but **v5** was not yet comprehensively tested.
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 std::str;
41use thiserror::Error;
42
43#[derive(PartialEq)]
44pub struct FoundVersion {
45    pub agesa_version: String,
46    pub absolute_address: usize,
47    pub surrounding_region: MemoryRegion,
48}
49
50impl FoundVersion {
51    /// Return this version's offset within its surrounding memory region.
52    #[must_use]
53    pub fn offset_in_region(&self) -> usize {
54        self.absolute_address - self.surrounding_region.start_address
55    }
56}
57
58#[derive(Error, Debug)]
59pub enum SearchError {
60    #[error("could not open /dev/mem")]
61    DevMemUnopenable(#[source] io::Error),
62
63    #[error("could not read iomem memory map")]
64    IomemUnreadable(#[source] io::Error),
65
66    #[error("could not read byte in physical memory")]
67    ByteUnreadable(#[source] io::Error),
68}
69
70pub type SearchResult = Result<Option<FoundVersion>, SearchError>;
71
72/// Search for the AGESA version within all `Reserved` memory regions.
73///
74/// # Errors
75///
76/// This function will return an error if no memory map could be obtained.
77/// It will also return errors for reading from physical memory according
78/// to [`find_agesa_version_in_memory_region`].
79pub fn find_agesa_version() -> SearchResult {
80    let possible_regions =
81        get_reserved_regions_in_extended_memory().map_err(SearchError::IomemUnreadable)?;
82
83    for region in possible_regions {
84        log::info!("Searching memory region: {}", region);
85        let search_result = find_agesa_version_in_memory_region(region)?;
86        if search_result.is_some() {
87            return Ok(search_result);
88        }
89    }
90
91    Ok(None)
92}
93
94/// Search for the AGESA version within the given memory region.
95///
96/// # Errors
97///
98/// This function will return an error if `/dev/mem` could not be opened
99/// or an unexpected read error occurred during the search.
100pub fn find_agesa_version_in_memory_region(region: MemoryRegion) -> SearchResult {
101    let file = File::open("/dev/mem").map_err(SearchError::DevMemUnopenable)?;
102    let buf_reader = SkippingBufReader::new(file, region.start_address, Some(region.end_address));
103
104    if let Some((agesa_version, absolute_address)) = find_agesa_version_in_reader(buf_reader)? {
105        return Ok(Some(FoundVersion {
106            agesa_version,
107            absolute_address,
108            surrounding_region: region,
109        }));
110    }
111
112    Ok(None)
113}
114
115/// The possible states of an ongoing search.
116enum SearchState {
117    Searching,
118    SignatureFound,
119    VersionStartFound,
120}
121
122/// The signature indicating the start of an AGESA v9 version in memory.
123const SIGNATURE_V9: &[u8] = b"AGESA!V";
124const SIGNATURE_LENGTH: usize = SIGNATURE_V9.len();
125
126/// The signature indicating the start of an AGESA v5 version in memory.
127///
128/// Per the Arch2008 spec, this is should be `!!AGESA `; however, on the
129/// [Kaveri platform] I tested, it is `!!!AGESA` immediately followed by
130/// the version string. The specified prefix should work for both cases.
131///
132/// [Kaveri platform]: https://github.com/fishbaoz/KaveriPI/blob/master/AGESA/AMD.h#L260
133const SIGNATURE_V5: &[u8; SIGNATURE_LENGTH] = b"!!AGESA";
134
135fn find_agesa_version_in_reader<R: Read + Seek>(
136    mut buf_reader: SkippingBufReader<R>,
137) -> Result<Option<(String, usize)>, SearchError> {
138    let mut version_string = Vec::new();
139
140    let mut search_state = SearchState::Searching;
141    let mut search_window = VecDeque::with_capacity(SIGNATURE_LENGTH);
142
143    for b in (&mut buf_reader).bytes() {
144        let byte = b.map_err(SearchError::ByteUnreadable)?;
145
146        match search_state {
147            SearchState::Searching => {
148                if search_window.len() == SIGNATURE_LENGTH {
149                    search_window.pop_front();
150                }
151                search_window.push_back(byte);
152
153                if search_window.eq(&SIGNATURE_V9) {
154                    // AGESA!V9␀CezannePI-FP6 1.0.1.1␀
155                    //       ^
156                    search_state = SearchState::SignatureFound;
157                } else if search_window.eq(&SIGNATURE_V5) {
158                    // !!!AGESAKaveriPI        V1.1.0.7    ␀
159                    //        ^
160                    // For AGESA v5, the version string starts right after
161                    // the signature, so there is no null byte to skip to
162                    search_state = SearchState::VersionStartFound;
163                }
164            }
165            SearchState::SignatureFound => {
166                if byte == b'\0' {
167                    // AGESA!V9␀CezannePI-FP6 1.0.1.1␀
168                    //         ^
169                    search_state = SearchState::VersionStartFound;
170                }
171            }
172            SearchState::VersionStartFound if byte == b'\0' => {
173                // AGESA!V9␀CezannePI-FP6 1.0.1.1␀
174                //                               ^
175                let trimmed_version = version_string.trim_ascii_start();
176                let absolute_address = buf_reader.position_in_file() - trimmed_version.len() - 1;
177                return Ok(Some((
178                    str::from_utf8(trimmed_version).unwrap().into(),
179                    absolute_address,
180                )));
181            }
182            SearchState::VersionStartFound => {
183                version_string.push(byte);
184            }
185        }
186    }
187
188    Ok(None)
189}
190
191/// The start address of extended memory.
192const EXTENDED_MEM_START: usize = 0x0000_0000_0010_0000;
193
194/// Find and return all `Reserved` regions in [extended memory] (> 1 MiB).
195///
196/// Testing on a few machines showed that at least one `Reserved` region in
197/// extended memory reliably contains the AGESA version – usually the first
198/// one at that. Even `Usable` regions may occasionally include it, but the
199/// initial (generally small) `Reserved` regions are much faster to search.
200///
201/// [extended memory]: https://wiki.osdev.org/Memory_Map_(x86)#Extended_Memory_(%3E_1_MiB)
202///
203/// # Errors
204///
205/// This function will return an error if `/proc/iomem` could not be read.
206pub fn get_reserved_regions_in_extended_memory() -> io::Result<Vec<MemoryRegion>> {
207    let all_regions = parse_proc_iomem()?;
208    let reserved_high_mem_regions: Vec<MemoryRegion> = all_regions
209        .into_iter()
210        .filter(|r| r.region_type == MemoryRegionType::Reserved)
211        .filter(|r| r.start_address >= EXTENDED_MEM_START)
212        .collect();
213
214    Ok(reserved_high_mem_regions)
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    mod found_version {
222        use super::*;
223
224        #[test]
225        fn offset_in_region_returns_expected_offset() {
226            let version = FoundVersion {
227                agesa_version: "CezannePI-FP6 1.0.1.1".into(),
228                absolute_address: 20,
229                surrounding_region: MemoryRegion {
230                    start_address: 5,
231                    end_address: 100,
232                    region_type: MemoryRegionType::Reserved,
233                },
234            };
235            assert_eq!(version.offset_in_region(), 15);
236        }
237    }
238
239    mod find_agesa_version_in_reader {
240        use super::*;
241        use indoc::formatdoc;
242        use rstest::rstest;
243        use std::io::Cursor;
244
245        #[rstest]
246        #[case::agesa_v9_signature(
247            "AGESA!V9\0CezannePI-FP6 1.0.1.1\0",
248            "CezannePI-FP6 1.0.1.1",
249            37
250        )]
251        #[case::agesa_v5_signature_arch2008(
252            "!!AGESA KaveriPI        V1.1.0.7    \0",
253            "KaveriPI        V1.1.0.7    ",
254            36
255        )]
256        #[case::agesa_v5_signature_alternative(
257            "!!!AGESAKaveriPI        V1.1.0.7    \0",
258            "KaveriPI        V1.1.0.7    ",
259            36
260        )]
261        fn returns_expected_version_string_and_absolute_address(
262            #[case] version_in_memory: String,
263            #[case] expected_version_string: String,
264            #[case] expected_absolute_address: usize,
265        ) {
266            let file = Cursor::new(formatdoc! {"
267                PreceedingUnrelated\0Bytes%p
268                {version_in_memory}
269                \0SubsequentUnrelatedBytes\0
270            "});
271            let buf_reader = SkippingBufReader::new(file, 0, None);
272
273            let result = find_agesa_version_in_reader(buf_reader).unwrap();
274            assert_eq!(
275                result,
276                Some((expected_version_string, expected_absolute_address))
277            );
278        }
279
280        #[test]
281        fn returns_none_if_no_agesa_signature_is_present() {
282            let file = Cursor::new(b"AESA!V9\0CezannePI-FP6 1.0.1.1\0");
283            let buf_reader = SkippingBufReader::new(file, 0, None);
284
285            let result = find_agesa_version_in_reader(buf_reader).unwrap();
286            assert_eq!(result, None);
287        }
288
289        #[test]
290        fn returns_none_if_version_string_does_not_end() {
291            let file = Cursor::new(b"AGESA!V9\0CezannePI-FP6 1.0.1.1");
292            let buf_reader = SkippingBufReader::new(file, 0, None);
293
294            let result = find_agesa_version_in_reader(buf_reader).unwrap();
295            assert_eq!(result, None);
296        }
297    }
298}