Merge pull request #3659 from MarcZX100/carry/win-getdents-name-limit-3327

fix(libkernel): reject oversized Windows directory entry names
This commit is contained in:
Dean 2026-10-12 21:35:24 +13:00 committed by GitHub
commit 7b03a42185
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 63 additions and 5 deletions

View File

@ -127,6 +127,10 @@ int ReadDirectoryDescriptor(int fd, char* buf, int nbytes) {
std::size_t used = 0;
while (state.cursor < state.entries.size()) {
const auto& [name, type] = state.entries[state.cursor];
if (name.size() > 255) {
if (used == 0) return SCE_KERNEL_ERROR_ENAMETOOLONG;
break;
}
const std::size_t record = (8 + name.size() + 1 + 3) & ~std::size_t{3};
if (used + record > static_cast<std::size_t>(nbytes)) {
if (used == 0) return SCE_KERNEL_ERROR_EINVAL;

View File

@ -17,6 +17,9 @@ int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t);
int APS5_VABI sceKernelClose(int);
int APS5_VABI sceKernelGetdents(int, char*, int);
int APS5_VABI sceKernelGetdirentries(int, char*, int, std::int64_t*);
int APS5_VABI getdents_nid_postfix(int, char*, int);
int APS5_VABI getdirentries_nid_postfix(int, char*, int, std::int64_t*);
int* APS5_VABI __error_nid_postfix();
}
static void Check(bool value, int line) {
@ -27,15 +30,15 @@ static void Check(bool value, int line) {
}
#define Require(value) Check((value), __LINE__)
int main() {
static void CheckName(const std::u8string& utf8Name) {
const auto root = std::filesystem::temp_directory_path() /
("anyps5-directory-buffer-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
Require(std::filesystem::create_directory(root));
const std::string name = "a-long-directory-entry-name.bin";
{ std::ofstream file(root / name); Require(static_cast<bool>(file)); }
const std::string name(utf8Name.begin(), utf8Name.end());
{ std::ofstream file(root / std::filesystem::path(utf8Name)); Require(static_cast<bool>(file)); }
const int directory = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0);
Require(directory >= 0);
std::array<char, 256> buffer;
std::array<char, 512> buffer;
buffer.fill('x');
std::int64_t base = -1;
Require(sceKernelGetdirentries(directory, buffer.data(), 8, &base) == SCE_KERNEL_ERROR_EINVAL);
@ -54,9 +57,60 @@ int main() {
const int recordSize = static_cast<int>((8 + name.size() + 1 + 3) & ~std::size_t{3});
Require(sceKernelGetdirentries(directory, buffer.data(), recordSize, &base) == recordSize);
Require(std::strcmp(buffer.data() + 8, name.c_str()) == 0);
Require(static_cast<unsigned char>(buffer[7]) == name.size());
std::uint16_t storedSize = 0;
std::memcpy(&storedSize, buffer.data() + 4, sizeof(storedSize));
Require(storedSize == recordSize);
const auto rootUtf8 = root.u8string();
const std::string enumeratedPath = std::string(rootUtf8.begin(), rootUtf8.end()) + '/' + (buffer.data() + 8);
const int file = sceKernelOpen(enumeratedPath.c_str(), SCE_KERNEL_O_RDONLY, 0);
Require(file >= 0 && sceKernelClose(file) == 0);
Require(sceKernelGetdents(directory, buffer.data(), 8) == 0);
Require(sceKernelGetdents(directory, buffer.data(), 256) == 0);
Require(sceKernelClose(directory) == 0);
Require(sceKernelGetdents(-1, buffer.data(), 256) == SCE_KERNEL_ERROR_ENOTDIR);
std::filesystem::remove_all(root);
}
static void CheckOversizedName() {
const auto root = std::filesystem::temp_directory_path() /
("anyps5-directory-name-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
Require(std::filesystem::create_directory(root));
std::u8string name;
for (int i = 0; i < 86; ++i) name += u8"\u30bb";
{ std::ofstream file(root / std::filesystem::path(name)); Require(static_cast<bool>(file)); }
const int directory = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0);
Require(directory >= 0);
std::array<char, 512> buffer;
buffer.fill('x');
Require(sceKernelGetdents(directory, buffer.data(), static_cast<int>(buffer.size())) == 24);
Require(std::strcmp(buffer.data() + 8, ".") == 0);
Require(std::strcmp(buffer.data() + 20, "..") == 0);
Require(std::all_of(buffer.begin() + 24, buffer.end(), [](char byte) { return byte == 'x'; }));
for (int attempt = 0; attempt < 2; ++attempt) {
buffer.fill('x');
Require(sceKernelGetdents(directory, buffer.data(), static_cast<int>(buffer.size())) == SCE_KERNEL_ERROR_ENAMETOOLONG);
Require(std::all_of(buffer.begin(), buffer.end(), [](char byte) { return byte == 'x'; }));
}
*__error_nid_postfix() = 0;
Require(getdents_nid_postfix(directory, buffer.data(), static_cast<int>(buffer.size())) == -1);
Require(*__error_nid_postfix() == 63);
*__error_nid_postfix() = 0;
std::int64_t base = -1;
Require(getdirentries_nid_postfix(directory, buffer.data(), static_cast<int>(buffer.size()), &base) == -1);
Require(*__error_nid_postfix() == 63);
Require(std::all_of(buffer.begin(), buffer.end(), [](char byte) { return byte == 'x'; }));
Require(sceKernelClose(directory) == 0);
std::filesystem::remove_all(root);
}
int main() {
CheckName(u8"a-long-directory-entry-name.bin");
CheckName(u8"caf\u00e9-music.bin");
CheckName(u8"\u30bb\u30fc\u30d6-music.bin");
CheckName(u8"\U0001f3ae-music.bin");
std::u8string longest;
for (int i = 0; i < 85; ++i) longest += u8"\u30bb";
CheckName(longest);
CheckOversizedName();
}

View File

@ -418,7 +418,7 @@ Throughout the project, every function at every stage either **does exactly what
- [sceHttpAbortWaitRequest, sceHttpWaitRequest](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the abort takes only the epoll handle, as in [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); behaviour and error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) (PS4), not confirmed on a PS5 title or hardware. An abort stays set: every later wait on the handle, a poll included, returns `SCE_HTTP_ERROR_ABORTED` (0x80431080) until the handle is destroyed. A wait woken by `sceHttpDestroyEpoll`, or by the `sceHttpTerm` that ends the last context, returns 0 events. The epoll calls return `BEFORE_INIT` (0x80431001) while no `sceHttpInit` context is alive and `INVALID_ID` (0x80431100) for an unknown context or handle, and `sceHttpTerm` returns the same errors instead of succeeding for any id. Handles are pointers, so a stale handle whose address a newer epoll reuses is taken as that epoll
- [system](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified
- [sceKernelWriteThrottlingStatus](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown
- [Windows directory descriptors](../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws, where the NUL descriptor used to return 0, because FreeBSD defines it relative to the directory size, which is not modelled. A negative result returns `EINVAL` and a `SEEK_CUR` offset that overflows returns `EOVERFLOW`, as FreeBSD `vn_seek` reports them. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies
- [Windows directory descriptors](../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws, where the NUL descriptor used to return 0, because FreeBSD defines it relative to the directory size, which is not modelled. A negative result returns `EINVAL` and a `SEEK_CUR` offset that overflows returns `EOVERFLOW`, as FreeBSD `vn_seek` reports them. Names whose UTF-8 encoding exceeds the one-byte `d_namlen` limit are not representable in this guest directory-record layout: `getdents` returns `ENAMETOOLONG` without consuming the entry. Repeated calls therefore report the same error and entries after that name cannot be reached through this descriptor; FreeBSD's `getdents` behavior for host filenames that exceed its `MAXNAMLEN` is not verified. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies
- [/dev/random and /dev/urandom](../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled with host OS entropy (`getentropy`, `rand_s` on Windows); both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, a `dup`ed descriptor reads as an empty file, `fstat`, `lseek`, `mmap` and `select` see the null device, and opening them for writing throws
- [Wide-character conversions](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported
- [realpath](../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target.