From c4c3ed7711332b61a1cd05d924b2b1ffa1442487 Mon Sep 17 00:00:00 2001 From: emirplus Date: Thu, 8 Oct 2026 19:18:36 +0300 Subject: [PATCH 1/3] fix(savedata): recover from metadata setup failure Report SAVE_DATA_ERROR_INTERNAL when setupSaveDataMemory2 fails to persist initial SaveDataParam metadata, and restore previous state so retrying setup can initialize cleanly. Closes #1667. --- .../libs/prx/libSceSaveData.native/Export.cpp | 11 ++++- core/libs/tests/SaveDataMemoryMetadata.cpp | 43 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index cfa6c5881..7db0d9691 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -633,6 +633,7 @@ static int setupSaveDataMemory2(const SaveDataMemorySetup2* setup_param, SaveDat if (have && !read_file_all(path, data)) { return SAVE_DATA_ERROR_INTERNAL; } + const std::vector originalData = data; data.resize(setup_param->memory_size, 0); if (!write_file_replace(path, data)) { return SAVE_DATA_ERROR_INTERNAL; @@ -640,7 +641,15 @@ static int setupSaveDataMemory2(const SaveDataMemorySetup2* setup_param, SaveDat if (setup_param->init_param != nullptr && (setup_param->option & 1u) != 0) { std::vector pd(sizeof(SaveDataParam)); std::memcpy(pd.data(), setup_param->init_param, sizeof(SaveDataParam)); - write_file_replace(mem_path(setup_param->user_id, setup_param->slot_id, "param"), pd); + if (!write_file_replace(mem_path(setup_param->user_id, setup_param->slot_id, "param"), pd)) { + if (!have) { + std::error_code removeError; + std::filesystem::remove(path, removeError); + } else { + write_file_replace(path, originalData); + } + return SAVE_DATA_ERROR_INTERNAL; + } } } if (result != nullptr) { diff --git a/core/libs/tests/SaveDataMemoryMetadata.cpp b/core/libs/tests/SaveDataMemoryMetadata.cpp index 70724bd18..c92d0f8f4 100644 --- a/core/libs/tests/SaveDataMemoryMetadata.cpp +++ b/core/libs/tests/SaveDataMemoryMetadata.cpp @@ -19,6 +19,7 @@ extern "C" { int APS5_VABI sceSaveDataInitialize3(const void*); int APS5_VABI sceSaveDataTerminate(); int APS5_VABI sceSaveDataSetSaveDataMemory2(const SaveDataMemorySet2*); +int APS5_VABI sceSaveDataSetupSaveDataMemory2(const SaveDataMemorySetup2*, SaveDataMemorySetupResult*); } static void Check(bool value, int line) { @@ -115,6 +116,48 @@ int main() { Require(sceSaveDataSetSaveDataMemory2(&set) == 0); std::copy(first.begin(), first.end(), memoryExpected.begin() + 4); Require(Read(memoryPath) == memoryExpected); + const auto setupParamPath = std::filesystem::path("_sd_mem/u42/slot0.param"); + const auto setupBinPath = std::filesystem::path("_sd_mem/u42/slot0.bin"); + const auto setupParamTemp = setupParamPath.string() + ".tmp"; + Require(std::filesystem::create_directories(setupParamPath.parent_path())); + Require(std::filesystem::create_directory(setupParamTemp)); + SaveDataParam setupParam{}; + setupParam.user_param = 100; + SaveDataMemorySetup2 setup2{}; + setup2.user_id = 42; + setup2.slot_id = 0; + setup2.memory_size = 128; + setup2.option = 1; + setup2.init_param = &setupParam; + SaveDataMemorySetupResult setupResult{}; + setupResult.existed_memory_size = 555; + Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == static_cast(0x809F000Bu)); + Require(setupResult.existed_memory_size == 555); + Require(!std::filesystem::exists(setupBinPath)); + Require(!std::filesystem::exists(setupParamPath)); + Require(!std::filesystem::exists(setupParamTemp)); + Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); + Require(setupResult.existed_memory_size == 0); + Require(Read(setupBinPath) == std::vector(128, 0)); + const auto* setupBytes = reinterpret_cast(&setupParam); + Require(Read(setupParamPath) == std::vector(setupBytes, setupBytes + sizeof(setupParam))); + const auto growthParamTemp = setupParamPath.string() + ".tmp"; + Require(std::filesystem::create_directory(growthParamTemp)); + SaveDataParam previousParam = setupParam; + setupParam.user_param = 200; + setup2.memory_size = 256; + setupResult.existed_memory_size = 999; + Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == static_cast(0x809F000Bu)); + Require(setupResult.existed_memory_size == 999); + Require(Read(setupBinPath) == std::vector(128, 0)); + const auto* previousBytes = reinterpret_cast(&previousParam); + Require(Read(setupParamPath) == std::vector(previousBytes, previousBytes + sizeof(previousParam))); + Require(!std::filesystem::exists(growthParamTemp)); + Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); + Require(setupResult.existed_memory_size == 128); + Require(Read(setupBinPath) == std::vector(256, 0)); + const auto* updatedBytes = reinterpret_cast(&setupParam); + Require(Read(setupParamPath) == std::vector(updatedBytes, updatedBytes + sizeof(setupParam))); Require(sceSaveDataTerminate() == 0); std::filesystem::current_path(previous); std::filesystem::remove_all(root); From a8c2100f5904bd9ca8b623c91764f1badbfe5532 Mon Sep 17 00:00:00 2001 From: emirplus Date: Thu, 8 Oct 2026 21:41:09 +0300 Subject: [PATCH 2/3] fix(savedata): ensure metadata precedes blob commit and handle rollback recovery Write initial SaveDataParam metadata before replacing the memory blob so metadata failure cannot leave a prematurely grown blob. Handle blob write failure with metadata rollback, and allow retries to recover slots with missing metadata. Expand tests with nonzero data, blob failure, and fresh readback. --- .../libs/prx/libSceSaveData.native/Export.cpp | 37 ++++++---- core/libs/tests/SaveDataMemoryMetadata.cpp | 67 ++++++++++++++++++- 2 files changed, 88 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 7db0d9691..3cc62db23 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -625,32 +625,41 @@ static int setupSaveDataMemory2(const SaveDataMemorySetup2* setup_param, SaveDat if (!have) { existed = 0; } + const bool needParam = (setup_param->init_param != nullptr && (setup_param->option & 1u) != 0); + const std::string paramPath = mem_path(setup_param->user_id, setup_param->slot_id, "param"); + std::size_t paramSize = 0; + const bool haveParam = file_size_of(paramPath, ¶mSize); // First run: create a zero-filled blob and report existed size 0 so the title treats it as a new save. - if (!have || existed < setup_param->memory_size) { + if (!have || existed < setup_param->memory_size || (needParam && !haveParam)) { std::error_code ec; std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec); + std::vector originalParam; + if (haveParam && !read_file_all(paramPath, originalParam)) { + return SAVE_DATA_ERROR_INTERNAL; + } std::vector data; if (have && !read_file_all(path, data)) { return SAVE_DATA_ERROR_INTERNAL; } - const std::vector originalData = data; - data.resize(setup_param->memory_size, 0); - if (!write_file_replace(path, data)) { - return SAVE_DATA_ERROR_INTERNAL; - } - if (setup_param->init_param != nullptr && (setup_param->option & 1u) != 0) { + if (needParam) { std::vector pd(sizeof(SaveDataParam)); std::memcpy(pd.data(), setup_param->init_param, sizeof(SaveDataParam)); - if (!write_file_replace(mem_path(setup_param->user_id, setup_param->slot_id, "param"), pd)) { - if (!have) { - std::error_code removeError; - std::filesystem::remove(path, removeError); - } else { - write_file_replace(path, originalData); - } + if (!write_file_replace(paramPath, pd)) { return SAVE_DATA_ERROR_INTERNAL; } } + data.resize(setup_param->memory_size, 0); + if (!write_file_replace(path, data)) { + if (needParam) { + if (!haveParam) { + std::error_code removeError; + std::filesystem::remove(paramPath, removeError); + } else { + write_file_replace(paramPath, originalParam); + } + } + return SAVE_DATA_ERROR_INTERNAL; + } } if (result != nullptr) { std::memset(result, 0, sizeof(*result)); diff --git a/core/libs/tests/SaveDataMemoryMetadata.cpp b/core/libs/tests/SaveDataMemoryMetadata.cpp index c92d0f8f4..0c858c8f8 100644 --- a/core/libs/tests/SaveDataMemoryMetadata.cpp +++ b/core/libs/tests/SaveDataMemoryMetadata.cpp @@ -141,6 +141,15 @@ int main() { Require(Read(setupBinPath) == std::vector(128, 0)); const auto* setupBytes = reinterpret_cast(&setupParam); Require(Read(setupParamPath) == std::vector(setupBytes, setupBytes + sizeof(setupParam))); + std::vector seededBin(128); + for (std::size_t i = 0; i < seededBin.size(); ++i) { + seededBin[i] = static_cast(static_cast(i * 3 + 7)); + } + { + std::ofstream seededFile(setupBinPath, std::ios::binary); + seededFile.write(seededBin.data(), static_cast(seededBin.size())); + Require(static_cast(seededFile)); + } const auto growthParamTemp = setupParamPath.string() + ".tmp"; Require(std::filesystem::create_directory(growthParamTemp)); SaveDataParam previousParam = setupParam; @@ -149,15 +158,69 @@ int main() { setupResult.existed_memory_size = 999; Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == static_cast(0x809F000Bu)); Require(setupResult.existed_memory_size == 999); - Require(Read(setupBinPath) == std::vector(128, 0)); + Require(Read(setupBinPath) == seededBin); const auto* previousBytes = reinterpret_cast(&previousParam); Require(Read(setupParamPath) == std::vector(previousBytes, previousBytes + sizeof(previousParam))); Require(!std::filesystem::exists(growthParamTemp)); Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); Require(setupResult.existed_memory_size == 128); - Require(Read(setupBinPath) == std::vector(256, 0)); + std::vector expectedGrownBin = seededBin; + expectedGrownBin.resize(256, 0); + Require(Read(setupBinPath) == expectedGrownBin); const auto* updatedBytes = reinterpret_cast(&setupParam); Require(Read(setupParamPath) == std::vector(updatedBytes, updatedBytes + sizeof(setupParam))); + const auto growthBinTemp = setupBinPath.string() + ".tmp"; + Require(std::filesystem::create_directory(growthBinTemp)); + SaveDataParam beforeBlobFailParam = setupParam; + setupParam.user_param = 300; + setup2.memory_size = 512; + setupResult.existed_memory_size = 888; + Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == static_cast(0x809F000Bu)); + Require(setupResult.existed_memory_size == 888); + Require(Read(setupBinPath) == expectedGrownBin); + const auto* beforeBlobFailBytes = reinterpret_cast(&beforeBlobFailParam); + Require(Read(setupParamPath) == std::vector(beforeBlobFailBytes, beforeBlobFailBytes + sizeof(beforeBlobFailParam))); + Require(!std::filesystem::exists(growthBinTemp)); + Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); + Require(setupResult.existed_memory_size == 256); + std::vector expectedBlob512 = expectedGrownBin; + expectedBlob512.resize(512, 0); + Require(Read(setupBinPath) == expectedBlob512); + const auto* param300Bytes = reinterpret_cast(&setupParam); + Require(Read(setupParamPath) == std::vector(param300Bytes, param300Bytes + sizeof(setupParam))); + const auto orphanParamPath = std::filesystem::path("_sd_mem/u99/slot0.param"); + const auto orphanBinPath = std::filesystem::path("_sd_mem/u99/slot0.bin"); + Require(std::filesystem::create_directories(orphanBinPath.parent_path())); + const std::vector orphanBin(64, 'k'); + { + std::ofstream orphanFile(orphanBinPath, std::ios::binary); + orphanFile.write(orphanBin.data(), static_cast(orphanBin.size())); + Require(static_cast(orphanFile)); + } + SaveDataParam orphanParam{}; + orphanParam.user_param = 42; + SaveDataMemorySetup2 orphanSetup{}; + orphanSetup.user_id = 99; + orphanSetup.slot_id = 0; + orphanSetup.memory_size = 64; + orphanSetup.option = 1; + orphanSetup.init_param = &orphanParam; + SaveDataMemorySetupResult orphanResult{}; + orphanResult.existed_memory_size = 333; + Require(sceSaveDataSetupSaveDataMemory2(&orphanSetup, &orphanResult) == 0); + Require(orphanResult.existed_memory_size == 64); + Require(Read(orphanBinPath) == orphanBin); + const auto* orphanBytes = reinterpret_cast(&orphanParam); + Require(Read(orphanParamPath) == std::vector(orphanBytes, orphanBytes + sizeof(orphanParam))); + Require(sceSaveDataTerminate() == 0); + Require(Read(setupBinPath) == expectedBlob512); + Require(Read(setupParamPath) == std::vector(param300Bytes, param300Bytes + sizeof(setupParam))); + Require(sceSaveDataInitialize3(nullptr) == 0); + setupResult.existed_memory_size = 111; + Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); + Require(setupResult.existed_memory_size == 512); + Require(Read(setupBinPath) == expectedBlob512); + Require(Read(setupParamPath) == std::vector(param300Bytes, param300Bytes + sizeof(setupParam))); Require(sceSaveDataTerminate() == 0); std::filesystem::current_path(previous); std::filesystem::remove_all(root); From 6a7cce415a1977e2827f7cbbaa50727ddb167c6b Mon Sep 17 00:00:00 2001 From: emirplus Date: Sat, 10 Oct 2026 21:09:00 +0300 Subject: [PATCH 3/3] fix(savedata): recover from metadata setup failure --- .../libs/prx/libSceSaveData.native/Export.cpp | 26 ++----- core/libs/tests/SaveDataMemoryGrowth.cpp | 4 +- core/libs/tests/SaveDataMemoryMetadata.cpp | 73 ++++++++++++++++++- 3 files changed, 79 insertions(+), 24 deletions(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 3cc62db23..86e94d97b 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -633,14 +633,6 @@ static int setupSaveDataMemory2(const SaveDataMemorySetup2* setup_param, SaveDat if (!have || existed < setup_param->memory_size || (needParam && !haveParam)) { std::error_code ec; std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec); - std::vector originalParam; - if (haveParam && !read_file_all(paramPath, originalParam)) { - return SAVE_DATA_ERROR_INTERNAL; - } - std::vector data; - if (have && !read_file_all(path, data)) { - return SAVE_DATA_ERROR_INTERNAL; - } if (needParam) { std::vector pd(sizeof(SaveDataParam)); std::memcpy(pd.data(), setup_param->init_param, sizeof(SaveDataParam)); @@ -648,17 +640,15 @@ static int setupSaveDataMemory2(const SaveDataMemorySetup2* setup_param, SaveDat return SAVE_DATA_ERROR_INTERNAL; } } - data.resize(setup_param->memory_size, 0); - if (!write_file_replace(path, data)) { - if (needParam) { - if (!haveParam) { - std::error_code removeError; - std::filesystem::remove(paramPath, removeError); - } else { - write_file_replace(paramPath, originalParam); - } + if (!have || existed < setup_param->memory_size) { + std::vector data; + if (have && !read_file_all(path, data)) { + return SAVE_DATA_ERROR_INTERNAL; + } + data.resize(setup_param->memory_size, 0); + if (!write_file_replace(path, data)) { + return SAVE_DATA_ERROR_INTERNAL; } - return SAVE_DATA_ERROR_INTERNAL; } } if (result != nullptr) { diff --git a/core/libs/tests/SaveDataMemoryGrowth.cpp b/core/libs/tests/SaveDataMemoryGrowth.cpp index 924c4d4c6..83f70044b 100644 --- a/core/libs/tests/SaveDataMemoryGrowth.cpp +++ b/core/libs/tests/SaveDataMemoryGrowth.cpp @@ -82,10 +82,10 @@ int main(int argc, char**) { std::fprintf(stderr, "Read denial unavailable for this user\n"); exitCode = 77; } else { - Require(Read(path) == original); Require(status == static_cast(0x809F000Bu)); + Require(Read(path) == original); Require(result.existed_memory_size == 123); - Require(!std::filesystem::exists("_sd_mem/u7531/slot0.param")); + Require(std::filesystem::is_regular_file("_sd_mem/u7531/slot0.param")); Require(!std::filesystem::exists(path.string() + ".tmp")); } } else { diff --git a/core/libs/tests/SaveDataMemoryMetadata.cpp b/core/libs/tests/SaveDataMemoryMetadata.cpp index 0c858c8f8..8f40cccf5 100644 --- a/core/libs/tests/SaveDataMemoryMetadata.cpp +++ b/core/libs/tests/SaveDataMemoryMetadata.cpp @@ -171,23 +171,88 @@ int main() { Require(Read(setupParamPath) == std::vector(updatedBytes, updatedBytes + sizeof(setupParam))); const auto growthBinTemp = setupBinPath.string() + ".tmp"; Require(std::filesystem::create_directory(growthBinTemp)); - SaveDataParam beforeBlobFailParam = setupParam; setupParam.user_param = 300; setup2.memory_size = 512; setupResult.existed_memory_size = 888; Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == static_cast(0x809F000Bu)); Require(setupResult.existed_memory_size == 888); Require(Read(setupBinPath) == expectedGrownBin); - const auto* beforeBlobFailBytes = reinterpret_cast(&beforeBlobFailParam); - Require(Read(setupParamPath) == std::vector(beforeBlobFailBytes, beforeBlobFailBytes + sizeof(beforeBlobFailParam))); + const auto* param300Bytes = reinterpret_cast(&setupParam); + Require(Read(setupParamPath) == std::vector(param300Bytes, param300Bytes + sizeof(setupParam))); Require(!std::filesystem::exists(growthBinTemp)); Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); Require(setupResult.existed_memory_size == 256); std::vector expectedBlob512 = expectedGrownBin; expectedBlob512.resize(512, 0); Require(Read(setupBinPath) == expectedBlob512); - const auto* param300Bytes = reinterpret_cast(&setupParam); Require(Read(setupParamPath) == std::vector(param300Bytes, param300Bytes + sizeof(setupParam))); + const auto freshFailBinPath = std::filesystem::path("_sd_mem/u55/slot0.bin"); + const auto freshFailParamPath = std::filesystem::path("_sd_mem/u55/slot0.param"); + Require(std::filesystem::create_directories(freshFailBinPath.parent_path())); + const auto freshFailBinTemp = freshFailBinPath.string() + ".tmp"; + Require(std::filesystem::create_directory(freshFailBinTemp)); + SaveDataParam freshFailParam{}; + freshFailParam.user_param = 55; + SaveDataMemorySetup2 freshFailSetup{}; + freshFailSetup.user_id = 55; + freshFailSetup.slot_id = 0; + freshFailSetup.memory_size = 64; + freshFailSetup.option = 1; + freshFailSetup.init_param = &freshFailParam; + SaveDataMemorySetupResult freshFailResult{}; + freshFailResult.existed_memory_size = 777; + Require(sceSaveDataSetupSaveDataMemory2(&freshFailSetup, &freshFailResult) == static_cast(0x809F000Bu)); + Require(freshFailResult.existed_memory_size == 777); + Require(!std::filesystem::exists(freshFailBinPath)); + const auto* freshFailBytes = reinterpret_cast(&freshFailParam); + Require(Read(freshFailParamPath) == std::vector(freshFailBytes, freshFailBytes + sizeof(freshFailParam))); + Require(!std::filesystem::exists(freshFailBinTemp)); + Require(sceSaveDataSetupSaveDataMemory2(&freshFailSetup, &freshFailResult) == 0); + Require(freshFailResult.existed_memory_size == 0); + Require(Read(freshFailBinPath) == std::vector(64, 0)); + Require(Read(freshFailParamPath) == std::vector(freshFailBytes, freshFailBytes + sizeof(freshFailParam))); + SaveDataParam noParam{}; + noParam.user_param = 5; + SaveDataMemorySetup2 noParamSetup{}; + SaveDataMemorySetupResult noParamResult{}; + noParamSetup.user_id = 56; + noParamSetup.slot_id = 0; + noParamSetup.memory_size = 16; + noParamSetup.option = 1; + noParamSetup.init_param = nullptr; + Require(sceSaveDataSetupSaveDataMemory2(&noParamSetup, &noParamResult) == 0); + Require(Read("_sd_mem/u56/slot0.bin") == std::vector(16, 0)); + Require(!std::filesystem::exists("_sd_mem/u56/slot0.param")); + noParamSetup.user_id = 57; + noParamSetup.option = 0; + noParamSetup.init_param = &noParam; + Require(sceSaveDataSetupSaveDataMemory2(&noParamSetup, &noParamResult) == 0); + Require(Read("_sd_mem/u57/slot0.bin") == std::vector(16, 0)); + Require(!std::filesystem::exists("_sd_mem/u57/slot0.param")); + SaveDataParam userA{}; + userA.user_param = 1; + SaveDataParam userB{}; + userB.user_param = 2; + SaveDataMemorySetup2 isoSetup{}; + SaveDataMemorySetupResult isoResult{}; + isoSetup.user_id = 60; + isoSetup.slot_id = 0; + isoSetup.memory_size = 8; + isoSetup.option = 1; + isoSetup.init_param = &userA; + Require(sceSaveDataSetupSaveDataMemory2(&isoSetup, &isoResult) == 0); + isoSetup.slot_id = 1; + isoSetup.init_param = &userB; + Require(sceSaveDataSetupSaveDataMemory2(&isoSetup, &isoResult) == 0); + const auto* userABytes = reinterpret_cast(&userA); + const auto* userBBytes = reinterpret_cast(&userB); + Require(Read("_sd_mem/u60/slot0.param") == std::vector(userABytes, userABytes + sizeof(userA))); + Require(Read("_sd_mem/u60/slot1.param") == std::vector(userBBytes, userBBytes + sizeof(userB))); + isoSetup.user_id = 61; + isoSetup.slot_id = 0; + Require(sceSaveDataSetupSaveDataMemory2(&isoSetup, &isoResult) == 0); + Require(Read("_sd_mem/u60/slot0.param") == std::vector(userABytes, userABytes + sizeof(userA))); + Require(Read("_sd_mem/u61/slot0.param") == std::vector(userBBytes, userBBytes + sizeof(userB))); const auto orphanParamPath = std::filesystem::path("_sd_mem/u99/slot0.param"); const auto orphanBinPath = std::filesystem::path("_sd_mem/u99/slot0.bin"); Require(std::filesystem::create_directories(orphanBinPath.parent_path()));