Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions core/libs/prx/libc/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,14 @@ if(WIN32)
endif()

if(BUILD_TESTING)
if(UNIX)
add_test_executable(guest_atomic_assert_tests ${LIBS_INCLUDE_DIR}/tests/GuestAtomicAssert.cpp)
target_include_directories(guest_atomic_assert_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_atomic_assert_tests PRIVATE libc)
add_test(NAME guest_atomic_assert COMMAND guest_atomic_assert_tests)
set_tests_properties(guest_atomic_assert PROPERTIES TIMEOUT 10)
endif()

add_test_executable(guest_fopen_errors_tests ${LIBS_INCLUDE_DIR}/tests/GuestFopenErrors.cpp)
target_include_directories(guest_fopen_errors_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_fopen_errors_tests PRIVATE libc)
Expand Down
7 changes: 0 additions & 7 deletions core/libs/prx/libc/Export.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,6 @@ extern "C" {
LibcHeapTraceInfo_nid_no_patch(info);
}

// Dead import of Cyberpunk 2077 (PPSA04029): no call sites, but the
// Windows loader resolves imports strictly, so it must be present.
int APS5_VABI _ZSt14_Atomic_assertPKcS0__nid_postfix() {
NotImplemented_nid_no_patch(__func__);
return 0;
}

APS5_EXPORT("H2e8t5ScQGc", libc___cxa_finalize);
void APS5_VABI libc___cxa_finalize(void* dsoHandle) {
CxaFinalize_nid_no_patch(dsoHandle);
Expand Down
2 changes: 1 addition & 1 deletion core/libs/prx/libc/include/Api.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ extern "C" {

int APS5_VABI std_execute_once_nid_postfix(int* flag, int (APS5_VABI *func)(void*, void*, void**), void* arg);
void APS5_VABI LibcHeapGetTraceInfo_nid_postfix(LibcHeapInfo* info);
int APS5_VABI _ZSt14_Atomic_assertPKcS0__nid_postfix();
[[noreturn]] void APS5_VABI _ZSt14_Atomic_assertPKcS0__nid_postfix(const char* expression, const char* function);
void APS5_VABI libc___cxa_finalize(void* dsoHandle);
char* APS5_VABI strndup_nid_postfix(const char* source, std::size_t limit);
char* APS5_VABI strdup_nid_postfix(const char* source);
Expand Down
13 changes: 13 additions & 0 deletions core/libs/prx/libc/src/StdioExtras.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,19 @@ int APS5_VABI sprintf_s_nid_postfix(char* buffer, size_t size, const char* forma
std::abort();
}

extern "C" FileStream* __stderrp_nid_postfix;

[[noreturn]] void APS5_VABI _ZSt14_Atomic_assertPKcS0__nid_postfix(const char* expression, const char* function) {
fputs_nid_postfix(expression, __stderrp_nid_postfix);
if (function) {
fputs_nid_postfix(" in function ", __stderrp_nid_postfix);
fputs_nid_postfix(function, __stderrp_nid_postfix);
}
fputs_nid_postfix("\n", __stderrp_nid_postfix);
fflush_nid_postfix(__stderrp_nid_postfix);
std::abort();
}

// Only the "C" locale exists.
const char* APS5_VABI setlocale_nid_postfix(int, const char* locale) {
if (locale == nullptr || locale[0] == 0 || std::strcmp(locale, "C") == 0) return "C";
Expand Down
88 changes: 88 additions & 0 deletions core/libs/tests/GuestAtomicAssert.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#include "prx/libc/include/Api.hpp"
#include "prx/libc/include/FileStream.hpp"
#include "prx/libc/libc.hpp"
#include <csignal>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <stdexcept>
#include <string>
#include <sys/wait.h>
#include <unistd.h>
#ifdef __linux__
#include <sys/prctl.h>
#endif

extern "C" FileStream* __stderrp_nid_postfix;

static void Require(bool value) { if (!value) std::abort(); }

struct Outcome {
std::string output;
int status = 0;
};

static Outcome Run(const char* expression, const char* function) {
int channel[2] = {-1, -1};
Require(::pipe(channel) == 0);
const pid_t child = ::fork();
Require(child >= 0);
if (child == 0) {
::close(channel[0]);
if (::dup2(channel[1], 2) != 2) ::_exit(70);
::close(channel[1]);
#ifdef __linux__
if (::prctl(PR_SET_DUMPABLE, 0) != 0) ::_exit(71);
#endif
_ZSt14_Atomic_assertPKcS0__nid_postfix(expression, function);
}
::close(channel[1]);
Outcome outcome;
char buffer[256];
for (ssize_t count; (count = ::read(channel[0], buffer, sizeof(buffer))) > 0;) outcome.output.append(buffer, static_cast<std::size_t>(count));
::close(channel[0]);
Require(::waitpid(child, &outcome.status, 0) == child);
return outcome;
}

int main() {
const auto named = Run("invalid memory order", "atomic_load");
Require(WIFSIGNALED(named.status) && WTERMSIG(named.status) == SIGABRT);
Require(named.output == "invalid memory order in function atomic_load\n");

const auto anonymous = Run("invalid memory order", nullptr);
Require(WIFSIGNALED(anonymous.status) && WTERMSIG(anonymous.status) == SIGABRT);
Require(anonymous.output == "invalid memory order\n");

const std::filesystem::path probe = "guest_stderr_probe_" + std::to_string(::getpid()) + ".txt";
const pid_t redirected = ::fork();
Require(redirected >= 0);
if (redirected == 0) {
if (freopen_nid_postfix(probe.c_str(), "w", __stderrp_nid_postfix) != __stderrp_nid_postfix) ::_exit(72);
if (setvbuf_nid_postfix(__stderrp_nid_postfix, nullptr, 0, 0) != 0) ::_exit(73);
#ifdef __linux__
if (::prctl(PR_SET_DUMPABLE, 0) != 0) ::_exit(71);
#endif
_ZSt14_Atomic_assertPKcS0__nid_postfix("redirected assertion", "sample_func");
}
int redirectedStatus = 0;
Require(::waitpid(redirected, &redirectedStatus, 0) == redirected);
Require(WIFSIGNALED(redirectedStatus) && WTERMSIG(redirectedStatus) == SIGABRT);
std::ifstream stream(probe, std::ios::binary);
const std::string content((std::istreambuf_iterator<char>(stream)), std::istreambuf_iterator<char>());
stream.close();
std::filesystem::remove(probe);
Require(content == "redirected assertion in function sample_func\n");

bool threw = false;
try {
_ZSt14_Atomic_assertPKcS0__nid_postfix(nullptr, "atomic_load");
} catch (const std::runtime_error& error) {
threw = true;
Require(std::string(error.what()).find("null string") != std::string::npos);
}
Require(threw);

return 0;
}
1 change: 1 addition & 0 deletions docs/dev/debt/libc.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
- [strndup and strdup](../../../core/libs/prx/libc/src/AllocatingStrings.cpp) (libc) return null with `errno` set to `EINVAL` (22) when `source` is null, preventing a host segfault on invalid guest input. POSIX.1-2008 and FreeBSD 11.4 libc leave `strndup(NULL)` and `strdup(NULL)` undefined (faulting). The `libc.prx` that titles ship has no null check either (read from disassembly of PPSA13934's copy, not measured on a console): `strdup` (0x6b440) calls `strlen` and `strndup` (0x6b650) calls `strnlen` on the source before allocating, so both fault on a null source there, except `strndup(NULL, 0)`, whose `strnlen` reads nothing and which returns an empty copy; here it returns null with `EINVAL`.
- [snprintf_s and vsnprintf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) - follow C11 K.3.5.3.5 and K.3.5.3.12 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`, as `snwprintf_s` does: a null buffer or format, `size` of 0 or above `RSIZE_MAX`, any `%n` and a null `%s`, `%ls` or `%S` argument return a negative value and set `buffer[0]` to zero when the buffer and size allow it; otherwise the output is truncated to the buffer and the full length is returned. A violation only returns: the constraint handler is not invoked, and the console's error value is unverified. Not confirmed on a PS5 title
- [vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) - returns the count of items converted before an undersized `%s`/`%c`/`%[` destination (0 when it is the first), not EOF. Not confirmed on a PS5 title
- [_ZSt14_Atomic_assertPKcS0_](../../../core/libs/prx/libc/src/StdioExtras.cpp) (libc, NID `u2tMGOLaqnE`) - writes the assertion message to the guest `_Stderr` stream and aborts. On a console (disassembly of SDK 7.00 `libc.prx` at 0x80170), a null expression faults when passed to `fputs`; in this runtime `fputs` throws `std::runtime_error("fputs: null string")` instead of faulting.
- [localeconv](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, re-exported by libSceAmpr) - the 0x90-byte Dinkumware struct (empty monetary strings, `CHAR_MAX` for the 14 `char` fields, `decimal_point` ".", `_False`/`_True` "false"/"true") is read from the `libc.prx` that Astro's Playroom ships. It is a fixed C-locale table, which `setlocale` cannot change: it accepts only "C" in the system `libSceLibcInternal` of PS5 firmware 13.60 and in the `libc.prx` titles ship. The console's own table is not measured, and `_Frac_grouping`, `_Frac_sep`, `_No`, `_Yes` are empty strings as in that dump
- [set_constraint_handler_s](../../../core/libs/prx/libc/src/WideConversion.cpp) (libc, re-exported by libSceAmpr) - the default handler is assumed to abort; each module keeps its own handler state, so a handler set through one library is not seen by the other. Within a module every `_s` function calls the installed handler, with the guest's calling convention. `ctime_s` and `asctime_s` follow the Dinkumware 26-byte format with `EINVAL` for a buffer under 26 bytes, a null pointer or an out-of-range weekday/month; `ctime_s` uses the guest time zone of `localtime_s`, and the console's exact checks are not verified
- [qsort_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) sorts as `qsort` does and passes its context as the comparator's third argument, as FreeBSD 13's [`qsort_s`](https://github.com/freebsd/freebsd-src/blob/release/13.0.0/lib/libc/stdlib/qsort.c#L97-L127) (C11 Annex K) does. It returns `EINVAL` after calling the constraint handler when `n` or `es` exceeds `RSIZE_MAX`, or when `n` is not 0 and `a` or `cmp` is null; FreeBSD 11, which the rest of libc follows, has no `qsort_s`. An element size of 0 sorts nothing, where FreeBSD loops over the same address. The console's behaviour is unverified; the Quake homebrew (#6698) imports it from its `libOSMesa.so.8`
Expand Down
Loading