From 3a3ac668cdef5638325363369643eaf0adff35af Mon Sep 17 00:00:00 2001 From: DealPotato <135158142+DealPotato@users.noreply.github.com> Date: Tue, 20 Oct 2026 11:32:36 +0300 Subject: [PATCH 1/4] feat(savedata): add sceSaveDataMount2 PS4 titles mount their save data with sceSaveDataMount2 (SDK 2.00 and later), whose SceSaveDataMount2 is Mount3's parameters without the system blocks and the resource field: user ID, directory name, blocks and mount mode. The function converts the parameters and mounts through Mount3, so the result, the mount point and the error codes are Mount3's. inFAMOUS Second Son (CUSA00004) calls it to open its saves. Co-Authored-By: Claude Opus 5.5 --- core/libs/SceTypes.hpp | 10 +++++ .../libs/prx/libSceSaveData.native/Export.cpp | 18 ++++++++ core/libs/tests/GuestSaveDataPs4.cpp | 43 +++++++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index d46f23177a..6e0a373808 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1545,6 +1545,16 @@ struct SaveDataMount3 { std::uint8_t reserved[32]; }; +struct SaveDataMount2 { + int user_id; + int pad; + const SceSaveDataDirName* dir_name; + std::uint64_t blocks; + std::uint32_t mount_mode; + std::uint8_t reserved[32]; + int pad2; +}; + struct SaveDataDirNameSearchCond { std::int32_t user_id; std::int32_t pad; diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index b5d52e5013..9ca1de6610 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -664,6 +664,24 @@ int APS5_VABI sceSaveDataMount3(const SaveDataMount3* mount, SaveDataMountResult return rc; } +static int mount2(const SaveDataMount2* mount, SaveDataMountResult* mount_result) { + if (mount == nullptr) { + throw std::runtime_error("sceSaveDataMount2: null argument"); + } + SaveDataMount3 converted{}; + converted.user_id = mount->user_id; + converted.dir_name = mount->dir_name; + converted.blocks = mount->blocks; + converted.mount_mode = mount->mount_mode; + return mount3(&converted, mount_result); +} + +int APS5_VABI sceSaveDataMount2(const SaveDataMount2* mount, SaveDataMountResult* mount_result) { + const int rc = mount2(mount, mount_result); + SAVEDATA_TRACE("mount2 user=%d dir=%.*s mode=0x%x blocks=%llu -> 0x%08x point=%.*s status=%u", mount != nullptr ? mount->user_id : -1, static_cast(sizeof(SceSaveDataDirName::data)), mount != nullptr && mount->dir_name != nullptr ? mount->dir_name->data : "(null)", mount != nullptr ? mount->mount_mode : 0u, mount != nullptr ? static_cast(mount->blocks) : 0ull, static_cast(rc), static_cast(sizeof(SaveDataMountPoint::data)), mount_result != nullptr ? mount_result->mount_point.data : "", mount_result != nullptr ? mount_result->mount_status : 0u); + return rc; +} + int APS5_VABI sceSaveDataPrepare(const SaveDataMountPoint* mount_point, const SaveDataPrepareParam* param) { (void)mount_point; (void)param; diff --git a/core/libs/tests/GuestSaveDataPs4.cpp b/core/libs/tests/GuestSaveDataPs4.cpp index 39e7b14d1e..bd6a123d48 100644 --- a/core/libs/tests/GuestSaveDataPs4.cpp +++ b/core/libs/tests/GuestSaveDataPs4.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "SceTypes.hpp" +#include #include #include #include @@ -10,10 +11,18 @@ extern "C" int APS5_VABI sceSaveDataTransferringMountPs4(const SaveDataTransferringMount*, SaveDataMountResult*); extern "C" int APS5_VABI sceSaveDataDirNameSearchPs4(const SaveDataDirNameSearchCond*, SaveDataDirNameSearchResult*); extern "C" int APS5_VABI sceSaveDataDirNameSearch(const SaveDataDirNameSearchCond*, SaveDataDirNameSearchResult*); +extern "C" int APS5_VABI sceSaveDataInitialize3(const void*); +extern "C" int APS5_VABI sceSaveDataMount2(const SaveDataMount2*, SaveDataMountResult*); +extern "C" int APS5_VABI sceSaveDataUmount2(std::uint32_t, const SaveDataMountPoint*); + +static_assert(offsetof(SaveDataMount2, dir_name) == 8 && offsetof(SaveDataMount2, blocks) == 16 && offsetof(SaveDataMount2, mount_mode) == 24 && sizeof(SaveDataMount2) == 64, "SaveDataMount2 has the PS4 layout"); namespace { constexpr int SaveDataErrorNotFound = -2137063416; +constexpr int SaveDataErrorBusy = -2137063421; +constexpr std::uint32_t MountReadWrite = 2; +constexpr std::uint32_t MountCreate2 = 32; int failures = 0; @@ -73,6 +82,39 @@ void TestDirNameSearchPs4() { Check(ThrowsRuntimeError([&] { sceSaveDataDirNameSearchPs4(&cond, nullptr); }), "null result throws"); } +void TestMount2() { + Check(sceSaveDataInitialize3(nullptr) == 0, "SaveData initializes"); + SceSaveDataDirName created{}; + std::strcpy(created.data, "mount2-new"); + SaveDataMount2 mount{}; + mount.user_id = 1; + mount.dir_name = &created; + mount.blocks = 96; + mount.mount_mode = MountReadWrite | MountCreate2; + SaveDataMountResult result{}; + Check(sceSaveDataMount2(&mount, &result) == 0, "Mount2 creates a missing save"); + Check(result.mount_status == 1u, "Mount2 reports the save as created"); + Check(result.mount_point.data[0] != '\0', "Mount2 reports a mount point"); + Check(std::filesystem::is_directory(std::filesystem::path("_sd") / "mount2-new"), "Mount2 creates the save directory"); + SaveDataMountResult busy{}; + Check(sceSaveDataMount2(&mount, &busy) == SaveDataErrorBusy, "Mount2 of a mounted save is BUSY"); + Check(sceSaveDataUmount2(0, &result.mount_point) == 0, "the Mount2 mount point unmounts"); + + SceSaveDataDirName kept{}; + std::strcpy(kept.data, "kept"); + mount.dir_name = &kept; + mount.mount_mode = MountReadWrite; + Check(sceSaveDataMount2(&mount, &result) == 0, "Mount2 opens an existing save"); + Check(result.mount_status == 0u, "Mount2 reports the existing save as opened"); + Check(sceSaveDataUmount2(0, &result.mount_point) == 0, "the opened save unmounts"); + + SceSaveDataDirName absent{}; + std::strcpy(absent.data, "absent"); + mount.dir_name = &absent; + Check(sceSaveDataMount2(&mount, &result) == SaveDataErrorNotFound, "Mount2 open of a missing save is NOT_FOUND"); + Check(ThrowsRuntimeError([&] { sceSaveDataMount2(nullptr, &result); }), "null mount throws"); +} + } int main() { @@ -83,6 +125,7 @@ int main() { TestTransferringMountPs4(); TestDirNameSearchPs4(); + TestMount2(); std::filesystem::current_path(previous); std::error_code error; From 29c972ae688e2d04c2ccd792cd987257c585be8d Mon Sep 17 00:00:00 2001 From: DealPotato <135158142+DealPotato@users.noreply.github.com> Date: Sat, 24 Oct 2026 02:43:54 +0300 Subject: [PATCH 2/4] fix(savedata): answer sceSaveDataMount2's PS4 parameter errors A PS4 Pro (firmware 10.71) answers sceSaveDataMount2 differently from Mount3 in three cases, measured by ntamas94 on #8285: before sceSaveDataInitialize3 it returns NOT_INITIALIZED, a mount mode of 0 is PARAMETER, and creating a save (CREATE or CREATE2) of fewer than 96 blocks is PARAMETER. Mount2 now checks these before it mounts through Mount3, so a refused create makes no directory. guest_savedata_ps4 covers each case. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceSaveData.native/Export.cpp | 7 +++++++ core/libs/prx/libSceSaveData.native/SaveData.hpp | 1 + core/libs/tests/GuestSaveDataPs4.cpp | 16 +++++++++++++++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 7388ed03bf..d958b3d829 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -668,6 +668,13 @@ static int mount2(const SaveDataMount2* mount, SaveDataMountResult* mount_result if (mount == nullptr) { throw std::runtime_error("sceSaveDataMount2: null argument"); } + if (g_initializations == 0) { + return SAVE_DATA_ERROR_NOT_INITIALIZED; + } + const bool creates = (mount->mount_mode & (SAVE_DATA_MOUNT_MODE_CREATE | SAVE_DATA_MOUNT_MODE_CREATE2)) != 0; + if (mount->mount_mode == 0 || (creates && mount->blocks < SAVE_DATA_BLOCKS_MIN2)) { + return SAVE_DATA_ERROR_PARAMETER; + } SaveDataMount3 converted{}; converted.user_id = mount->user_id; converted.dir_name = mount->dir_name; diff --git a/core/libs/prx/libSceSaveData.native/SaveData.hpp b/core/libs/prx/libSceSaveData.native/SaveData.hpp index 1a8681cb38..66e46322cc 100644 --- a/core/libs/prx/libSceSaveData.native/SaveData.hpp +++ b/core/libs/prx/libSceSaveData.native/SaveData.hpp @@ -25,6 +25,7 @@ constexpr std::uint32_t SAVE_DATA_MOUNT_MODE_RDWR = 2; constexpr std::uint32_t SAVE_DATA_MOUNT_MODE_CREATE = 4; constexpr std::uint32_t SAVE_DATA_MOUNT_MODE_CREATE2 = 32; +constexpr std::uint64_t SAVE_DATA_BLOCKS_MIN2 = 96; constexpr std::uint64_t SAVE_DATA_BLOCKS_MAX = 16384; constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_ALL = 0; constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_TITLE = 1; diff --git a/core/libs/tests/GuestSaveDataPs4.cpp b/core/libs/tests/GuestSaveDataPs4.cpp index bd6a123d48..ba1b3bf98c 100644 --- a/core/libs/tests/GuestSaveDataPs4.cpp +++ b/core/libs/tests/GuestSaveDataPs4.cpp @@ -19,9 +19,12 @@ static_assert(offsetof(SaveDataMount2, dir_name) == 8 && offsetof(SaveDataMount2 namespace { +constexpr int SaveDataErrorParameter = -2137063424; +constexpr int SaveDataErrorNotInitialized = -2137063423; constexpr int SaveDataErrorNotFound = -2137063416; constexpr int SaveDataErrorBusy = -2137063421; constexpr std::uint32_t MountReadWrite = 2; +constexpr std::uint32_t MountCreate = 4; constexpr std::uint32_t MountCreate2 = 32; int failures = 0; @@ -83,7 +86,6 @@ void TestDirNameSearchPs4() { } void TestMount2() { - Check(sceSaveDataInitialize3(nullptr) == 0, "SaveData initializes"); SceSaveDataDirName created{}; std::strcpy(created.data, "mount2-new"); SaveDataMount2 mount{}; @@ -92,6 +94,8 @@ void TestMount2() { mount.blocks = 96; mount.mount_mode = MountReadWrite | MountCreate2; SaveDataMountResult result{}; + Check(sceSaveDataMount2(&mount, &result) == SaveDataErrorNotInitialized, "Mount2 before initialization is NOT_INITIALIZED"); + Check(sceSaveDataInitialize3(nullptr) == 0, "SaveData initializes"); Check(sceSaveDataMount2(&mount, &result) == 0, "Mount2 creates a missing save"); Check(result.mount_status == 1u, "Mount2 reports the save as created"); Check(result.mount_point.data[0] != '\0', "Mount2 reports a mount point"); @@ -113,6 +117,16 @@ void TestMount2() { mount.dir_name = &absent; Check(sceSaveDataMount2(&mount, &result) == SaveDataErrorNotFound, "Mount2 open of a missing save is NOT_FOUND"); Check(ThrowsRuntimeError([&] { sceSaveDataMount2(nullptr, &result); }), "null mount throws"); + + SceSaveDataDirName small{}; + std::strcpy(small.data, "mount2-small"); + mount.dir_name = &small; + mount.mount_mode = 0; + Check(sceSaveDataMount2(&mount, &result) == SaveDataErrorParameter, "Mount2 with mount mode 0 is PARAMETER"); + mount.blocks = 1; + mount.mount_mode = MountReadWrite | MountCreate; + Check(sceSaveDataMount2(&mount, &result) == SaveDataErrorParameter, "Mount2 creating a save of fewer than 96 blocks is PARAMETER"); + Check(!std::filesystem::exists(std::filesystem::path("_sd") / "mount2-small"), "a refused Mount2 creates no directory"); } } From 7c3ba85c1fe963219eb37ec7c5e418c3595e5f7b Mon Sep 17 00:00:00 2001 From: DealPotato <135158142+DealPotato@users.noreply.github.com> Date: Mon, 26 Oct 2026 02:12:56 +0300 Subject: [PATCH 3/4] fix(savedata): follow the PS4 rows for Mount2 names and users, CREATE only ntamas94's second PS4 Pro run and harisibrar1's review: - a directory name containing "_" returns 0x809f0000 in either case; "surveytest" and "surveyTestAuto0" still mount; - user IDs -1 and 0 return 0x809f0011; - the 96-block minimum applies to CREATE only. CREATE2 was not measured, and applying it there refused opening an existing save with mode 0x22 and 0 blocks, which mount3 allows. guest_savedata_ps4 checks each one, and the Mount2 debt entry records what was measured and what was not. Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libSceSaveData.native/Export.cpp | 12 ++++++++- .../prx/libSceSaveData.native/SaveData.hpp | 1 + core/libs/tests/GuestSaveDataPs4.cpp | 26 +++++++++++++++++++ docs/dev/debt/libSceSaveData.native.md | 1 + 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 9ca1f27d98..727700ebe2 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/RemoveTree.hpp" @@ -674,10 +675,19 @@ static int mount2(const SaveDataMount2* mount, SaveDataMountResult* mount_result if (g_initializations == 0) { return SAVE_DATA_ERROR_NOT_INITIALIZED; } - const bool creates = (mount->mount_mode & (SAVE_DATA_MOUNT_MODE_CREATE | SAVE_DATA_MOUNT_MODE_CREATE2)) != 0; + if (mount->user_id == -1 || mount->user_id == 0) { + return SAVE_DATA_ERROR_INVALID_LOGIN_USER; + } + const bool creates = (mount->mount_mode & SAVE_DATA_MOUNT_MODE_CREATE) != 0; if (mount->mount_mode == 0 || (creates && mount->blocks < SAVE_DATA_BLOCKS_MIN2)) { return SAVE_DATA_ERROR_PARAMETER; } + if (mount->dir_name != nullptr) { + const std::string_view name(mount->dir_name->data, sizeof(mount->dir_name->data)); + if (name.substr(0, name.find('\0')).find('_') != std::string_view::npos) { + return SAVE_DATA_ERROR_PARAMETER; + } + } SaveDataMount3 converted{}; converted.user_id = mount->user_id; converted.dir_name = mount->dir_name; diff --git a/core/libs/prx/libSceSaveData.native/SaveData.hpp b/core/libs/prx/libSceSaveData.native/SaveData.hpp index 66e46322cc..ea77fecd69 100644 --- a/core/libs/prx/libSceSaveData.native/SaveData.hpp +++ b/core/libs/prx/libSceSaveData.native/SaveData.hpp @@ -18,6 +18,7 @@ constexpr int SAVE_DATA_ERROR_MOUNT_FULL = -2137063412; constexpr int SAVE_DATA_ERROR_EXISTS = -2137063417; constexpr int SAVE_DATA_ERROR_NOT_FOUND = -2137063416; constexpr int SAVE_DATA_ERROR_INTERNAL = -2137063413; // 0x809F000B +constexpr int SAVE_DATA_ERROR_INVALID_LOGIN_USER = -2137063407; // 0x809F0011 constexpr int SAVE_DATA_ERROR_MEMORY_NOT_READY = -2137063406; // 0x809F0012 constexpr std::uint32_t SAVE_DATA_MOUNT_MODE_RDONLY = 1; diff --git a/core/libs/tests/GuestSaveDataPs4.cpp b/core/libs/tests/GuestSaveDataPs4.cpp index 714e94885f..5126563ea6 100644 --- a/core/libs/tests/GuestSaveDataPs4.cpp +++ b/core/libs/tests/GuestSaveDataPs4.cpp @@ -20,6 +20,7 @@ constexpr int SaveDataErrorNotInitialized = -2137063423; constexpr int SaveDataErrorNotFound = -2137063416; constexpr int SaveDataErrorNotMounted = -2137063420; constexpr int SaveDataErrorBusy = -2137063421; +constexpr int SaveDataErrorInvalidLoginUser = -2137063407; constexpr std::uint32_t MountReadWrite = 2; constexpr std::uint32_t MountCreate = 4; constexpr std::uint32_t MountCreate2 = 32; @@ -113,6 +114,13 @@ void TestMount2() { Check(sceSaveDataMount2(&mount, &result) == 0, "Mount2 opens an existing save"); Check(result.mount_status == 0u, "Mount2 reports the existing save as opened"); Check(sceSaveDataUmount2(0, &result.mount_point) == 0, "the opened save unmounts"); + mount.blocks = 0; + mount.mount_mode = MountReadWrite | MountCreate2; + Check(sceSaveDataMount2(&mount, &result) == 0, "Mount2 CREATE2 with 0 blocks opens an existing save"); + Check(result.mount_status == 0u, "Mount2 CREATE2 reports the existing save as opened"); + Check(sceSaveDataUmount2(0, &result.mount_point) == 0, "the CREATE2 mount point unmounts"); + mount.blocks = 96; + mount.mount_mode = MountReadWrite; SceSaveDataDirName absent{}; std::strcpy(absent.data, "absent"); @@ -129,6 +137,24 @@ void TestMount2() { mount.mount_mode = MountReadWrite | MountCreate; Check(sceSaveDataMount2(&mount, &result) == SaveDataErrorParameter, "Mount2 creating a save of fewer than 96 blocks is PARAMETER"); Check(!std::filesystem::exists(std::filesystem::path("_sd") / "mount2-small"), "a refused Mount2 creates no directory"); + + mount.blocks = 96; + for (const char* name : {"survey_test", "SURVEY_TEST"}) { + SceSaveDataDirName underscored{}; + std::strcpy(underscored.data, name); + mount.dir_name = &underscored; + Check(sceSaveDataMount2(&mount, &result) == SaveDataErrorParameter, std::string("Mount2 of ") + name + " is PARAMETER"); + Check(!std::filesystem::exists(std::filesystem::path("_sd") / name), std::string("a refused Mount2 of ") + name + " creates no directory"); + } + SceSaveDataDirName mixed{}; + std::strcpy(mixed.data, "surveyTestAuto0"); + mount.dir_name = &mixed; + Check(sceSaveDataMount2(&mount, &result) == 0, "Mount2 creates a mixed-case save"); + Check(sceSaveDataUmount2(0, &result.mount_point) == 0, "the mixed-case save unmounts"); + for (const int user : {-1, 0}) { + mount.user_id = user; + Check(sceSaveDataMount2(&mount, &result) == SaveDataErrorInvalidLoginUser, "Mount2 of user " + std::to_string(user) + " is INVALID_LOGIN_USER"); + } } void TestSaveDataMemoryV1() { diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index dbb90986e5..1a7c84eccc 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -18,6 +18,7 @@ - [sceSaveDataDirNameSearch, sceSaveDataDirNameSearch2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - only the running title's saves are kept, so a search with another title's `title_id` reports zero hits, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/ec334904604ba41f5d4753f4a2498ec82540e5d6/src/core/libraries/save_data/savedata.cpp#L791-L802) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L592-L596) do when that title has no save directory; what a console returns for another title's saves is not verified. The running title's ID is the `titleId` in its `param.json` - [sceSaveDataDirNameSearch2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) runs the same search as `sceSaveDataDirNameSearch` with the same condition and result structures, as [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/SaveData/SaveDataExports.cs) does, its only source; what distinguishes it on the console is unknown. +- [sceSaveDataMount2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - answers as a PS4 Pro (firmware 10.71) did in [#8285](https://github.com/boykopovar/AnyPS5/pull/8285): `SAVE_DATA_ERROR_NOT_INITIALIZED` before `sceSaveDataInitialize3`, `SAVE_DATA_ERROR_INVALID_LOGIN_USER` for user IDs -1 and 0, and `SAVE_DATA_ERROR_PARAMETER` for mount mode 0, for a CREATE mount of fewer than 96 blocks and for a directory name containing `_` (in either case; `surveytest` and `surveyTestAuto0` mount). The 96-block minimum is applied to CREATE only, since CREATE2 was not measured, and the `_` rule was measured with CREATE mounts only. Other user IDs are not checked against the signed-in users. None of these rows were measured on a PS5 for `sceSaveDataMount3` - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a failed mount (BUSY, EXISTS, NOT_FOUND, MOUNT_FULL) leaves the result as it was, as shadPS4 does; not checked on hardware. Animal Well (PPSA22520) depends on it: it checks for its save with a mount it never unmounts, then saves with a second mount of the same directory, ignores its BUSY and keeps using the first mount point for `sceSaveDataPrepare`, its file and `sceSaveDataUmount2` - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - an unterminated or empty directory name, `.`, `..`, or a name containing `/`, `\` or `:` returns `SAVE_DATA_ERROR_PARAMETER`, which the [SDK 12.000 reference](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-mount3.md#L196-L204) lists for an invalid parameter, and leaves the result as it was, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L652-L658) does. The [SDK](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-dir-name.md#L32-L36) lets an application use only letters, digits, `-`, `.` and `@` in a name, and KytyPS5 also accepts `_` ([`valid_path_component`](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L276-L287)); which other names the console's mount rejects is not checked on hardware - [sceSaveDataBackup, sceSaveDataGetEventResult, sceSaveDataGetMountInfo, sceSaveDataGetParam, sceSaveDataLoadIcon, sceSaveDataSaveIcon, sceSaveDataSetParam, sceSaveDataUmount2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - null pointer arguments return `SAVE_DATA_ERROR_PARAMETER` instead of throwing C++ exceptions; unmounted targets return `SAVE_DATA_ERROR_NOT_MOUNTED`, and parameter type or buffer size violations return `SAVE_DATA_ERROR_PARAMETER` after slot lookup From c03058cb2ac759b6f1cccfa9535b3f7ac53c9a31 Mon Sep 17 00:00:00 2001 From: DealPotato <135158142+DealPotato@users.noreply.github.com> Date: Wed, 28 Oct 2026 12:06:26 +0300 Subject: [PATCH 4/4] fix(savedata): return PARAMETER for null Mount2 arguments Main now answers null arguments of sceSaveDataMount with SAVE_DATA_ERROR_PARAMETER instead of throwing (edf98688c8). Mount2 does the same for a null mount or a null result, and guest_savedata_ps4 checks both. AI-assisted: yes (Claude Code) Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceSaveData.native/Export.cpp | 4 ++-- core/libs/tests/GuestSaveDataPs4.cpp | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 7af4f2965e..f3ab8bbc53 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -669,8 +669,8 @@ int APS5_VABI sceSaveDataMount3(const SaveDataMount3* mount, SaveDataMountResult } static int mount2(const SaveDataMount2* mount, SaveDataMountResult* mount_result) { - if (mount == nullptr) { - throw std::runtime_error("sceSaveDataMount2: null argument"); + if (mount == nullptr || mount_result == nullptr) { + return SAVE_DATA_ERROR_PARAMETER; } if (g_initializations == 0) { return SAVE_DATA_ERROR_NOT_INITIALIZED; diff --git a/core/libs/tests/GuestSaveDataPs4.cpp b/core/libs/tests/GuestSaveDataPs4.cpp index 4a2656eee7..2aec1faa9f 100644 --- a/core/libs/tests/GuestSaveDataPs4.cpp +++ b/core/libs/tests/GuestSaveDataPs4.cpp @@ -116,7 +116,8 @@ void TestMount2() { std::strcpy(absent.data, "absent"); mount.dir_name = &absent; Check(sceSaveDataMount2(&mount, &result) == SaveDataErrorNotFound, "Mount2 open of a missing save is NOT_FOUND"); - Check(ThrowsRuntimeError([&] { sceSaveDataMount2(nullptr, &result); }), "null mount throws"); + Check(sceSaveDataMount2(nullptr, &result) == SaveDataErrorParameter, "Mount2 with a null mount is PARAMETER"); + Check(sceSaveDataMount2(&mount, nullptr) == SaveDataErrorParameter, "Mount2 with a null result is PARAMETER"); SceSaveDataDirName small{}; std::strcpy(small.data, "mount2-small");