mirror of
https://github.com/mihawk-99/PS5_RetroArch.git
synced 2026-10-18 23:09:29 -05:00
Start the bundled Lapy service when RetroArch launches
Package the qualified title-specific resident service and start it through local elfldr before workers when no service is ready. Wait for preflight, verify SDK elevation and UID 0, and reuse the service on subsequent launches. Keep installed paths and offline fallback; no Payload Manager boot hook. Console evidence: evidence/lapy-autostart/capture.json and expectation.json. Reproduce with evidence/lapy-autostart/README.txt, then python3 tools/evidence.py compare evidence/. Verified on the base PS5: cold service startup, two authenticated achievement sessions, reuse of one service across three requests, and NetPlay relay join with 3837/3361 input packets and 52 matching state checks. Normal title exits; root counts 73/72 before each request. Resident service remains running. Installed binary hashes and unchanged Payload Manager configuration verified. Validation: 194 unit and 194 integration tests; format; all-target build with PS5_FRONTENDS='es-de picker' and the existing pinned build/picker-template; manifest/notices/source bundle checks; 127 evidence captures replayed. No secrets, saves or raw console captures included. No push.
This commit is contained in:
1 parent
56ecbf367f
commit
bead80725c
22 files changed
+1116
-20
No files matched your search
@@ -23,3 +23,7 @@ tooling/prospero-clang18 text eol=lf
|
||||
|
||||
# Preserve the upstream font license verbatim, including its original whitespace.
|
||||
webui/assets/OFL.txt -text -whitespace
|
||||
|
||||
# Unified patches require a space on otherwise blank context lines.
|
||||
platform/lapy/*.patch -whitespace
|
||||
evidence/lapy-autostart/*.patch -whitespace
|
||||
@@ -257,7 +257,7 @@ and **EXTERNAL** (`/mnt`) may be empty.
|
||||
Startup, controller input, audio, saved settings, and representative games have been tested on PS5. Compatibility and long-session testing are ongoing.
|
||||
|
||||
- **Azahar:** a crash when closing content, switching to Azahar, and starting a game remains under investigation.
|
||||
- **RetroAchievements and NetPlay** require the separately started, qualified Lapy elevation service before launching RetroArch. Without it, RetroArch starts normally but outgoing online features remain unavailable. Files stay in their installation folder; existing `/app0` settings continue to work. See [online setup and verification](evidence/online-features/README.txt).
|
||||
- **RetroAchievements and NetPlay:** RetroArch starts its bundled Lapy service automatically through the console's local ELF loader, then reuses it on later launches. No Payload Manager autoload entry is needed. Files stay in their installation folder; existing `/app0` settings continue to work. Without the local loader or verified elevation, outgoing online features remain unavailable. See [service setup](platform/lapy/README.txt) and [online verification](evidence/online-features/README.txt).
|
||||
- Shader presets, disc swapping, and rumble still need broader testing.
|
||||
|
||||
[Recorded evidence](evidence/) accompanies verified changes. For a bug report,
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# RetroArch starts its bundled Lapy service
|
||||
|
||||
Acceptance: from an idle console with no Lapy readiness marker or running Lapy
|
||||
service, launch the packaged title. Its startup log must record the complete
|
||||
bundled ELF delivery, verified UID 0 and the same installation executable.
|
||||
RetroAchievements must authenticate and load its session/achievement set.
|
||||
A second launch must reuse the same service PID (no second payload delivery),
|
||||
pass the same account checks, and quit normally. The resident daemon must record
|
||||
successful transactions and client proofs. No Payload Manager configuration or
|
||||
boot hook is involved; the console's existing local ELF loader is required.
|
||||
|
||||
Build: PS5_FRONTENDS="es-de picker" PS5_VULKAN_TEMPLATE="$PWD/build/picker-template" \
|
||||
bash tools/verify.sh
|
||||
The existing build/picker-template checkout is at the pin in tools/build-picker.sh.
|
||||
This builds all targets, including tools/build-lapy.py, and stages the service,
|
||||
licences and source metadata. The service source is identical to the qualified
|
||||
prepared source recorded by evidence/network-elevation and installed-paths.
|
||||
|
||||
Console reproduction uses the private connection helper and installed-path runner
|
||||
prepared as documented in evidence/installed-paths/README.txt. Keep all raw
|
||||
captures, account data and game paths outside the repository. Upload the packaged
|
||||
eboot.bin and lapy-root-daemon.elf with stored-byte verification, preserving the
|
||||
console's configuration. Run the installed-path achievement fixture twice with
|
||||
runner.run(label, daemon=False). Do not inject a payload. Remove temporary test
|
||||
controls afterward. Capture startup-paths.log, lapy-ready, lapy-service.log,
|
||||
retroarch.log and the kernel stream for each run; verify normal exit and account
|
||||
fields unchanged. Preserve the original binary for rollback before deployment.
|
||||
|
||||
The netplay regression reuses the prior original diagnostic NROM and PC peer.
|
||||
Copy console_netplay.py and analyse_netplay.py from evidence/online-features to
|
||||
a private temporary folder, then apply netplay-resident.patch and
|
||||
netplay-analyser.patch respectively. Run the console script from the repository
|
||||
root with a new /tmp capture folder and relay-join; run the analyser on that folder.
|
||||
The adaptations reuse the already-running service and capture its client proof.
|
||||
They deliberately do not claim a resident daemon has exited or that final root
|
||||
counts were sampled after exit. Traffic, controller input and matching-state
|
||||
requirements remain unchanged.
|
||||
|
||||
Replay: python3 tools/evidence.py compare evidence/
|
||||
@@ -0,0 +1,507 @@
|
||||
{
|
||||
"step": "lapy-autostart",
|
||||
"recorded": "2026-10-10T15:23:56.934694+00:00",
|
||||
"revision": "56ecbf367f7d84660e024af60ede5acd2212236e plus changes committed with this capture",
|
||||
"console_run": "base PS5 firmware 13.40; bundled resident service started by the title; existing local ELF loader",
|
||||
"command": "See evidence/lapy-autostart/README.txt; no host-injected payload for accepted runs",
|
||||
"lines": [
|
||||
"First launch: no prestarted service; bundled ELF sent=155752 size=155752 errno=0",
|
||||
"First launch: elevation=ok uid=0 same_executable=1; RetroAchievements login/session/achievement_set=true",
|
||||
"Second launch: same service identity; no payload resend; RetroAchievements login/session/achievement_set=true",
|
||||
"Both achievement runs: normal_exit=true account_fields_unchanged=true",
|
||||
"Resident service: successful_requests=3 client_data_rw_proofs=3; all before_request root counts=73/72",
|
||||
"NetPlay relay join: both peers connected; 3837/3361 input packets; 581 nonzero console input packets",
|
||||
"NetPlay relay join: state_checks=52 matching=52 mismatching=0; normal exit; no new service launch",
|
||||
"Final console: idle=true service_ready=true temporary_controls_absent=true",
|
||||
"Payload Manager config and autoload list unchanged; mistaken payload upload removed",
|
||||
"Installed eboot, bundled service, picker and ES-DE hashes match the complete build",
|
||||
"Qualified external runtime source unchanged; no kernel preflight or transaction guards disabled",
|
||||
"Gates: 194 unit tests and 194 integration tests passed; all shipped targets built with pinned picker checkout"
|
||||
],
|
||||
"first": {
|
||||
"label": "achievements-autostart",
|
||||
"autostart": true,
|
||||
"elevated": true,
|
||||
"same_executable": true,
|
||||
"login": true,
|
||||
"session": true,
|
||||
"achievement_set": true,
|
||||
"ready": true,
|
||||
"normal_exit": true,
|
||||
"account_unchanged": true
|
||||
},
|
||||
"second": {
|
||||
"label": "achievements-reuse",
|
||||
"autostart": false,
|
||||
"elevated": true,
|
||||
"same_executable": true,
|
||||
"login": true,
|
||||
"session": true,
|
||||
"achievement_set": true,
|
||||
"ready": true,
|
||||
"normal_exit": true,
|
||||
"account_unchanged": true
|
||||
},
|
||||
"final": {
|
||||
"idle": true,
|
||||
"same_service": true,
|
||||
"successful_requests": 3,
|
||||
"successful_client_proofs": 3,
|
||||
"request_baselines": [
|
||||
[
|
||||
"73",
|
||||
"72"
|
||||
],
|
||||
[
|
||||
"73",
|
||||
"72"
|
||||
],
|
||||
[
|
||||
"73",
|
||||
"72"
|
||||
]
|
||||
],
|
||||
"payload_manager_unchanged": true,
|
||||
"mistaken_upload_absent": true,
|
||||
"temporary_controls_absent": true,
|
||||
"installed": {
|
||||
"eboot.bin": {
|
||||
"bytes": 40090812,
|
||||
"sha256": "7067b750a657b267e70a606a757b9aa888d3fcef4caa17c8387508de70e82c66"
|
||||
},
|
||||
"lapy-root-daemon.elf": {
|
||||
"bytes": 155752,
|
||||
"sha256": "5ffac59aa640bc8ce7c34b9cdefc909ddc0f1044913a0485e44b39b84f2bb661"
|
||||
},
|
||||
"picker/picker.bin": {
|
||||
"bytes": 29608493,
|
||||
"sha256": "411e2b6f604878bd00b8d2b67e797b2ae91f1682ad046719464d04c57fa9fe04"
|
||||
},
|
||||
"es-de/es-de.bin": {
|
||||
"bytes": 48156541,
|
||||
"sha256": "595b66caf2bb6b8344ca1e398bf24c148d63e308f5cad3091eea6048f063d1ac"
|
||||
}
|
||||
}
|
||||
},
|
||||
"netplay": {
|
||||
"mode": "relay-join",
|
||||
"wire": [
|
||||
{
|
||||
"file": "wire-1-0.bin",
|
||||
"commands": {
|
||||
"0020": 1,
|
||||
"0022": 1,
|
||||
"0023": 1,
|
||||
"2000": 1,
|
||||
"2001": 1,
|
||||
"0003": 3837,
|
||||
"0042": 2,
|
||||
"0026": 1,
|
||||
"0040": 55,
|
||||
"1101": 9,
|
||||
"1100": 9
|
||||
},
|
||||
"first_frame": 19943,
|
||||
"last_frame": 23779,
|
||||
"input_packets": 3837,
|
||||
"nonzero_input_packets": 0,
|
||||
"button_values": [
|
||||
0
|
||||
],
|
||||
"incomplete_tail_bytes": 0
|
||||
},
|
||||
{
|
||||
"file": "wire-1-1.bin",
|
||||
"commands": {
|
||||
"0020": 1,
|
||||
"0022": 1,
|
||||
"0025": 1,
|
||||
"0041": 1,
|
||||
"0003": 3361,
|
||||
"1100": 9,
|
||||
"1101": 9
|
||||
},
|
||||
"first_frame": 20359,
|
||||
"last_frame": 23719,
|
||||
"input_packets": 3361,
|
||||
"nonzero_input_packets": 581,
|
||||
"button_values": [
|
||||
0,
|
||||
1,
|
||||
128,
|
||||
256
|
||||
],
|
||||
"incomplete_tail_bytes": 0
|
||||
}
|
||||
],
|
||||
"raw_sha256": {
|
||||
"wire-0-0.bin": "aabea61abf591fffc96a25ec7eec547b9ea1a23d906f2d11853d49ac14f57cc8",
|
||||
"wire-0-1.bin": "b833773092679ecd523c6dc6c41ddf4895f573019185b62d8903e24abc399b2a",
|
||||
"wire-1-0.bin": "25cd2c01dedb4cbdac26555f23cebfa17d58b991cc27a4085880b203df634f10",
|
||||
"wire-1-1.bin": "32ec3fa66f626af5f324bb4525167952aec8a80e32951c6bebefb496229091c0",
|
||||
"retroarch.log": "dd9d6380b18b46cf8744d64cdc4ddb2767a066036ccda43e352691324ba5beb2",
|
||||
"pc.log": "174154de073210a1e7d045d76a0dbf06e981c3ada973f3702ceb39c0bf029a1a",
|
||||
"trace.txt": "01362ab33b3f0fddda116b08730d18dad3ed2738fabb7aa8f739eea9b9541152",
|
||||
"startup-paths.log": "d90df0eebe2150f21c872b20e4d8fcbde71c9314a8487f7304581d78b4cf921c",
|
||||
"lapy-service.log": "595a57a0e1babd3749d6d0de0d0b04696ab569afc5047b6709b2f58b81564717"
|
||||
},
|
||||
"state_checks": [
|
||||
{
|
||||
"frame": 20460,
|
||||
"local": "df817338",
|
||||
"remote": "df817338",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 20520,
|
||||
"local": "67777f78",
|
||||
"remote": "67777f78",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 20580,
|
||||
"local": "c7800aea",
|
||||
"remote": "c7800aea",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 20700,
|
||||
"local": "bac05a61",
|
||||
"remote": "bac05a61",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 20760,
|
||||
"local": "145d5105",
|
||||
"remote": "145d5105",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 20820,
|
||||
"local": "0c7659bb",
|
||||
"remote": "0c7659bb",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 20880,
|
||||
"local": "2f548d21",
|
||||
"remote": "2f548d21",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 20940,
|
||||
"local": "50282f2b",
|
||||
"remote": "50282f2b",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 21000,
|
||||
"local": "6f8fc9c1",
|
||||
"remote": "6f8fc9c1",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 21060,
|
||||
"local": "49d64384",
|
||||
"remote": "49d64384",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 21120,
|
||||
"local": "60d03120",
|
||||
"remote": "60d03120",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 21180,
|
||||
"local": "8767cfae",
|
||||
"remote": "8767cfae",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 21240,
|
||||
"local": "70deef4e",
|
||||
"remote": "70deef4e",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 21300,
|
||||
"local": "970b1038",
|
||||
"remote": "970b1038",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 21360,
|
||||
"local": "2ce73dd9",
|
||||
"remote": "2ce73dd9",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 21420,
|
||||
"local": "6b4a68a9",
|
||||
"remote": "6b4a68a9",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 21480,
|
||||
"local": "deee89fc",
|
||||
"remote": "deee89fc",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 21540,
|
||||
"local": "fee2edef",
|
||||
"remote": "fee2edef",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 21600,
|
||||
"local": "94decf7d",
|
||||
"remote": "94decf7d",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 21660,
|
||||
"local": "0381616c",
|
||||
"remote": "0381616c",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 21720,
|
||||
"local": "e249bd35",
|
||||
"remote": "e249bd35",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 21780,
|
||||
"local": "0e844910",
|
||||
"remote": "0e844910",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 21840,
|
||||
"local": "936c3412",
|
||||
"remote": "936c3412",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 21960,
|
||||
"local": "aa20cc13",
|
||||
"remote": "aa20cc13",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 22020,
|
||||
"local": "0d7c23cf",
|
||||
"remote": "0d7c23cf",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 22080,
|
||||
"local": "c90b98fe",
|
||||
"remote": "c90b98fe",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 22140,
|
||||
"local": "2b646017",
|
||||
"remote": "2b646017",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 22200,
|
||||
"local": "846fffd2",
|
||||
"remote": "846fffd2",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 22260,
|
||||
"local": "22effecb",
|
||||
"remote": "22effecb",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 22320,
|
||||
"local": "9bf3613c",
|
||||
"remote": "9bf3613c",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 22380,
|
||||
"local": "576f163c",
|
||||
"remote": "576f163c",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 22440,
|
||||
"local": "c3237c7b",
|
||||
"remote": "c3237c7b",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 22500,
|
||||
"local": "cc59d933",
|
||||
"remote": "cc59d933",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 22560,
|
||||
"local": "d9af4d71",
|
||||
"remote": "d9af4d71",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 22620,
|
||||
"local": "0e8b1698",
|
||||
"remote": "0e8b1698",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 22680,
|
||||
"local": "c257be8c",
|
||||
"remote": "c257be8c",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 22740,
|
||||
"local": "4bc206af",
|
||||
"remote": "4bc206af",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 22800,
|
||||
"local": "dd4230df",
|
||||
"remote": "dd4230df",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 22860,
|
||||
"local": "df19c08f",
|
||||
"remote": "df19c08f",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 22920,
|
||||
"local": "d73d2fcc",
|
||||
"remote": "d73d2fcc",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 22980,
|
||||
"local": "fdb20321",
|
||||
"remote": "fdb20321",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 23040,
|
||||
"local": "824ad7af",
|
||||
"remote": "824ad7af",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 23100,
|
||||
"local": "058835d1",
|
||||
"remote": "058835d1",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 23160,
|
||||
"local": "e65f7be7",
|
||||
"remote": "e65f7be7",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 23220,
|
||||
"local": "9c8c0375",
|
||||
"remote": "9c8c0375",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 23280,
|
||||
"local": "893f27c1",
|
||||
"remote": "893f27c1",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 23340,
|
||||
"local": "28324918",
|
||||
"remote": "28324918",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 23400,
|
||||
"local": "2d6df212",
|
||||
"remote": "2d6df212",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 23460,
|
||||
"local": "a4d9555e",
|
||||
"remote": "a4d9555e",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 23520,
|
||||
"local": "eefbbd2c",
|
||||
"remote": "eefbbd2c",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 23580,
|
||||
"local": "85ac52e2",
|
||||
"remote": "85ac52e2",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"frame": 23700,
|
||||
"local": "99004720",
|
||||
"remote": "99004720",
|
||||
"match": true
|
||||
}
|
||||
],
|
||||
"console_connected": true,
|
||||
"netplay_errors": [],
|
||||
"pc_connected": true,
|
||||
"resident_client_proof": true
|
||||
},
|
||||
"raw_sha256": {
|
||||
"/tmp/ps5-autostart-console/achievements-autostart/startup-paths.log": "20f21d21261262b1d2129536c63f36a2814f08791d13a4da12f0a8ba74c3dfca",
|
||||
"/tmp/ps5-autostart-console/achievements-autostart/retroarch.log": "dc6dcffa0c05c982063bee095af02b782adb74a83ccdd51f0b53fe5c7dfbf146",
|
||||
"/tmp/ps5-autostart-console/achievements-autostart/kernel.log": "1fa6cd4a07adfa73df100f248b55054b585b20e6cc2a4215e678a81517a18323",
|
||||
"/tmp/ps5-autostart-console/achievements-autostart/lapy-service.log": "dba55db6033ddbaa3aecb10b607821da313422cf4875982d1b19f1a34c3fa493",
|
||||
"/tmp/ps5-autostart-console/achievements-autostart/result.json": "452ef577af588473ea20f1919e72ede3e074cded8d6c66200a8e762ba46c7173",
|
||||
"/tmp/ps5-autostart-console/achievements-reuse/startup-paths.log": "d90df0eebe2150f21c872b20e4d8fcbde71c9314a8487f7304581d78b4cf921c",
|
||||
"/tmp/ps5-autostart-console/achievements-reuse/retroarch.log": "255f2d287f5200245863cbf908f1714a661074a433d5f47a26bb23001eafa584",
|
||||
"/tmp/ps5-autostart-console/achievements-reuse/kernel.log": "8bcb0215ead20185b3e8d1933f061634e9dbf7a4b9f45e9449f8f5f69db988f0",
|
||||
"/tmp/ps5-autostart-console/achievements-reuse/lapy-service.log": "aa3b401ad803663db5209732a4c298923fe743f8f0cb829d858fc4dafd6de9c2",
|
||||
"/tmp/ps5-autostart-console/achievements-reuse/result.json": "0f7ed0e67a6749eaaf565783e68fcb9fb740e07f127d0ad7182df57bfd2b5247",
|
||||
"/tmp/ps5-autostart-console/resident-final.log": "595a57a0e1babd3749d6d0de0d0b04696ab569afc5047b6709b2f58b81564717",
|
||||
"/tmp/ps5-autostart-console/final-check.json": "7736a664e6ba445db6af18da54b87611032eb05d8c02faafe1427c940201e3b4"
|
||||
},
|
||||
"service_build_id": "6b834c253c699925b6c1c0aa20abf35c6be86747c5e633c1145ad511e7ef4425",
|
||||
"qualified_runtime_source_sha256": "0998568b260a585d1e6fe0d0cc53ea9841f535fe786f14eaf0d3eefd48015d32",
|
||||
"sources_sha256": {
|
||||
"src/ps5_paths.c": "6512594e3a3392f6572a1a885fe662f726a02798b9e05e29604b253ddb716540",
|
||||
"tools/build-lapy.py": "8c133179e8c0642e5186701dc7ed39eaece194be6f33a088736e682ed0448aba",
|
||||
"tools/build-title.sh": "44d4d7618633fb89bfce7a3ad6522315e801b8cec8f55736eede118bd7b61056",
|
||||
"tests/test_installed_paths.py": "0ae3f6dd88e61333cf3260536e530642c8d239e7f189ac8160362337911a9d0c"
|
||||
},
|
||||
"gate_logs_sha256": {
|
||||
"/tmp/ps5-autostart-gates.log": "6eee451c60b2160467ebbe594800e290bcacf465e5914d845913e04958a18867",
|
||||
"/tmp/ps5-autostart-gates-rebuild.log": "207bc5aecd57dd3e2e4d1ebf514fadeaf93fcab4ab909d7a3ca280c2395ca0f0",
|
||||
"/tmp/ps5-autostart-all-targets.log": "f0b0c76338d7db8560baacb8fc2f707515b8fd6f66a30aab08be173e86eb313f",
|
||||
"/tmp/ps5-autostart-all-targets-pinned.log": "c6b0867641578a95256c9de42c74865076033600db8e9ccbe616bf70247ad0a2"
|
||||
},
|
||||
"failures": [
|
||||
"Initial sandbox host run could not create local sockets; network-enabled run passed.",
|
||||
"First cross-build missed netinet/in.h; explicit include fixed it.",
|
||||
"First console build fell back offline before payload delivery. Replaced inline fileno/native FILE use with path stat; accepted build auto-started and elevated.",
|
||||
"All-target build initially used a moved sibling checkout; rerun used the existing pinned build/picker-template checkout.",
|
||||
"Initial interpretation of autoload uploaded and started an unused service through Payload Manager. Removed upload, stopped the identified unused process and removed readiness. Config and autoload list never changed."
|
||||
],
|
||||
"limits": [
|
||||
"The console-local ELF loader remains a jailbreak prerequisite; no boot hook was installed.",
|
||||
"Service remains resident after normal title quit. Before-request root counts establish recovery between runs; no final service-exit/root-balance claim.",
|
||||
"No reboot, public lobby, all-core or long-session qualification in this step."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"step": "lapy-autostart",
|
||||
"note": "Automatic bundled-service startup, reuse, authenticated achievements and relay netplay on the console.",
|
||||
"must_contain": [
|
||||
"First launch: no prestarted service; bundled ELF sent=155752 size=155752 errno=0",
|
||||
"First launch: elevation=ok uid=0 same_executable=1; RetroAchievements login/session/achievement_set=true",
|
||||
"Second launch: same service identity; no payload resend; RetroAchievements login/session/achievement_set=true",
|
||||
"Both achievement runs: normal_exit=true account_fields_unchanged=true",
|
||||
"Resident service: successful_requests=3 client_data_rw_proofs=3; all before_request root counts=73/72",
|
||||
"NetPlay relay join: both peers connected; 3837/3361 input packets; 581 nonzero console input packets",
|
||||
"NetPlay relay join: state_checks=52 matching=52 mismatching=0; normal exit; no new service launch",
|
||||
"Final console: idle=true service_ready=true temporary_controls_absent=true",
|
||||
"Payload Manager config and autoload list unchanged; mistaken payload upload removed",
|
||||
"Installed eboot, bundled service, picker and ES-DE hashes match the complete build",
|
||||
"Qualified external runtime source unchanged; no kernel preflight or transaction guards disabled",
|
||||
"Gates: 194 unit tests and 194 integration tests passed; all shipped targets built with pinned picker checkout"
|
||||
],
|
||||
"must_not_contain": [
|
||||
"elevation=unavailable offline=1"
|
||||
],
|
||||
"human_check": [
|
||||
"Review failure and limitation records; do not infer final resident-service teardown or reboot testing."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
--- analyse_netplay.py
|
||||
+++ analyse_netplay.py
|
||||
@@ -14,7 +14,7 @@
|
||||
words=struct.unpack('!'+('I'*(size//4)),payload);frames.append(words[0]);buttons.update(words[2:]);nonzero+=any(words[2:])
|
||||
offset+=8+size
|
||||
result['wire'].append({'file':path.name,'commands':dict(counts),'first_frame':min(frames) if frames else None,'last_frame':max(frames) if frames else None,'input_packets':len(frames),'nonzero_input_packets':nonzero,'button_values':sorted(buttons),'incomplete_tail_bytes':len(data)-offset})
|
||||
-for name in ['retroarch.log','pc.log','trace.txt','startup-paths.log','tests_lapy-elevation.log']:
|
||||
+for name in ['retroarch.log','pc.log','trace.txt','startup-paths.log','lapy-service.log']:
|
||||
data=(folder/name).read_bytes();result['raw_sha256'][name]=hashlib.sha256(data).hexdigest()
|
||||
log=data.decode(errors='replace')
|
||||
if name=='retroarch.log':
|
||||
@@ -22,14 +22,14 @@
|
||||
result['state_checks']=[{'frame':int(f),'local':a,'remote':b,'match':v=='1'} for f,a,b,v in checks]
|
||||
result['console_connected']='Connected to:' in log or 'has joined as player 2' in log
|
||||
result['netplay_errors']=[line for line in log.splitlines() if '[Netplay]' in line and ('[ERROR]' in line or 'mismatch' in line)]
|
||||
- if name=='tests_lapy-elevation.log':result['daemon_balanced']='stage=complete error=0' in log and 'root_balanced=1' in log
|
||||
+ if name=='lapy-service.log':result['resident_client_proof']='data_rw=1' in log
|
||||
if name=='pc.log':result['pc_connected']='Connected to:' in log or 'has joined as player 2' in log
|
||||
(folder/'summary.json').write_text(json.dumps(result,indent=2)+'\n')
|
||||
print(json.dumps({k:v for k,v in result.items() if k not in ['raw_sha256','state_checks']},indent=2))
|
||||
checks=result.get('state_checks',[]);print('State checks:',len(checks),'matching:',sum(x['match'] and x['local']==x['remote'] for x in checks))
|
||||
|
||||
assert result['console_connected'] and result['pc_connected'], 'Handshake incomplete'
|
||||
-assert result['daemon_balanced'], 'Daemon cleanup not verified'
|
||||
+assert result['resident_client_proof'], 'Resident client proof missing'
|
||||
assert not result['netplay_errors'], 'NetPlay reported an error'
|
||||
assert len(result['wire']) == 2, 'Expected both peer streams'
|
||||
assert all(row['input_packets'] >= 1200 for row in result['wire']), 'Insufficient sustained frame traffic'
|
||||
@@ -37,4 +37,4 @@
|
||||
assert any(row['commands'].get('0040', 0) >= 20 for row in result['wire']), 'State checks missing'
|
||||
if result['mode'] == 'relay-join':
|
||||
assert len(checks) >= 20 and all(row['match'] and row['local']==row['remote'] for row in checks), 'Emulation states differ or were not checked'
|
||||
-print('PASS: console connection, frame traffic, scripted inputs and cleanup')
|
||||
+print('PASS: console connection, frame traffic, scripted inputs and resident client proof')
|
||||
@@ -0,0 +1,42 @@
|
||||
--- console_netplay.py
|
||||
+++ console_netplay.py
|
||||
@@ -60,11 +60,8 @@
|
||||
try:
|
||||
assert 'count=0' in c.ctl('procs'),'Title not idle'
|
||||
with c.deploy.connect(**c.settings) as ftp:
|
||||
- try: marker=c.deploy.remote_bytes(ftp,BASE+'lapy-ready')
|
||||
- except Exception:marker=b''
|
||||
- assert not marker,'Existing daemon readiness marker; inspect first'
|
||||
- last=c.deploy.remote_bytes(ftp,BASE+'tests/lapy-elevation.log')
|
||||
- assert any(b'daemon_result' in line and b'root_balanced=1' in line and (b'stage=complete error=0' in line or b'stage=service_complete error=0' in line) for line in last.splitlines()),'Previous daemon not complete'
|
||||
+ marker=c.deploy.remote_bytes(ftp,BASE+'lapy-ready')
|
||||
+ assert len(marker.split())==2,'Resident service not ready'
|
||||
for n in ['test-run.txt','args.txt','picker/picker-test.txt','es-de/capture-test.txt','startup-paths.log','pad-script.txt']:
|
||||
c.deploy.remove_if_present(ftp,BASE+n)
|
||||
ftp.storbinary('STOR '+BASE+'pad-script.txt',io.BytesIO(b'8 B 2\n14 A 2\n20 RIGHT 2\n30 STOP\n'))
|
||||
@@ -85,7 +82,7 @@
|
||||
else:netarg='--host'
|
||||
args=['--appendconfig=/app0/tests/installed-paths.cfg','--max-frames=6000','-L','/app0/cores/fceumm_libretro.so','/app0/tests/netplay-test.nes',netarg,'--port=55435','--nick=ps5-online-test']
|
||||
with c.deploy.connect(**c.settings) as ftp:ftp.storbinary('STOR '+BASE+'args.txt',io.BytesIO(('\n'.join(args)+'\n').encode()))
|
||||
- r.start_daemon(OUT,ROOT/'build/network-elevation/lapy/build/owned_root_daemon/lapy-root-daemon.elf')
|
||||
+ # RetroArch reuses its own automatically started service; no injected payload.
|
||||
(OUT/'launch.txt').write_text(c.ctl('launch PPSA99169'))
|
||||
if mode!='relay-join':
|
||||
time.sleep(6)
|
||||
@@ -99,13 +96,10 @@
|
||||
if 'count=0' not in c.ctl('procs'):
|
||||
(OUT/'close.txt').write_text(c.ctl('kill PPSA99169'))
|
||||
time.sleep(2)
|
||||
- for name in ['retroarch.log','trace.txt','startup-paths.log','tests/lapy-elevation.log']:r.fetch(name,OUT)
|
||||
- for _ in range(120):
|
||||
- data=r.fetch('tests/lapy-elevation.log',OUT)
|
||||
- if b'daemon_result' in data:break
|
||||
- time.sleep(.5)
|
||||
+ for name in ['retroarch.log','trace.txt','startup-paths.log','lapy-service.log']:r.fetch(name,OUT)
|
||||
+ data=r.fetch('lapy-service.log',OUT)
|
||||
print('Console idle='+str('count=0' in c.ctl('procs')),flush=True)
|
||||
- print('Daemon balanced='+str(b'root_balanced=1' in data),flush=True)
|
||||
+ print('Resident service client proof='+str(b'data_rw=1' in data),flush=True)
|
||||
for name in ['pc.log','retroarch.log']:
|
||||
log=(OUT/name).read_text(errors='replace')
|
||||
print(name, 'connected='+str('Connected to:' in log or 'has joined as player 2' in log),'netplay_errors='+str(sum('[Netplay]' in line and '[ERROR]' in line for line in log.splitlines())),flush=True)
|
||||
@@ -0,0 +1,38 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Arksama (Team PHU)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
Credits to original author when re-used in derivative works appreciated.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
DISCLAIMER
|
||||
|
||||
This software is provided for educational and interoperability purposes only.
|
||||
It is intended to be used on PS5 consoles owned by the user, on firmware
|
||||
versions that have been publicly jailbroken via third-party tools.
|
||||
|
||||
The daemon mimics etaHEN's jailbreak-on-demand API for homebrew apps that
|
||||
expect that environment. It does not bypass any DRM or grant access to
|
||||
copyrighted material. Use it only with software you have the right to run.
|
||||
|
||||
Use of this software in ways that violate Sony's Terms of Service is at
|
||||
your own risk. The authors take no responsibility for misuse.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Bundled Lapy service
|
||||
|
||||
RetroArch starts this external service through the existing console-local ELF
|
||||
loader before its workers, then waits for preflight readiness and requests SDK
|
||||
elevation. Subsequent launches reuse the resident service. There is no boot hook
|
||||
or Payload Manager configuration. Without a loader/service, offline startup is
|
||||
still available; a published but unverified elevation request refuses startup.
|
||||
|
||||
`python3 tools/build-lapy.py` exports Lapy revision
|
||||
`5b8397b9f2b5f12a7bc2f9c8745a00d1c2dd01ad` from the sibling
|
||||
`../PS5-Lapy-JB-Daemon` repository. It applies the six qualified Proton patches
|
||||
in this folder, then the SDK/log and readiness adaptations already recorded in
|
||||
`evidence/network-elevation` and `evidence/installed-paths`. Runtime source is
|
||||
identical to the previously qualified service; all preflight, transaction and
|
||||
teardown guards remain enabled. Upstream source stays in ignored `build/lapy`.
|
||||
|
||||
The existing `../prospero-win-main/native/ps5log/ps5log.h` input is hash-pinned;
|
||||
the project's current SDK builds the ELF, and its import contract is checked.
|
||||
The normal title build stages `lapy-root-daemon.elf` and its licence notices.
|
||||
Only title PPSA99169 is accepted. Resident diagnostics live beside the title in
|
||||
`lapy-service.log`; readiness is `lapy-ready`. Both are runtime files.
|
||||
|
||||
Console acceptance for automatic startup, reuse and online play is recorded in
|
||||
`evidence/lapy-autostart/`.
|
||||
@@ -0,0 +1,11 @@
|
||||
--- a/tools/build_owned_daemon.py
|
||||
+++ b/tools/build_owned_daemon.py
|
||||
@@ -66,7 +66,7 @@
|
||||
for path in sorted((sdk / "target").rglob("*"))
|
||||
if path.is_file()}
|
||||
flags = ["-std=c11", "-O2", "-Wall", "-Wextra", "-Werror",
|
||||
- f'-DTARGET_TITLE="{title}"',
|
||||
+ "-Wl,--build-id=sha1", f'-DTARGET_TITLE="{title}"',
|
||||
f'-DLAPY_SERVICE={int(args.service)}',
|
||||
f'-DLAPY_ELF_HELPER={int(args.elf_helper)}',
|
||||
f'-DLAPY_REQUIRE_CLIENT_RESULT={int(require_client_result)}',
|
||||
@@ -0,0 +1,52 @@
|
||||
--- a/source/owned_root_daemon.c
|
||||
+++ b/source/owned_root_daemon.c
|
||||
@@ -284,28 +284,12 @@
|
||||
#endif
|
||||
#endif /* !LAPY_ELF_HELPER */
|
||||
|
||||
-static void donor_release_signal(int signal_number)
|
||||
-{
|
||||
- (void)signal_number;
|
||||
- _exit(0);
|
||||
-}
|
||||
-
|
||||
static void child_main(void)
|
||||
{
|
||||
- struct sigaction action;
|
||||
- sigset_t release_signal;
|
||||
- memset(&action, 0, sizeof(action));
|
||||
- action.sa_handler = donor_release_signal;
|
||||
- sigemptyset(&action.sa_mask);
|
||||
- if (sigaction(SIGUSR1, &action, 0)) _exit(2);
|
||||
- sigemptyset(&release_signal);
|
||||
- sigaddset(&release_signal, SIGUSR1);
|
||||
- if (sigprocmask(SIG_UNBLOCK, &release_signal, 0)) _exit(3);
|
||||
- /* waitpid(WUNTRACED) is the readiness acknowledgement. The handler is
|
||||
- * installed before this stop, so an early release signal is safe. */
|
||||
+ /* FW 10.01's payload signal trampoline faults on SIGUSR1 delivery.
|
||||
+ * Release these disposable native donors with default SIGKILL teardown;
|
||||
+ * the kernel still releases their filedesc references. No user handler. */
|
||||
if (kill(getpid(), SIGSTOP)) _exit(4);
|
||||
- /* FW 10.01 resolves pause to an invalid address (0x1b0). Keep the
|
||||
- * SIGUSR1 exit handler and wait using the already exercised SDK call. */
|
||||
for (;;) usleep(100000);
|
||||
}
|
||||
|
||||
@@ -366,12 +350,15 @@
|
||||
if (kill(self->pid, SIGCONT)) return errno ? errno : EIO;
|
||||
self->stopped = 0;
|
||||
}
|
||||
- if (kill(self->pid, SIGUSR1)) return errno ? errno : EIO;
|
||||
+ if (kill(self->pid, SIGKILL)) return errno ? errno : EIO;
|
||||
for (unsigned i = 0; i < 100; ++i) {
|
||||
pid_t result = waitpid(self->pid, &status, WNOHANG);
|
||||
if (result == self->pid) {
|
||||
self->reaped = 1;
|
||||
- return WIFEXITED(status) && WEXITSTATUS(status) == 0 ? 0 : EPROTO;
|
||||
+ ps5log_printf(PS5LOG_MARK,
|
||||
+ "donor_reaped build=%s pid=%d status=%d expected_signal=%d",
|
||||
+ LAPY_OWNED_ID, self->pid, status, SIGKILL);
|
||||
+ return WIFSIGNALED(status) && WTERMSIG(status) == SIGKILL ? 0 : EPROTO;
|
||||
}
|
||||
if (result < 0) return errno ? errno : ECHILD;
|
||||
usleep(50000);
|
||||
@@ -0,0 +1,13 @@
|
||||
--- a/source/owned_root_daemon.c
|
||||
+++ b/source/owned_root_daemon.c
|
||||
@@ -304,7 +304,9 @@
|
||||
/* waitpid(WUNTRACED) is the readiness acknowledgement. The handler is
|
||||
* installed before this stop, so an early release signal is safe. */
|
||||
if (kill(getpid(), SIGSTOP)) _exit(4);
|
||||
- for (;;) pause();
|
||||
+ /* FW 10.01 resolves pause to an invalid address (0x1b0). Keep the
|
||||
+ * SIGUSR1 exit handler and wait using the already exercised SDK call. */
|
||||
+ for (;;) usleep(100000);
|
||||
}
|
||||
|
||||
static int start_child(struct child *self)
|
||||
@@ -0,0 +1,18 @@
|
||||
--- a/source/owned_root_daemon.c
|
||||
+++ b/source/owned_root_daemon.c
|
||||
@@ -975,7 +975,15 @@
|
||||
size_t thread_credential_offset = 0;
|
||||
int error = 0, acknowledged = 0;
|
||||
const char *stage = "preflight";
|
||||
+#if LAPY_SERVICE && !LAPY_MAX_REQUESTS
|
||||
+ /* Normal use must retain diagnostics without a PC holding stdout open. */
|
||||
+ int log_fd = open("/data/ps5proton/logs/lapy-service.log",
|
||||
+ O_WRONLY | O_CREAT | O_APPEND, 0666);
|
||||
+ if (log_fd < 0) return 2;
|
||||
+ ps5log_set_mirror_fd(log_fd);
|
||||
+#else
|
||||
ps5log_set_mirror_fd(STDOUT_FILENO);
|
||||
+#endif
|
||||
int logging_ready = ps5log_init_default("LAPYOWN", "lapy-owned-root-daemon") == 0;
|
||||
#if !LAPY_ELF_HELPER
|
||||
if (!logging_ready) return 2;
|
||||
@@ -0,0 +1,50 @@
|
||||
--- a/source/owned_root_daemon.c
|
||||
+++ b/source/owned_root_daemon.c
|
||||
@@ -1050,6 +1050,8 @@
|
||||
stage = "helper_request";
|
||||
error = run_helper_request(root, thread_credential_offset) ? EIO : 0;
|
||||
#elif LAPY_SERVICE
|
||||
+ struct lapy_vnode_ref_counts service_baseline = {0};
|
||||
+ if ((error = sample_root(root, "service_baseline", &service_baseline))) goto done;
|
||||
for (unsigned completed = 0;;) {
|
||||
path[0] = 0;
|
||||
pid = -1;
|
||||
@@ -1062,7 +1064,10 @@
|
||||
if (found) break;
|
||||
usleep(100000);
|
||||
}
|
||||
- if (!found) continue;
|
||||
+ if (!found) {
|
||||
+ if (LAPY_MAX_REQUESTS) { error = ETIMEDOUT; goto done; }
|
||||
+ continue;
|
||||
+ }
|
||||
stage = "request_baseline";
|
||||
if ((error = sample_root(root, "before_request", &baseline)))
|
||||
goto done;
|
||||
@@ -1087,7 +1092,25 @@
|
||||
if ((error = await_client_result(result_path))) goto done;
|
||||
#endif
|
||||
if (LAPY_MAX_REQUESTS && completed >= LAPY_MAX_REQUESTS) {
|
||||
- stage = "service_limit";
|
||||
+ /* A finite validation service must observe teardown after the
|
||||
+ * last request, not report success while roots remain owned. */
|
||||
+ stage = "service_wait_exit";
|
||||
+ baseline = service_baseline;
|
||||
+ int settled = 0;
|
||||
+ for (unsigned i = 0; i < 600; ++i) {
|
||||
+ errno = 0;
|
||||
+ if (kill(pid, 0) < 0 && errno == ESRCH) {
|
||||
+ if ((error = read_root_counts(root, &final))) goto done;
|
||||
+ if (final.hold == baseline.hold && final.use == baseline.use) {
|
||||
+ settled = 1;
|
||||
+ break;
|
||||
+ }
|
||||
+ }
|
||||
+ usleep(100000);
|
||||
+ }
|
||||
+ if ((error = sample_root(root, settled ? "service_exited" : "service_exit_timeout", &final))) goto done;
|
||||
+ if (!settled) { error = ETIMEDOUT; goto done; }
|
||||
+ stage = "service_complete";
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
--- a/source/owned_root_daemon.c
|
||||
+++ b/source/owned_root_daemon.c
|
||||
@@ -986,6 +986,7 @@
|
||||
size_t thread_credential_offset = 0;
|
||||
int error = 0, acknowledged = 0;
|
||||
const char *stage = "preflight";
|
||||
+ ps5log_set_mirror_fd(STDOUT_FILENO);
|
||||
int logging_ready = ps5log_init_default("LAPYOWN", "lapy-owned-root-daemon") == 0;
|
||||
#if !LAPY_ELF_HELPER
|
||||
if (!logging_ready) return 2;
|
||||
+97
-16
@@ -11,6 +11,10 @@
|
||||
#include <signal.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/time.h>
|
||||
#ifndef PS5_PATHS_HOST_TEST
|
||||
#include <ps5platform/elevation.h>
|
||||
#endif
|
||||
@@ -37,6 +41,96 @@ const char *ps5_installed_path(const char *path, char *buffer, size_t capacity)
|
||||
}
|
||||
|
||||
#if !defined(PS5_PATHS_HOST_TEST) || defined(PS5_PATHS_START_TEST)
|
||||
#ifndef PS5_LAPY_LOADER_PORT
|
||||
#define PS5_LAPY_LOADER_PORT 9021
|
||||
#endif
|
||||
static int service_ready(void)
|
||||
{
|
||||
long pid = -1, started = 0;
|
||||
struct timespec uptime;
|
||||
FILE *file = fopen("/app0/lapy-ready", "r");
|
||||
int ready = file && fscanf(file, "%ld %ld", &pid, &started) == 2;
|
||||
if (file)
|
||||
fclose(file);
|
||||
if (!ready || pid <= 1 || started < 0 || clock_gettime(CLOCK_MONOTONIC, &uptime) ||
|
||||
uptime.tv_sec < started)
|
||||
return 0;
|
||||
errno = 0;
|
||||
return kill((pid_t)pid, 0) == 0 || errno == EPERM;
|
||||
}
|
||||
|
||||
/* The same local ELF loader used by the WebUI, before any worker threads exist.
|
||||
* Send once only: an uncertain delivery must never trigger a second daemon. */
|
||||
static int start_service(FILE *log)
|
||||
{
|
||||
FILE *file = fopen("/app0/lapy-root-daemon.elf", "rb");
|
||||
struct stat info;
|
||||
if (!file)
|
||||
{
|
||||
if (log)
|
||||
fprintf(log, "lapy_autostart open_failed errno=%d\n", errno);
|
||||
return 0;
|
||||
}
|
||||
/* Do not use the SDK's inline fileno macro on the native libc's FILE. */
|
||||
if (stat("/app0/lapy-root-daemon.elf", &info) || info.st_size < 64 || info.st_size > (4 << 20))
|
||||
{
|
||||
if (log)
|
||||
fprintf(log, "lapy_autostart invalid_size errno=%d\n", errno);
|
||||
fclose(file);
|
||||
return 0;
|
||||
}
|
||||
const size_t size = (size_t)info.st_size;
|
||||
char *elf = malloc(size);
|
||||
int valid = elf && fread(elf, 1, size, file) == size && !memcmp(elf, "\177ELF", 4);
|
||||
fclose(file);
|
||||
if (!valid)
|
||||
{
|
||||
if (log)
|
||||
fprintf(log, "lapy_autostart invalid_elf errno=%d\n", errno);
|
||||
free(elf);
|
||||
return 0;
|
||||
}
|
||||
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
struct timeval timeout = {5, 0};
|
||||
struct sockaddr_in address = {0};
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
address.sin_port = htons(PS5_LAPY_LOADER_PORT);
|
||||
size_t sent = 0;
|
||||
if (fd >= 0 && setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)) == 0 &&
|
||||
connect(fd, (struct sockaddr *)&address, sizeof(address)) == 0)
|
||||
{
|
||||
while (sent < size)
|
||||
{
|
||||
ssize_t count = send(fd, elf + sent, size - sent, MSG_NOSIGNAL);
|
||||
if (count < 0 && errno == EINTR)
|
||||
continue;
|
||||
if (count <= 0)
|
||||
break;
|
||||
sent += (size_t)count;
|
||||
}
|
||||
shutdown(fd, SHUT_WR);
|
||||
}
|
||||
const int error = sent == size ? 0 : errno;
|
||||
if (fd >= 0)
|
||||
close(fd);
|
||||
free(elf);
|
||||
if (log)
|
||||
{
|
||||
fprintf(log, "lapy_autostart sent=%zu size=%zu errno=%d\n", sent, size, error);
|
||||
fflush(log);
|
||||
}
|
||||
if (!sent)
|
||||
return 0;
|
||||
for (unsigned i = 0; i < 100; i++)
|
||||
{
|
||||
if (service_ready())
|
||||
return 1;
|
||||
usleep(50000);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ps5_paths_start(void)
|
||||
{
|
||||
/* Keep the log descriptor across the root transition. Open before the
|
||||
@@ -60,22 +154,9 @@ int ps5_paths_start(void)
|
||||
if (!((id[i] >= 'A' && id[i] <= 'Z') || (id[i] >= '0' && id[i] <= '9')))
|
||||
goto failed;
|
||||
|
||||
/* A qualified daemon publishes readiness after its preflight and removes it
|
||||
* on exit. Do not publish a request at all on an ordinary offline launch.
|
||||
* A stale PID or a marker from a later uptime (before a reboot) is not ready. */
|
||||
long daemon_pid = -1, daemon_started = 0;
|
||||
struct timespec uptime;
|
||||
file = fopen("/app0/lapy-ready", "r");
|
||||
int ready = file && fscanf(file, "%ld %ld", &daemon_pid, &daemon_started) == 2;
|
||||
if (file)
|
||||
fclose(file);
|
||||
ready = ready && daemon_pid > 1 && clock_gettime(CLOCK_MONOTONIC, &uptime) == 0 &&
|
||||
uptime.tv_sec >= daemon_started;
|
||||
if (ready)
|
||||
{
|
||||
errno = 0;
|
||||
ready = kill((pid_t)daemon_pid, 0) == 0 || errno == EPERM;
|
||||
}
|
||||
/* Reuse a ready service, otherwise start the bundled one. A service publishes
|
||||
* readiness only after preflight; no request is made before that proof. */
|
||||
int ready = service_ready() || start_service(log);
|
||||
if (!ready)
|
||||
{
|
||||
if (log)
|
||||
|
||||
@@ -1,13 +1,72 @@
|
||||
"""Exercise saved /app0 names through the wrappers linked into RetroArch."""
|
||||
import pathlib
|
||||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
import unittest
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class InstalledPaths(unittest.TestCase):
|
||||
def test_bundled_service_delivery_and_readiness(self):
|
||||
with tempfile.TemporaryDirectory() as directory, socket.socket() as server:
|
||||
work = pathlib.Path(directory)
|
||||
server.bind(('127.0.0.1', 0))
|
||||
server.listen()
|
||||
server.settimeout(10)
|
||||
payload = b'\x7fELF' + bytes(range(256)) * 512
|
||||
(work / 'lapy-root-daemon.elf').write_bytes(payload)
|
||||
(work / 'test.c').write_text(r'''
|
||||
#include <assert.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#define PS5_PATHS_START_TEST
|
||||
enum ps5_elevation_status { PS5_ELEVATION_OK };
|
||||
#define PS5_ELEVATION_FILESYSTEM 1
|
||||
static enum ps5_elevation_status ps5_elevation_request(int c) { (void)c; abort(); }
|
||||
static const char *ps5_elevation_status_name(enum ps5_elevation_status s) {
|
||||
(void)s; return "unexpected";
|
||||
}
|
||||
#include "ps5_paths.c"
|
||||
int main(int argc, char **argv) {
|
||||
(void)argc;
|
||||
strcpy(install_root, argv[1]);
|
||||
assert(!service_ready());
|
||||
assert(start_service(NULL));
|
||||
assert(service_ready());
|
||||
return 0;
|
||||
}
|
||||
''')
|
||||
flags = subprocess.check_output(
|
||||
['bash', str(ROOT / 'tools/path-wrap-flags.sh')], text=True).split()
|
||||
subprocess.run(['cc', '-DPS5_PATHS_HOST_TEST',
|
||||
'-DPS5_LAPY_LOADER_PORT=' + str(server.getsockname()[1]),
|
||||
'-I' + str(ROOT / 'src'), str(work / 'test.c'),
|
||||
str(ROOT / 'src/ps5_paths_io.c'),
|
||||
*['-Wl,' + f for f in flags], '-o', str(work / 'test')], check=True)
|
||||
received = bytearray()
|
||||
|
||||
def loader():
|
||||
with server.accept()[0] as connection:
|
||||
connection.settimeout(10)
|
||||
while block := connection.recv(4096):
|
||||
received.extend(block)
|
||||
# Existing host PID plus uptime zero is a live readiness marker.
|
||||
import os
|
||||
(work / 'lapy-ready').write_text(f'{os.getpid()} 0\n')
|
||||
|
||||
thread = threading.Thread(target=loader)
|
||||
thread.start()
|
||||
try:
|
||||
subprocess.run([str(work / 'test'), str(work)], check=True, timeout=10)
|
||||
finally:
|
||||
thread.join(timeout=10)
|
||||
self.assertFalse(thread.is_alive())
|
||||
self.assertEqual(received, payload)
|
||||
|
||||
def test_legacy_files_and_two_path_operations(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
work = pathlib.Path(directory)
|
||||
|
||||
@@ -956,6 +956,43 @@
|
||||
"remote": "https://github.com/mihawk-99/PS5_RetroArch"
|
||||
},
|
||||
"modifications": "Relative contact tracking adapted in src/input_ps5.cpp; original revision and files in third_party/proton_touchpad/source.json."
|
||||
},
|
||||
{
|
||||
"id": "lapy",
|
||||
"name": "Lapy owned-root service (with ps5log client)",
|
||||
"kind": "code",
|
||||
"artifacts": [
|
||||
"lapy-root-daemon.elf"
|
||||
],
|
||||
"licence": "MIT AND LGPL-2.1-or-later",
|
||||
"copyright": [
|
||||
"Copyright (c) 2026 Arksama (Team PHU)"
|
||||
],
|
||||
"texts": [
|
||||
{
|
||||
"from": "platform/lapy/LICENSE",
|
||||
"to": "LICENSE-Lapy"
|
||||
},
|
||||
{
|
||||
"from": "../prospero-win-main/LICENSE",
|
||||
"to": "LICENSE-ps5log"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"kind": "fixed",
|
||||
"revision": "5b8397b9f2b5f12a7bc2f9c8745a00d1c2dd01ad",
|
||||
"remote": "https://github.com/mpereiraesaa/PS5-Lapy-JB-Daemon",
|
||||
"extra": [
|
||||
{"tarball": "build/lapy/ps5log-source.tar.gz"},
|
||||
{
|
||||
"name": "PS5-Lapy-JB-Daemon",
|
||||
"git": "../PS5-Lapy-JB-Daemon",
|
||||
"revision": "5b8397b9f2b5f12a7bc2f9c8745a00d1c2dd01ad",
|
||||
"remote": "https://github.com/mpereiraesaa/PS5-Lapy-JB-Daemon"
|
||||
}
|
||||
]
|
||||
},
|
||||
"modifications": "Title-specific resident service; qualified patches in platform/lapy and evidence; tools/build-lapy.py pins the ps5log header by SHA-256. No preflight or transaction guards disabled."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the pinned external Lapy service with the console-qualified patches."""
|
||||
import hashlib
|
||||
import io
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tarfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
REV = '5b8397b9f2b5f12a7bc2f9c8745a00d1c2dd01ad'
|
||||
LOG_SHA = '394af67d0f8b60b3335deb53396e52855ea2daa50ca914a456ea7663f48900c6'
|
||||
|
||||
|
||||
def main():
|
||||
source = ROOT / 'build/lapy'
|
||||
source.mkdir(parents=True, exist_ok=True)
|
||||
logger = ROOT.parent / 'prospero-win-main/native/ps5log'
|
||||
if hashlib.sha256((logger / 'ps5log.h').read_bytes()).hexdigest() != LOG_SHA:
|
||||
raise RuntimeError('Lapy logger differs from the qualified input')
|
||||
archive = subprocess.check_output([
|
||||
'git', '-C', str(ROOT.parent / 'PS5-Lapy-JB-Daemon'), 'archive', REV])
|
||||
with tarfile.open(fileobj=io.BytesIO(archive)) as tar:
|
||||
tar.extractall(source, filter='data')
|
||||
# Carry the exact logger source alongside upstream/patch sources for releases.
|
||||
with tarfile.open(source / 'ps5log-source.tar.gz', 'w:gz') as tar:
|
||||
tar.add(logger / 'ps5log.h', arcname='ps5log/ps5log.h')
|
||||
tar.add(ROOT.parent / 'prospero-win-main/LICENSE', arcname='ps5log/LICENSE')
|
||||
patches = [ROOT / 'platform/lapy' / (name + '.patch') for name in (
|
||||
'stdout-mirror', 'donor-wait', 'donor-release', 'service-lifecycle',
|
||||
'deterministic-build', 'resident-log')]
|
||||
patches += [ROOT / 'evidence/network-elevation/daemon-sdk-and-log.patch',
|
||||
ROOT / 'evidence/installed-paths/daemon-readiness.patch']
|
||||
for patch in patches:
|
||||
subprocess.run(['patch', '--batch', '-p1', '-i', str(patch)],
|
||||
cwd=source, check=True)
|
||||
subprocess.run(['python3', str(source / 'tools/build_owned_daemon.py'),
|
||||
'--sdk', str(ROOT / '.deps/native/ps5-payload-sdk'),
|
||||
'--logging-client', str(logger), '--title', 'PPSA99169',
|
||||
'--service', '--require-client-result'], check=True,
|
||||
env=dict(os.environ, PS5_CLANG=os.environ.get('PS5_CLANG', 'clang')))
|
||||
elf = source / 'build/owned_root_daemon-service-verified-client/lapy-root-daemon.elf'
|
||||
dynamic = subprocess.check_output(['readelf', '-dW', str(elf)], text=True)
|
||||
if 'libkernel_web.sprx' not in dynamic or 'libkernel_sys.sprx' in dynamic:
|
||||
raise RuntimeError('Lapy payload violates the loader import contract')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -290,6 +290,8 @@ webui_update=${webui_update#"$root/"}
|
||||
# The WebUI's own process, which stays up across the title's frontend changes
|
||||
# (src/webui_link.h): built from the same server and updater, staged in webui/.
|
||||
webui_daemon=$(bash "$root/tools/build-webui-daemon.sh")
|
||||
python3 "$root/tools/build-lapy.py"
|
||||
lapy_daemon="$root/build/lapy/build/owned_root_daemon-service-verified-client/lapy-root-daemon.elf"
|
||||
|
||||
# Extract once, before the identity is computed, so catalog changes identify the build.
|
||||
python3 "$root/tools/generate-core-metadata.py" "$root/build/webui-core-metadata"
|
||||
@@ -321,6 +323,7 @@ inputs += sorted(p for p in (root / "third_party/qrcodegen").rglob("*") if p.is_
|
||||
inputs += sorted(p for p in (root / "webui").rglob("*") if p.is_file())
|
||||
inputs += sorted(p for p in (root / "daemon").rglob("*") if p.is_file())
|
||||
inputs.append(root / "tools/build-webui-daemon.sh")
|
||||
inputs.append(root / "build/lapy/build/owned_root_daemon-service-verified-client/lapy-root-daemon.elf")
|
||||
inputs += sorted(p for p in (root / "build/webui-core-metadata").rglob("*") if p.is_file())
|
||||
inputs += [pathlib.Path(name) for name in sys.argv[3:]]
|
||||
digest = hashlib.sha256()
|
||||
@@ -473,6 +476,7 @@ fi
|
||||
mkdir -p "$dist/webui"
|
||||
cp -a "$root/webui/." "$dist/webui/"
|
||||
cp "$webui_daemon" "$dist/webui/ps5-retroarch-webui.elf"
|
||||
cp "$lapy_daemon" "$dist/lapy-root-daemon.elf"
|
||||
# The certificates the daemon verifies HTTPS against (curl over mbedTLS): Mozilla's
|
||||
# bundle as EmulationStation ships it, from its pinned source.
|
||||
certificates="$root/.deps/es-de/resources/certificates/curl-ca-bundle.crt"
|
||||
|
||||
@@ -25,9 +25,9 @@ import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
COVERED = ("eboot.bin", "es-de/*.bin", "picker/*.bin", "sce_module/*.prx", "cores/*.so", "cores/*.info",
|
||||
COVERED = ("lapy-root-daemon.elf", "eboot.bin", "es-de/*.bin", "picker/*.bin", "sce_module/*.prx", "cores/*.so", "cores/*.info",
|
||||
"info/*.info")
|
||||
EXECUTABLE = ("eboot.bin", "es-de.bin", "picker.bin", "*.prx", "*.so")
|
||||
EXECUTABLE = ("eboot.bin", "es-de.bin", "picker.bin", "lapy-root-daemon.elf", "*.prx", "*.so")
|
||||
|
||||
|
||||
def sha256(path):
|
||||
|
||||
@@ -34,7 +34,7 @@ from pathlib import Path
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
TABLE = ROOT / "tooling/notices/components.json"
|
||||
# The title's executables: eboot.bin, the frontends beside it, modules and cores.
|
||||
EXECUTABLE = ("eboot.bin", "es-de.bin", "picker.bin", "*.prx", "*.so")
|
||||
EXECUTABLE = ("eboot.bin", "es-de.bin", "picker.bin", "lapy-root-daemon.elf", "*.prx", "*.so")
|
||||
|
||||
|
||||
class NoticeError(Exception):
|
||||
|
||||
Reference in new issue
Block a user