From 43dc8a75c7456293aa33db365c46432bb13a8bb9 Mon Sep 17 00:00:00 2001 From: slopgobblercobbler Date: Thu, 29 Oct 2026 00:50:48 +0100 Subject: [PATCH] fix(libSceSaveData.native): validate directory name in sceSaveDataBackup --- .../libs/prx/libSceSaveData.native/Export.cpp | 5 +++-- core/libs/tests/GuestSaveDataMountStatus.cpp | 19 +++++++++++++++++++ docs/dev/debt/libSceSaveData.native.md | 1 + 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 5f4458136f..92ea31d184 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -302,8 +302,9 @@ int APS5_VABI sceSaveDataBackup(const SaveDataBackup* backup) { if (backup == nullptr || backup->dir_name == nullptr) { return SAVE_DATA_ERROR_PARAMETER; } - // The system copies the save directory to its backup area asynchronously; the copy itself is - // not observable by the title, only its completion event. + if (std::memchr(backup->dir_name->data, '\0', sizeof(backup->dir_name->data)) == nullptr) { + return SAVE_DATA_ERROR_PARAMETER; + } SaveDataEvent event{}; event.type = SAVE_DATA_EVENT_TYPE_BACKUP; event.error_code = SAVE_DATA_OK; diff --git a/core/libs/tests/GuestSaveDataMountStatus.cpp b/core/libs/tests/GuestSaveDataMountStatus.cpp index 3717f20b4a..954bde5854 100644 --- a/core/libs/tests/GuestSaveDataMountStatus.cpp +++ b/core/libs/tests/GuestSaveDataMountStatus.cpp @@ -62,10 +62,29 @@ void CheckUnmountedCalls() { Check(sceSaveDataBackup(nullptr) == SaveDataErrorParameter, "backup null backup is PARAMETER"); SaveDataBackup nullDirBackup{}; Check(sceSaveDataBackup(&nullDirBackup) == SaveDataErrorParameter, "backup null dir_name is PARAMETER"); + char unterminatedBackup[sizeof(SceSaveDataDirName::data)]; + std::memset(unterminatedBackup, 'a', sizeof(unterminatedBackup)); + SaveDataBackup unterminatedBackupParam{}; + unterminatedBackupParam.user_id = 0x10000000; + unterminatedBackupParam.dir_name = reinterpret_cast(unterminatedBackup); + Check(sceSaveDataBackup(&unterminatedBackupParam) == SaveDataErrorParameter, "backup unterminated dir_name is PARAMETER"); Check(sceSaveDataGetEventResult(nullptr, nullptr) == SaveDataErrorParameter, "getEventResult null event is PARAMETER"); SaveDataEvent emptyEvent{}; Check(sceSaveDataGetEventResult(nullptr, &emptyEvent) == SaveDataErrorNotFound, "getEventResult on empty queue returns NOT_FOUND"); Check(sceSaveDataGetEventResult(&emptyEvent, nullptr) == SaveDataErrorParameter, "getEventResult null result pointer is PARAMETER"); + SceSaveDataDirName validDir{}; + std::snprintf(validDir.data, sizeof(validDir.data), "valid_backup"); + SaveDataBackup validBackup{}; + validBackup.user_id = 0x10000000; + validBackup.dir_name = &validDir; + Check(sceSaveDataBackup(&validBackup) == 0, "backup valid dir_name succeeds"); + SaveDataEvent backupEvent{}; + Check(sceSaveDataGetEventResult(nullptr, &backupEvent) == 0, "getEventResult retrieves backup event"); + Check(backupEvent.type == 2, "backup event type is 2"); + Check(backupEvent.error_code == 0, "backup event error code is 0"); + Check(backupEvent.user_id == 0x10000000, "backup event user_id matches"); + Check(std::strcmp(backupEvent.dir_name.data, "valid_backup") == 0, "backup event dir_name matches"); + Check(sceSaveDataGetEventResult(nullptr, &emptyEvent) == SaveDataErrorNotFound, "event queue is empty after retrieving backup event"); } void CheckMountInfoAndParam(const SaveDataMountPoint* point) { diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index a83060f774..1f77dadd6a 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -24,6 +24,7 @@ - [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. One title (PPSA35669, in a plugin) disassembled for this entry has one call site (0x2eda71); it clears 56 bytes for the result structure first (the size of the structure used here), puts a value returned by a call with the argument 0x8000 at offset 8 and 0x400 at offset 0x10, and afterwards branches on a negative result and reads the 32-bit field at offset 0, skipping its loop when it is 0. That agrees with the result structure of `sceSaveDataDirNameSearch`. The condition structure was not read. - [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`. In PPSA05347 the function that calls it at 0x235fbb5 zeroes its 0x40-byte result before the call and, for a negative result other than 0x809f0008 and 0x809f000f (BUSY included), logs the code and returns it without recording the mount; no abort follows. - [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](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - an unterminated directory name returns `SAVE_DATA_ERROR_PARAMETER` to prevent unbounded copy; which other directory names the console's backup rejects is unverified on hardware. - [sceSaveDataBackup, sceSaveDataConvert, sceSaveDataDelete, sceSaveDataDirNameSearch, sceSaveDataDirNameSearchPs4, sceSaveDataGetEventResult, sceSaveDataGetMountInfo, sceSaveDataGetParam, sceSaveDataLoadIcon, sceSaveDataMount, sceSaveDataMount3, 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, an implementation assumption matching guest error expectations not checked on hardware; unmounted targets return `SAVE_DATA_ERROR_NOT_MOUNTED`, and parameter type or buffer size violations return `SAVE_DATA_ERROR_PARAMETER` after slot lookup - [sceSaveDataSyncSaveDataMemory](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - validates library initialization and parameters; since in-memory saves flush synchronously on write, sync confirms existence and readiness of the memory blob