From 3bb39f4e337e0c3d1eb4ace49adfd558dca203c4 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Sun, 9 Aug 2026 02:31:19 +1000 Subject: [PATCH] ELFFile: Skip embedded PS-X EXE header segments Ignore the metadata PT_LOAD segment emitted by PS1 linker scripts and validate loadable segment ranges before invoking the loader callback. Fixes kernel crash when loading. --- src/util-tests/elf_parser_tests.cpp | 90 +++++++++++++++++++++++++++++ src/util/elf_file.cpp | 46 ++++++++++++++- 2 files changed, 133 insertions(+), 3 deletions(-) diff --git a/src/util-tests/elf_parser_tests.cpp b/src/util-tests/elf_parser_tests.cpp index e1b124235..823c71868 100644 --- a/src/util-tests/elf_parser_tests.cpp +++ b/src/util-tests/elf_parser_tests.cpp @@ -141,6 +141,48 @@ ELFFile::DataArray CreateOutOfRangeProgramHeaderELFData() return data; } +ELFFile::DataArray CreateELFDataWithEmbeddedPSXExecutableHeader() +{ + auto data = CreateValidELFData(); + data.resize(0x400); + auto* ehdr = reinterpret_cast(data.data()); + auto* phdr = reinterpret_cast(data.data() + ehdr->e_phoff); + + // Make the first loadable segment contain an embedded PS-X EXE header, as produced by some PS1 linker scripts. + phdr[0].p_offset = 0; + phdr[0].p_vaddr = 0x80000000; + phdr[0].p_filesz = 0x100; + phdr[0].p_memsz = 0x100; + phdr[1].p_type = ELFFile::PT_LOAD; + phdr[1].p_offset = 0x100; + phdr[1].p_vaddr = 0x80010000; + phdr[1].p_filesz = 0x100; + phdr[1].p_memsz = 0x100; + + ehdr->e_shoff = 0x300; + ehdr->e_shnum = 4; + auto* shdr = reinterpret_cast(data.data() + ehdr->e_shoff); + std::memset(shdr, 0, sizeof(ELFFile::Elf32_Shdr) * ehdr->e_shnum); + shdr[1].sh_name = 1; + shdr[1].sh_type = ELFFile::SHT_PROGBITS; + shdr[1].sh_addr = 0x80010000; + shdr[1].sh_offset = 0x100; + shdr[1].sh_size = 0x100; + shdr[2].sh_name = 7; + shdr[2].sh_type = ELFFile::SHT_STRTAB; + shdr[2].sh_offset = 0x200; + shdr[2].sh_size = 0x30; + shdr[3].sh_name = 17; + shdr[3].sh_type = ELFFile::SHT_PROGBITS; + shdr[3].sh_addr = 0x80000000; + shdr[3].sh_offset = 0; + shdr[3].sh_size = 0x100; + + char* strtab = reinterpret_cast(data.data() + shdr[2].sh_offset); + strcpy(strtab + 17, ".PSX_EXE_Header"); + return data; +} + class ELFParserTest : public ::testing::Test { protected: @@ -365,6 +407,24 @@ TEST_F(ELFParserTest, LoadExecutableSections) EXPECT_EQ(loaded_sections[0].data[0], 0xAAu); // Check first byte of our fake code } +TEST_F(ELFParserTest, SkipsEmbeddedPSXExecutableHeader) +{ + ELFFile elf; + Error error; + ASSERT_TRUE(elf.Open(CreateELFDataWithEmbeddedPSXExecutableHeader(), &error)); + + std::vector loaded_addresses; + ASSERT_TRUE(elf.LoadExecutableSections( + [&loaded_addresses](std::span, u32 dest_vaddr, u32, Error*) { + loaded_addresses.push_back(dest_vaddr); + return true; + }, + &error)); + + ASSERT_EQ(loaded_addresses.size(), 1u); + EXPECT_EQ(loaded_addresses[0], 0x80010000u); +} + TEST_F(ELFParserTest, MissingEntryPoint) { ELFFile elf; @@ -390,3 +450,33 @@ TEST_F(ELFParserTest, OutOfRangeProgramHeader) EXPECT_FALSE(result); EXPECT_TRUE(error.IsValid()); } + +TEST_F(ELFParserTest, RejectsProgramHeaderWithSmallerMemorySize) +{ + auto data = CreateValidELFData(); + const auto* ehdr = reinterpret_cast(data.data()); + auto* phdr = reinterpret_cast(data.data() + ehdr->e_phoff); + phdr[0].p_memsz = phdr[0].p_filesz - 1; + + ELFFile elf; + Error error; + ASSERT_TRUE(elf.Open(std::move(data), &error)); + EXPECT_FALSE(elf.LoadExecutableSections([](std::span, u32, u32, Error*) { return true; }, &error)); + EXPECT_TRUE(error.IsValid()); +} + +TEST_F(ELFParserTest, RejectsProgramHeaderAddressOverflow) +{ + auto data = CreateValidELFData(); + const auto* ehdr = reinterpret_cast(data.data()); + auto* phdr = reinterpret_cast(data.data() + ehdr->e_phoff); + phdr[0].p_vaddr = 0xFFFFFFF0; + phdr[0].p_memsz = 0x20; + phdr[0].p_filesz = 0x20; + + ELFFile elf; + Error error; + ASSERT_TRUE(elf.Open(std::move(data), &error)); + EXPECT_FALSE(elf.LoadExecutableSections([](std::span, u32, u32, Error*) { return true; }, &error)); + EXPECT_TRUE(error.IsValid()); +} diff --git a/src/util/elf_file.cpp b/src/util/elf_file.cpp index 1ae3f298f..03f9b155b 100644 --- a/src/util/elf_file.cpp +++ b/src/util/elf_file.cpp @@ -7,10 +7,13 @@ #include "common/file_system.h" #include "common/log.h" +#include + LOG_CHANNEL(FileLoader); static constexpr const u8 EXPECTED_ELF_HEADER[4] = {'\177', 'E', 'L', 'F'}; static constexpr s64 MAX_ELF_FILE_SIZE = 32 * 1024 * 1024; +static constexpr std::string_view PSX_EXE_HEADER_SECTION_NAME = ".PSX_EXE_Header"; ELFFile::ELFFile() = default; @@ -175,10 +178,47 @@ bool ELFFile::LoadExecutableSections(const LoadExecutableSectionCallback& callba continue; } + // Some PS1 linker scripts place an embedded PS-X EXE header in a PT_LOAD segment at the start of the ELF. This + // segment is metadata for a standalone PS-X EXE and must not overwrite the already-running kernel when sideloading. + if (phdr->p_offset == 0) + { + bool contains_psx_exe_header = false; + for (u32 section_index = 0; section_index < GetSectionCount(); section_index++) + { + const Elf32_Shdr* shdr = GetSectionHeader(section_index); + if (!shdr || GetSectionName(*shdr) != PSX_EXE_HEADER_SECTION_NAME || shdr->sh_offset >= phdr->p_filesz || + shdr->sh_size > (phdr->p_filesz - shdr->sh_offset)) + { + continue; + } + + contains_psx_exe_header = true; + break; + } + + if (contains_psx_exe_header) + { + DEV_LOG("Skipping embedded PS-X EXE header segment at 0x{:08X}", phdr->p_vaddr); + continue; + } + } + + if (phdr->p_memsz < phdr->p_filesz) + { + Error::SetStringFmt(error, "Program header {} has a memory size smaller than its file size.", i); + return false; + } + + if (phdr->p_memsz > 0 && phdr->p_vaddr > (std::numeric_limits::max() - (phdr->p_memsz - 1))) + { + Error::SetStringFmt(error, "Program header {} address range overflows.", i); + return false; + } + std::span data; if (phdr->p_filesz > 0) { - if ((phdr->p_offset + static_cast(phdr->p_filesz)) > m_data.size()) + if (phdr->p_offset > m_data.size() || phdr->p_filesz > (m_data.size() - phdr->p_offset)) { Error::SetStringFmt(error, "Program header {} is out of file range {} {} {}", i, phdr->p_offset, phdr->p_filesz, m_data.size()); @@ -188,10 +228,10 @@ bool ELFFile::LoadExecutableSections(const LoadExecutableSectionCallback& callba data = m_data.cspan(phdr->p_offset, phdr->p_filesz); } - if (!callback(data, phdr->p_vaddr, std::max(phdr->p_memsz, phdr->p_filesz), error)) + if (!callback(data, phdr->p_vaddr, phdr->p_memsz, error)) return false; - loaded_entry |= (entry >= phdr->p_vaddr && entry < (phdr->p_vaddr + phdr->p_memsz)); + loaded_entry |= (entry >= phdr->p_vaddr && (entry - phdr->p_vaddr) < phdr->p_memsz); } if (!loaded_entry)