mirror of
https://github.com/mihawk-99/PS5_RetroArch.git
synced 2026-10-18 23:09:29 -05:00
Beetle PSX HW, Mupen64Plus-Next, Beetle Saturn, VICE x64sc, MAME 0.289, DeSmuME and Azahar, each built from my fork of it at a pinned revision (tools/core-fork.sh, tools/build-<core>.sh), with the PS5 defaults at the most each offers that holds full speed. The loader grew to fit them. A core's file and image come from direct memory, up to 1 GiB (MAME's image is 413 MB), and each image is zeroed when it is reserved: direct memory is not, and a reloaded core's .bss held its predecessor's bytes, which faulted every core on a reload. Each image's .eh_frame is registered with the title's unwinder, for the cores that throw. Initializers are capped at 65,536 (Azahar has 2,476). A load that misses imports lists all of them. Aligned operator new comes from the core heap, a core's atexit belongs to the core, and the imports the new cores need are bound to the platform layer's getcwd, tmpfile, mkstemp and the rest (SDK fork ebd0fe2).
257 lines
12 KiB
Python
257 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" unsigned ps5_core_threads_live() { return 0; }
|
|
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 (1): 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)
|