Executable loading changes (#393)

Load ZBIC-compressed NSO modules with checked sizes and reads. Keep LZ4 support and ordinary ZSTD separate. Allow the embedded codec to build with AddressSanitizer.

Adapted from Eden PR #4482 by PavelBARABANOV and Eden contributors:
https://git.eden-emu.dev/eden-emu/eden/pulls/4482

The pinned ZBIC dependency comes from kinnay/zbic and retains its original notices.
This commit is contained in:
Bruno 2026-10-09 15:35:32 +02:00 committed by GitHub
parent 4a0bc1497c
commit 57b81f46b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 167 additions and 15 deletions

4
.gitmodules vendored
View File

@ -67,3 +67,7 @@
[submodule "external/libadrenotools"] [submodule "external/libadrenotools"]
path = external/libadrenotools path = external/libadrenotools
url = https://github.com/bylaws/libadrenotools.git url = https://github.com/bylaws/libadrenotools.git
[submodule "external/zbic"]
path = external/zbic
url = https://github.com/kinnay/zbic.git

1
external/zbic vendored Submodule

@ -0,0 +1 @@
Subproject commit 11b08f2712264bbed731545085cbd9702096ceb7

View File

@ -1,8 +1,13 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <cinttypes> #include <cinttypes>
#include <cstring> #include <cstring>
#include <limits>
#include <span>
#include <vector> #include <vector>
#include "core/core.h" #include "core/core.h"
@ -16,6 +21,7 @@
#include "yuzu_common/hex_util.h" #include "yuzu_common/hex_util.h"
#include "yuzu_common/logging/log.h" #include "yuzu_common/logging/log.h"
#include "yuzu_common/lz4_compression.h" #include "yuzu_common/lz4_compression.h"
#include "yuzu_common/zbic_compression.h"
#include "yuzu_common/settings.h" #include "yuzu_common/settings.h"
#include "yuzu_common/swap.h" #include "yuzu_common/swap.h"
@ -35,15 +41,6 @@ struct MODHeader
}; };
static_assert(sizeof(MODHeader) == 0x1c, "MODHeader has incorrect size."); static_assert(sizeof(MODHeader) == 0x1c, "MODHeader has incorrect size.");
std::vector<u8> DecompressSegment(const std::vector<u8> & compressed_data, const NSOSegmentHeader & header)
{
std::vector<u8> uncompressed_data = Common::Compression::DecompressDataLZ4(compressed_data, header.size);
ASSERT_MSG(uncompressed_data.size() == header.size, "{} != {}", header.size, uncompressed_data.size());
return uncompressed_data;
}
constexpr u32 PageAlignSize(u32 size) constexpr u32 PageAlignSize(u32 size)
{ {
return static_cast<u32>((size + Core::Memory::YUZU_PAGEMASK) & ~Core::Memory::YUZU_PAGEMASK); return static_cast<u32>((size + Core::Memory::YUZU_PAGEMASK) & ~Core::Memory::YUZU_PAGEMASK);
@ -56,6 +53,7 @@ bool NSOHeader::IsSegmentCompressed(size_t segment_num) const
return ((flags >> segment_num) & 1) != 0; return ((flags >> segment_num) & 1) != 0;
} }
AppLoader_NSO::AppLoader_NSO(FileSys::VirtualFile file_) : AppLoader_NSO::AppLoader_NSO(FileSys::VirtualFile file_) :
AppLoader(std::move(file_)) AppLoader(std::move(file_))
{ {
@ -102,19 +100,74 @@ std::optional<VAddr> AppLoader_NSO::LoadModule(Systemloader & loader, ISystemMod
Kernel::PhysicalMemory program_image; Kernel::PhysicalMemory program_image;
for (std::size_t i = 0; i < nso_header.segments.size(); ++i) for (std::size_t i = 0; i < nso_header.segments.size(); ++i)
{ {
std::vector<u8> data = nso_file.ReadBytes(nso_header.segments_compressed_size[i], nso_header.segments[i].offset); const auto & segment = nso_header.segments[i];
if (nso_header.IsSegmentCompressed(i)) const bool compressed = nso_header.IsSegmentCompressed(i);
const u32 stored_size = compressed ? nso_header.segments_compressed_size[i] : segment.size;
const u64 file_size = nso_file.GetSize();
// Check in wide arithmetic before allocating or calling the int-sized LZ4 API.
const u64 image_end = module_start + static_cast<u64>(segment.location) + segment.size;
if (segment.offset > file_size || stored_size > file_size - segment.offset ||
segment.size > static_cast<u32>(std::numeric_limits<int>::max()) ||
stored_size > static_cast<u32>(std::numeric_limits<int>::max()) ||
image_end > std::numeric_limits<u32>::max() - Core::Memory::YUZU_PAGEMASK)
{ {
data = DecompressSegment(data, nso_header.segments[i]); LOG_ERROR(Loader, "Invalid NSO segment {} in {} (offset={:#x}, stored={}, size={})",
i, nso_file.GetName(), segment.offset, stored_size, segment.size);
return std::nullopt;
}
std::vector<u8> data = nso_file.ReadBytes(stored_size, segment.offset);
if (data.size() != stored_size)
{
LOG_ERROR(Loader, "Truncated NSO segment {} in {}", i, nso_file.GetName());
return std::nullopt;
}
if (compressed)
{
if (nso_header.IsZBICCompressed())
{
std::vector<u8> uncompressed_data(segment.size);
if (!Common::Compression::DecompressDataZBIC(uncompressed_data, data))
{
LOG_ERROR(Loader, "ZBIC decompression failed for segment {} in {}: expected {} bytes",
i, nso_file.GetName(), segment.size);
return std::nullopt;
}
data = std::move(uncompressed_data);
}
else
{
data = Common::Compression::DecompressDataLZ4(data, segment.size);
if (data.size() != segment.size)
{
LOG_ERROR(Loader, "LZ4 decompression failed for segment {} in {}: expected {} bytes",
i, nso_file.GetName(), segment.size);
return std::nullopt;
}
}
}
// Preserve earlier segments even when a later segment is empty or located below them.
program_image.resize((std::max)(program_image.size(), static_cast<size_t>(image_end)));
if (!data.empty())
{
std::memcpy(program_image.data() + module_start + segment.location, data.data(), data.size());
} }
program_image.resize(module_start + nso_header.segments[i].location + static_cast<u32>(data.size()));
std::memcpy(program_image.data() + module_start + nso_header.segments[i].location, data.data(), data.size());
codeset.segments[i].addr = module_start + nso_header.segments[i].location; codeset.segments[i].addr = module_start + nso_header.segments[i].location;
codeset.segments[i].offset = module_start + nso_header.segments[i].location; codeset.segments[i].offset = module_start + nso_header.segments[i].location;
codeset.segments[i].size = nso_header.segments[i].size; codeset.segments[i].size = nso_header.segments[i].size;
} }
if (should_pass_arguments && !Settings::values.program_args.GetValue().empty()) const u64 argument_size = should_pass_arguments && !Settings::values.program_args.GetValue().empty()
? NSO_ARGUMENT_DATA_ALLOCATION_SIZE : 0;
if (program_image.size() + argument_size + static_cast<u64>(nso_header.segments[2].bss_size) >
std::numeric_limits<u32>::max() - Core::Memory::YUZU_PAGEMASK ||
(argument_size != 0 && Settings::values.program_args.GetValue().size() >
NSO_ARGUMENT_DATA_ALLOCATION_SIZE - sizeof(NSOArgumentHeader)))
{
LOG_ERROR(Loader, "Invalid NSO BSS or argument size in {}", nso_file.GetName());
return std::nullopt;
}
if (argument_size != 0)
{ {
const auto arg_data{Settings::values.program_args.GetValue()}; const auto arg_data{Settings::values.program_args.GetValue()};

View File

@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
@ -47,6 +50,9 @@ struct NSOHeader {
std::array<SHA256Hash, 3> segment_hashes; std::array<SHA256Hash, 3> segment_hashes;
bool IsSegmentCompressed(size_t segment_num) const; bool IsSegmentCompressed(size_t segment_num) const;
bool IsZBICCompressed() const {
return ((flags >> 7) & 1) != 0;
}
}; };
static_assert(sizeof(NSOHeader) == 0x100, "NSOHeader has incorrect size."); static_assert(sizeof(NSOHeader) == 0x100, "NSOHeader has incorrect size.");
static_assert(std::is_trivially_copyable_v<NSOHeader>, "NSOHeader must be trivially copyable."); static_assert(std::is_trivially_copyable_v<NSOHeader>, "NSOHeader must be trivially copyable.");

View File

@ -116,6 +116,8 @@ add_library(yuzu_common STATIC
wall_clock.h wall_clock.h
yuzu_assert.cpp yuzu_assert.cpp
yuzu_assert.h yuzu_assert.h
zbic_compression.cpp
zbic_compression.h
zstd_compression.cpp zstd_compression.cpp
zstd_compression.h zstd_compression.h
) )
@ -170,6 +172,10 @@ endif()
set_source_files_properties(stb.cpp PROPERTIES COMPILE_OPTIONS "-Wno-implicit-fallthrough;-Wno-missing-declarations;-Wno-missing-field-initializers") set_source_files_properties(stb.cpp PROPERTIES COMPILE_OPTIONS "-Wno-implicit-fallthrough;-Wno-missing-declarations;-Wno-missing-field-initializers")
set_source_files_properties(zbic_compression.cpp PROPERTIES
INCLUDE_DIRECTORIES "${CMAKE_SOURCE_DIR}/external/zbic/src"
COMPILE_OPTIONS "$<$<CXX_COMPILER_ID:Clang,GNU>:-Wno-unused-function;-Wno-missing-declarations;-Wno-shadow>")
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
target_compile_options(yuzu_common PRIVATE target_compile_options(yuzu_common PRIVATE
-fsized-deallocation -fsized-deallocation

View File

@ -135,6 +135,7 @@
<ClInclude Include="x64\xbyak_abi.h" /> <ClInclude Include="x64\xbyak_abi.h" />
<ClInclude Include="x64\xbyak_util.h" /> <ClInclude Include="x64\xbyak_util.h" />
<ClInclude Include="yuzu_assert.h" /> <ClInclude Include="yuzu_assert.h" />
<ClInclude Include="zbic_compression.h" />
<ClInclude Include="zstd_compression.h" /> <ClInclude Include="zstd_compression.h" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -177,6 +178,9 @@
<ClCompile Include="x64\native_clock.cpp" /> <ClCompile Include="x64\native_clock.cpp" />
<ClCompile Include="x64\rdtsc.cpp" /> <ClCompile Include="x64\rdtsc.cpp" />
<ClCompile Include="yuzu_assert.cpp" /> <ClCompile Include="yuzu_assert.cpp" />
<ClCompile Include="zbic_compression.cpp">
<AdditionalIncludeDirectories>$(SolutionDir)external\zbic\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<ClCompile Include="zstd_compression.cpp" /> <ClCompile Include="zstd_compression.cpp" />
</ItemGroup> </ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" /> <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />

View File

@ -306,6 +306,9 @@
<ClInclude Include="socket_types.h"> <ClInclude Include="socket_types.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="zbic_compression.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="zstd_compression.h"> <ClInclude Include="zstd_compression.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
@ -458,6 +461,9 @@
<ClCompile Include="stb.cpp"> <ClCompile Include="stb.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="zbic_compression.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="zstd_compression.cpp"> <ClCompile Include="zstd_compression.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>

View File

@ -0,0 +1,56 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// Adapted from Eden PR #4482 (PavelBARABANOV and Eden contributors).
// https://git.eden-emu.dev/eden-emu/eden/pulls/4482
#include "yuzu_common/zbic_compression.h"
// The C++ amalgamation must use the sanitizer runtime's C declarations.
#if defined(__SANITIZE_ADDRESS__)
#include <sanitizer/asan_interface.h>
#elif defined(__has_feature)
#if __has_feature(address_sanitizer)
#include <sanitizer/asan_interface.h>
#endif
#endif
#define ZSTD_ZBIC_SUPPORT 1
#define ZSTDLIB_VISIBLE static
#define ZSTDLIB_HIDDEN static
#define ZSTDERRORLIB_VISIBLE static
#define ZSTDERRORLIB_HIDDEN static
#define ZDICTLIB_VISIBLE static
#define ZDICTLIB_HIDDEN static
#define g_debuglevel g_ZSTD_zbic_debuglevel
#undef ZSTD_MULTITHREAD
#if defined(__ANDROID__)
#undef _GNU_SOURCE
#endif
#include "zstd.h"
#define g_ZSTD_threading_useless_symbol g_ZSTD_zbic_threading_useless_symbol
#include "zstd.c"
#undef g_ZSTD_threading_useless_symbol
#undef g_debuglevel
namespace Common::Compression {
bool IsZBIC(std::span<const u8> src) {
if (src.size() < sizeof(u32)) {
return false;
}
return src[0] == 'Z' && src[1] == 'B' && src[2] == 'I' && src[3] == 'C';
}
bool DecompressDataZBIC(std::span<u8> dst, std::span<const u8> src) {
if (!IsZBIC(src)) {
return false;
}
const size_t res = ZSTD_decompress(dst.data(), dst.size(), src.data(), src.size());
return !ZSTD_isError(res) && res == dst.size();
}
} // namespace Common::Compression

View File

@ -0,0 +1,16 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <span>
#include "yuzu_common/common_types.h"
namespace Common::Compression {
[[nodiscard]] bool IsZBIC(std::span<const u8> src);
// Succeeds only if the frame produces exactly the caller's expected segment size.
[[nodiscard]] bool DecompressDataZBIC(std::span<u8> dst, std::span<const u8> src);
} // namespace Common::Compression