From 57b81f46b5dd60aed50cb2df05a4b49ac31c6adc Mon Sep 17 00:00:00 2001 From: Bruno <56844494+fab918@users.noreply.github.com> Date: Fri, 9 Oct 2026 15:35:32 +0200 Subject: [PATCH] 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. --- .gitmodules | 4 + external/zbic | 1 + src/nxemu-loader/core/loader/nso.cpp | 83 +++++++++++++++++---- src/nxemu-loader/core/loader/nso.h | 6 ++ src/yuzu_common/CMakeLists.txt | 6 ++ src/yuzu_common/yuzu_common.vcxproj | 4 + src/yuzu_common/yuzu_common.vcxproj.filters | 6 ++ src/yuzu_common/zbic_compression.cpp | 56 ++++++++++++++ src/yuzu_common/zbic_compression.h | 16 ++++ 9 files changed, 167 insertions(+), 15 deletions(-) create mode 160000 external/zbic create mode 100644 src/yuzu_common/zbic_compression.cpp create mode 100644 src/yuzu_common/zbic_compression.h diff --git a/.gitmodules b/.gitmodules index a107a4e3..d239966e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -67,3 +67,7 @@ [submodule "external/libadrenotools"] path = external/libadrenotools url = https://github.com/bylaws/libadrenotools.git + +[submodule "external/zbic"] + path = external/zbic + url = https://github.com/kinnay/zbic.git diff --git a/external/zbic b/external/zbic new file mode 160000 index 00000000..11b08f27 --- /dev/null +++ b/external/zbic @@ -0,0 +1 @@ +Subproject commit 11b08f2712264bbed731545085cbd9702096ceb7 diff --git a/src/nxemu-loader/core/loader/nso.cpp b/src/nxemu-loader/core/loader/nso.cpp index cb22ba85..c6ac79d5 100644 --- a/src/nxemu-loader/core/loader/nso.cpp +++ b/src/nxemu-loader/core/loader/nso.cpp @@ -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-License-Identifier: GPL-2.0-or-later #include #include +#include +#include #include #include "core/core.h" @@ -16,6 +21,7 @@ #include "yuzu_common/hex_util.h" #include "yuzu_common/logging/log.h" #include "yuzu_common/lz4_compression.h" +#include "yuzu_common/zbic_compression.h" #include "yuzu_common/settings.h" #include "yuzu_common/swap.h" @@ -35,15 +41,6 @@ struct MODHeader }; static_assert(sizeof(MODHeader) == 0x1c, "MODHeader has incorrect size."); -std::vector DecompressSegment(const std::vector & compressed_data, const NSOSegmentHeader & header) -{ - std::vector 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) { return static_cast((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; } + AppLoader_NSO::AppLoader_NSO(FileSys::VirtualFile file_) : AppLoader(std::move(file_)) { @@ -102,19 +100,74 @@ std::optional AppLoader_NSO::LoadModule(Systemloader & loader, ISystemMod Kernel::PhysicalMemory program_image; for (std::size_t i = 0; i < nso_header.segments.size(); ++i) { - std::vector data = nso_file.ReadBytes(nso_header.segments_compressed_size[i], nso_header.segments[i].offset); - if (nso_header.IsSegmentCompressed(i)) + const auto & segment = nso_header.segments[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(segment.location) + segment.size; + if (segment.offset > file_size || stored_size > file_size - segment.offset || + segment.size > static_cast(std::numeric_limits::max()) || + stored_size > static_cast(std::numeric_limits::max()) || + image_end > std::numeric_limits::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 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 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(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(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].offset = module_start + nso_header.segments[i].location; 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(nso_header.segments[2].bss_size) > + std::numeric_limits::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()}; diff --git a/src/nxemu-loader/core/loader/nso.h b/src/nxemu-loader/core/loader/nso.h index cb35813b..2e484683 100644 --- a/src/nxemu-loader/core/loader/nso.h +++ b/src/nxemu-loader/core/loader/nso.h @@ -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-License-Identifier: GPL-2.0-or-later @@ -47,6 +50,9 @@ struct NSOHeader { std::array segment_hashes; 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(std::is_trivially_copyable_v, "NSOHeader must be trivially copyable."); diff --git a/src/yuzu_common/CMakeLists.txt b/src/yuzu_common/CMakeLists.txt index 84556701..602ba8cb 100644 --- a/src/yuzu_common/CMakeLists.txt +++ b/src/yuzu_common/CMakeLists.txt @@ -116,6 +116,8 @@ add_library(yuzu_common STATIC wall_clock.h yuzu_assert.cpp yuzu_assert.h + zbic_compression.cpp + zbic_compression.h zstd_compression.cpp 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(zbic_compression.cpp PROPERTIES + INCLUDE_DIRECTORIES "${CMAKE_SOURCE_DIR}/external/zbic/src" + COMPILE_OPTIONS "$<$:-Wno-unused-function;-Wno-missing-declarations;-Wno-shadow>") + if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") target_compile_options(yuzu_common PRIVATE -fsized-deallocation diff --git a/src/yuzu_common/yuzu_common.vcxproj b/src/yuzu_common/yuzu_common.vcxproj index 81baa4da..e004bad1 100644 --- a/src/yuzu_common/yuzu_common.vcxproj +++ b/src/yuzu_common/yuzu_common.vcxproj @@ -135,6 +135,7 @@ + @@ -177,6 +178,9 @@ + + $(SolutionDir)external\zbic\src;%(AdditionalIncludeDirectories) + diff --git a/src/yuzu_common/yuzu_common.vcxproj.filters b/src/yuzu_common/yuzu_common.vcxproj.filters index 8383e74e..f3b49b0f 100644 --- a/src/yuzu_common/yuzu_common.vcxproj.filters +++ b/src/yuzu_common/yuzu_common.vcxproj.filters @@ -306,6 +306,9 @@ Header Files + + Header Files + Header Files @@ -458,6 +461,9 @@ Source Files + + Source Files + Source Files diff --git a/src/yuzu_common/zbic_compression.cpp b/src/yuzu_common/zbic_compression.cpp new file mode 100644 index 00000000..48d6bced --- /dev/null +++ b/src/yuzu_common/zbic_compression.cpp @@ -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 +#elif defined(__has_feature) +#if __has_feature(address_sanitizer) +#include +#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 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 dst, std::span 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 diff --git a/src/yuzu_common/zbic_compression.h b/src/yuzu_common/zbic_compression.h new file mode 100644 index 00000000..7cc3148e --- /dev/null +++ b/src/yuzu_common/zbic_compression.h @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include "yuzu_common/common_types.h" + +namespace Common::Compression { + +[[nodiscard]] bool IsZBIC(std::span src); + +// Succeeds only if the frame produces exactly the caller's expected segment size. +[[nodiscard]] bool DecompressDataZBIC(std::span dst, std::span src); + +} // namespace Common::Compression