Skip to main content

memf_linux/
framebuffer.rs

1/// Linux framebuffer walker — reads `boot_params.screen_info` to locate the
2/// EFI/VESA linear framebuffer and encode it as PNG.
3///
4/// Offsets within `boot_params` (screen_info is the first field, so these
5/// are offsets from the boot_params symbol address):
6///   +0x10  lfb_base        u32  — PA of linear framebuffer
7///   +0x14  lfb_width       u16  — width in pixels
8///   +0x16  lfb_height      u16  — height in pixels
9///   +0x18  lfb_depth       u16  — bits per pixel (16, 24, or 32)
10///   +0x1A  lfb_linelength  u32  — row stride in bytes
11use memf_core::framebuffer::{encode_png, FramebufferResult, PixelFormat};
12use memf_core::object_reader::ObjectReader;
13use memf_format::PhysicalMemoryProvider;
14
15use crate::Result;
16
17const MAX_FB_BYTES: u64 = 32 * 1024 * 1024; // 32 MiB hard cap
18
19const LFB_BASE_OFF: u64 = 0x10;
20const LFB_WIDTH_OFF: u64 = 0x14;
21const LFB_HEIGHT_OFF: u64 = 0x16;
22const LFB_DEPTH_OFF: u64 = 0x18;
23const LFB_STRIDE_OFF: u64 = 0x1A;
24
25/// Walk the Linux `boot_params.screen_info` structure to locate and capture
26/// the linear framebuffer, returning a [`FramebufferResult`] with PNG bytes.
27pub fn walk_framebuffer_linux<P: PhysicalMemoryProvider + Clone>(
28    reader: &ObjectReader<P>,
29) -> Result<FramebufferResult> {
30    let boot_params_va =
31        reader
32            .required_symbol("boot_params")
33            .map_err(|_| crate::Error::WalkFailed {
34                walker: "framebuffer",
35                reason: "boot_params symbol not found".into(),
36            })?;
37
38    let lfb_base = {
39        let b = reader
40            .read_bytes(boot_params_va + LFB_BASE_OFF, 4)
41            .map_err(|e| crate::Error::WalkFailed {
42                walker: "framebuffer",
43                reason: format!("read lfb_base: {e}"),
44            })?;
45        u64::from(b.try_into().map_or(0, u32::from_le_bytes))
46    };
47
48    let width = {
49        let b = reader
50            .read_bytes(boot_params_va + LFB_WIDTH_OFF, 2)
51            .map_err(|e| crate::Error::WalkFailed {
52                walker: "framebuffer",
53                reason: format!("read lfb_width: {e}"),
54            })?;
55        u32::from(b.try_into().map_or(0, u16::from_le_bytes))
56    };
57
58    let height = {
59        let b = reader
60            .read_bytes(boot_params_va + LFB_HEIGHT_OFF, 2)
61            .map_err(|e| crate::Error::WalkFailed {
62                walker: "framebuffer",
63                reason: format!("read lfb_height: {e}"),
64            })?;
65        u32::from(b.try_into().map_or(0, u16::from_le_bytes))
66    };
67
68    let depth = {
69        let b = reader
70            .read_bytes(boot_params_va + LFB_DEPTH_OFF, 2)
71            .map_err(|e| crate::Error::WalkFailed {
72                walker: "framebuffer",
73                reason: format!("read lfb_depth: {e}"),
74            })?;
75        b.try_into().map_or(0, u16::from_le_bytes)
76    };
77
78    let stride = {
79        let b = reader
80            .read_bytes(boot_params_va + LFB_STRIDE_OFF, 4)
81            .map_err(|e| crate::Error::WalkFailed {
82                walker: "framebuffer",
83                reason: format!("read lfb_linelength: {e}"),
84            })?;
85        b.try_into().map_or(0, u32::from_le_bytes)
86    };
87
88    if width == 0 || height == 0 || lfb_base == 0 {
89        return Err(crate::Error::WalkFailed {
90            walker: "framebuffer",
91            reason: "screen_info has zero dimensions or base address".into(),
92        });
93    }
94
95    let pixel_format = match depth {
96        32 => PixelFormat::Xbgr8888,
97        24 => PixelFormat::Bgr24,
98        16 => PixelFormat::Rgb565,
99        d => PixelFormat::Unknown(d as u8),
100    };
101
102    let fb_size = u64::from(stride) * u64::from(height);
103    if fb_size > MAX_FB_BYTES {
104        return Err(crate::Error::WalkFailed {
105            walker: "framebuffer",
106            reason: format!("framebuffer size {fb_size} exceeds 32 MiB"),
107        });
108    }
109
110    let mut fb_bytes = vec![0u8; fb_size as usize];
111    reader
112        .vas()
113        .physical()
114        .read_phys(lfb_base, &mut fb_bytes)
115        .map_err(|_| crate::Error::WalkFailed {
116            walker: "framebuffer",
117            reason: format!("could not read {fb_size} bytes from PA {lfb_base:#x}"),
118        })?;
119
120    let png_bytes = encode_png(&fb_bytes, width, height, pixel_format).map_err(|e| {
121        crate::Error::WalkFailed {
122            walker: "framebuffer",
123            reason: format!("PNG encode: {e}"),
124        }
125    })?;
126
127    Ok(FramebufferResult {
128        width,
129        height,
130        stride,
131        pixel_format: format!("{pixel_format:?}"),
132        phys_base: lfb_base,
133        source: "boot_params.screen_info".into(),
134        png_bytes,
135    })
136}
137
138#[cfg(test)]
139mod tests {
140    use memf_core::object_reader::ObjectReader;
141    use memf_core::test_builders::{flags as ptf, PageTableBuilder};
142    use memf_core::vas::{TranslationMode, VirtualAddressSpace};
143    use memf_symbols::isf::IsfResolver;
144    use memf_symbols::test_builders::IsfBuilder;
145
146    use super::walk_framebuffer_linux;
147
148    // -----------------------------------------------------------------------
149    // Memory layout (all within the default 16 MiB SyntheticPhysMem):
150    //   boot_params VA = 0xFFFF_8800_00A0_0000 → PA 0x00A0_0000 (10 MiB)
151    //   framebuffer PA =                           0x00B0_0000 (11 MiB)
152    //
153    // screen_info offsets from boot_params base:
154    //   +0x10  lfb_base        u32 LE = 0x00B0_0000
155    //   +0x14  lfb_width       u16 LE = 4
156    //   +0x16  lfb_height      u16 LE = 4
157    //   +0x18  lfb_depth       u16 LE = 32
158    //   +0x1A  lfb_linelength  u32 LE = 16  (4 px × 4 bytes)
159    //
160    // Pixel data: 4×4 × 4 bytes = 64 bytes of XBGR8888 at PA 0x00B0_0000.
161    // -----------------------------------------------------------------------
162
163    const BOOT_PARAMS_VA: u64 = 0xFFFF_8800_00A0_0000;
164    const BOOT_PARAMS_PA: u64 = 0x00A0_0000; // 10 MiB
165    const FB_PA: u64 = 0x00B0_0000; // 11 MiB
166
167    fn build_reader() -> ObjectReader<memf_core::test_builders::SyntheticPhysMem> {
168        let mut page = [0u8; 4096];
169
170        // +0x10: lfb_base (u32 LE)
171        page[0x10..0x14].copy_from_slice(&(FB_PA as u32).to_le_bytes());
172        // +0x14: lfb_width (u16 LE) = 4
173        page[0x14..0x16].copy_from_slice(&4u16.to_le_bytes());
174        // +0x16: lfb_height (u16 LE) = 4
175        page[0x16..0x18].copy_from_slice(&4u16.to_le_bytes());
176        // +0x18: lfb_depth (u16 LE) = 32
177        page[0x18..0x1a].copy_from_slice(&32u16.to_le_bytes());
178        // +0x1A: lfb_linelength (u32 LE) = 16
179        page[0x1a..0x1e].copy_from_slice(&16u32.to_le_bytes());
180
181        // 4×4 pixels of XBGR8888
182        let fb_data: Vec<u8> = [0x10u8, 0x20, 0x30, 0xFF]
183            .iter()
184            .copied()
185            .cycle()
186            .take(64)
187            .collect();
188
189        let isf = IsfBuilder::new()
190            .add_symbol("boot_params", BOOT_PARAMS_VA)
191            .build_json();
192        let resolver = IsfResolver::from_value(&isf).unwrap();
193
194        let (cr3, mem) = PageTableBuilder::new()
195            .map_4k(BOOT_PARAMS_VA, BOOT_PARAMS_PA, ptf::WRITABLE)
196            .write_phys(BOOT_PARAMS_PA, &page)
197            .write_phys(FB_PA, &fb_data)
198            .build();
199
200        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
201        ObjectReader::new(vas, Box::new(resolver))
202    }
203
204    fn build_reader_no_sym() -> ObjectReader<memf_core::test_builders::SyntheticPhysMem> {
205        let isf = IsfBuilder::new().build_json();
206        let resolver = IsfResolver::from_value(&isf).unwrap();
207        let (cr3, mem) = PageTableBuilder::new().build();
208        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
209        ObjectReader::new(vas, Box::new(resolver))
210    }
211
212    #[test]
213    fn linux_framebuffer_extracts_4x4_from_boot_params() {
214        let reader = build_reader();
215        let result = walk_framebuffer_linux(&reader).expect("should find framebuffer");
216        assert_eq!(result.width, 4);
217        assert_eq!(result.height, 4);
218        assert_eq!(result.source, "boot_params.screen_info");
219        assert_eq!(result.phys_base, FB_PA);
220    }
221
222    #[test]
223    fn linux_framebuffer_png_output_has_png_magic() {
224        let reader = build_reader();
225        let result = walk_framebuffer_linux(&reader).expect("should succeed");
226        assert!(
227            result.png_bytes.starts_with(b"\x89PNG\r\n\x1a\n"),
228            "PNG magic not found; first bytes: {:?}",
229            &result.png_bytes[..result.png_bytes.len().min(16)]
230        );
231    }
232
233    #[test]
234    fn linux_framebuffer_missing_symbol_returns_err() {
235        let reader = build_reader_no_sym();
236        let result = walk_framebuffer_linux(&reader);
237        assert!(
238            result.is_err(),
239            "expected Err when boot_params symbol absent"
240        );
241    }
242}