From 2f31d26fd4fda184330f99d2006c8808dab55690 Mon Sep 17 00:00:00 2001 From: Acts1631 Date: Thu, 23 Jul 2026 14:31:14 -0400 Subject: [PATCH 1/3] Fix PKG extraction path traversal PKG entry names could contain traversal components and escape the installation directory. Reject absolute and special components, then require the resolved target to remain below the installation root before creating or writing files. --- rpcs3/Crypto/unpkg.cpp | 68 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 59 insertions(+), 9 deletions(-) diff --git a/rpcs3/Crypto/unpkg.cpp b/rpcs3/Crypto/unpkg.cpp index c9a49348a53d..bffa5ea500d9 100644 --- a/rpcs3/Crypto/unpkg.cpp +++ b/rpcs3/Crypto/unpkg.cpp @@ -877,6 +877,25 @@ bool package_reader::fill_data(std::map& all_instal return false; } + std::error_code path_ec; + auto install_path = std::filesystem::weakly_canonical(m_install_path, path_ec); + if (path_ec) + { + pkg_log.warning("Failed to canonicalize installation path '%s' (%s); falling back to lexical normalization.", m_install_path, path_ec.message()); + install_path = std::filesystem::path(m_install_path).lexically_normal(); + } + + if (install_path.empty()) + { + pkg_log.error("Failed to normalize installation path for '%s'", m_install_path); + return false; + } + + const auto is_inside_install_path = [&install_path](const std::filesystem::path& path) + { + return std::mismatch(install_path.begin(), install_path.end(), path.begin(), path.end()).first == install_path.end(); + }; + m_install_entries.clear(); m_bootable_file_path.clear(); m_entry_indexer = 0; @@ -914,7 +933,46 @@ bool package_reader::fill_data(std::map& all_instal std::string_view name = fmt::trim_back_sv(name_buf, "\0"sv); + const std::filesystem::path entry_path{name}; + if (entry_path.is_absolute()) + { + num_failures++; + pkg_log.error("PKG entry path is absolute: '%s'", name); + break; + } + + for (const auto& component : entry_path) + { + if (component == "." || component == "..") + { + num_failures++; + pkg_log.error("PKG entry path contains a special component: '%s'", name); + break; + } + } + + if (num_failures) + { + break; + } + std::string path = m_install_path + vfs::escape(name); + path_ec.clear(); + auto canonical_path = std::filesystem::weakly_canonical(path, path_ec); + if (path_ec) + { + pkg_log.warning("Failed to canonicalize package path '%s' (%s); falling back to lexical normalization.", path, path_ec.message()); + canonical_path = std::filesystem::path(path).lexically_normal(); + } + + if (canonical_path.empty() || !is_inside_install_path(canonical_path)) + { + num_failures++; + pkg_log.error("PKG entry path escapes installation directory: '%s'", name); + break; + } + + path = canonical_path.string(); if (entry.pad || (entry.type & ~PKG_FILE_ENTRY_KNOWN_BITS)) { @@ -952,15 +1010,7 @@ bool package_reader::fill_data(std::map& all_instal default: { // TODO: check for valid utf8 characters - const std::string true_path = std::filesystem::path(path).lexically_normal().string(); - if (true_path.empty()) - { - num_failures++; - pkg_log.error("Failed to normalize package path for '%s'", path); - break; - } - - auto map_ptr = &*all_install_entries.try_emplace(true_path).first; + auto map_ptr = &*all_install_entries.try_emplace(path).first; m_install_entries.push_back({ .weak_reference = map_ptr, From 1c2c62f94751f2cc4eaf856b8a643c64df7790f4 Mon Sep 17 00:00:00 2001 From: Acts1631 Date: Thu, 23 Jul 2026 14:31:14 -0400 Subject: [PATCH 2/3] Bound PKG entry table allocation PKG file_count was used for vector allocation before validating that the encrypted package data contained the entry table. Limit the count and require the entry table to fit in the declared data region before allocating. --- rpcs3/Crypto/unpkg.cpp | 4 ++-- rpcs3/Crypto/unpkg.h | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/rpcs3/Crypto/unpkg.cpp b/rpcs3/Crypto/unpkg.cpp index bffa5ea500d9..e9a2cf8ea3c2 100644 --- a/rpcs3/Crypto/unpkg.cpp +++ b/rpcs3/Crypto/unpkg.cpp @@ -121,9 +121,9 @@ bool package_reader::read_header() return false; } - if (u64{umax} / sizeof(PKGEntry) < u64(m_header.file_count)) + if (m_header.file_count > PKG_MAX_FILE_COUNT || u64(m_header.file_count) > u64(m_header.data_size) / sizeof(PKGEntry)) { - pkg_log.error("PKG file count is too large! (0x%x)", m_header.file_count); + pkg_log.error("PKG file count is invalid! (count=0x%x, data_size=0x%llx)", m_header.file_count, m_header.data_size); return false; } diff --git a/rpcs3/Crypto/unpkg.h b/rpcs3/Crypto/unpkg.h index f7802492af7e..3579104b74e8 100644 --- a/rpcs3/Crypto/unpkg.h +++ b/rpcs3/Crypto/unpkg.h @@ -14,6 +14,7 @@ enum : u32 { PKG_HEADER_SIZE = 0xC0, // sizeof(pkg_header) + sizeof(pkg_unk_checksum) PKG_HEADER_SIZE2 = 0x280, + PKG_MAX_FILE_COUNT = 1'000'000, PKG_MAX_FILENAME_SIZE = 256, }; From b516a41ed5ce23fe06f4f3fc93d0e927f282e1e6 Mon Sep 17 00:00:00 2001 From: Acts1631 Date: Wed, 2 Sep 2026 14:35:19 -0400 Subject: [PATCH 3/3] Abort on parent-directory PKG entries --- rpcs3/Crypto/unpkg.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rpcs3/Crypto/unpkg.cpp b/rpcs3/Crypto/unpkg.cpp index e9a2cf8ea3c2..ae3097362077 100644 --- a/rpcs3/Crypto/unpkg.cpp +++ b/rpcs3/Crypto/unpkg.cpp @@ -943,7 +943,12 @@ bool package_reader::fill_data(std::map& all_instal for (const auto& component : entry_path) { - if (component == "." || component == "..") + if (component == "..") + { + fmt::throw_exception("PKG entry path contains a parent directory component: '%s'", name); + } + + if (component == ".") { num_failures++; pkg_log.error("PKG entry path contains a special component: '%s'", name);