mirror of
https://github.com/mihawk-99/PS5_RetroArch.git
synced 2026-10-22 00:22:29 -05:00
Build pinned libretro Snes9x 1.63 with this project's SDK and official metadata.
Convert final RGB565 frames to independent XRGB8888 storage for the existing
Vulkan upload path. Integrate staging, native imports and loader diagnostics.
Keep C++ destructor registration inside the unloadable core. Validate bounded
fini arrays before constructors and execute cleanup at last-reference close.
Test all 65536 colours through GPU upload, pitches/bounds/cached frames and
C++ destruction/reload/malformed finalizers. Existing core bytes are unchanged.
Verification: bash tools/verify.sh passed format/unit/build/integration/evidence
with 60 tests. bash tools/run-title.sh --no-build --core-test=snes9x --watch 180
passed loader/recovery and remained running until runner close. Owner: Works
flawlessly, answering colours/audio/input and menu/content-transition checks.
Zero Vulkan refusals, GPU API failures or kernel fatal-signal reports.
Evidence: evidence/snes9x-native/{capture,expectation,iterations,abi,build}.json,
loader/recovery reports and exact linked-driver hashes. Replay with
bash tools/verify.sh evidence. Frontend identity:
64bef2e7826e5a63a6644a933e24ef5403d28c754230c0038eb8a76b4baa1c53.
PS5_Vulkan was not modified; special-mode/save-state/performance coverage is
not implied by this ordinary-game acceptance.
256 lines
12 KiB
Python
256 lines
12 KiB
Python
"""Execute the real loader on small relocated ELFs; reject malformed input."""
|
|
import ctypes
|
|
from pathlib import Path
|
|
import struct
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
class CoreLoader(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.tmp = tempfile.TemporaryDirectory()
|
|
cls.addClassCleanup(cls.tmp.cleanup)
|
|
cls.folder = Path(cls.tmp.name)
|
|
c = cls.folder / 'core.c'
|
|
c.write_text('''extern int native_add(int);
|
|
int counter;
|
|
int *pointer = &counter;
|
|
int (*call_import)(int) = native_add;
|
|
int run(void) { return call_import(++*pointer); }
|
|
''')
|
|
cls.core = cls.folder / 'fixture.so'
|
|
subprocess.run(['cc', '-shared', '-nostdlib', '-fPIC', str(c),
|
|
'-Wl,-z,max-page-size=16384', '-Wl,-T,' + str(ROOT / 'tooling/native/ps5-core.ld'),
|
|
'-o', str(cls.core)], check=True, capture_output=True)
|
|
data = bytearray(cls.core.read_bytes())
|
|
data[7] = 9
|
|
cls.core.write_bytes(data)
|
|
cls.original = bytes(data)
|
|
wrapper = cls.folder / 'runtime.cpp'
|
|
wrapper.write_text('''#include <cstring>
|
|
#include <cstdlib>
|
|
#include <pthread.h>
|
|
static bool allow = true;
|
|
static int events = 0;
|
|
static void native_record(int value) { events = events * 10 + value; }
|
|
extern "C" void clear_events() { events = 0; }
|
|
extern "C" int get_events() { return events; }
|
|
extern "C" void allow_import(bool value) { allow = value; }
|
|
static int native_add(int value) { return value + 40; }
|
|
extern "C" void *ps5_core_import(const char *name) {
|
|
if (allow && !std::strcmp(name, "native_add")) return reinterpret_cast<void *>(&native_add);
|
|
#define BIND(symbol) if (!std::strcmp(name, #symbol)) return reinterpret_cast<void *>(&symbol)
|
|
BIND(native_record); BIND(malloc); BIND(free);
|
|
BIND(pthread_mutex_lock); BIND(pthread_mutex_unlock); BIND(pthread_mutex_destroy);
|
|
#undef BIND
|
|
return nullptr;
|
|
}
|
|
''')
|
|
harness = cls.folder / 'loader.so'
|
|
subprocess.run(['c++', '-std=c++20', '-shared', '-fPIC', '-pthread', '-O1', '-g',
|
|
'-Wall', '-Wextra', '-Werror', str(ROOT / 'src/core_loader_ps5.cpp'),
|
|
str(wrapper), '-o', str(harness)], check=True, capture_output=True)
|
|
cls.lib = ctypes.CDLL(str(harness))
|
|
cls.lib.ps5_core_dlopen.argtypes = [ctypes.c_char_p, ctypes.c_int]
|
|
cls.lib.ps5_core_dlopen.restype = ctypes.c_void_p
|
|
cls.lib.ps5_core_dlsym.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
|
|
cls.lib.ps5_core_dlsym.restype = ctypes.c_void_p
|
|
cls.lib.ps5_core_dlclose.argtypes = [ctypes.c_void_p]
|
|
cls.lib.ps5_core_dlerror.restype = ctypes.c_char_p
|
|
cls.lib.allow_import.argtypes = [ctypes.c_bool]
|
|
|
|
def setUp(self):
|
|
self.lib.allow_import(True)
|
|
|
|
def open(self, path):
|
|
return self.lib.ps5_core_dlopen(str(path).encode(), 0)
|
|
|
|
def reject(self, data, reason):
|
|
path = self.folder / 'invalid.so'
|
|
path.write_bytes(data)
|
|
handle = self.open(path)
|
|
if handle:
|
|
self.lib.ps5_core_dlclose(handle)
|
|
self.assertFalse(handle)
|
|
self.assertIn(reason, self.lib.ps5_core_dlerror().decode())
|
|
|
|
def test_relocations_imports_reference_count_and_reload(self):
|
|
for _ in range(8):
|
|
a = self.open(self.core)
|
|
self.assertTrue(a, self.lib.ps5_core_dlerror())
|
|
b = self.open(self.core)
|
|
self.assertEqual(a, b)
|
|
run = ctypes.CFUNCTYPE(ctypes.c_int)(self.lib.ps5_core_dlsym(a, b'run'))
|
|
self.assertEqual(run(), 41)
|
|
self.assertEqual(self.lib.ps5_core_dlclose(a), 0)
|
|
self.assertEqual(run(), 42)
|
|
self.assertFalse(self.lib.ps5_core_dlsym(b, b'absent'))
|
|
self.assertEqual(self.lib.ps5_core_dlclose(b), 0)
|
|
self.assertEqual(self.lib.ps5_core_dlclose(1), -1)
|
|
|
|
def test_missing_import_is_rejected_then_valid_load_recovers(self):
|
|
self.lib.allow_import(False)
|
|
self.assertFalse(self.open(self.core))
|
|
self.assertIn(b'unresolved native runtime import: native_add', self.lib.ps5_core_dlerror())
|
|
self.lib.allow_import(True)
|
|
handle = self.open(self.core)
|
|
self.assertTrue(handle)
|
|
self.lib.ps5_core_dlclose(handle)
|
|
|
|
def test_bad_header_and_header_table_bounds(self):
|
|
self.reject(b'not an ELF', 'file size')
|
|
data = bytearray(self.original)
|
|
data[7] = 0
|
|
self.reject(data, 'expected PS5 ELF64')
|
|
data = bytearray(self.original)
|
|
struct.pack_into('<Q', data, 32, 2**64 - 8)
|
|
self.reject(data, 'header tables')
|
|
|
|
def test_bad_segments_and_unsupported_tls(self):
|
|
phoff = struct.unpack_from('<Q', self.original, 32)[0]
|
|
data = bytearray(self.original)
|
|
struct.pack_into('<I', data, phoff + 4, 7)
|
|
self.reject(data, 'writable-executable')
|
|
data = bytearray(self.original)
|
|
struct.pack_into('<Q', data, phoff + 8, len(data) + 0x4000)
|
|
self.reject(data, 'load segment')
|
|
data = bytearray(self.original)
|
|
struct.pack_into('<I', data, phoff, 7) # PT_TLS
|
|
self.reject(data, 'TLS')
|
|
|
|
def test_bad_relocation_target_and_encoding(self):
|
|
shoff = struct.unpack_from('<Q', self.original, 40)[0]
|
|
count = struct.unpack_from('<H', self.original, 60)[0]
|
|
offset = next(struct.unpack_from('<Q', self.original, shoff + i * 64 + 24)[0]
|
|
for i in range(count)
|
|
if struct.unpack_from('<I', self.original, shoff + i * 64 + 4)[0] == 4)
|
|
data = bytearray(self.original)
|
|
struct.pack_into('<Q', data, offset, 2**64 - 8)
|
|
self.reject(data, 'relocation destination')
|
|
data = bytearray(self.original)
|
|
struct.pack_into('<I', data, offset + 8, 5) # R_X86_64_COPY
|
|
self.reject(data, 'unsupported x86-64 relocation')
|
|
|
|
def test_constructor_runs_once_per_mapping_and_rejects_bad_array(self):
|
|
source = self.folder / 'ctor.c'
|
|
source.write_text('''int ready;
|
|
__attribute__((constructor)) static void init(void) { ++ready; }
|
|
int state(void) { return ready; }
|
|
''')
|
|
path = self.folder / 'ctor.so'
|
|
subprocess.run(['cc', '-shared', '-nostdlib', '-fPIC', str(source),
|
|
'-Wl,-z,max-page-size=16384', '-Wl,-T,' + str(ROOT / 'tooling/native/ps5-core.ld'),
|
|
'-o', str(path)], check=True, capture_output=True)
|
|
data = bytearray(path.read_bytes())
|
|
data[7] = 9
|
|
path.write_bytes(data)
|
|
for _ in range(3):
|
|
handle = self.open(path)
|
|
self.assertTrue(handle, self.lib.ps5_core_dlerror())
|
|
other = self.open(path)
|
|
self.assertEqual(handle, other)
|
|
state = ctypes.CFUNCTYPE(ctypes.c_int)(self.lib.ps5_core_dlsym(handle, b'state'))
|
|
self.assertEqual(state(), 1)
|
|
self.lib.ps5_core_dlclose(other)
|
|
self.lib.ps5_core_dlclose(handle)
|
|
phoff = struct.unpack_from('<Q', data, 32)[0]
|
|
phcount = struct.unpack_from('<H', data, 56)[0]
|
|
dynamic = next(struct.unpack_from('<Q', data, phoff + i * 56 + 8)[0]
|
|
for i in range(phcount)
|
|
if struct.unpack_from('<I', data, phoff + i * 56)[0] == 2)
|
|
at = dynamic
|
|
while struct.unpack_from('<q', data, at)[0] != 25: # DT_INIT_ARRAY
|
|
self.assertNotEqual(struct.unpack_from('<q', data, at)[0], 0)
|
|
at += 16
|
|
original = bytearray(data)
|
|
init_address = struct.unpack_from('<Q', data, at + 8)[0]
|
|
struct.pack_into('<Q', data, at + 8, 2**64 - 8)
|
|
self.reject(data, 'initializer array')
|
|
data = original
|
|
writable = next(struct.unpack_from('<Q', data, phoff + i * 56 + 16)[0]
|
|
for i in range(phcount)
|
|
if struct.unpack_from('<II', data, phoff + i * 56) == (1, 6))
|
|
shoff = struct.unpack_from('<Q', data, 40)[0]
|
|
shcount = struct.unpack_from('<H', data, 60)[0]
|
|
changed = False
|
|
for i in range(shcount):
|
|
if struct.unpack_from('<I', data, shoff + i * 64 + 4)[0] != 4:
|
|
continue
|
|
offset, size = struct.unpack_from('<QQ', data, shoff + i * 64 + 24)
|
|
for reloc in range(offset, offset + size, 24):
|
|
if struct.unpack_from('<Q', data, reloc)[0] == init_address:
|
|
struct.pack_into('<q', data, reloc + 16, writable)
|
|
changed = True
|
|
self.assertTrue(changed)
|
|
self.reject(data, 'initializer callback outside executable segment')
|
|
|
|
|
|
def test_cpp_destructors_run_on_last_close_and_bad_finalizer_rejected(self):
|
|
source = self.folder / 'dtor.cpp'
|
|
source.write_text('''extern "C" void native_record(int);
|
|
struct Object {
|
|
int marker;
|
|
Object(int value) : marker(value) { native_record(value); }
|
|
~Object() { native_record(marker + 1); }
|
|
};
|
|
static Object a(1), b(3);
|
|
''')
|
|
path = self.folder / 'dtor.so'
|
|
subprocess.run(['c++', '-shared', '-nostdlib', '-fPIC', '-fno-exceptions', '-fno-rtti',
|
|
str(source), str(ROOT / 'tooling/native/core_cxx_runtime.cpp'),
|
|
'-Wl,--no-eh-frame-hdr',
|
|
'-Wl,-z,max-page-size=16384', '-Wl,-T,' + str(ROOT / 'tooling/native/ps5-core.ld'),
|
|
'-o', str(path)], check=True, capture_output=True)
|
|
data = bytearray(path.read_bytes())
|
|
data[7] = 9
|
|
path.write_bytes(data)
|
|
for _ in range(4):
|
|
self.lib.clear_events()
|
|
handle = self.open(path)
|
|
self.assertTrue(handle, self.lib.ps5_core_dlerror())
|
|
other = self.open(path)
|
|
self.assertEqual(handle, other)
|
|
self.assertEqual(self.lib.get_events(), 13)
|
|
self.lib.ps5_core_dlclose(other)
|
|
self.assertEqual(self.lib.get_events(), 13)
|
|
self.lib.ps5_core_dlclose(handle)
|
|
self.assertEqual(self.lib.get_events(), 1342)
|
|
# Invalid finalizer is rejected BEFORE any constructor can run.
|
|
self.lib.clear_events()
|
|
phoff = struct.unpack_from('<Q', data, 32)[0]
|
|
phcount = struct.unpack_from('<H', data, 56)[0]
|
|
dynamic = next(struct.unpack_from('<Q', data, phoff + i * 56 + 8)[0]
|
|
for i in range(phcount)
|
|
if struct.unpack_from('<I', data, phoff + i * 56)[0] == 2)
|
|
at = dynamic
|
|
while struct.unpack_from('<q', data, at)[0] != 26: # DT_FINI_ARRAY
|
|
self.assertNotEqual(struct.unpack_from('<q', data, at)[0], 0)
|
|
at += 16
|
|
fini_address = struct.unpack_from('<Q', data, at + 8)[0]
|
|
original = bytes(data)
|
|
struct.pack_into('<Q', data, at + 8, 2**64 - 8)
|
|
self.reject(data, 'finalizer array')
|
|
self.assertEqual(self.lib.get_events(), 0)
|
|
data = bytearray(original)
|
|
writable = next(struct.unpack_from('<Q', data, phoff + i * 56 + 16)[0]
|
|
for i in range(phcount)
|
|
if struct.unpack_from('<II', data, phoff + i * 56) == (1, 6))
|
|
shoff = struct.unpack_from('<Q', data, 40)[0]
|
|
shcount = struct.unpack_from('<H', data, 60)[0]
|
|
changed = False
|
|
for i in range(shcount):
|
|
if struct.unpack_from('<I', data, shoff + i * 64 + 4)[0] != 4:
|
|
continue
|
|
offset, size = struct.unpack_from('<QQ', data, shoff + i * 64 + 24)
|
|
for reloc in range(offset, offset + size, 24):
|
|
if struct.unpack_from('<Q', data, reloc)[0] == fini_address:
|
|
struct.pack_into('<q', data, reloc + 16, writable)
|
|
changed = True
|
|
self.assertTrue(changed)
|
|
self.reject(data, 'finalizer callback outside executable segment')
|
|
self.assertEqual(self.lib.get_events(), 0)
|