diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 1edbc7e5b99..37e66afb772 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -5,6 +5,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Equeue/src/EventAccessors.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/DirectoryDescriptor.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/NativeStat.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/File/src/Terminal.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/Open.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/Stdio.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Semaphore/src/Semaphore.cpp @@ -133,6 +134,16 @@ if(BUILD_TESTING) set_tests_properties(guest_filesystem PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") 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) + configure_windows_unwind(guest_terminal_tests) + add_test(NAME guest_terminal COMMAND guest_terminal_tests) + set_tests_properties(guest_terminal PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_terminal PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_kernel_sync_tests ${LIBS_INCLUDE_DIR}/tests/GuestKernelSync.cpp) target_include_directories(guest_kernel_sync_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_kernel_sync_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/File/src/Terminal.cpp b/core/libs/prx/libkernel/File/src/Terminal.cpp new file mode 100644 index 00000000000..f013a865fb9 --- /dev/null +++ b/core/libs/prx/libkernel/File/src/Terminal.cpp @@ -0,0 +1,72 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/Socket/include/SocketRuntime.hpp" +#include +#include +#ifdef _WIN32 +#include +#include +#else +#include +#include +#endif + +extern "C" int* APS5_VABI __error_nid_postfix(); +#ifdef _WIN32 +extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); +#endif + +namespace { +constexpr int BadDescriptor = 9; +constexpr int BadAddress = 14; +constexpr int InvalidArgument = 22; +constexpr int NotTerminal = 25; +constexpr int OperationNotSupported = 45; +constexpr int AddressNotAvailable = 49; +constexpr int SetNow = 0; +constexpr int SetDrain = 1; +constexpr int SetFlush = 2; +constexpr int SetSoft = 0x10; +constexpr int InternetFamily = 2; + +int Fail(int error) { + *__error_nid_postfix() = error; + return -1; +} + +#ifdef _WIN32 +void IgnoreInvalidParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {} +bool HostOpen(int descriptor) { + const auto previous = _set_thread_local_invalid_parameter_handler(IgnoreInvalidParameter); + const auto handle = ::_get_osfhandle(descriptor); + _set_thread_local_invalid_parameter_handler(previous); + return reinterpret_cast(handle) != INVALID_HANDLE_VALUE; +} +#else +bool HostOpen(int descriptor) { return ::fcntl(descriptor, F_GETFD) != -1; } +#endif + +int TerminalError(int descriptor) { + if (descriptor >= GuestSockets::FirstDescriptor) { + const int family = GuestSockets::Family(descriptor); + if (family < 0) return BadDescriptor; + return family == InternetFamily ? AddressNotAvailable : OperationNotSupported; + } + if (descriptor < 0 || !HostOpen(descriptor)) return BadDescriptor; + return NotTerminal; +} +} + +extern "C" { +int APS5_VABI tcgetattr_nid_postfix(int descriptor, void* attributes) { + static_cast(attributes); + return Fail(TerminalError(descriptor)); +} + +int APS5_VABI tcsetattr_nid_postfix(int descriptor, int action, const void* attributes) { + if ((action & SetSoft) != 0 && attributes == nullptr) throw std::invalid_argument("tcsetattr: attributes is null"); + const int mode = action & ~SetSoft; + if (mode != SetNow && mode != SetDrain && mode != SetFlush) return Fail(InvalidArgument); + if (attributes == nullptr) return Fail(BadAddress); + return Fail(TerminalError(descriptor)); +} +} diff --git a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp index 0ef89de1006..2c094e86d17 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp @@ -3,4 +3,5 @@ namespace GuestSockets { constexpr int FirstDescriptor = 0x10000000; int Close(int descriptor); bool IsOpen(int descriptor); +int Family(int descriptor); } diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 5b856cb0358..6db58625453 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -180,6 +180,11 @@ bool GuestSockets::IsOpen(int descriptor) { std::lock_guard lock(socketsMutex); return sockets.contains(descriptor); } +int GuestSockets::Family(int descriptor) { + std::lock_guard lock(socketsMutex); + const auto found = sockets.find(descriptor); + return found == sockets.end() ? -1 : found->second->family; +} namespace { #ifdef _WIN32 diff --git a/core/libs/tests/GuestTerminal.cpp b/core/libs/tests/GuestTerminal.cpp new file mode 100644 index 00000000000..19653acc334 --- /dev/null +++ b/core/libs/tests/GuestTerminal.cpp @@ -0,0 +1,97 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI tcgetattr_nid_postfix(int, void*); +int APS5_VABI tcsetattr_nid_postfix(int, int, const void*); +int APS5_VABI open_nid_postfix(const char*, int, int); +int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI close_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "terminal check failed at line %d, errno %d\n", line, *__error_nid_postfix()); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +constexpr int Now = 0; +constexpr int Drain = 1; +constexpr int Flush = 2; +constexpr int Soft = 0x10; +constexpr int NotTerminal = 25; +constexpr int BadDescriptor = 9; +constexpr int InvalidArgument = 22; +constexpr int BadAddress = 14; +constexpr int AddressNotAvailable = 49; + +static bool Fails(int result, int error) { return result == -1 && *__error_nid_postfix() == error; } + +static void RequireNotTerminal(int descriptor) { + unsigned char attributes[44]; + std::memset(attributes, 0xaa, sizeof(attributes)); + unsigned char untouched[44]; + std::memset(untouched, 0xaa, sizeof(untouched)); + *__error_nid_postfix() = 0; + Require(Fails(tcgetattr_nid_postfix(descriptor, attributes), NotTerminal)); + Require(std::memcmp(attributes, untouched, sizeof(attributes)) == 0); + for (const int action : {Now, Drain, Flush, Now | Soft, Flush | Soft}) { + *__error_nid_postfix() = 0; + Require(Fails(tcsetattr_nid_postfix(descriptor, action, attributes), NotTerminal)); + } +} + +int main() { + const auto name = "anyps5-terminal-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + const int file = open_nid_postfix(name.c_str(), 0x202, 0644); + Require(file >= 0); + RequireNotTerminal(file); + RequireNotTerminal(0); + RequireNotTerminal(1); + RequireNotTerminal(2); + + int ends[2] = {-1, -1}; + Require(pipe_nid_postfix(ends) == 0); + RequireNotTerminal(ends[0]); + RequireNotTerminal(ends[1]); + + const int socket = socket_nid_postfix(2, 2, 0); + Require(socket >= 0); + unsigned char untouched[44]; + std::memset(untouched, 0xaa, sizeof(untouched)); + unsigned char probe[44]; + std::memset(probe, 0xaa, sizeof(probe)); + Require(Fails(tcgetattr_nid_postfix(socket, probe), AddressNotAvailable)); + Require(std::memcmp(probe, untouched, sizeof(probe)) == 0); + Require(Fails(tcsetattr_nid_postfix(socket, Flush, probe), AddressNotAvailable)); + + unsigned char attributes[44]{}; + Require(Fails(tcsetattr_nid_postfix(file, 3, attributes), InvalidArgument)); + Require(Fails(tcsetattr_nid_postfix(file, -1, attributes), InvalidArgument)); + Require(Fails(tcsetattr_nid_postfix(file, Soft | 3, attributes), InvalidArgument)); + Require(Fails(tcsetattr_nid_postfix(-1, 3, attributes), InvalidArgument)); + Require(Fails(tcsetattr_nid_postfix(file, Now, nullptr), BadAddress)); + Require(Fails(tcsetattr_nid_postfix(-1, Now, nullptr), BadAddress)); + Require(Fails(tcgetattr_nid_postfix(-1, attributes), BadDescriptor)); + Require(Fails(tcsetattr_nid_postfix(-1, Now, attributes), BadDescriptor)); + + Require(close_nid_postfix(socket) == 0); + Require(Fails(tcgetattr_nid_postfix(socket, attributes), BadDescriptor)); + Require(Fails(tcsetattr_nid_postfix(socket, Now, attributes), BadDescriptor)); + Require(close_nid_postfix(ends[0]) == 0 && close_nid_postfix(ends[1]) == 0); + Require(close_nid_postfix(file) == 0); + Require(Fails(tcgetattr_nid_postfix(file, attributes), BadDescriptor)); + Require(Fails(tcsetattr_nid_postfix(file, Flush, attributes), BadDescriptor)); + std::filesystem::remove(name); + return 0; +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 202e3461a36..b63fed500b1 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -138,6 +138,7 @@ Throughout the project, every function at every stage either **does exactly what - [_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 - [_sceLibcInternalForceTlsDestructor](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - behaviour follows `sceLibcForceTlsDestructor(int)` in a title's `libc.prx` (disassembly): the calling thread's destructors whose dso belongs to the module run newest first, only if the dso's module is loaded and the dso points to itself, in up to 4 passes. The return type is unknown (that function returns nothing), 0 is returned. The module of each dso is looked up when the destructors are forced, not when they are registered as `libc.prx` does, and the lookup is Linux only, so on Windows the call throws when the thread has destructors - [std_execute_once](../../core/libs/prx/libc/Export.cpp) (libc) - assumed to be the plain-named twin of `std::_Execute_once` (`_ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1_`, the callee of Dinkumware `call_once`): it delegates there and gets the [thread.once.callonce] semantics already verified for that symbol (one invocation per flag, concurrent callers block until it completes, and a failing or throwing invocation leaves the flag unready so the next caller runs it). The name equivalence is not confirmed on a title, and the callback convention (called with (null, arg, null), non-zero return meaning success) is the twin's, unverified for this symbol +- [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 - [__progname](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - unknown data export - [pthread_barrierattr_setpshared](../../core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp) (libkernel) - PTHREAD_PROCESS_SHARED is accepted on the assumption that the console follows FreeBSD 11.0 libthr, which accepts it (9.0 rejects it with EINVAL); unverified on hardware. Guest code runs in one process, so a shared barrier behaves as a private one - [sceSslClose](../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature