Compare commits

...
Author SHA1 Message Date
OatmealDome c77bbaa0f3 ScmRevGen: Bump version to 2606a 2026-08-10 22:54:32 -04:00
OatmealDome 7345c016c9 BuildMacOSUniversalBinary: Add flag to enable CCache 2026-08-10 22:47:44 -04:00
JosJuiceandOatmealDome d3c420c6a4 IOS/FS: Fix loading savestate when files are open
We already had code to close open host files when reading or writing a
savestate, but due to d35fe1b we also need to close open guest files
when reading a savestate, otherwise DoStateRead fails to delete them.

I was considering an alternative solution where instead of copying and
clearing m_handles, we just set `handle.opened = false;` for each handle
before reading a savestate (but not before writing a savestate).
However, this wouldn't solve the problem of DoStateWriteOrMeasure's
calls to OpenFile failing due to all handles being open. I'm not aware
of any games that have that many handles open, though.
2026-08-10 22:47:44 -04:00
Acts1631andOatmealDome 5ea9d7f8a3 Core: log invalid ELF input
Log each rejected ELF header, range, and symbol reference. This
provides actionable diagnostics for malformed files without changing the
validation behavior.
2026-08-10 22:47:44 -04:00
Acts1631andOatmealDome 40cdf26dc6 Core: validate standalone ELF input ranges
ElfReader trusted table offsets and counts from standalone ELF files.
Malformed input could make it read and write past the loaded file buffer.

Validate the ELF header, table ranges, segment data, section data, and
string-table references before accessing them. Invalid files use the
existing executable boot failure path.
2026-08-10 22:47:43 -04:00
Acts1631andOatmealDome 47bb60e564 NetPlay: bound LZO decompression output
NetPlay save synchronization decoded remote LZO blocks with the unsafe
decoder and no output capacity. A malicious host could overflow a client
buffer with a block larger than its declared size.

Use the bounds-checking decoder, validate the declared output length, and
grow buffer results only after each checked block has been decoded.
2026-08-10 22:47:43 -04:00
Scott MansellandOatmealDome 690b51b396 DolReader: Fix integer wraparound
A malicious dol could theoretically use integer wraparound to bypass
bounds checking and cause DolReader to read past the end of m_bytes.

Could result in crashes, wasting large amounts of memory, or even the
disclosure of heap memory contents.
2026-08-10 22:47:43 -04:00
DacoTacoandOatmealDome 1496e7a06c fixes: make dolreader validate section addresses and sizes
IOS and IPL reject non-32byte aligned sections
2026-08-10 22:47:43 -04:00
Scott MansellandOatmealDome 289ff60cc2 GCZ: use 64-bit for m_data_offset
A malicious GCZ file could probably force this to be negative.
Shouldn't cause any issues other than file read failures, but need to fix
because it is causing errors on MSVC.
2026-08-10 22:47:43 -04:00
Scott MansellandOatmealDome fe4b08bc37 GCZ: validate while loading 2026-08-10 22:47:43 -04:00
Scott MansellandOatmealDome 22f15fbb97 GCZ: Don't trust block_num either
SectorReader::ReadChunk does do some validation on it, but it only
checks against the original disc size (reported by the GCZ file).
It has no idea how many blocks the header claimed the disc had.

A maliciously crafted GCZ file could trigger read overflows off the end
of the m_block_pointers/m_hashes arrays.
2026-08-10 22:47:43 -04:00
Scott MansellandOatmealDome 7bcf78b7ea GCZ: Don't trust GetBlockCompressedSize
It comes unverified from the file, and a maliciously crafted file could
trigger not one, but two buffer overflows in the heap.
2026-08-10 22:47:42 -04:00
Admiral H. CurtissandOatmealDome 87d5e731af IOS/NetIPTopDevice: Zero-initialize sockaddr structs
Fixes https://github.com/dolphin-emu/dolphin/security/advisories/GHSA-5fqv-9qrg-gm4j
2026-08-10 22:47:42 -04:00
Scott MansellandOatmealDome 74857b28bb Fix stack overflow in ZeldaHLE
Independently spotted by both @Dentomologist and me while reviewing
PR #14805
The previous limit was correct for valid VPBs, but an invalid VPB
controlled by a malicious game could contain a non-fractional value
in current_pos_frac, which would allow writing an extra 15 samples
(30 bytes) into the stack. With AFC encoding, this is rounded up to 16
samples, but with much less control over which bytes are written.

Maybe we should be doing some validation, or bounds checking, but I'm
pretty sure this issue was copied from the original ucode, and we kinda
want to stay compatible.

The simpler fix is to just increase the size of raw_input_samples.
I've checked other code paths, and 0x514 samples seems to be the limit.
2026-08-10 22:47:42 -04:00
Tillmann KarrasandOatmealDome 5888eb6eb6 DSPHLE/Zelda: prevent out-of-bounds stack read
Reported by @RickdeJager.
2026-08-10 22:47:42 -04:00
Admiral H. CurtissandOatmealDome 44dd6aad30 HW/DSPHLE/AXVoice: Prefer BitCastPtr over BitCastToArray in ApplyUpdatesForMs() 2026-08-10 22:47:42 -04:00
Admiral H. CurtissandOatmealDome 02f2a23f89 HW/DSPHLE/AXVoice: Check array bounds in ApplyUpdatesForMs()
Fixes https://github.com/dolphin-emu/dolphin/security/advisories/GHSA-4q28-hhjv-hf3f
2026-08-10 22:47:41 -04:00
13 changed files with 469 additions and 100 deletions
+12 -1
View File
@@ -69,6 +69,9 @@ DEFAULT_CONFIG = {
# Whether our autoupdate functionality is enabled or not.
"autoupdate": True,
# Whether CCache is used for the build or not.
"ccache": False,
# The distributor for this build.
"distributor": "None"
}
@@ -119,6 +122,12 @@ def parse_args(conf=DEFAULT_CONFIG):
action=argparse.BooleanOptionalAction,
default=conf["autoupdate"])
parser.add_argument(
"--ccache",
help="Enables CCache",
action=argparse.BooleanOptionalAction,
default=conf["ccache"])
parser.add_argument(
"--distributor",
help="Sets the distributor for this build",
@@ -304,7 +313,9 @@ def build(config):
# iconv, bzip2, and curl
"-DUSE_SYSTEM_ICONV=ON",
"-DUSE_SYSTEM_BZIP2=ON",
"-DUSE_SYSTEM_CURL=ON"
"-DUSE_SYSTEM_CURL=ON",
"-DENABLE_CCACHE="
+ python_to_cmake_bool(config["ccache"]),
],
env=env, cwd=arch)
+1 -1
View File
@@ -34,7 +34,7 @@ string(TIMESTAMP DOLPHIN_WC_BUILD_DATE "%Y-%m-%d" UTC)
# version number
set(DOLPHIN_VERSION_MAJOR "2606")
set(DOLPHIN_VERSION_MINOR "0")
set(DOLPHIN_VERSION_MINOR "1")
set(DOLPHIN_VERSION_PATCH ${DOLPHIN_WC_REVISION})
# If Dolphin is not built from a Git repository, default the version info to
+25 -7
View File
@@ -53,11 +53,22 @@ bool DolReader::Initialize(std::span<const u8> buffer)
{
if (m_dolheader.textSize[i] != 0)
{
if (buffer.size() < m_dolheader.textOffset[i] + m_dolheader.textSize[i])
if ((m_dolheader.textAddress[i] & 31) != 0 || (m_dolheader.textSize[i] & 31) != 0)
{
ERROR_LOG_FMT(BOOT,
"Text section {} is not 32-byte aligned: address = 0x{:08x}, size = 0x{:x}",
i, m_dolheader.textAddress[i], m_dolheader.textSize[i]);
return false;
}
const std::size_t section_offset = m_dolheader.textOffset[i];
const std::size_t section_size = m_dolheader.textSize[i];
if (buffer.size() < section_offset || (buffer.size() - section_offset) < section_size)
return false;
const u8* text_start = &buffer[m_dolheader.textOffset[i]];
m_text_sections.emplace_back(text_start, &text_start[m_dolheader.textSize[i]]);
const u8* text_start = &buffer[section_offset];
m_text_sections.emplace_back(text_start, &text_start[section_size]);
for (unsigned int j = 0; !m_is_wii && j < (m_dolheader.textSize[i] / sizeof(u32)); ++j)
{
@@ -78,15 +89,22 @@ bool DolReader::Initialize(std::span<const u8> buffer)
{
if (m_dolheader.dataSize[i] != 0)
{
u32 section_size = m_dolheader.dataSize[i];
u32 section_offset = m_dolheader.dataOffset[i];
const std::size_t section_size = m_dolheader.dataSize[i];
const std::size_t section_offset = m_dolheader.dataOffset[i];
if ((m_dolheader.dataAddress[i] & 31) != 0 || (section_size & 31) != 0)
{
ERROR_LOG_FMT(BOOT,
"Data section {} is not 32-byte aligned: address = 0x{:08x}, size = 0x{:x}",
i, m_dolheader.dataAddress[i], section_size);
return false;
}
if (buffer.size() < section_offset)
return false;
std::vector<u8> data(section_size);
const u8* data_start = &buffer[section_offset];
std::memcpy(&data[0], data_start,
std::min((size_t)section_size, buffer.size() - section_offset));
std::memcpy(&data[0], data_start, std::min(section_size, buffer.size() - section_offset));
m_data_sections.emplace_back(data);
}
else
+101 -18
View File
@@ -3,6 +3,7 @@
#include "Core/Boot/ElfReader.h"
#include <cstring>
#include <string>
#include <utility>
@@ -70,62 +71,113 @@ static void byteswapSection(Elf32_Shdr& sec)
ElfReader::ElfReader(std::vector<u8> buffer) : BootExecutableReader(std::move(buffer))
{
Initialize(m_bytes.data());
m_is_valid = Initialize();
}
ElfReader::ElfReader(File::IOFile file) : BootExecutableReader(std::move(file))
{
Initialize(m_bytes.data());
m_is_valid = Initialize();
}
ElfReader::ElfReader(const std::string& filename) : BootExecutableReader(filename)
{
Initialize(m_bytes.data());
m_is_valid = Initialize();
}
ElfReader::~ElfReader() = default;
void ElfReader::Initialize(u8* ptr)
bool ElfReader::Initialize()
{
base = (char*)ptr;
base32 = (u32*)ptr;
header = (Elf32_Ehdr*)ptr;
if (m_bytes.size() < sizeof(Elf32_Ehdr))
{
ERROR_LOG_FMT(BOOT, "ELF file is too small.");
return false;
}
base = reinterpret_cast<char*>(m_bytes.data());
base32 = reinterpret_cast<u32*>(m_bytes.data());
header = reinterpret_cast<Elf32_Ehdr*>(m_bytes.data());
if (header->e_ident[EI_MAG0] != ELFMAG0 || header->e_ident[EI_MAG1] != ELFMAG1 ||
header->e_ident[EI_MAG2] != ELFMAG2 || header->e_ident[EI_MAG3] != ELFMAG3 ||
header->e_ident[EI_CLASS] != ELFCLASS32 || header->e_ident[EI_DATA] != ELFDATA2MSB)
{
ERROR_LOG_FMT(BOOT, "Invalid ELF header.");
return false;
}
byteswapHeader(*header);
segments = (Elf32_Phdr*)(base + header->e_phoff);
sections = (Elf32_Shdr*)(base + header->e_shoff);
const auto is_range_valid = [this](size_t offset, size_t size) {
return offset <= m_bytes.size() && size <= m_bytes.size() - offset;
};
if (header->e_ehsize != sizeof(Elf32_Ehdr) ||
(header->e_phnum != 0 && header->e_phentsize != sizeof(Elf32_Phdr)) ||
(header->e_shnum != 0 && header->e_shentsize != sizeof(Elf32_Shdr)) ||
!is_range_valid(header->e_phoff, sizeof(Elf32_Phdr) * header->e_phnum) ||
!is_range_valid(header->e_shoff, sizeof(Elf32_Shdr) * header->e_shnum) ||
(header->e_shstrndx != SHN_UNDEF && header->e_shstrndx >= header->e_shnum))
{
ERROR_LOG_FMT(BOOT, "Invalid ELF header table.");
return false;
}
segments = reinterpret_cast<Elf32_Phdr*>(base + header->e_phoff);
sections = reinterpret_cast<Elf32_Shdr*>(base + header->e_shoff);
for (int i = 0; i < GetNumSegments(); i++)
{
byteswapSegment(segments[i]);
if (!is_range_valid(segments[i].p_offset, segments[i].p_filesz) ||
segments[i].p_filesz > segments[i].p_memsz)
{
ERROR_LOG_FMT(BOOT, "Invalid ELF program header {}.", i);
return false;
}
}
for (int i = 0; i < GetNumSections(); i++)
{
byteswapSection(sections[i]);
if (sections[i].sh_type != SHT_NOBITS &&
!is_range_valid(sections[i].sh_offset, sections[i].sh_size))
{
ERROR_LOG_FMT(BOOT, "Invalid ELF section header {}.", i);
return false;
}
}
entryPoint = header->e_entry;
bRelocate = (header->e_type != ET_EXEC);
return true;
}
const char* ElfReader::GetSectionName(int section) const
{
if (sections[section].sh_type == SHT_NULL)
if (!m_is_valid || section < 0 || section >= header->e_shnum ||
sections[section].sh_type == SHT_NULL)
{
return nullptr;
}
int nameOffset = sections[section].sh_name;
char* ptr = (char*)GetSectionDataPtr(header->e_shstrndx);
const Elf32_Shdr& string_section = sections[header->e_shstrndx];
const size_t name_offset = sections[section].sh_name;
const char* const ptr = reinterpret_cast<const char*>(GetSectionDataPtr(header->e_shstrndx));
if (ptr)
return ptr + nameOffset;
else
if (!ptr || name_offset >= string_section.sh_size ||
!std::memchr(ptr + name_offset, '\0', string_section.sh_size - name_offset))
{
return nullptr;
}
return ptr + name_offset;
}
// This is just a simple elf loader, good enough to load elfs generated by devkitPPC
bool ElfReader::LoadIntoMemory(Core::System& system, bool only_in_mem1) const
{
if (!m_is_valid)
return false;
INFO_LOG_FMT(BOOT, "String section: {}", header->e_shstrndx);
if (bRelocate)
@@ -183,15 +235,36 @@ SectionID ElfReader::GetSectionByName(const char* name, int firstSection) const
bool ElfReader::LoadSymbols(const Core::CPUThreadGuard& guard, PPCSymbolDB& ppc_symbol_db,
const std::string& filename) const
{
if (!m_is_valid)
return false;
bool hasSymbols = false;
SectionID sec = GetSectionByName(".symtab");
if (sec != -1)
{
int stringSection = sections[sec].sh_link;
const char* stringBase = (const char*)GetSectionDataPtr(stringSection);
const u32 string_section_index = sections[sec].sh_link;
if (string_section_index >= header->e_shnum)
{
ERROR_LOG_FMT(BOOT, "Invalid ELF symbol string table.");
return false;
}
const Elf32_Shdr& string_section = sections[string_section_index];
const char* stringBase = (const char*)GetSectionDataPtr(string_section_index);
if (!stringBase)
{
ERROR_LOG_FMT(BOOT, "ELF symbol string table has no data.");
return false;
}
// We have a symbol table!
Elf32_Sym* symtab = (Elf32_Sym*)(GetSectionDataPtr(sec));
if (!symtab)
{
ERROR_LOG_FMT(BOOT, "ELF symbol table has no data.");
return false;
}
int numSymbols = sections[sec].sh_size / sizeof(Elf32_Sym);
for (int sym = 0; sym < numSymbols; sym++)
{
@@ -203,7 +276,14 @@ bool ElfReader::LoadSymbols(const Core::CPUThreadGuard& guard, PPCSymbolDB& ppc_
int type = symtab[sym].st_info & 0xF;
int sectionIndex = Common::swap16(symtab[sym].st_shndx);
int value = Common::swap32(symtab[sym].st_value);
const char* name = stringBase + Common::swap32(symtab[sym].st_name);
const size_t name_offset = Common::swap32(symtab[sym].st_name);
if (name_offset >= string_section.sh_size ||
!std::memchr(stringBase + name_offset, '\0', string_section.sh_size - name_offset))
{
ERROR_LOG_FMT(BOOT, "Invalid ELF symbol name {}.", sym);
return false;
}
const char* name = stringBase + name_offset;
if (bRelocate)
value += sectionAddrs[sectionIndex];
@@ -229,6 +309,9 @@ bool ElfReader::LoadSymbols(const Core::CPUThreadGuard& guard, PPCSymbolDB& ppc_
bool ElfReader::IsWii() const
{
if (!m_is_valid)
return false;
// Use the same method as the DOL loader uses: search for mfspr from HID4,
// which should only be used in Wii ELFs.
//
+11 -11
View File
@@ -38,8 +38,7 @@ public:
bool LoadIntoMemory(Core::System& system, bool only_in_mem1 = false) const override;
bool LoadSymbols(const Core::CPUThreadGuard& guard, PPCSymbolDB& ppc_symbol_db,
const std::string& filename) const override;
// TODO: actually check for validity.
bool IsValid() const override { return true; }
bool IsValid() const override { return m_is_valid; }
bool IsWii() const override;
int GetNumSegments() const { return (int)(header->e_phnum); }
@@ -65,16 +64,17 @@ public:
bool DidRelocate() const { return bRelocate; }
private:
void Initialize(u8* bytes);
bool Initialize();
char* base;
u32* base32;
char* base = nullptr;
u32* base32 = nullptr;
Elf32_Ehdr* header;
Elf32_Phdr* segments;
Elf32_Shdr* sections;
Elf32_Ehdr* header = nullptr;
Elf32_Phdr* segments = nullptr;
Elf32_Shdr* sections = nullptr;
u32* sectionAddrs;
bool bRelocate;
u32 entryPoint;
u32* sectionAddrs = nullptr;
bool bRelocate = false;
u32 entryPoint = 0;
bool m_is_valid = false;
};
+13 -8
View File
@@ -59,21 +59,26 @@ PBUpdateData LoadPBUpdates(Memory::MemoryManager& memory, const PB_TYPE& pb)
// Apply updates to a PB.
void ApplyUpdatesForMs(int curr_ms, PB_TYPE& pb, u16* num_updates, const PBUpdateData& updates)
{
auto pb_mem = Common::BitCastToArray<u16>(pb);
u32 start_idx = 0;
for (int i = 0; i < curr_ms; ++i)
start_idx += num_updates[i];
for (u32 i = start_idx; i < start_idx + num_updates[curr_ms]; ++i)
if (start_idx < updates.size())
{
u16 update_off = updates[i].pb_offset;
u16 update_val = updates[i].new_value;
const u16 count = num_updates[curr_ms];
if (count <= updates.size() - start_idx)
{
const u32 end_idx = start_idx + count;
for (u32 i = start_idx; i < end_idx; ++i)
{
const u16 update_off = updates[i].pb_offset;
const u16 update_val = updates[i].new_value;
pb_mem[update_off] = update_val;
if (update_off < (sizeof(pb) / sizeof(u16)))
Common::BitCastPtr<u16>(&pb)[update_off] = update_val;
}
}
}
pb = std::bit_cast<PB_TYPE>(pb_mem);
}
// Used to pass a large amount of buffers to the mixing function.
+16 -3
View File
@@ -1468,9 +1468,16 @@ void ZeldaAudioRenderer::LoadInputSamples(MixingBuffer* buffer, VPB* vpb)
// the end of processing, if needed.
//
// Maximum of 0x500 samples here - see NeededRawSamplesCount to understand
// this practical limit (resampling_ratio = 0xFFFF -> 0x500 samples). Add a
// margin of 4 that is needed for samples source that do resampling.
std::array<s16, 0x500 + 4> raw_input_samples;
// this practical limit (resampling_ratio = 0xFFFF -> 0x500 samples).
//
// If current_pos_frac contains an (invalid) non-fractional part, it can push
// this up by another 15 samples. Which DownloadAFCSamplesFromARAM then rounds
// up to the next multiple of 16. So add an extra 0x10 samples to be safe.
//
// Plus we need an extra four samples at the start to hold the last four
// samples from the previous frame.
std::array<s16, 4 + 0x500 + 0x10> raw_input_samples;
for (size_t i = 0; i < 4; ++i)
raw_input_samples[i] = vpb->resample_buffer[i];
@@ -1715,6 +1722,12 @@ void ZeldaAudioRenderer::DownloadAFCSamplesFromARAM(s16* dst, VPB* vpb, u16 requ
return;
}
if (vpb->afc_remaining_decoded_samples > 0x10) [[unlikely]]
{
ERROR_LOG_FMT(DSPHLE, "afc_remaining_decoded_samples > 0x10");
vpb->afc_remaining_decoded_samples = 0x10;
}
// Try several things until we have output enough samples.
while (true)
{
+14 -2
View File
@@ -279,8 +279,18 @@ HostFileSystem::FstEntry* HostFileSystem::GetFstEntryForPath(const std::string&
void HostFileSystem::DoState(PointerWrap& p)
{
// Temporarily close the file, to prevent any issues with the savestating of files/folders.
for (Handle& handle : m_handles)
// This piece of code is handling four separate problems:
// 1. Close host handles by calling reset on them, in case DoStateRead needs to modify a file that
// was open.
// 2. Close guest handles by setting opened to false on each element in m_handles, in case
// DoStateRead needs to modify a file that was open.
// 3. Close guest handles by setting opened to false on each element in m_handles, because if all
// of them were open, it would make DoStateRead/DoStateWriteOrMeasure's calls to OpenFile fail.
// 4. Create a copy of m_handles that we can restore later in case we're writing/measuring,
// because OpenFile happily stomps over elements in m_handles that have opened set to false.
auto handles_copy = std::move(m_handles);
m_handles = {};
for (Handle& handle : handles_copy)
handle.host_file.reset();
// The format for the next part of the save state is follows:
@@ -316,6 +326,8 @@ void HostFileSystem::DoState(PointerWrap& p)
memcpy(nand_size_ptr, &size_of_nand, sizeof(size_of_nand));
}
}
m_handles = std::move(handles_copy);
}
else // case where we're in read mode.
{
+2 -2
View File
@@ -730,7 +730,7 @@ IPCReply NetIPTopDevice::HandleGetSockNameRequest(const IOCtlRequest& request)
request.Log(GetDeviceName(), Common::Log::LogType::IOS_WC24);
sockaddr sa;
sockaddr sa{};
socklen_t sa_len = sizeof(sa);
const int ret =
getsockname(GetEmulationKernel().GetSocketManager()->GetHostSocket(fd), &sa, &sa_len);
@@ -758,7 +758,7 @@ IPCReply NetIPTopDevice::HandleGetPeerNameRequest(const IOCtlRequest& request)
u32 fd = memory.Read_U32(request.buffer_in);
sockaddr sa;
sockaddr sa{};
socklen_t sa_len = sizeof(sa);
const int ret =
getpeername(GetEmulationKernel().GetSocketManager()->GetHostSocket(fd), &sa, &sa_len);
+37 -12
View File
@@ -173,7 +173,7 @@ bool CompressBufferIntoPacket(std::span<const u8> in_buffer, sf::Packet& packet)
bool DecompressPacketIntoFile(sf::Packet& packet, const std::string& file_path)
{
u64 file_size = Common::PacketReadU64(packet);
const u64 file_size = Common::PacketReadU64(packet);
if (file_size == 0)
return true;
@@ -187,11 +187,12 @@ bool DecompressPacketIntoFile(sf::Packet& packet, const std::string& file_path)
std::vector<u8> in_buffer(LZO_OUT_LEN);
std::vector<u8> out_buffer(LZO_IN_LEN);
u64 bytes_written = 0;
while (true)
{
u32 cur_len = 0; // number of bytes to read
lzo_uint new_len = 0; // number of bytes to write
u32 cur_len = 0; // number of bytes to read
lzo_uint new_len = out_buffer.size(); // output buffer capacity
packet >> cur_len;
if (!cur_len)
@@ -208,21 +209,29 @@ bool DecompressPacketIntoFile(sf::Packet& packet, const std::string& file_path)
packet >> in_buffer[j];
}
if (lzo1x_decompress(in_buffer.data(), cur_len, out_buffer.data(), &new_len, nullptr) !=
if (lzo1x_decompress_safe(in_buffer.data(), cur_len, out_buffer.data(), &new_len, nullptr) !=
LZO_E_OK)
{
PanicAlertFmtT("Internal LZO Error - decompression failed");
return false;
}
if (new_len > file_size - bytes_written)
{
PanicAlertFmtT("LZO error - output is too large");
return false;
}
if (!file.WriteBytes(out_buffer.data(), new_len))
{
PanicAlertFmtT("Error writing file: {0}", file_path);
return false;
}
bytes_written += new_len;
}
return true;
return bytes_written == file_size;
}
static bool DecompressPacketIntoFolderInternal(sf::Packet& packet, const std::string& folder_path)
@@ -268,20 +277,21 @@ bool DecompressPacketIntoFolder(sf::Packet& packet, const std::string& folder_pa
std::optional<std::vector<u8>> DecompressPacketIntoBuffer(sf::Packet& packet)
{
u64 size = Common::PacketReadU64(packet);
const u64 size = Common::PacketReadU64(packet);
std::vector<u8> out_buffer(size);
std::vector<u8> out_buffer;
if (size == 0)
return out_buffer;
std::vector<u8> in_buffer(LZO_OUT_LEN);
std::vector<u8> decompressed_buffer(LZO_IN_LEN);
lzo_uint i = 0;
u64 decompressed_size = 0;
while (true)
{
u32 cur_len = 0; // number of bytes to read
lzo_uint new_len = 0; // number of bytes to write
u32 cur_len = 0; // number of bytes to read
lzo_uint new_len = decompressed_buffer.size(); // output buffer capacity
packet >> cur_len;
if (!cur_len)
@@ -298,13 +308,28 @@ std::optional<std::vector<u8>> DecompressPacketIntoBuffer(sf::Packet& packet)
packet >> in_buffer[j];
}
if (lzo1x_decompress(in_buffer.data(), cur_len, &out_buffer[i], &new_len, nullptr) != LZO_E_OK)
if (lzo1x_decompress_safe(in_buffer.data(), cur_len, decompressed_buffer.data(), &new_len,
nullptr) != LZO_E_OK)
{
PanicAlertFmtT("Internal LZO Error - decompression failed");
return {};
}
i += new_len;
if (new_len > size - decompressed_size || new_len > out_buffer.max_size() - out_buffer.size())
{
PanicAlertFmtT("LZO error - output is too large");
return {};
}
out_buffer.insert(out_buffer.end(), decompressed_buffer.begin(),
decompressed_buffer.begin() + new_len);
decompressed_size += new_len;
}
if (decompressed_size != size)
{
PanicAlertFmtT("LZO error - output size mismatch");
return {};
}
return out_buffer;
+118 -25
View File
@@ -31,42 +31,84 @@
namespace DiscIO
{
static constexpr u64 uncompressed_flag = 1ULL << 63;
bool IsGCZBlob(File::DirectIOFile& file);
CompressedBlobReader::CompressedBlobReader(File::DirectIOFile file, std::string filename)
: m_file(std::move(file)), m_file_name(std::move(filename))
{
m_valid = Initialize();
}
bool CompressedBlobReader::Initialize()
{
m_file_size = m_file.GetSize();
m_file.Seek(0, File::SeekOrigin::Begin);
m_file.Read(Common::AsWritableU8Span(m_header));
if (!m_file.Read(Common::AsWritableU8Span(m_header)))
return false;
SetSectorSize(m_header.block_size);
if (m_header.magic_cookie != GCZ_MAGIC)
return false;
size_t block_pointers_size = m_header.num_blocks * sizeof(u64);
size_t hashes_size = m_header.num_blocks * sizeof(u32);
size_t header_size = sizeof(CompressedBlobHeader) + block_pointers_size + hashes_size;
// Basic sanity check for size before we start allocating
if (header_size > m_file_size)
{
ERROR_LOG_FMT(DISCIO, "Headers' size is larger than file size");
return false;
}
if ((header_size + m_header.compressed_data_size) > m_file_size)
{
ERROR_LOG_FMT(DISCIO, "Data size is larger than file size.");
return false;
}
if (m_header.num_blocks == 0)
{
ERROR_LOG_FMT(DISCIO, "GCZ file has zero blocks");
return false;
}
// cache block pointers and hashes
m_block_pointers.resize(m_header.num_blocks);
m_file.Read(Common::AsWritableU8Span(m_block_pointers));
if (!m_file.Read(Common::AsWritableU8Span(m_block_pointers)))
return false;
m_hashes.resize(m_header.num_blocks);
m_file.Read(Common::AsWritableU8Span(m_hashes));
if (!m_file.Read(Common::AsWritableU8Span(m_hashes)))
return false;
m_data_offset = (sizeof(CompressedBlobHeader)) +
(sizeof(u64)) * m_header.num_blocks // skip block pointers
+ (sizeof(u32)) * m_header.num_blocks; // skip hashes
m_data_offset = header_size;
// A compressed block is never ever longer than a decompressed block, so just header.block_size
// should be fine.
// I still add some safety margin.
const u32 zlib_buffer_size = m_header.block_size + 64;
m_zlib_buffer.resize(zlib_buffer_size);
SetSectorSize(m_header.block_size);
return ValidateBlockPointers();
}
std::unique_ptr<CompressedBlobReader> CompressedBlobReader::Create(File::DirectIOFile file,
const std::string& filename)
{
if (IsGCZBlob(file))
return std::unique_ptr<CompressedBlobReader>(
{
std::unique_ptr<CompressedBlobReader> reader(
new CompressedBlobReader(std::move(file), filename));
if (reader->m_valid)
return reader;
}
return nullptr;
}
@@ -80,9 +122,9 @@ std::unique_ptr<BlobReader> CompressedBlobReader::CopyReader() const
// IMPORTANT: Calling this function invalidates all earlier pointers gotten from this function.
u64 CompressedBlobReader::GetBlockCompressedSize(u64 block_num) const
{
u64 start = m_block_pointers[block_num];
u64 start = m_block_pointers[block_num] & ~uncompressed_flag;
if (block_num < m_header.num_blocks - 1)
return m_block_pointers[block_num + 1] - start;
return (m_block_pointers[block_num + 1] & ~uncompressed_flag) - start;
else if (block_num == m_header.num_blocks - 1)
return m_header.compressed_data_size - start;
else
@@ -92,22 +134,33 @@ u64 CompressedBlobReader::GetBlockCompressedSize(u64 block_num) const
bool CompressedBlobReader::GetBlock(u64 block_num, u8* out_ptr)
{
if (block_num >= m_header.num_blocks)
return false;
bool uncompressed = false;
u32 comp_block_size = (u32)GetBlockCompressedSize(block_num);
u64 read_size = GetBlockCompressedSize(block_num);
u64 offset = m_block_pointers[block_num] + m_data_offset;
if (offset & (1ULL << 63))
if (offset & uncompressed_flag)
{
if (comp_block_size != m_header.block_size)
if (read_size != m_header.block_size)
{
ERROR_LOG_FMT(DISCIO, "Uncompressed block with wrong size");
return false;
}
uncompressed = true;
offset &= ~(1ULL << 63);
offset &= ~uncompressed_flag;
}
else
{
if (read_size > m_zlib_buffer.size())
{
ERROR_LOG_FMT(DISCIO, "Compressed block is too large");
return false;
}
}
// clear unused part of zlib buffer. maybe this can be deleted when it works fully.
memset(&m_zlib_buffer[comp_block_size], 0, m_zlib_buffer.size() - comp_block_size);
if (!m_file.OffsetRead(offset, m_zlib_buffer.data(), comp_block_size))
if (!m_file.OffsetRead(offset, m_zlib_buffer.data(), read_size))
{
ERROR_LOG_FMT(DISCIO, "The disc image \"{}\" is truncated, some of the data is missing.",
m_file_name);
@@ -115,7 +168,7 @@ bool CompressedBlobReader::GetBlock(u64 block_num, u8* out_ptr)
}
// First, check hash.
const u32 block_hash = Common::HashAdler32(m_zlib_buffer.data(), comp_block_size);
const u32 block_hash = Common::HashAdler32(m_zlib_buffer.data(), read_size);
if (block_hash != m_hashes[block_num])
{
ERROR_LOG_FMT(DISCIO,
@@ -126,13 +179,13 @@ bool CompressedBlobReader::GetBlock(u64 block_num, u8* out_ptr)
if (uncompressed)
{
std::copy_n(m_zlib_buffer.begin(), comp_block_size, out_ptr);
std::copy_n(m_zlib_buffer.begin(), m_header.block_size, out_ptr);
}
else
{
z_stream z = {};
z.next_in = m_zlib_buffer.data();
z.avail_in = comp_block_size;
z.avail_in = read_size;
if (z.avail_in > m_header.block_size)
{
ERROR_LOG_FMT(DISCIO, "Compressed block size is larger than uncompressed block size");
@@ -158,6 +211,46 @@ bool CompressedBlobReader::GetBlock(u64 block_num, u8* out_ptr)
return true;
}
bool CompressedBlobReader::ValidateBlockPointers() const
{
size_t valid_pointers = 0;
// Validate block pointers
for (u32 i = 0; i < m_header.num_blocks; ++i)
{
u64 next;
if (i + 1 < m_header.num_blocks)
next = m_block_pointers[i + 1] & ~uncompressed_flag;
else
next = m_header.compressed_data_size;
if (next > m_header.compressed_data_size)
continue;
u64 offset = m_block_pointers[i] & ~uncompressed_flag;
if (offset > m_header.compressed_data_size)
continue;
bool uncompressed = m_block_pointers[i] & uncompressed_flag;
u64 size = next - offset;
if (uncompressed && size != m_header.block_size)
continue;
if (!uncompressed && size > m_zlib_buffer.size())
continue;
valid_pointers++;
}
size_t invalid_pointers = m_header.num_blocks - valid_pointers;
if (invalid_pointers > 0)
ERROR_LOG_FMT(DISCIO, "GCZ file has {} invalid block pointers", invalid_pointers);
return invalid_pointers == 0;
}
struct CompressThreadState
{
CompressThreadState() : z{} {}
@@ -245,7 +338,7 @@ static ConversionResultCode Output(OutputParameters parameters, File::DirectIOFi
{
u64 offset = *position;
if (!parameters.compressed)
offset |= 0x8000000000000000ULL;
offset |= uncompressed_flag;
(*offsets)[parameters.block_number] = offset;
*position += parameters.data.size();
@@ -293,10 +386,10 @@ bool ConvertToGCZ(BlobReader* infile, const std::string& infile_path,
header.magic_cookie = GCZ_MAGIC;
header.sub_type = sub_type;
header.block_size = block_size;
header.data_size = infile->GetDataSize();
header.disc_size = infile->GetDataSize();
// round upwards!
header.num_blocks = (u32)((header.data_size + (block_size - 1)) / block_size);
header.num_blocks = (u32)((header.disc_size + (block_size - 1)) / block_size);
std::vector<u64> offsets(header.num_blocks);
std::vector<u32> hashes(header.num_blocks);
@@ -332,7 +425,7 @@ bool ConvertToGCZ(BlobReader* infile, const std::string& infile_path,
if (compressor.GetStatus() != ConversionResultCode::Success)
break;
const u64 bytes_to_read = std::min<u64>(block_size, header.data_size - inpos);
const u64 bytes_to_read = std::min<u64>(block_size, header.disc_size - inpos);
if (!infile->Read(inpos, bytes_to_read, in_buf.data()))
{
+13 -10
View File
@@ -35,7 +35,7 @@ struct CompressedBlobHeader // 32 bytes
u32 magic_cookie; // 0xB10BB10B
u32 sub_type; // GC image, whatever
u64 compressed_data_size;
u64 data_size;
u64 disc_size;
u32 block_size;
u32 num_blocks;
};
@@ -53,7 +53,7 @@ public:
std::unique_ptr<BlobReader> CopyReader() const override;
u64 GetRawSize() const override { return m_file_size; }
u64 GetDataSize() const override { return m_header.data_size; }
u64 GetDataSize() const override { return m_header.disc_size; }
DataSizeType GetDataSizeType() const override { return DataSizeType::Accurate; }
u64 GetBlockSize() const override { return m_header.block_size; }
@@ -66,15 +66,18 @@ public:
private:
CompressedBlobReader(File::DirectIOFile file, std::string filename);
bool Initialize();
bool ValidateBlockPointers() const;
CompressedBlobHeader m_header;
std::vector<u64> m_block_pointers;
std::vector<u32> m_hashes;
int m_data_offset;
File::DirectIOFile m_file;
u64 m_file_size;
std::vector<u8> m_zlib_buffer;
std::string m_file_name;
CompressedBlobHeader m_header = {};
std::vector<u64> m_block_pointers = {};
std::vector<u32> m_hashes = {};
u64 m_data_offset = 0;
File::DirectIOFile m_file = {};
u64 m_file_size = 0;
std::vector<u8> m_zlib_buffer = {};
std::string m_file_name = {};
bool m_valid = false;
};
} // namespace DiscIO
@@ -9,6 +9,7 @@
#include <gtest/gtest.h>
#include "Common/ChunkFile.h"
#include "Common/CommonTypes.h"
#include "Common/FileUtil.h"
#include "Core/IOS/FS/FileSystem.h"
@@ -469,3 +470,108 @@ TEST_F(FileSystemTest, CreateFullPath)
EXPECT_EQ(m_fs->CreateFullPath(Uid{0x1000}, Gid{1}, "/shared2/wc24/mbox/Readme.txt", 0, modes),
ResultCode::Success);
}
TEST_F(FileSystemTest, DoState)
{
const std::string TEST_DATA_1 = "123";
const std::string TEST_DATA_2 = "4567";
std::array<u8, 4> read_buffer;
ASSERT_EQ(m_fs->CreateDirectory(Uid{1}, Gid{2}, "/tmp/a", 0, modes), ResultCode::Success);
ASSERT_EQ(m_fs->CreateDirectory(Uid{1}, Gid{2}, "/tmp/a/b", 0, modes), ResultCode::Success);
ASSERT_EQ(m_fs->CreateDirectory(Uid{0}, Gid{0}, "/tmp/a/c", 0, modes), ResultCode::Success);
ASSERT_EQ(m_fs->CreateFile(Uid{1}, Gid{2}, "/tmp/a/d", 0, modes), ResultCode::Success);
ASSERT_EQ(m_fs->CreateFile(Uid{3}, Gid{4}, "/tmp/e", 0, modes), ResultCode::Success);
{
Result<FileHandle> file1 = m_fs->OpenFile(Uid{1}, Gid{2}, "/tmp/a/d", Mode::ReadWrite);
ASSERT_TRUE(file1.has_value());
ASSERT_TRUE(file1->Write(TEST_DATA_1.data(), TEST_DATA_1.size()).has_value());
}
std::array<u8, 1024> state_buffer;
size_t state_size;
{
Result<FileHandle> file2 = m_fs->OpenFile(Uid{3}, Gid{4}, "/tmp/e", Mode::ReadWrite);
ASSERT_TRUE(file2.has_value());
ASSERT_TRUE(file2->Write(TEST_DATA_2.data(), TEST_DATA_2.size()).has_value());
u8* state_pointer = state_buffer.data();
PointerWrap p(&state_pointer, state_buffer.size(), PointerWrap::Mode::Write);
m_fs->DoState(p);
ASSERT_TRUE(p.IsWriteMode());
ASSERT_TRUE(file2->Seek(2, SeekMode::Set).has_value());
ASSERT_TRUE(file2->Write("_", 1).has_value());
Fd fd = file2->Release();
p.Do(fd);
ASSERT_TRUE(p.IsWriteMode());
state_size = state_pointer - state_buffer.data();
}
ASSERT_EQ(m_fs->Delete(Uid{0}, Gid{0}, "/tmp/a"), ResultCode::Success);
ASSERT_EQ(m_fs->GetMetadata(Uid{0}, Gid{0}, "/tmp/a").error(), ResultCode::NotFound);
ASSERT_EQ(m_fs->GetMetadata(Uid{0}, Gid{0}, "/tmp/a/b").error(), ResultCode::NotFound);
ASSERT_EQ(m_fs->GetMetadata(Uid{0}, Gid{0}, "/tmp/a/c").error(), ResultCode::NotFound);
ASSERT_EQ(m_fs->GetMetadata(Uid{0}, Gid{0}, "/tmp/a/d").error(), ResultCode::NotFound);
ASSERT_EQ(m_fs->CreateFile(Uid{5}, Gid{6}, "/tmp/f", 0, modes), ResultCode::Success);
ASSERT_EQ(m_fs->CreateDirectory(Uid{7}, Gid{8}, "/tmp/g", 0, modes), ResultCode::Success);
u8* state_pointer = state_buffer.data();
PointerWrap p(&state_pointer, state_size, PointerWrap::Mode::Read);
m_fs->DoState(p);
ASSERT_TRUE(p.IsReadMode());
constexpr auto check_directory_metadata = [](const Result<Metadata>& metadata, Uid uid, Gid gid) {
ASSERT_TRUE(metadata.has_value());
ASSERT_EQ(metadata->uid, uid);
ASSERT_EQ(metadata->gid, gid);
ASSERT_FALSE(metadata->is_file);
};
constexpr auto check_file_metadata = [](const Result<Metadata>& metadata, Uid uid, Gid gid,
u32 size) {
ASSERT_TRUE(metadata.has_value());
ASSERT_EQ(metadata->uid, uid);
ASSERT_EQ(metadata->gid, gid);
ASSERT_TRUE(metadata->is_file);
ASSERT_EQ(metadata->size, size);
};
check_directory_metadata(m_fs->GetMetadata(Uid{0}, Gid{0}, "/tmp/a"), Uid{1}, Gid{2});
check_directory_metadata(m_fs->GetMetadata(Uid{0}, Gid{0}, "/tmp/a/b"), Uid{1}, Gid{2});
check_directory_metadata(m_fs->GetMetadata(Uid{0}, Gid{0}, "/tmp/a/c"), Uid{0}, Gid{0});
check_file_metadata(m_fs->GetMetadata(Uid{0}, Gid{0}, "/tmp/a/d"), Uid{1}, Gid{2}, 3);
check_file_metadata(m_fs->GetMetadata(Uid{0}, Gid{0}, "/tmp/e"), Uid{3}, Gid{4}, 4);
ASSERT_EQ(m_fs->GetMetadata(Uid{0}, Gid{0}, "/tmp/f").error(), ResultCode::NotFound);
ASSERT_EQ(m_fs->GetMetadata(Uid{0}, Gid{0}, "/tmp/g").error(), ResultCode::NotFound);
Fd fd{};
p.Do(fd);
ASSERT_TRUE(p.IsReadMode());
{
FileHandle file2(m_fs.get(), fd);
ASSERT_EQ(file2.GetStatus()->offset, 4u);
ASSERT_TRUE(file2.Seek(0, SeekMode::Set).has_value());
ASSERT_TRUE(file2.Read(read_buffer.data(), TEST_DATA_2.size()).has_value());
for (size_t i = 0; i < TEST_DATA_2.size(); ++i)
ASSERT_EQ(read_buffer[i], TEST_DATA_2[i]);
}
{
Result<FileHandle> file1 = m_fs->OpenFile(Uid{5}, Gid{6}, "/tmp/a/d", Mode::Read);
ASSERT_TRUE(file1->Read(read_buffer.data(), TEST_DATA_1.size()).has_value());
for (size_t i = 0; i < TEST_DATA_1.size(); ++i)
ASSERT_EQ(read_buffer[i], TEST_DATA_1[i]);
}
}