Merge pull request #8850 from UnhealthyKraken/feat/libkernel-cfsetspeed

feat(libkernel): implement cfsetspeed
This commit is contained in:
Dean authored and GitHub committed 2026-10-22 00:13:16 +13:00
commit 3e91c8edf5
4 files changed
+131

No files matched your search

+10
View File
@@ -380,6 +380,16 @@ if(BUILD_TESTING)
set_tests_properties(guest_truncate_race PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
endif()
add_test_executable(guest_termios_speed_tests ${LIBS_INCLUDE_DIR}/tests/GuestTermiosSpeed.cpp)
target_include_directories(guest_termios_speed_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_termios_speed_tests PRIVATE libkernel libc ${CMAKE_DL_LIBS})
configure_windows_unwind(guest_termios_speed_tests)
add_test(NAME guest_termios_speed COMMAND guest_termios_speed_tests $<TARGET_FILE:libkernel>)
if(WIN32)
set_tests_properties(guest_termios_speed PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
endif()
add_test_executable(guest_terminal_tests ${LIBS_INCLUDE_DIR}/tests/GuestTerminal.cpp)
target_include_directories(guest_terminal_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_terminal_tests PRIVATE libkernel libc)
@@ -1,6 +1,8 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include "prx/libkernel/Socket/include/SocketRuntime.hpp"
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <stdexcept>
#ifdef _WIN32
#include <io.h>
@@ -17,6 +19,15 @@ extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handle
#endif
namespace {
struct GuestTermios {
std::uint32_t flags[4];
std::uint8_t controlCharacters[20];
std::uint32_t inputSpeed;
std::uint32_t outputSpeed;
};
static_assert(sizeof(GuestTermios) == 44 && alignof(GuestTermios) == 4);
static_assert(offsetof(GuestTermios, inputSpeed) == 36 && offsetof(GuestTermios, outputSpeed) == 40);
constexpr int BadDescriptor = 9;
constexpr int BadAddress = 14;
constexpr int InvalidArgument = 22;
@@ -73,6 +84,14 @@ int ProcessGroupError(int descriptor, const char* pipeMessage) {
}
extern "C" {
int APS5_VABI cfsetspeed_nid_postfix(void* attributes, std::uint32_t speed) {
if (attributes == nullptr) throw std::invalid_argument("cfsetspeed: attributes is null");
auto* bytes = static_cast<std::byte*>(attributes);
std::memcpy(bytes + offsetof(GuestTermios, inputSpeed), &speed, sizeof(speed));
std::memcpy(bytes + offsetof(GuestTermios, outputSpeed), &speed, sizeof(speed));
return 0;
}
int APS5_VABI tcgetattr_nid_postfix(int descriptor, void* attributes) {
static_cast<void>(attributes);
return Fail(TerminalError(descriptor));
+101
View File
@@ -0,0 +1,101 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <stdexcept>
#ifdef _WIN32
#include <windows.h>
#else
#include <dlfcn.h>
#include <sys/mman.h>
#include <unistd.h>
#endif
extern "C" int* APS5_VABI __error_nid_postfix();
namespace {
using SetSpeed = int (APS5_VABI*)(void*, std::uint32_t);
void Require(bool value, const char* message) {
if (!value) {
std::fprintf(stderr, "cfsetspeed: %s\n", message);
std::abort();
}
}
std::uint32_t Read(const unsigned char* data) {
std::uint32_t result;
std::memcpy(&result, data, sizeof(result));
return result;
}
void Check(SetSpeed setSpeed, unsigned char* attributes, std::uint32_t speed) {
std::array<unsigned char, 44> before;
std::memcpy(before.data(), attributes, before.size());
*__error_nid_postfix() = 123;
Require(setSpeed(attributes, speed) == 0, "valid call failed");
Require(*__error_nid_postfix() == 123, "successful call changed errno");
Require(Read(attributes + 36) == speed, "incorrect input speed");
Require(Read(attributes + 40) == speed, "incorrect output speed");
Require(std::memcmp(before.data(), attributes, 36) == 0, "flags or control characters changed");
}
}
int main(int argc, char** argv) {
Require(argc == 2, "expected the kernel library path");
#ifdef _WIN32
const auto library = LoadLibraryA(argv[1]);
Require(library != nullptr, "could not load libkernel");
const auto setSpeed = reinterpret_cast<SetSpeed>(GetProcAddress(library, "cfsetspeed_nid_postfix"));
SYSTEM_INFO info{};
GetSystemInfo(&info);
const std::size_t page = info.dwPageSize;
auto* memory = static_cast<unsigned char*>(VirtualAlloc(nullptr, 2 * page, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
Require(memory != nullptr, "could not allocate boundary fixture");
DWORD previous = 0;
Require(VirtualProtect(memory + page, page, PAGE_NOACCESS, &previous) != 0, "could not protect boundary fixture");
#else
const auto library = dlopen(argv[1], RTLD_NOW | RTLD_LOCAL);
Require(library != nullptr, "could not load libkernel");
const auto setSpeed = reinterpret_cast<SetSpeed>(dlsym(library, "cfsetspeed_nid_postfix"));
const long pageSize = sysconf(_SC_PAGESIZE);
Require(pageSize > 0, "could not query page size");
const auto page = static_cast<std::size_t>(pageSize);
auto* memory = static_cast<unsigned char*>(mmap(nullptr, 2 * page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
Require(memory != MAP_FAILED, "could not allocate boundary fixture");
Require(mprotect(memory + page, page, PROT_NONE) == 0, "could not protect boundary fixture");
#endif
Require(setSpeed != nullptr, "export is missing");
for (const auto speed : {0u, 1u, 50u, 9600u, 115200u, 0x12345678u, 0x80000000u, 0xffffffffu}) {
alignas(8) std::array<unsigned char, 60> bytes;
bytes.fill(0xa5);
Check(setSpeed, bytes.data() + 8, speed);
for (std::size_t i = 0; i < 8; ++i) {
Require(bytes[i] == 0xa5 && bytes[52 + i] == 0xa5, "write escaped the termios object");
}
auto* boundary = memory + page - 44;
std::memset(boundary - 8, 0x5a, 52);
Check(setSpeed, boundary, speed);
for (std::size_t i = 0; i < 8; ++i) Require(boundary[-8 + static_cast<int>(i)] == 0x5a, "write preceded the termios object");
}
bool threw = false;
try {
static_cast<void>(setSpeed(nullptr, 9600));
} catch (const std::invalid_argument&) {
threw = true;
}
Require(threw, "null pointer must be rejected explicitly");
#ifdef _WIN32
Require(VirtualFree(memory, 0, MEM_RELEASE) != 0, "could not free boundary fixture");
FreeLibrary(library);
#else
Require(munmap(memory, 2 * page) == 0, "could not free boundary fixture");
dlclose(library);
#endif
return 0;
}
+1
View File
@@ -75,6 +75,7 @@
- [Mapping hints with no room above them](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - a mapping without `MAP_FIXED` whose hint lies above the guest address space, or has no free range at or above it, is placed from the bottom of the guest arena, as FreeBSD 11.4's [`vm_mmap_object`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L1569) retries through [`vm_map_find_min`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_map.c#L1555-L1584) from its minimum address. FreeBSD's minimum is the end of the largest possible data segment rather than the start of the arena, and the console's address-space layout is not measured. PS5CEMU-HAR (PPSA99360) gives `sceKernelReserveVirtualRange` a hint near its own code, which is a host address here because the relinked executable lies outside the guest arena, so its JIT gets memory more than 2 GiB from its code and does not start
- [_sceKernelRtldThreadAtexitIncrement, _sceKernelRtldThreadAtexitDecrement](../../../core/libs/prx/libkernel/System/src/Rtld.cpp) (libkernel) - the argument is the `dso` address a title's `libc.prx` passes to `__cxa_thread_atexit` (PPSA14632), which fails when Increment returns nonzero. As in the [fpPS4](https://github.com/red-prig/fpPS4) reimplementation, Increment takes a reference on the loaded image containing the address and Decrement releases one; the variants without the leading underscore are assumed to match
- [tcgetattr, tcsetattr](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) - every file and pipe descriptor is taken as not a terminal (`ENOTTY`), and a socket gets `EADDRNOTAVAIL` for `AF_INET` and `EOPNOTSUPP` otherwise, as FreeBSD's `in_control` and `pru_control_notsupp` answer a termios ioctl; what descriptors 0 to 2 of a title are on the console, and whether any of them is a terminal there, is unknown
- [cfsetspeed](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel, NID `C1MQ6OaXRjo`) - console behavior and module/library versions are unverified. The prototype, uint32 speed and 44-byte termios layout (input speed at 36, output speed at 40) follow the public PS5 payload SDK [termios.h](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/termios.h) and [sys/_termios.h](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/sys/_termios.h). The module association follows its libkernel stub table and the NID rehashes to the aerolib name. Both speed fields accept every uint32 value and no flags or control characters change, as [FreeBSD 11.4 termios.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/gen/termios.c) does. No terminal I/O is performed. Null pointers throw where the reference dereferences them.
- [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - descriptor 0 is never reported readable and descriptors 1 and 2 are always writable, as for a console title with no input; reads from descriptor 0 still go to the host stdin. `/dev/random` and `/dev/urandom` follow `randomdev_poll` for a seeded device under [`selscan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L1178-L1236), which asks once with the flags of every set the descriptor is in: in the read set they are readable and reported in no other set, and otherwise they are reported in the write and exception sets they were asked in. A regular file, a directory or a file read from app0.zar is always readable and writable and never in the exception set, as `vop_stdpoll` answers, on every host. Guest sockets and libSceNet sockets are polled. A kqueue is readable while one of its events is triggered and is never writable or in the exception set, as FreeBSD 11.4's [`kqueue_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_event.c#L1900-L1924) answers, on every host. Other descriptors are polled through the host on Linux, while on Windows pipes are checked every millisecond with the readiness test `poll` uses and any other descriptor throws. Dungeons (PPSA16064) selects on descriptor 0, and RetroArch (PPSA99169) selects on its web server's wake-up pipe next to its listening socket
- [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - a descriptor the socket poller reports as unknown that is below `GuestSockets::FirstDescriptor` is polled on the host instead of throwing (on Windows only a pipe, as above); an invalid host descriptor fails with `EBADF`, and mixed host and socket sets are polled in short alternating slices so neither side waits out the full slice alone. A descriptor that stops being a socket, a kqueue or a pipe during the wait fails the call with `EBADF` when it is closed and is selected again with the rest of the timeout when its number was reused, as FreeBSD 11.4's [`kern_select`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L1129-L1140) looks every descriptor up again in each pass. A close that has already removed the descriptor from the guest's kqueue, pipe or file tables but not yet closed its host descriptor is seen by that new look-up as a descriptor of unknown kind, which throws on Windows and is polled through the host on Linux. Wobbly Life (PPSA29413) selects on file descriptors while streaming its bundles
- [__progname](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - unknown data export