mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-19 18:36:53 +02:00
3da05eff71
Fix PS5 boot failure when an optional empty sound path opens /app0/ and the game seeks and reads that directory. Pack directory records once and share their bytes and position across raw reads, enumeration, seeking, and size reporting. Apply seek and read error semantics with checked offsets. Add multiblock directory regression coverage using the existing production Vulkan resource lifecycle. Existing CMake build and kernel_file_system test pass. Independent correctness, fixture, runtime, and final compactness audits passed; launch.json run reached Jurassic Park gameplay with 4560 sampled flips, no Vulkan validation errors, and normal window-close exit.
403 lines
18 KiB
C++
403 lines
18 KiB
C++
#include "SDL.h"
|
|
#include "common/emulatorConfig.h"
|
|
#include "common/file.h"
|
|
#include "common/logging/log.h"
|
|
#include "common/subsystems.h"
|
|
#include "common/threads.h"
|
|
#include "graphics/presentation/window/windowInternal.h"
|
|
#include "kernel/fileSystem.h"
|
|
#include "libs/errno.h"
|
|
#include "libs/network.h"
|
|
#include "loader/symbolDatabase.h"
|
|
|
|
#include <array>
|
|
|
|
#include <chrono>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <limits>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <vector>
|
|
|
|
namespace Libs::LibKernelApr {
|
|
void InitLibKernel_1_Apr(Loader::SymbolDatabase *symbols);
|
|
}
|
|
|
|
namespace {
|
|
|
|
namespace FileSystem = Libs::LibKernel::FileSystem;
|
|
|
|
void Check(bool value, const char *text) {
|
|
if (!value) {
|
|
std::fprintf(stderr, "KernelFileSystemTests: failed: %s\n", text);
|
|
std::abort();
|
|
}
|
|
}
|
|
|
|
class TempDirectory {
|
|
public:
|
|
TempDirectory() {
|
|
const auto unique =
|
|
std::chrono::steady_clock::now().time_since_epoch().count();
|
|
m_path = std::filesystem::temp_directory_path() /
|
|
("kyty_kernel_file_system_" + std::to_string(unique));
|
|
Check(std::filesystem::create_directories(m_path),
|
|
"create temporary directory");
|
|
}
|
|
|
|
~TempDirectory() {
|
|
std::error_code error;
|
|
std::filesystem::remove_all(m_path, error);
|
|
}
|
|
|
|
[[nodiscard]] const std::filesystem::path &Path() const { return m_path; }
|
|
|
|
KYTY_CLASS_NO_COPY(TempDirectory);
|
|
|
|
private:
|
|
std::filesystem::path m_path;
|
|
};
|
|
|
|
void CheckSaveRename(const std::filesystem::path &root,
|
|
std::string_view payload) {
|
|
constexpr char Source[] = "/savedata0/STEMP000.DAT";
|
|
constexpr char Target[] = "/savedata0/SDATA000.DAT";
|
|
constexpr char Suffix[] = "-after-rename";
|
|
|
|
const int fd = FileSystem::KernelOpen(Source, 0x601, 0777);
|
|
Check(fd >= 3, "open temporary save file");
|
|
Check(FileSystem::KernelWrite(fd, payload.data(), payload.size()) ==
|
|
payload.size(),
|
|
"write save payload");
|
|
Check(FileSystem::KernelRename(Source, Target) == OK,
|
|
"rename open save file");
|
|
Check(FileSystem::KernelWrite(fd, Suffix, sizeof(Suffix) - 1) ==
|
|
sizeof(Suffix) - 1,
|
|
"write through renamed descriptor");
|
|
Check(FileSystem::KernelClose(fd) == OK, "close renamed descriptor");
|
|
|
|
Common::File result(root / "SDATA000.DAT", Common::File::Mode::Read);
|
|
Check(!result.IsInvalid(), "open renamed save file");
|
|
const auto data = result.ReadWholeBuffer();
|
|
const std::string expected = std::string(payload) + Suffix;
|
|
Check(data.Size() == expected.size(), "renamed save size");
|
|
Check(std::memcmp(data.GetData(), expected.data(), expected.size()) == 0,
|
|
"renamed save contents");
|
|
}
|
|
|
|
void CheckMountRoot(const std::filesystem::path &root) {
|
|
Common::File cache;
|
|
Check(cache.Create(root / "rpf.cache"), "create directory listing fixture");
|
|
cache.Close();
|
|
FileSystem::Mount(root, "/app0");
|
|
Check(FileSystem::GetRealFilename("/app0/rpf.cache") == root / "rpf.cache",
|
|
"resolve mount descendant");
|
|
Check(FileSystem::GetRealFilename("/app01/rpf.cache") == "/app01/rpf.cache",
|
|
"mount prefix must end at a path component");
|
|
|
|
for (const char *path : {"/app0", "/app0/"}) {
|
|
for (const int flags : {0, 0x00020000}) {
|
|
const int fd = FileSystem::KernelOpen(path, flags, 0);
|
|
Check(fd >= 3, "open mounted root with O_RDONLY or O_DIRECTORY");
|
|
std::array<char, 512> entries {};
|
|
const int size = FileSystem::KernelGetdents(fd, entries.data(), entries.size());
|
|
Check(size > 0 && size <= entries.size(), "enumerate mounted root");
|
|
bool found = false;
|
|
for (int offset = 0; offset < size;) {
|
|
// Directory record: inode, record length, type, name length, name.
|
|
Check(size - offset >= 8, "directory record header fits");
|
|
uint16_t length = 0;
|
|
std::memcpy(&length, entries.data() + offset + 4, sizeof(length));
|
|
const auto name_length = static_cast<uint8_t>(entries[offset + 7]);
|
|
Check(length >= 8 + name_length + 1 && length <= size - offset,
|
|
"directory record and name fit");
|
|
if (std::string_view(entries.data() + offset + 8, name_length) == "rpf.cache") {
|
|
Check(entries[offset + 6] == 8, "cache directory entry is a regular file");
|
|
found = true;
|
|
}
|
|
offset += length;
|
|
}
|
|
Check(found, "mounted root listing contains rpf.cache");
|
|
Check(FileSystem::KernelClose(fd) == OK, "close mounted root");
|
|
}
|
|
}
|
|
FileSystem::Umount("/app0");
|
|
}
|
|
|
|
void CheckDirectoryStream(const std::filesystem::path &root) {
|
|
const auto directory = root / "directory-stream";
|
|
Check(std::filesystem::create_directory(directory),
|
|
"create seek fixture directory");
|
|
for (int i = 0; i < 48; ++i) {
|
|
Common::File fixture;
|
|
Check(fixture.Create(directory / ("directory-entry-" + std::to_string(i))),
|
|
"create enough entries to cross directory blocks");
|
|
fixture.Close();
|
|
}
|
|
FileSystem::Mount(directory, "/app0");
|
|
const int fd = FileSystem::KernelOpen("/app0/", 0, 0);
|
|
Check(fd >= 3, "open directory as read-only asset");
|
|
const auto end = FileSystem::KernelLseek(fd, 0, 2);
|
|
Check(end > 512 && end % 512 == 0,
|
|
"directory SEEK_END uses padded stream size");
|
|
FileSystem::FileStat stat{};
|
|
Check(FileSystem::KernelFstat(fd, &stat) == OK && stat.st_size == end &&
|
|
stat.st_blksize == 512,
|
|
"directory stat agrees with seek and enumeration");
|
|
Check(FileSystem::KernelStat("/app0/", &stat) == OK && stat.st_size == end &&
|
|
stat.st_blocks == end / 512,
|
|
"path and descriptor directory stat agree");
|
|
Check(FileSystem::KernelLseek(fd, 0, 0) == 0,
|
|
"rewind directory for asset read");
|
|
std::vector<char> raw(static_cast<size_t>(end));
|
|
Check(FileSystem::KernelRead(fd, raw.data(), raw.size()) == end &&
|
|
FileSystem::KernelRead(fd, raw.data(), 1) == 0,
|
|
"raw directory read reaches EOF");
|
|
Check(FileSystem::KernelLseek(fd, -end, 1) == 0,
|
|
"directory SEEK_CUR uses the position advanced by read");
|
|
|
|
std::array<char, 512> block{};
|
|
int64_t base = -1;
|
|
for (int64_t offset = 0; offset < end; offset += block.size()) {
|
|
Check(FileSystem::KernelGetdirentries(fd, block.data(), block.size(),
|
|
&base) == block.size() &&
|
|
base == offset &&
|
|
std::memcmp(block.data(), raw.data() + offset, block.size()) == 0,
|
|
"directory enumeration shares raw bytes and reports each block "
|
|
"position");
|
|
for (size_t pos = 0; pos < block.size();) {
|
|
Check(block.size() - pos >= 8, "directory record header fits");
|
|
uint16_t length = 0;
|
|
std::memcpy(&length, block.data() + pos + 4, sizeof(length));
|
|
const auto name_length = static_cast<uint8_t>(block[pos + 7]);
|
|
Check(length >= 9 + name_length && length <= block.size() - pos &&
|
|
block[pos + 8 + name_length] == '\0',
|
|
"directory entries remain complete within every block");
|
|
pos += length;
|
|
}
|
|
}
|
|
Check(FileSystem::KernelGetdirentries(fd, block.data(), block.size(),
|
|
&base) == 0 &&
|
|
base == end,
|
|
"directory enumeration reports EOF position");
|
|
Check(FileSystem::KernelLseek(fd, 512, 0) == 512 &&
|
|
FileSystem::KernelGetdirentries(fd, block.data(), block.size(),
|
|
&base) == block.size() &&
|
|
base == 512 &&
|
|
std::memcmp(block.data(), raw.data() + 512, block.size()) == 0,
|
|
"restore and reread a directory enumeration position");
|
|
|
|
const auto position = FileSystem::KernelLseek(fd, 0, 1);
|
|
Check(
|
|
FileSystem::KernelLseek(fd, 0, 9) ==
|
|
Libs::LibKernel::KERNEL_ERROR_EINVAL &&
|
|
FileSystem::KernelLseek(fd, -1, 0) ==
|
|
Libs::LibKernel::KERNEL_ERROR_EINVAL &&
|
|
FileSystem::KernelLseek(fd, std::numeric_limits<int64_t>::min(), 1) ==
|
|
Libs::LibKernel::KERNEL_ERROR_EINVAL &&
|
|
FileSystem::KernelLseek(fd, std::numeric_limits<int64_t>::max(), 1) ==
|
|
Libs::LibKernel::KERNEL_ERROR_EOVERFLOW &&
|
|
FileSystem::KernelLseek(fd, 0, 1) == position,
|
|
"invalid and overflowing directory seeks preserve the position");
|
|
Check(FileSystem::KernelLseek(fd, -19, 2) == end - 19 &&
|
|
FileSystem::KernelRead(fd, block.data(), block.size()) == 19 &&
|
|
std::memcmp(block.data(), raw.data() + end - 19, 19) == 0,
|
|
"raw directory reads support byte positions and stop at EOF");
|
|
Check(FileSystem::KernelLseek(fd, 1, 0) == 1 &&
|
|
FileSystem::KernelGetdents(fd, block.data(), block.size()) ==
|
|
Libs::LibKernel::KERNEL_ERROR_EINVAL &&
|
|
FileSystem::KernelLseek(fd, 0, 1) == 1,
|
|
"directory enumeration rejects an incomplete record position");
|
|
Check(FileSystem::KernelLseek(fd, 512, 2) == end + 512 &&
|
|
FileSystem::KernelGetdirentries(fd, block.data(), block.size(),
|
|
&base) ==
|
|
Libs::LibKernel::KERNEL_ERROR_EINVAL &&
|
|
FileSystem::KernelLseek(fd, 0, 1) == end + 512,
|
|
"directory enumeration rejects a position beyond EOF");
|
|
Check(FileSystem::KernelRead(
|
|
fd, block.data(),
|
|
static_cast<size_t>(std::numeric_limits<int>::max()) + 1) ==
|
|
Libs::LibKernel::KERNEL_ERROR_EINVAL &&
|
|
FileSystem::KernelLseek(fd, 0, 1) == end + 512,
|
|
"oversized read fails without changing the directory position");
|
|
Check(FileSystem::KernelClose(fd) == OK, "close directory stream");
|
|
Check(FileSystem::KernelLseek(fd, 0, 0) ==
|
|
Libs::LibKernel::KERNEL_ERROR_EBADF,
|
|
"seek rejects a closed descriptor");
|
|
FileSystem::Umount("/app0");
|
|
}
|
|
|
|
void CheckAprPaths(const std::filesystem::path &root) {
|
|
Loader::SymbolDatabase symbols;
|
|
Libs::LibKernelApr::InitLibKernel_1_Apr(&symbols);
|
|
const auto *resolve_symbol = symbols.FindByNid("w5fcCG+t31g", Loader::SymbolType::Func);
|
|
const auto *each_symbol = symbols.FindByNid("C+Khtbbx2g8", Loader::SymbolType::Func);
|
|
Check(resolve_symbol && each_symbol, "APR path exports are registered");
|
|
using Resolve = int (KYTY_SYSV_ABI *)(const char *, const char *const *, uint32_t,
|
|
uint32_t *, uint64_t *, uint32_t *);
|
|
using ResolveEach = int (KYTY_SYSV_ABI *)(const char *, const char *const *, uint32_t,
|
|
uint32_t *, uint64_t *, int *);
|
|
const auto resolve = reinterpret_cast<Resolve>(resolve_symbol->vaddr);
|
|
const auto resolve_each = reinterpret_cast<ResolveEach>(each_symbol->vaddr);
|
|
Common::File fixture;
|
|
Check(fixture.Create(root / "apr.dat"), "create APR fixture");
|
|
fixture.Write("APR", 3);
|
|
fixture.Close();
|
|
FileSystem::Mount(root, "/app0");
|
|
|
|
uint32_t expected_id = 0xffffffffu;
|
|
for (const auto &parts : {std::array{"", "/app0/apr.dat"},
|
|
std::array{"/app0/", "apr.dat"},
|
|
std::array{"/", "app0/apr.dat"},
|
|
std::array{"/app", "0/apr.dat"}}) {
|
|
uint32_t id = 0xffffffffu, error_index = 0xffffffffu;
|
|
uint64_t size = 0;
|
|
Check(resolve(parts[0], &parts[1], 1, &id, &size, &error_index) == OK &&
|
|
id != 0xffffffffu && size == 3,
|
|
"APR concatenates empty, one-character and partial-component prefixes");
|
|
if (expected_id == 0xffffffffu) {
|
|
expected_id = id;
|
|
}
|
|
Check(id == expected_id, "equivalent APR paths return the same ID");
|
|
}
|
|
|
|
const char *paths[] = {"/app0/missing.dat", "/app0/apr.dat"};
|
|
uint32_t ids[2] = {}, error_index = 0xffffffffu;
|
|
uint64_t sizes[2] = {1, 1};
|
|
int results[2] = {};
|
|
Check(resolve_each("", paths, 2, ids, sizes, results) == 1 &&
|
|
results[0] == Libs::LibKernel::KERNEL_ERROR_ENOENT && results[1] == OK &&
|
|
ids[0] == 0xffffffffu && ids[1] == expected_id && sizes[0] == 0 && sizes[1] == 3,
|
|
"APR foreach reports a missing path and continues to the valid file");
|
|
|
|
// PATH_MAX includes NUL; all components remain below NAME_MAX (255).
|
|
std::string longest = "/app0/";
|
|
for (int i = 0; i < 3; ++i) {
|
|
longest += std::string(254, 'a') + '/';
|
|
}
|
|
longest += std::string(1023 - longest.size(), 'b');
|
|
paths[0] = longest.c_str();
|
|
Check(resolve("", paths, 1, ids, sizes, &error_index) == -1 &&
|
|
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_ENOENT && error_index == 0,
|
|
"APR accepts a pathname whose final NUL is at PATH_MAX minus one");
|
|
Check(resolve("/", paths, 1, ids, sizes, &error_index) == -1 &&
|
|
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_ENAMETOOLONG,
|
|
"APR rejects concatenated paths exceeding PATH_MAX");
|
|
std::array<char, 1024> unterminated;
|
|
unterminated.fill('/');
|
|
paths[0] = unterminated.data();
|
|
Check(resolve("", paths, 1, ids, sizes, &error_index) == -1 &&
|
|
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_ENAMETOOLONG,
|
|
"APR rejects an unterminated pathname");
|
|
paths[0] = "apr.dat";
|
|
Check(resolve(unterminated.data(), paths, 1, ids, sizes, &error_index) == -1 &&
|
|
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_ENAMETOOLONG,
|
|
"APR rejects an unterminated prefix");
|
|
FileSystem::Umount("/app0");
|
|
}
|
|
|
|
void CheckSocketWakeup() {
|
|
namespace Net = Libs::Network::Net;
|
|
// Guest sockaddr_in: length, family, network-order port/address, padding.
|
|
std::array<uint8_t, 16> address {16, 2, 0, 0, 127, 0, 0, 1};
|
|
const int listener = Net::Socket(2, 1, 0);
|
|
Check(listener >= 0, "create loopback listener");
|
|
Check(Net::Bind(listener, address.data(), address.size()) == 0, "bind loopback");
|
|
Check(Net::Listen(listener, 1) == 0, "listen on loopback");
|
|
uint32_t address_size = address.size();
|
|
Check(Net::Getsockname(listener, address.data(), &address_size) == 0,
|
|
"get assigned loopback port");
|
|
const int writer = Net::Socket(2, 1, 0);
|
|
Check(writer >= 0 && Net::Connect(writer, address.data(), address_size) == 0,
|
|
"connect wake socket");
|
|
const int reader = Net::Accept(listener, nullptr, nullptr);
|
|
Check(reader >= 0, "accept wake socket");
|
|
Check(Net::SocketClose(listener) == 0, "close listener");
|
|
const int enabled = 1;
|
|
Check(Net::Setsockopt(writer, 6, 1, &enabled, sizeof(enabled)) == 0,
|
|
"enable TCP_NODELAY");
|
|
int socket_error = -1;
|
|
uint32_t error_size = sizeof(socket_error);
|
|
*Libs::Posix::GetErrorAddr() = Libs::Posix::POSIX_EINVAL;
|
|
Check(Net::Getsockopt(writer, 0xffff, 0x1007, &socket_error, &error_size) == 0 &&
|
|
socket_error == 0 && error_size == sizeof(socket_error) &&
|
|
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EINVAL,
|
|
"SO_ERROR reports socket status without changing guest errno");
|
|
|
|
std::array<uint64_t, 16> readable {};
|
|
const auto bit = uint64_t {1} << (reader % 64);
|
|
readable[reader / 64] = bit;
|
|
const std::array<int64_t, 2> immediate {0, 0};
|
|
Check(Net::Select(reader + 1, readable.data(), nullptr, nullptr,
|
|
immediate.data()) == 0 && readable[reader / 64] == 0,
|
|
"empty socket is not readable");
|
|
const char payload[] = "wake";
|
|
Check(Net::Send(writer, payload, sizeof(payload), 0x20000) == sizeof(payload),
|
|
"send wake bytes with guest MSG_NOSIGNAL");
|
|
readable[reader / 64] = bit;
|
|
const std::array<int64_t, 2> deadline {1, 0};
|
|
Check(Net::Select(reader + 1, readable.data(), nullptr, nullptr,
|
|
deadline.data()) == 1 && readable[reader / 64] == bit,
|
|
"select reports the guest descriptor after wake");
|
|
std::array<char, sizeof(payload)> received {};
|
|
Check(Net::Recv(reader, received.data(), received.size(), 0x42) == sizeof(payload) &&
|
|
std::memcmp(received.data(), payload, sizeof(payload)) == 0,
|
|
"guest PEEK and WAITALL preserve the wake bytes");
|
|
Check(Net::Recv(reader, received.data(), received.size(), 0x40) == sizeof(payload),
|
|
"consume wake bytes with guest WAITALL");
|
|
#if !defined(_WIN32)
|
|
Check(Net::Recv(reader, received.data(), received.size(), 0x80) == -1 &&
|
|
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EWOULDBLOCK,
|
|
"empty nonblocking receive translates guest errno");
|
|
#endif
|
|
Check(Net::SocketClose(reader) == 0 && Net::SocketClose(writer) == 0,
|
|
"close wake sockets");
|
|
readable[reader / 64] = bit;
|
|
Check(Net::Select(reader + 1, readable.data(), nullptr, nullptr,
|
|
immediate.data()) == -1 &&
|
|
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EBADF &&
|
|
readable[reader / 64] == bit,
|
|
"closed descriptor fails without clearing input fd_set");
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
Common::InitializeThreads();
|
|
Common::Subsystems subsystems;
|
|
subsystems.Initialize<Config::Lifecycle>();
|
|
Config::ConfigOptions options;
|
|
options.printf_direction = Config::OutputDirection::Silent;
|
|
Config::Load(options);
|
|
subsystems.Initialize<Log::Lifecycle>();
|
|
|
|
Check(SDL_InitSubSystem(SDL_INIT_VIDEO) == 0, "initialize Vulkan test video");
|
|
auto graphics = std::make_unique<Libs::Graphics::WindowContext>();
|
|
graphics->graphic_ctx.screen_width = 64;
|
|
graphics->graphic_ctx.screen_height = 64;
|
|
graphics->window = SDL_CreateWindow("KernelFileSystemTests", 0, 0, 64, 64,
|
|
SDL_WINDOW_VULKAN | SDL_WINDOW_HIDDEN);
|
|
Check(graphics->window != nullptr, "create hidden Vulkan test window");
|
|
graphics->CreateVulkan();
|
|
|
|
TempDirectory temporary;
|
|
FileSystem::Initialize();
|
|
CheckMountRoot(temporary.Path());
|
|
CheckDirectoryStream(temporary.Path());
|
|
CheckAprPaths(temporary.Path());
|
|
FileSystem::Mount(temporary.Path(), "/savedata0");
|
|
CheckSaveRename(temporary.Path(), "first-save");
|
|
CheckSaveRename(temporary.Path(), "replacement-save");
|
|
FileSystem::Shutdown();
|
|
CheckSocketWakeup();
|
|
graphics.reset();
|
|
subsystems.Destroy();
|
|
|
|
std::printf("KernelFileSystemTests: all cases passed\n");
|
|
return 0;
|
|
}
|