From 8c31d80b8bce35054a8a136a30c22c86dea1dd9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 6 Nov 2026 14:01:34 +0100 Subject: [PATCH 1/4] PSAR: Pad the second block size from the first block to 16 bytes The 6.61 updater records its second block as 408 bytes, but the block is padded to 416 and only decrypts at that size, so the install failed with "Couldn't read the PSAR's second block". Co-Authored-By: Claude Opus 5.5 --- Core/Util/PSARUnpack.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Core/Util/PSARUnpack.cpp b/Core/Util/PSARUnpack.cpp index 734732f3db..f5ef599f3f 100644 --- a/Core/Util/PSARUnpack.cpp +++ b/Core/Util/PSARUnpack.cpp @@ -642,8 +642,9 @@ bool PSARReader::Init(std::string *error) { if (!oldschool_) { // The second block's size isn't recorded anywhere, so try the ones real updaters use. - // 100 covers most, 2.7x is bigger, and some store it in the first block. - const u32 candidates[] = { 100, 144, ReadU16(block_.data() + 0x90) }; + // 100 covers most, 2.7x is bigger, and some store it in the first block. That one is the + // unpadded size (408 in a 6.61 updater) and the block is padded to 16 bytes. + const u32 candidates[] = { 100, 144, (ReadU16(block_.data() + 0x90) + 15u) & ~15u }; decoded = -1; for (u32 candidate : candidates) { if (candidate == 0) { From a0af2bc403a0958a4b4e47c8d84342ea7f79929a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 6 Nov 2026 14:01:34 +0100 Subject: [PATCH 2/4] Firmware install: Create flash1/vsh/theme The shell refuses to install a theme without it, and the updater's archive doesn't have it. Co-Authored-By: Claude Opus 5.5 --- Core/Util/PSARUnpack.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Core/Util/PSARUnpack.cpp b/Core/Util/PSARUnpack.cpp index f5ef599f3f..121b9d1155 100644 --- a/Core/Util/PSARUnpack.cpp +++ b/Core/Util/PSARUnpack.cpp @@ -1747,6 +1747,13 @@ bool InstallFirmware(const Path &updater, const Path &nandRoot, const PSARUnpack ok = SwapStagingIntoPlace(nandRoot, staging, error); } if (ok) { + // The shell refuses to install a theme without this directory, and the updater's archive + // doesn't have it (6.61's has nothing under flash1), so the real updater must create it. + const Path themeDir = nandRoot / "flash1/vsh/theme"; + if (!File::CreateFullPath(themeDir)) { + WARN_LOG(Log::Loader, "Couldn't create %s", themeDir.c_str()); + } + g_Config.iSceRegCategoryVersion = SceRegCategoryVersionAfterInstall(g_Config.iSceRegCategoryVersion, SceRegCategoryVersionForFirmware(stats->firmwareVersion)); } From 004dcdfe936552fa916d27822ebbce3eb9cf9990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 6 Nov 2026 14:08:57 +0100 Subject: [PATCH 3/4] PSAR: Accept zlib 1.4's error for streams without a checksum zlib 1.4's uncompress2 returns Z_DATA_ERROR, not Z_BUF_ERROR, when the input runs out with the output full, so every file in the pre-decrypted 3.60 archive failed to inflate after the zlib update. Co-Authored-By: Claude Opus 5.5 --- Core/Util/PSARUnpack.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Core/Util/PSARUnpack.cpp b/Core/Util/PSARUnpack.cpp index 121b9d1155..56064df306 100644 --- a/Core/Util/PSARUnpack.cpp +++ b/Core/Util/PSARUnpack.cpp @@ -739,8 +739,9 @@ void PSARReader::EnsureContents() { uLongf destLen = contentExpandedSize_; uLong sourceLen = decoded; int zResult = uncompress2(entryData_.data(), &destLen, block2_.data(), &sourceLen); - // Handle zip streams missing their Adler-32 checksum. - if (zResult == Z_BUF_ERROR && sourceLen == (uLong)decoded && destLen == contentExpandedSize_ && decrypted_) { + // Handle zip streams missing their Adler-32 checksum (the pre-decrypted 3.60 archive's). The + // input runs out with the output full, which zlib 1.4 reports as Z_DATA_ERROR, 1.3 as Z_BUF_ERROR. + if ((zResult == Z_BUF_ERROR || zResult == Z_DATA_ERROR) && sourceLen == (uLong)decoded && destLen == contentExpandedSize_ && decrypted_) { zResult = Z_OK; } if (zResult != Z_OK || destLen != contentExpandedSize_) { From 1908ac5e92d6e520e452c973c1df625026c4a871 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 6 Nov 2026 14:08:58 +0100 Subject: [PATCH 4/4] PSAR: Bound entries by the file size, not the declared size The last entry's contents can run past the size the header declares (1.x updaters, and the 4.21 and 5.70 PSARs), so the wraparound check from c5673294ab refused them. The file size is enough to stop the wrap. Co-Authored-By: Claude Opus 5.5 --- Core/Util/PSARUnpack.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Core/Util/PSARUnpack.cpp b/Core/Util/PSARUnpack.cpp index 56064df306..2ff1ed082d 100644 --- a/Core/Util/PSARUnpack.cpp +++ b/Core/Util/PSARUnpack.cpp @@ -699,7 +699,9 @@ int PSARReader::NextEntry(std::string *error) { const u32 chunkSize = ReadU32(block_.data() + 0x104); const u32 expandedSize = ReadU32(block_.data() + 0x108); // pos_ is a u32, so a huge chunk would wrap it back to an earlier entry and loop forever. - if (chunkSize > limit_ - std::min((size_t)pos_, limit_)) { + // Check against the file, not limit_: the last entry's contents can run past the size the + // header declares (1.x updaters, and the 3.60, 4.21 and 5.70 PSARs). + if (chunkSize > size_ - std::min((size_t)pos_, size_)) { *error = StringFromFormat("Entry '%s' runs past the end of the archive (%u bytes)", entryName_.c_str(), chunkSize); return -1; }