Skip to content
Merged
15 changes: 0 additions & 15 deletions core/libs/prx/libSceAmpr/Export.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -845,11 +845,6 @@ int APS5_VABI sceAmprAmmWaitCommandBufferCompletion(std::uint32_t id) {
return AmmSubmitted_nid_no_patch(id) ? 0 : SCE_KERNEL_ERROR_ESRCH;
}

int APS5_VABI _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}

void APS5_VABI _Unlock_shared_ptr_spin_lock_nid_postfix(void) {
sharedPtrSpinLock.clear(std::memory_order_release);
}
Expand All @@ -858,21 +853,11 @@ int APS5_VABI _Cnd_init_with_name_nid_postfix(void** handle, const char*) {
return _Cnd_init_nid_postfix(handle);
}

int APS5_VABI _ZTVSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}

int APS5_VABI _ZNKSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE3getES3_S3_RSt8ios_baseRNSt5_IosbIiE8_IostateEP2tmPKcSE__nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}

int APS5_VABI _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}

int APS5_VABI _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE7_GetcatEPPKNSt6locale5facetEPKS5__nid_postfix(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
Expand Down
3 changes: 3 additions & 0 deletions core/libs/prx/libc/src/LocaleSupport.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,9 @@ std::uint64_t _ZNSt7codecvtIcc9_MbstatetE2idE_nid_postfix = 0;
std::uintptr_t _ZTVSt7codecvtIcc9_MbstatetE_nid_postfix[16] {};
std::uint64_t _ZNSt7num_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 0;
std::uintptr_t _ZTVSt7num_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix[12] {};
std::uint64_t _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 0;
std::uintptr_t _ZTVSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix[17] {};
std::uint64_t _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 0;

std::streamoff _ZSt7_BADOFF_nid_postfix = -1;
std::fpos_t _ZSt4_Fpz_nid_postfix {};
Expand Down
14 changes: 14 additions & 0 deletions core/libs/tests/GuestLocale.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ void APS5_VABI _Locksyslock_nid_postfix();
void APS5_VABI _Unlocksyslock_nid_postfix();
std::uint32_t APS5_VABI _Towctrans_nid_postfix(std::uint32_t wc, std::uint64_t desc);
int* APS5_VABI __error_nid_postfix();
extern std::uint64_t _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix;
extern std::uint64_t _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix;
extern std::uintptr_t _ZTVSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix[17];

}

Expand All @@ -52,6 +55,16 @@ static void RequireException(TAction action) {
Require(caught);
}

static void CheckInputFacetObjects() {
Require(_ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix == 0 && _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix == 0);
_ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 7;
_ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 9;
Require(_ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix == 7 && _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix == 9);
_ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 0;
_ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description relies on libSceAmpr.prx-qualified imports falling back to the same libc object, but these tests only check the objects through the direct libc-linked test and import_audit provider inventory. Could you add a resolver/entry-stub regression with a scoped libSceAmpr.prx import that verifies its search order reaches libc (or a Windows runtime probe showing both qualified imports resolve to the same address)? That would pin the cross-module identity claim independently of the current resolver implementation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked the current fallback path: on Windows a host import qualified to libSceAmpr.prx searches that module and then libc.prx, so the intended shared provider looks plausible by inspection. A narrow regression could use WindowsEntryStubBuilder with libraries = {"libSceAmpr.prx", "libc.prx"} and one of these NIDs qualified to Ampr, then assert the search table contains both indexes in that order; the libc-qualified case should resolve directly to the same libc index. That would exercise the resolver plan without needing a proprietary title.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the resolver-plan regression on my fork at MarcZX10:codex/pr-8880-ampr-libc-resolver-test (commit babde2b5bd). It asserts that an Ampr-qualified num_get::id import searches Ampr then libc, while the libc-qualified import searches libc directly. On this PR head, entry_stub_tests builds and passes on Linux; python tools/check_conventions.py --base upstream/main --head HEAD and git diff --check also pass. This verifies the generated search order, not a runtime address comparison; the branch is available to cherry-pick since direct push to your PR branch is unavailable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both are in now. Your search-order regression is cherry-picked as 7157ebb with your authorship; on this head entry_stub_tests passes on Windows as well. The runtime side is 2657a81: windows_import_identity (core/relinker/elfpatcher/tests/WindowsImportIdentityTests.cpp, registered next to windows_dependency_diagnostics). It generates two probes with WindowsEntryStubBuilder, WindowsImportBuilder and WindowsPeWriter, the way #8853's generator and the diagnostics test do, and runs them beside the patched libraries (core/libs/libs, so it depends on libs):

  • same.exe imports the three NIDs twice each, qualified to libSceAmpr.prx and to libc.prx, into six GOT slots, and its entry compares each pair (mov rax,[a]; cmp rax,[b]; jne fail), exiting 0 only if all three resolved to one address.
  • different.exe compares the first NID from libSceAmpr.prx with getpagesize from libkernel.prx and must exit 1, so a comparison that could not tell addresses apart would fail the test.

A failed probe's stub output lands in the test's message (an empty library folder gives Failed to load module: ...libSceAmpr.prx, GetLastError: 126). Qualifying the second import to libSceLibcInternal.prx is not possible at this layer, the stub builder only accepts modules from its load list, and the relinker adds libc.prx for that name before this stage (GuestModuleBuilder.cpp), so libc.prx is the direct reference here.

Result on this PC (Windows 11, MinGW GCC 15.2.0, Release): windows_import_identity passes (ctest -R windows_import_identity, 0.2 s) after ninja windows_import_identity_tests, which builds libs first. The description's Tested section lists both.

for (const auto slot : _ZTVSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix) Require(slot == 0);
}

static void CheckGuestCalls() {
auto* locale = _ZSt21_sceLibcClassicLocale_nid_postfix;
Require(locale != nullptr && locale == _ZNSt6locale16_GetgloballocaleEv_nid_postfix());
Expand Down Expand Up @@ -249,6 +262,7 @@ int main() {
CheckStreamWidth();
CheckStandardStreams();
CheckWideCaseTranslation();
CheckInputFacetObjects();
_Locksyslock_nid_postfix();
_Locksyslock_nid_postfix();
_Unlocksyslock_nid_postfix();
Expand Down
10 changes: 10 additions & 0 deletions core/relinker/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,16 @@ if(WIN32)
add_test(NAME windows_dependency_diagnostics COMMAND windows_dependency_diagnostics_tests)
endif()

if(NOT ANYPS5_RELINKER_ONLY)
add_test_executable(windows_import_identity_tests EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/WindowsImportIdentityTests.cpp)
target_link_libraries(windows_import_identity_tests PRIVATE relinker_core)
if(BUILD_TESTING)
set_target_properties(windows_import_identity_tests PROPERTIES EXCLUDE_FROM_ALL FALSE)
add_dependencies(windows_import_identity_tests libs)
add_test(NAME windows_import_identity COMMAND windows_import_identity_tests "${CMAKE_BINARY_DIR}/core/libs/libs")
endif()
endif()

add_test_executable(amd64_only_windows_tests EXCLUDE_FROM_ALL
${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/Amd64OnlyWindowsTests.cpp
${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsTrampolineBuilder.cpp
Expand Down
18 changes: 18 additions & 0 deletions core/relinker/elfpatcher/tests/EntryStubTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,23 @@ void windowsStubRejectsAnOverlongAllocatorPath() {
throw std::runtime_error("an allocator path past the startup buffer was accepted");
}

void windowsLibSceAmprObjectImportsFallBackToTheLibcProvider() {
using namespace Elfpatcher::Windows;
const auto nativeImports = WindowsImportBuilder().Build(0x1000);
const std::vector<std::string> libraries{"libSceAmpr.prx", "libc.prx"};
const std::vector<PeImport> amprImport{{.Name = "-mLzBSk-VGs", .TargetRva = 0x2000, .Addend = 0, .Library = "libSceAmpr.prx"}};
const auto amprStub = WindowsEntryStubBuilder().Build(0x40000, 0x1000, nativeImports, libraries, amprImport, "$ORIGIN/libs", false, false);
const std::vector<std::uint8_t> fallbackOrder{0, 0, 0, 0, 1, 0, 0, 0};
require(std::search(amprStub.Data.Data.begin(), amprStub.Data.Data.end(), fallbackOrder.begin(), fallbackOrder.end()) != amprStub.Data.Data.end(),
"a libSceAmpr-qualified locale object import does not fall back to libc");

const std::vector<PeImport> libcImport{{.Name = "-mLzBSk-VGs", .TargetRva = 0x2000, .Addend = 0, .Library = "libc.prx"}};
const auto libcStub = WindowsEntryStubBuilder().Build(0x40000, 0x1000, nativeImports, libraries, libcImport, "$ORIGIN/libs", false, false);
const std::vector<std::uint8_t> libcOrder{1, 0, 0, 0};
require(std::search(libcStub.Data.Data.begin(), libcStub.Data.Data.end(), libcOrder.begin(), libcOrder.end()) != libcStub.Data.Data.end(),
"a libc-qualified locale object import does not resolve through libc");
}

void windowsKernelVariantImportsSearchCanonicalKernelAfterTheRequestedVariant() {
using namespace Elfpatcher::Windows;
const auto nativeImports = WindowsImportBuilder().Build(0x1000);
Expand Down Expand Up @@ -153,6 +170,7 @@ int main() {
windowsStubLoadsTheAllocatorOnRequest("C:/Games/Title/libs", u"C:\\Games\\Title\\libs\\", {0x48, 0x8d, 0x0d});
windowsStubLoadsTheAllocatorOnRequest("$ORIGIN/libs", u"libs\\", {0x48, 0x8d, 0x35});
windowsStubRejectsAnOverlongAllocatorPath();
windowsLibSceAmprObjectImportsFallBackToTheLibcProvider();
windowsKernelVariantImportsSearchCanonicalKernelAfterTheRequestedVariant();
windowsKernelVariantDependenciesLoadCanonicalKernel();
} catch (const std::exception& error) {
Expand Down
162 changes: 162 additions & 0 deletions core/relinker/elfpatcher/tests/WindowsImportIdentityTests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>
#include <elfpatcher/windows/WindowsEntryStubBuilder.hpp>
#include <elfpatcher/windows/WindowsImportBuilder.hpp>
#include <elfpatcher/windows/WindowsPeWriter.hpp>
#include <array>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <random>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>

namespace {

namespace Fs = std::filesystem;
using namespace Elfpatcher::Windows;

constexpr std::uint32_t GotRva = LoadRva + 0x1000;
constexpr std::array<const char*, 3> LocaleObjects{"-mLzBSk-VGs", "a54t8+k7KpY", "KfcTPbeaOqg"};

void require(const bool condition, const std::string& message) {
if (!condition) throw std::runtime_error(message);
}

Fs::path createFixtureDirectory(const Fs::path& parent) {
std::random_device random;
for (int attempt = 0; attempt < 100; ++attempt) {
auto name = Fs::path("windows-import-identity-");
name += std::to_string(GetCurrentProcessId()) + "-" + std::to_string(random());
const auto directory = parent / name;
if (Fs::create_directory(directory)) return directory;
}
throw std::runtime_error("Cannot create a unique import identity fixture directory");
}

void appendDisplacement(std::vector<std::uint8_t>& code, const std::int32_t displacement) {
for (int i = 0; i < 4; ++i) code.push_back(static_cast<std::uint8_t>((static_cast<std::uint32_t>(displacement) >> (8 * i)) & 0xffu));
}

// For each pair of GOT slots: mov rax,[a]; cmp rax,[b]; jne fail. Then xor eax,eax; ret. fail: mov eax,1; ret.
std::vector<std::uint8_t> compareCode(const std::vector<std::pair<int, int>>& pairs) {
std::vector<std::uint8_t> code;
const auto failOffset = static_cast<std::int32_t>(pairs.size() * 16 + 3);
for (std::size_t i = 0; i < pairs.size(); ++i) {
const auto base = static_cast<std::uint32_t>(i * 16);
code.insert(code.end(), {0x48, 0x8b, 0x05});
appendDisplacement(code, static_cast<std::int32_t>(GotRva + 8u * static_cast<std::uint32_t>(pairs[i].first)) - static_cast<std::int32_t>(LoadRva + base + 7));
code.insert(code.end(), {0x48, 0x3b, 0x05});
appendDisplacement(code, static_cast<std::int32_t>(GotRva + 8u * static_cast<std::uint32_t>(pairs[i].second)) - static_cast<std::int32_t>(LoadRva + base + 14));
code.push_back(0x75);
code.push_back(static_cast<std::uint8_t>(failOffset - static_cast<std::int32_t>(base + 16)));
}
code.insert(code.end(), {0x31, 0xc0, 0xc3});
code.insert(code.end(), {0xb8, 0x01, 0x00, 0x00, 0x00, 0xc3});
return code;
}

std::uint32_t slot(const int index) {
return GotRva + 8u * static_cast<std::uint32_t>(index);
}

void generateProbe(const Fs::path& file, const std::string& libraries, const std::vector<PeImport>& peImports, const std::vector<std::pair<int, int>>& pairs) {
const std::vector<std::string> loaded{"libSceAmpr.prx", "libkernel.prx", "libc.prx"};
PeSection entry{".guest", LoadRva, SectionRead | SectionExecute | 0x20u, compareCode(pairs)};
PeSection got{".got", GotRva, SectionRead | SectionWrite | 0x40u, std::vector<std::uint8_t>(8 * 8)};
const auto imports = WindowsImportBuilder().Build(LoadRva + 0x2000);
const auto startup = WindowsEntryStubBuilder().Build(LoadRva + 0x3000, entry.Rva, imports, loaded, peImports, libraries, false, false);
std::array<PeDirectory, 16> directories{};
directories[1] = imports.Directory;
directories[3] = startup.ExceptionDirectory;
directories[12] = imports.AddressTable;
const auto image = WindowsPeWriter().Write({entry, got, imports.Section, startup.Data, startup.Code}, startup.Code.Rva, directories, false, ImageBase, false);
std::ofstream stream(file, std::ios::binary);
stream.write(reinterpret_cast<const char*>(image.data()), static_cast<std::streamsize>(image.size()));
require(static_cast<bool>(stream), "Cannot write " + file.string());
}

DWORD runProbe(const Fs::path& probe, std::string& output) {
SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE};
HANDLE input = nullptr;
HANDLE written = nullptr;
require(CreatePipe(&input, &written, &security, 0) && SetHandleInformation(input, HANDLE_FLAG_INHERIT, 0), "Cannot create the probe's pipe");
STARTUPINFOW startup{};
startup.cb = sizeof(startup);
startup.dwFlags = STARTF_USESTDHANDLES;
startup.hStdOutput = written;
startup.hStdError = written;
startup.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
PROCESS_INFORMATION process{};
const auto filename = probe.wstring();
require(CreateProcessW(filename.c_str(), nullptr, nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr, probe.parent_path().wstring().c_str(), &startup, &process) != 0,
"Cannot start " + probe.filename().string() + ": " + std::to_string(GetLastError()));
CloseHandle(written);
if (WaitForSingleObject(process.hProcess, 120000) != WAIT_OBJECT_0) {
TerminateProcess(process.hProcess, 1);
throw std::runtime_error(probe.filename().string() + " timed out");
}
DWORD status = 0;
require(GetExitCodeProcess(process.hProcess, &status) != 0, "Cannot read the probe's exit code");
char buffer[4096];
DWORD size = 0;
while (ReadFile(input, buffer, sizeof(buffer), &size, nullptr) && size != 0) output.append(buffer, size);
CloseHandle(input);
CloseHandle(process.hThread);
CloseHandle(process.hProcess);
return status;
}

// The generated probes load libc.prx next to the other libraries; its compiler runtime DLLs sit next to this test.
void copyRuntime(const Fs::path& directory) {
wchar_t self[MAX_PATH];
const auto length = GetModuleFileNameW(nullptr, self, MAX_PATH);
if (length == 0 || length >= MAX_PATH) return;
const auto own = Fs::path(self).parent_path();
for (const char* name : {"libgcc_s_seh-1.dll", "libstdc++-6.dll", "libwinpthread-1.dll"}) {
std::error_code error;
if (Fs::exists(own / name, error)) Fs::copy_file(own / name, directory / name, Fs::copy_options::overwrite_existing, error);
}
}

}

int main(int argc, char** argv) {
if (argc != 2) {
std::cerr << "usage: windows_import_identity_tests <patched libraries directory>\n";
return 2;
}
try {
const auto libraries = Fs::absolute(argv[1]).string();
const auto directory = createFixtureDirectory(Fs::temp_directory_path());
copyRuntime(directory);
std::vector<PeImport> same;
for (int i = 0; i < 3; ++i) {
same.push_back({.Name = LocaleObjects[i], .TargetRva = slot(2 * i), .Addend = 0, .Library = "libSceAmpr.prx"});
same.push_back({.Name = LocaleObjects[i], .TargetRva = slot(2 * i + 1), .Addend = 0, .Library = "libc.prx"});
}
generateProbe(directory / "same.exe", libraries, same, {{0, 1}, {2, 3}, {4, 5}});
const std::vector<PeImport> different{
{.Name = LocaleObjects[0], .TargetRva = slot(0), .Addend = 0, .Library = "libSceAmpr.prx"},
{.Name = "k+AXqu2-eBc", .TargetRva = slot(6), .Addend = 0, .Library = "libkernel.prx"},
};
generateProbe(directory / "different.exe", libraries, different, {{0, 6}});
std::string output;
const auto sameStatus = runProbe(directory / "same.exe", output);
require(sameStatus == 0, "A libSceAmpr.prx-qualified import of the locale objects did not resolve to libc's object (exit " + std::to_string(sameStatus) + ")\n" + output);
output.clear();
const auto differentStatus = runProbe(directory / "different.exe", output);
require(differentStatus == 1, "The probe did not tell two different addresses apart (exit " + std::to_string(differentStatus) + ")\n" + output);
std::error_code error;
Fs::remove_all(directory, error);
std::cout << "Windows import identity tests passed\n";
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
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 @@ -134,3 +134,4 @@
- [fopen_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - returns the errno of the failed open, as C11 K.3.5.2.1 leaves the nonzero value open and Microsoft's CRT returns errno; the console's Dinkumware `fopen_s` is not verified
- [ios_base standard streams](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) `_Addstd` queues the stream in the standard stream table of Dinkumware [ios.cpp](https://github.com/ganboing/crt.vc12/blob/master/src/ios.cpp) (`stdstr`, `stdopens`, `_Addstd`, `_Ios_base_dtor`): it takes the first of slots 1 to 7 that is free or already holds the stream, and slot 8 (`NSTDSTR`) when none is, so slot 8 is shared by every stream past the first seven and registration never fails. It stores the 1-based slot index in the guest `IosBase::standardStream` (Dinkumware's `_Stdstr`, where 0 means not a standard stream) and counts the stream objects registered per slot; registering the same stream again reuses its slot and raises its count. Measured on a PS5 (firmware 13.60) from a homebrew payload calling the system `libSceLibcInternal` export: in a process whose own standard streams already hold slots 1 to 7, twelve distinct streams and a repeated one all get 8, written as a 64-bit value at offset 0x8 with no other byte of the object touched; the assignment of slots 1 to 7 to fresh streams is not observable there and follows VC12. The counts are `int`, not VC12's `char`, and the registration and the destructor lock the table where VC12 locks only `_Addstd`; a slot index past the table throws in the destructor instead of indexing out of bounds. The destructor decrements its slot's count and suppresses the rest of the destruction while the count stays positive, as `_Ios_base_dtor` does, and the slots of destroyed streams keep their pointer and stay reserved for that stream, as `stdstr` does. The streams themselves are destroyed by the title's own code as it registers them through `__cxa_atexit`; when the last reference closes, the unsupported stream storage and callback cleanup is still the throw it was and the locale is released as for a non-standard stream.
- [std::_Pad, _Thrd_join, _Thrd_detach, _Thrd_id](../../../core/libs/prx/libc/src/Threads.cpp) (libc) follow Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), without console verification. `_Thrd_id` throws because whether the console returns `pthread_t` or a kernel thread ID and at what width is unknown; `_Launch` omits `_Cnd_do_broadcast_at_thread_exit` and calls `pthread_create` registered by libkernel with default attributes. The 32-byte pad layout (`_Go()` vtable at 0x0, `_Cnd_t` at 0x8, `_Mtx_t` at 0x10, `bool _Started` at 0x18) is assumed from VC12, unverified. Failing primitives or thread start throw the guest `std::system_error`; null arguments throw. `_Thrd_join` returns `_Thrd_error` (4) on `pthread_join` failure and truncates the exit code to `int`; `_Thrd_detach` returns `_Thrd_error` (4) on `pthread_detach` failure and throws when none is registered
- [num_get<char>::id, time_get<char>::id and the num_get<char> vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) are data objects, as the public SDK's libSceLibcInternal table declares them and as the other facet ids and the `num_put` and `codecvt` vtables are kept: the ids start at zero and the vtable is 17 zeroed slots, the 136-byte size of `KfcTPbeaOqg` in the console's libSceLibcInternal, whose relocations fill the offset-to-top and typeinfo entries, the two destructors, `locale::facet::_Incref` and `_Decref`, and the eleven `do_get` of Dinkumware's crt.vc12 `xlocnum`, so a guest resolves the symbols and reads data, and a facet call through the vtable faults. libSceAmpr exported the three as throwing functions, which a data consumer reads as code; its qualified imports fall back to libc.
Loading