diff --git a/psptool/blob.py b/psptool/blob.py index 64dd5a6..42345d6 100644 --- a/psptool/blob.py +++ b/psptool/blob.py @@ -30,6 +30,9 @@ class Blob(NestedBuffer): _FIRMWARE_ENTRY_MAGIC = b'\xAA\x55\xAA\x55' # All structures per Rom must be in 16MB windows _MAX_PAGE_SIZE = 16 * 1024 * 1024 + # Assumed only when a file entry points past the ROM size taken from the + # input size, so a 32 MB ROM inside a bigger file keeps its size + _BIG_ROM_SIZE = 64 * 1024 * 1024 # ROM-relative offsets at which a FET may legitimately appear, in the # try-order used by the parser (the loop breaks on first successful @@ -60,7 +63,7 @@ def __init__(self, buffer: bytearray, size: int, psptool): rom_size = _rom_size * 1024 * 1024 self.psptool.ph.print_warning(f"Input file is {self.buffer_size:#x}, will assume ROM size of {_rom_size}M") - # For each FET, we try to create a 16MB ROM starting at `FET - offset` + # For each FET, we try to create a ROM starting at `FET - offset` for fet_location in self._find_fets(): fet_parsed = False for fet_offset in self.POSSIBLE_FET_OFFSETS: @@ -75,7 +78,7 @@ def __init__(self, buffer: bytearray, size: int, psptool): rom_offset = fet_location - fet_offset # e.g. 0x20800 - 0x20000 = 0x0800 # W/A for images with single ROMs that have entries crossing 16MB boundary if rom_page == 0: - potential_rom = Rom(self, rom_size, rom_offset, fet_offset, psptool) + potential_rom = self._create_first_page_rom(rom_size, rom_offset, fet_offset) else: potential_rom = Rom(self, min(rom_size, self._MAX_PAGE_SIZE), rom_offset, fet_offset, psptool) self.roms.append(potential_rom) @@ -95,6 +98,26 @@ def __init__(self, buffer: bytearray, size: int, psptool): def __repr__(self): return f'Blob({self.roms=})' + def _create_first_page_rom(self, rom_size, rom_offset, fet_offset): + psptool = self.psptool + directories = dict(psptool.directories_by_offset) + files = dict(psptool.files_by_offset) + references = {file: len(file.references) for file in files.values()} + try: + return Rom(self, rom_size, rom_offset, fet_offset, psptool) + except AssertionError as e: + if 'overflows ROM bounds' not in str(e) or rom_offset + self._BIG_ROM_SIZE > self.buffer_size: + raise + + # Drop what the failed attempt registered before parsing again + psptool.directories_by_offset = directories + psptool.files_by_offset = files + for file, count in references.items(): + del file.references[count:] + psptool.ph.print_warning(f"File offset past {rom_size >> 20}M, will assume ROM size of " + f"{self._BIG_ROM_SIZE >> 20}M") + return Rom(self, self._BIG_ROM_SIZE, rom_offset, fet_offset, psptool) + def _construct_range_dict(self): all_files = self.unique_files() diff --git a/psptool/directory.py b/psptool/directory.py index 7e85c59..eb29816 100644 --- a/psptool/directory.py +++ b/psptool/directory.py @@ -89,9 +89,9 @@ def create_directories_if_not_exist(cls, offset, fet, zen_generation=None) -> Li if "Empty entry" in str(e): fet.psptool.ph.print_warning(f"Skipping empty directory entry at offset 0x{offset:x}") return [] - else: - # Re-raise other parse errors - raise + # Some slot headers point at other data, such as an APCB + fet.psptool.ph.print_warning(f"Skipping non-directory at offset 0x{offset:x}") + return [] # 2. Recursively add secondary directories referenced by the just created directory, if applicable for secondary_directory_offset in directory.secondary_directory_offsets: @@ -101,17 +101,26 @@ def create_directories_if_not_exist(cls, offset, fet, zen_generation=None) -> Li # 3. Recursively add tertiary directories (double references introduced in Zen 4), if applicable for tertiary_directory_offset in directory.tertiary_directory_offsets: directory_body = fet.rom.get_bytes(tertiary_directory_offset, 32) + + # Some ROMs point straight at the L2 directory instead of at a + # header holding its offset, so there is nothing to resolve + if directory_body[:4] in cls.DIRECTORY_MAGICS + BiosDirectory.DIRECTORY_MAGICS: + created_directories += cls.create_directories_if_not_exist( + tertiary_directory_offset, fet, zen_generation) + continue + actual_tertiary_offset = int.from_bytes(directory_body[16:20], 'little') zen_generation_id = directory_body[21:24] - zen_generation = cls.get_possible_zen_generation(zen_generation_id) - if zen_generation == 'unknown': + tertiary_zen_generation = cls.get_possible_zen_generation(zen_generation_id) + if tertiary_zen_generation == 'unknown': fet.psptool.ph.print_warning(f"Unknown {zen_generation_id=}") zen_generation_id = hex(int.from_bytes(directory_body[20:24], 'little')) - zen_generation += f' (PSP ID {zen_generation_id})' + tertiary_zen_generation += f' (PSP ID {zen_generation_id})' # Resolve one more indirection - tertiary_directories = cls.create_directories_if_not_exist(actual_tertiary_offset, fet, zen_generation) + tertiary_directories = cls.create_directories_if_not_exist( + actual_tertiary_offset, fet, tertiary_zen_generation) created_directories += tertiary_directories return created_directories @@ -257,12 +266,18 @@ def update_entry_fields(self, file: File, type_, size, offset): self.update_checksum() def update_zen_generation(self, fet, zen_generation): - if zen_generation is not None: - if zen_generation not in self.zen_generation: - self.zen_generation += '\n' + zen_generation - for offset in self.secondary_directory_offsets: - dir = fet.psptool.directories_by_offset[offset] - dir.update_zen_generation(fet, zen_generation) + if zen_generation is None: + return + # A directory first reached without a generation takes the new one + if self.zen_generation is None: + self.zen_generation = zen_generation + elif zen_generation not in self.zen_generation: + self.zen_generation += '\n' + zen_generation + else: + return + for offset in self.secondary_directory_offsets: + dir = fet.psptool.directories_by_offset[offset] + dir.update_zen_generation(fet, zen_generation) class BiosDirectory(Directory): diff --git a/psptool/entry.py b/psptool/entry.py index a5f9005..010343c 100644 --- a/psptool/entry.py +++ b/psptool/entry.py @@ -46,6 +46,11 @@ def file_offset(self): # entry address mode must be taken into account, otherwise ignored. if addr_mode == 2 or addr_mode == 3: addr_mode = self.address_mode + # coreboot's amdfwtool writes APOB NV entries on older SoCs as an x86 + # physical address (entry address mode 0) in a mode 1 directory. No + # flash offset reaches 0xff000000. + elif addr_mode == 1 and self.address_mode == 0 and self.offset >= 0xFF000000: + addr_mode = 0 if addr_mode == 0: # x86 physical address, should be in range 0xff000000 - 0xffffffff diff --git a/psptool/header_file.py b/psptool/header_file.py index 7b443a6..5febf23 100644 --- a/psptool/header_file.py +++ b/psptool/header_file.py @@ -63,6 +63,10 @@ def _parse(self): if self.has_sha256_checksum and self.has_sha384_checksum: raise File.ParseError('File should not have both sha256 and sha384 checksum bits set!') + if self.buffer_offset + self.rom_size > self.parent_buffer.buffer_size: + raise File.ParseError(f'Header size 0x{self.rom_size:x} overflows the parent buffer, ' + f'likely not a PSP header') + if self.rom_size == 0: self.rom_size = self.buffer_size elif self.rom_size > self.buffer_size: diff --git a/tests/unit/test_rom_layouts.py b/tests/unit/test_rom_layouts.py new file mode 100644 index 0000000..9def5a8 --- /dev/null +++ b/tests/unit/test_rom_layouts.py @@ -0,0 +1,189 @@ +# PSPTool - Display, extract and manipulate PSP firmware inside UEFI images +# Copyright (C) 2026 contributors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +"""Synthetic ROMs for layouts seen in Lenovo ThinkPad client firmware. + +- A 64 MB flash whose BIOS L2 directory points past 32 MB (Z13 Gen 2), + and 32 MB ROMs in files of 64 MB that must keep their size. +- 0x48/0x4a entries pointing straight at a $PL2 directory rather than at + a header holding its offset (L13 Gen 4). +- A BIOS directory entry holding a plain PE image, whose bytes at the + header's size field are text (L13 Gen 4). +- A 0x4a slot header pointing at an APCB rather than at a $PL2 directory + (T14 Gen 7 AMD). +- An x86 physical address with entry address mode 0 in a directory with + address mode 1, as coreboot writes APOB NV entries (StarBook Mk VI AMD). +""" + +import contextlib +import io +import os +import struct +import tempfile +import unittest + +from psptool import PSPTool +from psptool.blob import Blob +from psptool.utils import fletcher32 + +MB = 1024 * 1024 +FET_OFFSET = Blob.POSSIBLE_FET_OFFSETS[0] +PSP_DIR_OFFSET = FET_OFFSET + 0x1000 +HEADER_FILE_SIZE = 0x100 + +# Directory additional_info: version bit set, address mode 1 (flash offset) +FLASH_OFFSET_MODE = (1 << 31) | (1 << 24) + + +def directory(magic: bytes, entries) -> bytes: + body = struct.pack(' bytes: + header = bytearray(HEADER_FILE_SIZE) + header[0x62] = 0x0C # boot loader major in BOOTLOADER_VERSION_TO_ZEN, else PSPTool warns + header[0x6c:0x70] = struct.pack(' bytes: + blob = bytearray(size) + blob[FET_OFFSET - 4:FET_OFFSET] = b'\xff' * 4 + blob[FET_OFFSET:FET_OFFSET + 4] = Blob._FIRMWARE_ENTRY_MAGIC + blob[FET_OFFSET + 4:FET_OFFSET + 8] = struct.pack('