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