fix(appcontent): track temporary storage mounts

Preserve files across unmount, block new lookups until remount, and validate mount-dependent operations. Keep prior-run storage inaccessible after initialization.

Written with AI assistance.
This commit is contained in:
TibixDev 2026-10-08 02:32:12 +03:00 committed by Adrià Franch
parent 084aeeb67a
commit 33555244c3
5 changed files with 135 additions and 16 deletions

View File

@ -6,10 +6,13 @@
#include <algorithm>
#include <cstring>
#include <filesystem>
#include <mutex>
#include <stdexcept>
#include <string>
static constexpr int SCE_APP_CONTENT_ERROR_PARAMETER = static_cast<int>(0x80D90002);
static constexpr int SCE_APP_CONTENT_ERROR_BUSY = static_cast<int>(0x80D90003);
static constexpr int SCE_APP_CONTENT_ERROR_NOT_MOUNTED = static_cast<int>(0x80D90004);
static constexpr int SCE_APP_CONTENT_ERROR_NOT_FOUND = static_cast<int>(0x80D90005);
static constexpr int SCE_APP_CONTENT_ERROR_DRM_NO_ENTITLEMENT = static_cast<int>(0x80D90007);
static constexpr uint32_t APPPARAM_ID_SKU_FLAG = 0;
@ -24,9 +27,25 @@ struct AppContentAddcontInfo {
uint32_t status;
};
static std::filesystem::path TemporaryDirectory(const AppContentMountPoint* mount_point) {
if (!mount_point || std::strncmp(mount_point->data, TEMPORARY_MOUNT_POINT, sizeof(mount_point->data)) != 0) APS5_INVALID_ARG_EX;
return ResolvePath_nid_no_patch(TEMPORARY_MOUNT_POINT);
namespace {
struct TemporaryData {
std::mutex mutex;
const std::filesystem::path directory = ResolvePath_nid_no_patch(TEMPORARY_MOUNT_POINT);
bool mounted = false;
TemporaryData() { BlockPathAlias_nid_no_patch(TEMPORARY_MOUNT_POINT); }
};
TemporaryData& Temporary() {
static TemporaryData state;
return state;
}
bool IsTemporaryMountPoint(const AppContentMountPoint* mountPoint) {
return std::strncmp(mountPoint->data, TEMPORARY_MOUNT_POINT, sizeof(mountPoint->data)) == 0;
}
}
static void ClearDirectory(const std::filesystem::path& directory) {
@ -96,29 +115,50 @@ int APS5_VABI sceAppContentDownloadDataGetAvailableSpaceKb(const AppContentMount
int APS5_VABI sceAppContentInitialize(const AppContentInitParam* init_param, AppContentBootParam* boot_param) {
(void)init_param;
if (!boot_param) return SCE_APP_CONTENT_ERROR_PARAMETER;
Temporary();
std::memset(boot_param, 0, sizeof(*boot_param));
return 0;
}
int APS5_VABI sceAppContentTemporaryDataFormat(const AppContentMountPoint* mount_point) {
ClearDirectory(TemporaryDirectory(mount_point));
int APS5_VABI sceAppContentTemporaryDataFormat(const AppContentMountPoint* mountPoint) {
if (!mountPoint) return SCE_APP_CONTENT_ERROR_PARAMETER;
auto& state = Temporary();
std::lock_guard lock(state.mutex);
if (!state.mounted || !IsTemporaryMountPoint(mountPoint)) return SCE_APP_CONTENT_ERROR_NOT_MOUNTED;
ClearDirectory(state.directory);
return 0;
}
int APS5_VABI sceAppContentTemporaryDataGetAvailableSpaceKb(const AppContentMountPoint* mount_point, size_t* available_space_kb) {
if (!available_space_kb) return SCE_APP_CONTENT_ERROR_PARAMETER;
*available_space_kb = static_cast<size_t>(std::filesystem::space(TemporaryDirectory(mount_point)).available / 1024);
int APS5_VABI sceAppContentTemporaryDataGetAvailableSpaceKb(const AppContentMountPoint* mountPoint, size_t* availableSpaceKb) {
if (!mountPoint || !availableSpaceKb) return SCE_APP_CONTENT_ERROR_PARAMETER;
auto& state = Temporary();
std::lock_guard lock(state.mutex);
if (!state.mounted || !IsTemporaryMountPoint(mountPoint)) return SCE_APP_CONTENT_ERROR_NOT_MOUNTED;
*availableSpaceKb = static_cast<size_t>(std::filesystem::space(state.directory).available / 1024);
return 0;
}
int APS5_VABI sceAppContentTemporaryDataMount2(uint32_t option, AppContentMountPoint* mount_point) {
if (!mount_point) return SCE_APP_CONTENT_ERROR_PARAMETER;
if (option > TEMPORARY_DATA_OPTION_FORMAT) throw std::invalid_argument(std::string(__func__) + ": unknown option " + std::to_string(option));
std::memset(mount_point->data, 0, sizeof(mount_point->data));
std::memcpy(mount_point->data, TEMPORARY_MOUNT_POINT, sizeof(TEMPORARY_MOUNT_POINT));
const auto directory = TemporaryDirectory(mount_point);
if (std::filesystem::create_directories(directory)) RecordWrittenPath_nid_no_patch(directory);
if (option == TEMPORARY_DATA_OPTION_FORMAT) ClearDirectory(directory);
int APS5_VABI sceAppContentTemporaryDataMount2(uint32_t option, AppContentMountPoint* mountPoint) {
if (!mountPoint || option > TEMPORARY_DATA_OPTION_FORMAT) return SCE_APP_CONTENT_ERROR_PARAMETER;
auto& state = Temporary();
std::lock_guard lock(state.mutex);
if (state.mounted) return SCE_APP_CONTENT_ERROR_BUSY;
if (std::filesystem::create_directories(state.directory)) RecordWrittenPath_nid_no_patch(state.directory);
if (option == TEMPORARY_DATA_OPTION_FORMAT) ClearDirectory(state.directory);
AddPathAlias_nid_no_patch(TEMPORARY_MOUNT_POINT, state.directory.string().c_str());
state.mounted = true;
std::memset(mountPoint->data, 0, sizeof(mountPoint->data));
std::memcpy(mountPoint->data, TEMPORARY_MOUNT_POINT, sizeof(TEMPORARY_MOUNT_POINT));
return 0;
}
int APS5_VABI sceAppContentTemporaryDataUnmount(const AppContentMountPoint* mountPoint) {
if (!mountPoint) return SCE_APP_CONTENT_ERROR_PARAMETER;
auto& state = Temporary();
std::lock_guard lock(state.mutex);
if (!state.mounted || !IsTemporaryMountPoint(mountPoint)) return SCE_APP_CONTENT_ERROR_NOT_MOUNTED;
BlockPathAlias_nid_no_patch(TEMPORARY_MOUNT_POINT);
state.mounted = false;
return 0;
}

View File

@ -20,6 +20,7 @@ extern "C" void RemovePathAlias_nid_no_patch(const char* guestPrefix);
extern "C" void RecordWrittenPath_nid_no_patch(const std::filesystem::path& path);
extern "C" std::vector<std::filesystem::path> WrittenPaths_nid_no_patch();
extern "C" void SyncWrittenPaths_nid_no_patch();
extern "C" void BlockPathAlias_nid_no_patch(const char* guestPrefix);
#define APS5_INVALID_ARG_EX throw std::invalid_argument(std::string(__func__) + ": invalid argument")

View File

@ -31,6 +31,7 @@ namespace {
struct PathAliases {
std::mutex mutex;
std::vector<std::pair<std::string, std::string>> entries;
std::vector<std::string> blocked;
};
PathAliases& Aliases() {
@ -55,6 +56,10 @@ std::optional<std::filesystem::path> ResolveAlias(const std::string& guestPath)
const auto relative = TrimSlashes(guestPath.c_str());
auto& aliases = Aliases();
std::lock_guard lock(aliases.mutex);
for (const auto& prefix : aliases.blocked) {
if (relative == prefix || (relative.size() > prefix.size() && relative.compare(0, prefix.size(), prefix) == 0 && relative[prefix.size()] == '/'))
throw std::filesystem::filesystem_error("Guest mount is unavailable", guestPath, std::make_error_code(std::errc::no_such_file_or_directory));
}
for (const auto& [prefix, host] : aliases.entries) {
if (relative.size() < prefix.size() || relative.compare(0, prefix.size(), prefix) != 0) continue;
if (relative.size() == prefix.size()) return std::filesystem::path(host).make_preferred();
@ -136,6 +141,7 @@ extern "C" void AddPathAlias_nid_no_patch(const char* guestPrefix, const char* h
auto& aliases = Aliases();
std::lock_guard lock(aliases.mutex);
const auto prefix = TrimSlashes(guestPrefix);
std::erase(aliases.blocked, prefix);
for (auto& entry : aliases.entries) {
if (entry.first == prefix) {
entry.second = hostPath;
@ -211,6 +217,17 @@ extern "C" void SyncWrittenPaths_nid_no_patch() {
#endif
}
extern "C" void BlockPathAlias_nid_no_patch(const char* guestPrefix) {
if (guestPrefix == nullptr) { APS5_INVALID_ARG_EX; }
const auto prefix = TrimSlashes(guestPrefix);
if (prefix.empty()) { APS5_INVALID_ARG_EX; }
auto& aliases = Aliases();
std::lock_guard lock(aliases.mutex);
std::erase_if(aliases.entries, [&](const auto& entry) { return entry.first == prefix; });
std::erase(aliases.blocked, prefix);
aliases.blocked.push_back(prefix);
}
extern "C" std::filesystem::path ResolvePath_nid_no_patch(const char* path) {
if (!path) { APS5_INVALID_ARG_EX; }
auto& state = Directories();

View File

@ -1,12 +1,21 @@
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include <chrono>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
extern "C" {
int APS5_VABI sceAppContentInitialize(const AppContentInitParam*, AppContentBootParam*);
int APS5_VABI sceAppContentAddcontMount(uint32_t, const NpUnifiedEntitlementLabel*, AppContentMountPoint*);
int APS5_VABI sceAppContentAddcontUnmount(const AppContentMountPoint*);
int APS5_VABI sceAppContentTemporaryDataMount2(uint32_t, AppContentMountPoint*);
int APS5_VABI sceAppContentTemporaryDataUnmount(const AppContentMountPoint*);
int APS5_VABI sceAppContentTemporaryDataFormat(const AppContentMountPoint*);
int APS5_VABI sceAppContentTemporaryDataGetAvailableSpaceKb(const AppContentMountPoint*, size_t*);
int APS5_VABI access_nid_postfix(const char*, int);
int* APS5_VABI __error_nid_postfix();
int APS5_VABI sceAppContentGetAddcontInfo(uint32_t, const NpUnifiedEntitlementLabel*, void*);
int APS5_VABI sceAppContentGetAddcontInfoList(uint32_t, void*, uint32_t, uint32_t*);
int APS5_VABI sceAppContentDownloadDataGetAvailableSpaceKb(const AppContentMountPoint*, size_t*);
@ -16,9 +25,15 @@ int APS5_VABI sceAppContentAppParamGetInt(uint32_t, int32_t*);
static constexpr int ErrorParameter = static_cast<int>(0x80D90002);
static constexpr int ErrorNotFound = static_cast<int>(0x80D90005);
static constexpr int ErrorDrmNoEntitlement = static_cast<int>(0x80D90007);
static constexpr int ErrorBusy = static_cast<int>(0x80D90003);
static constexpr int ErrorNotMounted = static_cast<int>(0x80D90004);
static void Require(bool value) { if (!value) std::abort(); }
int main() {
const auto original = std::filesystem::current_path();
const auto directory = original / ("appcontent-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
Require(std::filesystem::create_directory(directory));
std::filesystem::current_path(directory);
std::filesystem::create_directories("app0/sce_sys");
{
std::ofstream param("app0/sce_sys/param.json", std::ios::binary);
@ -57,6 +72,49 @@ int main() {
Require(sceAppContentAddcontUnmount(&mountPoint) == ErrorNotFound);
Require(sceAppContentAddcontUnmount(nullptr) == ErrorParameter);
std::filesystem::create_directory(directory / "temp0");
{ std::ofstream file(directory / "temp0" / "old.txt"); file << "previous run"; }
AppContentBootParam boot{};
Require(sceAppContentInitialize(nullptr, &boot) == 0);
Require(access_nid_postfix("/temp0/old.txt", 0) == -1);
Require(sceAppContentTemporaryDataUnmount(nullptr) == ErrorParameter);
Require(sceAppContentTemporaryDataUnmount(&mountPoint) == ErrorNotMounted);
Require(sceAppContentTemporaryDataMount2(0, nullptr) == ErrorParameter);
std::memset(&mountPoint, 0x5a, sizeof(mountPoint));
Require(sceAppContentTemporaryDataMount2(2, &mountPoint) == ErrorParameter);
Require(std::memcmp(&mountPoint, &untouched, sizeof(mountPoint)) == 0);
Require(sceAppContentTemporaryDataMount2(0, &mountPoint) == 0);
Require(std::strcmp(mountPoint.data, "/temp0") == 0);
const auto backing = ResolvePath_nid_no_patch("/temp0");
{ std::ofstream file(backing / "keep.txt"); file << "retained"; }
{ std::ofstream file(directory / "temp01"); file << "unrelated"; }
AppContentMountPoint second = untouched;
Require(sceAppContentTemporaryDataMount2(1, &second) == ErrorBusy);
Require(std::memcmp(&second, &untouched, sizeof(second)) == 0);
Require(std::filesystem::exists(backing / "keep.txt"));
Require(sceAppContentTemporaryDataUnmount(&second) == ErrorNotMounted);
Require(access_nid_postfix("/temp0/keep.txt", 0) == 0);
size_t available = 0;
Require(sceAppContentTemporaryDataGetAvailableSpaceKb(&mountPoint, &available) == 0 && available > 0);
Require(sceAppContentTemporaryDataUnmount(&mountPoint) == 0);
Require(std::filesystem::exists(backing / "keep.txt"));
Require(access_nid_postfix("/temp0/keep.txt", 0) == -1 && *__error_nid_postfix() == 2);
Require(access_nid_postfix("/temp0", 0) == -1 && *__error_nid_postfix() == 2);
Require(access_nid_postfix("/temp01", 0) == 0);
Require(sceAppContentTemporaryDataUnmount(&mountPoint) == ErrorNotMounted);
available = 123;
Require(sceAppContentTemporaryDataGetAvailableSpaceKb(&mountPoint, &available) == ErrorNotMounted && available == 123);
Require(sceAppContentTemporaryDataFormat(&mountPoint) == ErrorNotMounted);
Require(sceAppContentTemporaryDataMount2(0, &mountPoint) == 0);
Require(access_nid_postfix("/temp0/keep.txt", 0) == 0);
Require(sceAppContentTemporaryDataFormat(nullptr) == ErrorParameter);
Require(sceAppContentTemporaryDataFormat(&mountPoint) == 0);
Require(std::filesystem::is_empty(backing));
{ std::ofstream file(backing / "remove.txt"); file << "format"; }
Require(sceAppContentTemporaryDataUnmount(&mountPoint) == 0);
Require(sceAppContentTemporaryDataMount2(1, &mountPoint) == 0);
Require(std::filesystem::is_empty(backing));
Require(sceAppContentTemporaryDataUnmount(&mountPoint) == 0);
unsigned char info[24];
std::memset(info, 0x5a, sizeof(info));
unsigned char untouchedInfo[24];
@ -83,4 +141,6 @@ int main() {
Require(sceAppContentGetAddcontInfoList(0, nullptr, 0, nullptr) == ErrorParameter);
Require(sceAppContentGetAddcontInfoList(0, info, 0, nullptr) == ErrorParameter);
Require(sceAppContentGetAddcontInfoList(0, nullptr, 1, nullptr) == ErrorParameter);
std::filesystem::current_path(original);
std::filesystem::remove_all(directory);
}

View File

@ -318,3 +318,4 @@ Throughout the project, every function at every stage either **does exactly what
- [sceImeKeyboardGetResourceId](../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) reports that no USB keyboard is connected (`0x80bc0004`, as [shadPS4](https://github.com/shadps4-emu/shadPS4) does) because IME keyboard events are not delivered to the handler, although [libSceKeyboard](../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) exposes the host keyboard
- [Player review dialog](../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) (libSceCdlgPlayerReview) shows nothing: `scePlayerReviewDialogOpen` finishes at once and the result reports that the user canceled, as `sceWebBrowserDialogOpen` does
- [NP reachability callbacks](../../core/libs/prx/libSceNpManager/Export.cpp) retain one callback and its user data; duplicate registration and unregistering an absent callback return the errors used by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/libraries/np/np_manager.cpp). No callback is delivered because the existing NP model remains signed out with reachability unavailable.
- [Temporary app-content storage](../../core/libs/prx/libSceAppContent/Export.cpp) tracks one `/temp0` mount, preserves its files on unmount, and rejects new path resolutions until remount. Mount, format, space-query and unmount errors follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_path.pas); the pointer ABI is also declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs). Existing open host descriptors remain usable after unmount; console behaviour with open files is unverified. Host filesystem failures still throw.