From b2c4d543dd87e5344516d212ee29d256caa2adf1 Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Wed, 22 Jul 2026 03:21:19 +0200 Subject: [PATCH 01/16] HW/DSPHLE/AXVoice: Check array bounds in ApplyUpdatesForMs() Fixes https://github.com/dolphin-emu/dolphin/security/advisories/GHSA-4q28-hhjv-hf3f --- Source/Core/Core/HW/DSPHLE/UCodes/AXVoice.h | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Source/Core/Core/HW/DSPHLE/UCodes/AXVoice.h b/Source/Core/Core/HW/DSPHLE/UCodes/AXVoice.h index 54531a85f6..1655e28074 100644 --- a/Source/Core/Core/HW/DSPHLE/UCodes/AXVoice.h +++ b/Source/Core/Core/HW/DSPHLE/UCodes/AXVoice.h @@ -65,12 +65,21 @@ void ApplyUpdatesForMs(int curr_ms, PB_TYPE& pb, u16* num_updates, const PBUpdat 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 < pb_mem.size()) + pb_mem[update_off] = update_val; + } + } } pb = std::bit_cast(pb_mem); From 6d2e3a1ecc1f2b3b140b650eda9d9be5dc0a92e3 Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Wed, 22 Jul 2026 12:18:43 +0200 Subject: [PATCH 02/16] HW/DSPHLE/AXVoice: Prefer BitCastPtr over BitCastToArray in ApplyUpdatesForMs() --- Source/Core/Core/HW/DSPHLE/UCodes/AXVoice.h | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Source/Core/Core/HW/DSPHLE/UCodes/AXVoice.h b/Source/Core/Core/HW/DSPHLE/UCodes/AXVoice.h index 1655e28074..636c22ce65 100644 --- a/Source/Core/Core/HW/DSPHLE/UCodes/AXVoice.h +++ b/Source/Core/Core/HW/DSPHLE/UCodes/AXVoice.h @@ -59,8 +59,6 @@ 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(pb); - u32 start_idx = 0; for (int i = 0; i < curr_ms; ++i) start_idx += num_updates[i]; @@ -76,13 +74,11 @@ void ApplyUpdatesForMs(int curr_ms, PB_TYPE& pb, u16* num_updates, const PBUpdat const u16 update_off = updates[i].pb_offset; const u16 update_val = updates[i].new_value; - if (update_off < pb_mem.size()) - pb_mem[update_off] = update_val; + if (update_off < (sizeof(pb) / sizeof(u16))) + Common::BitCastPtr(&pb)[update_off] = update_val; } } } - - pb = std::bit_cast(pb_mem); } // Used to pass a large amount of buffers to the mixing function. From 5aa711db4144228435c41c6d4eacbab9cd6171db Mon Sep 17 00:00:00 2001 From: Tillmann Karras Date: Sat, 8 Aug 2026 19:32:53 +0100 Subject: [PATCH 03/16] DSPHLE/Zelda: prevent out-of-bounds stack read Reported by @RickdeJager. --- Source/Core/Core/HW/DSPHLE/UCodes/Zelda.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Source/Core/Core/HW/DSPHLE/UCodes/Zelda.cpp b/Source/Core/Core/HW/DSPHLE/UCodes/Zelda.cpp index 5db3c55aa8..d73d965bcd 100644 --- a/Source/Core/Core/HW/DSPHLE/UCodes/Zelda.cpp +++ b/Source/Core/Core/HW/DSPHLE/UCodes/Zelda.cpp @@ -1715,6 +1715,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) { From b6282fb9abfa30e849fc4644356a58fd40f5589b Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Mon, 20 Jul 2026 22:27:08 +0200 Subject: [PATCH 04/16] IOS/NetIPTopDevice: Zero-initialize sockaddr structs Fixes https://github.com/dolphin-emu/dolphin/security/advisories/GHSA-5fqv-9qrg-gm4j --- Source/Core/Core/IOS/Network/IP/Top.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Core/Core/IOS/Network/IP/Top.cpp b/Source/Core/Core/IOS/Network/IP/Top.cpp index 5040bfb7f8..86d206d8e4 100644 --- a/Source/Core/Core/IOS/Network/IP/Top.cpp +++ b/Source/Core/Core/IOS/Network/IP/Top.cpp @@ -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); From 2f5232fbf4c6b5856523e69b0634e36715981716 Mon Sep 17 00:00:00 2001 From: Scott Mansell Date: Sun, 26 Jul 2026 18:13:13 +1200 Subject: [PATCH 05/16] 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. --- Source/Core/DiscIO/CompressedBlob.cpp | 36 +++++++++++++++++---------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/Source/Core/DiscIO/CompressedBlob.cpp b/Source/Core/DiscIO/CompressedBlob.cpp index 5c2ab84cdd..eae4d4ccaf 100644 --- a/Source/Core/DiscIO/CompressedBlob.cpp +++ b/Source/Core/DiscIO/CompressedBlob.cpp @@ -31,6 +31,8 @@ namespace DiscIO { +static constexpr u64 uncompressed_flag = 1ULL << 63; + bool IsGCZBlob(File::DirectIOFile& file); CompressedBlobReader::CompressedBlobReader(File::DirectIOFile file, std::string filename) @@ -80,9 +82,9 @@ std::unique_ptr 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 @@ -93,21 +95,29 @@ u64 CompressedBlobReader::GetBlockCompressedSize(u64 block_num) const bool CompressedBlobReader::GetBlock(u64 block_num, u8* out_ptr) { 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 +125,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 +136,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"); From 8f1e33a5f47dc350dc32f168252ecb2eba9ed2a2 Mon Sep 17 00:00:00 2001 From: Scott Mansell Date: Sun, 26 Jul 2026 18:15:07 +1200 Subject: [PATCH 06/16] 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. --- Source/Core/DiscIO/CompressedBlob.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Source/Core/DiscIO/CompressedBlob.cpp b/Source/Core/DiscIO/CompressedBlob.cpp index eae4d4ccaf..036e139d0b 100644 --- a/Source/Core/DiscIO/CompressedBlob.cpp +++ b/Source/Core/DiscIO/CompressedBlob.cpp @@ -94,6 +94,9 @@ 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; u64 read_size = GetBlockCompressedSize(block_num); u64 offset = m_block_pointers[block_num] + m_data_offset; From 4eb90c7314bed84a007ef2ec6fa9c0e35b5d72d1 Mon Sep 17 00:00:00 2001 From: Scott Mansell Date: Sun, 26 Jul 2026 20:20:08 +1200 Subject: [PATCH 07/16] GCZ: validate while loading --- Source/Core/DiscIO/CompressedBlob.cpp | 104 +++++++++++++++++++++++--- Source/Core/DiscIO/CompressedBlob.h | 23 +++--- 2 files changed, 105 insertions(+), 22 deletions(-) diff --git a/Source/Core/DiscIO/CompressedBlob.cpp b/Source/Core/DiscIO/CompressedBlob.cpp index 036e139d0b..46573b6111 100644 --- a/Source/Core/DiscIO/CompressedBlob.cpp +++ b/Source/Core/DiscIO/CompressedBlob.cpp @@ -37,38 +37,78 @@ 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::Create(File::DirectIOFile file, const std::string& filename) { if (IsGCZBlob(file)) - return std::unique_ptr( + { + std::unique_ptr reader( new CompressedBlobReader(std::move(file), filename)); + if (reader->m_valid) + return reader; + } + return nullptr; } @@ -171,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{} {} @@ -258,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(); @@ -306,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 offsets(header.num_blocks); std::vector hashes(header.num_blocks); @@ -345,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(block_size, header.data_size - inpos); + const u64 bytes_to_read = std::min(block_size, header.disc_size - inpos); if (!infile->Read(inpos, bytes_to_read, in_buf.data())) { diff --git a/Source/Core/DiscIO/CompressedBlob.h b/Source/Core/DiscIO/CompressedBlob.h index 943353b169..6f44e77976 100644 --- a/Source/Core/DiscIO/CompressedBlob.h +++ b/Source/Core/DiscIO/CompressedBlob.h @@ -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 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 m_block_pointers; - std::vector m_hashes; - int m_data_offset; - File::DirectIOFile m_file; - u64 m_file_size; - std::vector m_zlib_buffer; - std::string m_file_name; + CompressedBlobHeader m_header = {}; + std::vector m_block_pointers = {}; + std::vector m_hashes = {}; + int m_data_offset = 0; + File::DirectIOFile m_file = {}; + u64 m_file_size = 0; + std::vector m_zlib_buffer = {}; + std::string m_file_name = {}; + bool m_valid = false; }; } // namespace DiscIO From b6d8da24b3d023c2a49498ab48b73258807ef688 Mon Sep 17 00:00:00 2001 From: Scott Mansell Date: Sun, 26 Jul 2026 22:22:18 +1200 Subject: [PATCH 08/16] 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. --- Source/Core/DiscIO/CompressedBlob.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Core/DiscIO/CompressedBlob.h b/Source/Core/DiscIO/CompressedBlob.h index 6f44e77976..712bb93917 100644 --- a/Source/Core/DiscIO/CompressedBlob.h +++ b/Source/Core/DiscIO/CompressedBlob.h @@ -72,7 +72,7 @@ private: CompressedBlobHeader m_header = {}; std::vector m_block_pointers = {}; std::vector m_hashes = {}; - int m_data_offset = 0; + u64 m_data_offset = 0; File::DirectIOFile m_file = {}; u64 m_file_size = 0; std::vector m_zlib_buffer = {}; From 611d86489714606b75e426d68be78e0d5a24fcc6 Mon Sep 17 00:00:00 2001 From: DacoTaco Date: Wed, 13 May 2026 20:26:47 +0200 Subject: [PATCH 09/16] fixes: make dolreader validate section addresses and sizes IOS and IPL reject non-32byte aligned sections --- Source/Core/Core/Boot/DolReader.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Source/Core/Core/Boot/DolReader.cpp b/Source/Core/Core/Boot/DolReader.cpp index 0ec828c519..aa5f3dae0d 100644 --- a/Source/Core/Core/Boot/DolReader.cpp +++ b/Source/Core/Core/Boot/DolReader.cpp @@ -53,6 +53,14 @@ bool DolReader::Initialize(std::span buffer) { if (m_dolheader.textSize[i] != 0) { + 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; + } + if (buffer.size() < m_dolheader.textOffset[i] + m_dolheader.textSize[i]) return false; @@ -80,6 +88,14 @@ bool DolReader::Initialize(std::span buffer) { u32 section_size = m_dolheader.dataSize[i]; u32 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; From 5e237fd28491f94e72ac3d87e854bb40ff4eeec2 Mon Sep 17 00:00:00 2001 From: Scott Mansell Date: Sun, 26 Jul 2026 20:44:20 +1200 Subject: [PATCH 10/16] 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. --- Source/Core/Core/Boot/DolReader.cpp | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/Source/Core/Core/Boot/DolReader.cpp b/Source/Core/Core/Boot/DolReader.cpp index aa5f3dae0d..b6bf74d808 100644 --- a/Source/Core/Core/Boot/DolReader.cpp +++ b/Source/Core/Core/Boot/DolReader.cpp @@ -55,17 +55,20 @@ bool DolReader::Initialize(std::span buffer) { if ((m_dolheader.textAddress[i] & 31) != 0 || (m_dolheader.textSize[i] & 31) != 0) { - ERROR_LOG_FMT(BOOT, + 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; } - if (buffer.size() < m_dolheader.textOffset[i] + m_dolheader.textSize[i]) + 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) { @@ -86,11 +89,11 @@ bool DolReader::Initialize(std::span 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, + 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; @@ -101,8 +104,7 @@ bool DolReader::Initialize(std::span buffer) std::vector 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 From a92697415379491f2cdccdee2cc956da2ca07fe1 Mon Sep 17 00:00:00 2001 From: Acts1631 Date: Sun, 19 Jul 2026 21:28:01 -0400 Subject: [PATCH 11/16] 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. --- Source/Core/Core/NetPlayCommon.cpp | 49 ++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/Source/Core/Core/NetPlayCommon.cpp b/Source/Core/Core/NetPlayCommon.cpp index 89ec2864a1..b1ecd04502 100644 --- a/Source/Core/Core/NetPlayCommon.cpp +++ b/Source/Core/Core/NetPlayCommon.cpp @@ -173,7 +173,7 @@ bool CompressBufferIntoPacket(std::span 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 in_buffer(LZO_OUT_LEN); std::vector 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> DecompressPacketIntoBuffer(sf::Packet& packet) { - u64 size = Common::PacketReadU64(packet); + const u64 size = Common::PacketReadU64(packet); - std::vector out_buffer(size); + std::vector out_buffer; if (size == 0) return out_buffer; std::vector in_buffer(LZO_OUT_LEN); + std::vector 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> 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; From 7c4ed02b27ab6e73656697b54b35f0d1d92fd1d4 Mon Sep 17 00:00:00 2001 From: Acts1631 Date: Mon, 20 Jul 2026 19:50:54 -0400 Subject: [PATCH 12/16] 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. --- Source/Core/Core/Boot/ElfReader.cpp | 102 +++++++++++++++++++++++----- Source/Core/Core/Boot/ElfReader.h | 22 +++--- 2 files changed, 95 insertions(+), 29 deletions(-) diff --git a/Source/Core/Core/Boot/ElfReader.cpp b/Source/Core/Core/Boot/ElfReader.cpp index f667387ab3..1a6e86e93e 100644 --- a/Source/Core/Core/Boot/ElfReader.cpp +++ b/Source/Core/Core/Boot/ElfReader.cpp @@ -3,6 +3,7 @@ #include "Core/Boot/ElfReader.h" +#include #include #include @@ -70,62 +71,106 @@ static void byteswapSection(Elf32_Shdr& sec) ElfReader::ElfReader(std::vector 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)) + return false; + + base = reinterpret_cast(m_bytes.data()); + base32 = reinterpret_cast(m_bytes.data()); + header = reinterpret_cast(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) + { + 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)) + { + return false; + } + + segments = reinterpret_cast(base + header->e_phoff); + sections = reinterpret_cast(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) + { + 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)) + { + 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(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 +228,27 @@ 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) + return false; + + const Elf32_Shdr& string_section = sections[string_section_index]; + const char* stringBase = (const char*)GetSectionDataPtr(string_section_index); + if (!stringBase) + return false; // We have a symbol table! Elf32_Sym* symtab = (Elf32_Sym*)(GetSectionDataPtr(sec)); + if (!symtab) + return false; + int numSymbols = sections[sec].sh_size / sizeof(Elf32_Sym); for (int sym = 0; sym < numSymbols; sym++) { @@ -203,7 +260,13 @@ 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)) + { + return false; + } + const char* name = stringBase + name_offset; if (bRelocate) value += sectionAddrs[sectionIndex]; @@ -229,6 +292,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. // diff --git a/Source/Core/Core/Boot/ElfReader.h b/Source/Core/Core/Boot/ElfReader.h index 83fd878012..cb6c895496 100644 --- a/Source/Core/Core/Boot/ElfReader.h +++ b/Source/Core/Core/Boot/ElfReader.h @@ -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; }; From f29ed945d0913f76e3c67eaf4e806f30244bf042 Mon Sep 17 00:00:00 2001 From: Acts1631 Date: Mon, 20 Jul 2026 21:10:29 -0400 Subject: [PATCH 13/16] 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. --- Source/Core/Core/Boot/ElfReader.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Source/Core/Core/Boot/ElfReader.cpp b/Source/Core/Core/Boot/ElfReader.cpp index 1a6e86e93e..8cdc1770a5 100644 --- a/Source/Core/Core/Boot/ElfReader.cpp +++ b/Source/Core/Core/Boot/ElfReader.cpp @@ -89,7 +89,10 @@ ElfReader::~ElfReader() = default; bool ElfReader::Initialize() { if (m_bytes.size() < sizeof(Elf32_Ehdr)) + { + ERROR_LOG_FMT(BOOT, "ELF file is too small."); return false; + } base = reinterpret_cast(m_bytes.data()); base32 = reinterpret_cast(m_bytes.data()); @@ -98,6 +101,7 @@ bool ElfReader::Initialize() 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; } @@ -113,6 +117,7 @@ bool ElfReader::Initialize() !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; } @@ -125,6 +130,7 @@ bool ElfReader::Initialize() 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; } } @@ -135,6 +141,7 @@ bool ElfReader::Initialize() 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; } } @@ -237,17 +244,26 @@ bool ElfReader::LoadSymbols(const Core::CPUThreadGuard& guard, PPCSymbolDB& ppc_ { 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++) @@ -264,6 +280,7 @@ bool ElfReader::LoadSymbols(const Core::CPUThreadGuard& guard, PPCSymbolDB& ppc_ 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; From 6376e0c1f130d7c3036f6be2bc38af1ea113242c Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 26 Jul 2026 10:48:23 +0200 Subject: [PATCH 14/16] 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. --- Source/Core/Core/IOS/FS/HostBackend/FS.cpp | 16 ++- .../UnitTests/Core/IOS/FS/FileSystemTest.cpp | 106 ++++++++++++++++++ 2 files changed, 120 insertions(+), 2 deletions(-) diff --git a/Source/Core/Core/IOS/FS/HostBackend/FS.cpp b/Source/Core/Core/IOS/FS/HostBackend/FS.cpp index 8a058abf7e..31597950a6 100644 --- a/Source/Core/Core/IOS/FS/HostBackend/FS.cpp +++ b/Source/Core/Core/IOS/FS/HostBackend/FS.cpp @@ -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. { diff --git a/Source/UnitTests/Core/IOS/FS/FileSystemTest.cpp b/Source/UnitTests/Core/IOS/FS/FileSystemTest.cpp index 37d047815a..14306cb46c 100644 --- a/Source/UnitTests/Core/IOS/FS/FileSystemTest.cpp +++ b/Source/UnitTests/Core/IOS/FS/FileSystemTest.cpp @@ -9,6 +9,7 @@ #include +#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 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 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 state_buffer; + size_t state_size; + + { + Result 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, 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, 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 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]); + } +} From 94b20d52f044ec3c19e0471ac505fe713980fe50 Mon Sep 17 00:00:00 2001 From: OatmealDome Date: Wed, 22 Jul 2026 17:11:18 -0400 Subject: [PATCH 15/16] BuildMacOSUniversalBinary: Add flag to enable CCache --- BuildMacOSUniversalBinary.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/BuildMacOSUniversalBinary.py b/BuildMacOSUniversalBinary.py index 3fc84a1c15..879b80ae09 100755 --- a/BuildMacOSUniversalBinary.py +++ b/BuildMacOSUniversalBinary.py @@ -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) From 596d71fbba607c4d887d851671901817e6cc3e50 Mon Sep 17 00:00:00 2001 From: OatmealDome Date: Mon, 10 Aug 2026 18:06:46 -0400 Subject: [PATCH 16/16] ScmRevGen: Bump version to 2606a --- CMake/ScmRevGen.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMake/ScmRevGen.cmake b/CMake/ScmRevGen.cmake index baa2d0ae41..5826f517c0 100644 --- a/CMake/ScmRevGen.cmake +++ b/CMake/ScmRevGen.cmake @@ -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