Compare commits

...
120 Commits
Author SHA1 Message Date
Roman Tarasov ad5afc5689 Reduce scanner watchers and release USB watches during sleep 2026-07-13 02:38:50 +03:00
Roman Tarasov 6d8622c829 Support up to 8192 games across 2048 images 2026-07-13 02:16:16 +03:00
Roman Tarasov 9f85a3b421 Use SceShellCore bridge for title registration 2026-07-13 01:37:49 +03:00
Roman Tarasov 0fb241cba5 Handle game mounts through SceShellCore lifecycle 2026-07-13 01:37:49 +03:00
Roman Tarasov 09589b7ae2 Add SceShellCore hooks and runtime image mounts 2026-07-13 01:37:43 +03:00
Roman Tarasov 8566c0294c Revert "Revert "Attempting to restore occupied mount points for nullfs""
This reverts commit 16873e01f2.
2026-06-28 16:53:42 +03:00
Roman Tarasov 56dd338abd Prefer fakelib2 over fakelib for game overlays
Check app0/fakelib2 before resolving the legacy per-game fakelib
source. Fall back to app0/fakelib when fakelib2 is missing or is
not a directory.
2026-06-28 16:49:30 +03:00
Drakmor 090723879c Update README.md 2026-06-07 02:43:46 +03:00
Roman Tarasov 16873e01f2 Revert "Attempting to restore occupied mount points for nullfs"
This reverts commit c41c3eeba8.
2026-06-03 21:30:59 +03:00
Roman Tarasov f20b97db65 funding 2026-06-03 17:53:44 +03:00
Roman Tarasov 9d6ab61369 Added description of compressed PFS format 2026-06-03 17:49:43 +03:00
Roman Tarasov 25182da557 Fix PFS mount 2026-06-03 16:09:11 +03:00
Drakmor 72cf4460a9 Merge pull request #55 from drakmor/alert-autofix-1
Potential fix for code scanning alert no. 1: Workflow does not contain permissions
2026-06-03 02:26:06 +03:00
DrakmorandCopilot Autofix powered by AI cc0f4e0684 Potential fix for code scanning alert no. 1: Workflow does not contain permissions
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-06-03 01:49:51 +03:00
Roman Tarasov 8cdcd112f5 Nested images 2026-06-03 01:34:23 +03:00
Roman Tarasov c41c3eeba8 Attempting to restore occupied mount points for nullfs 2026-06-03 01:19:38 +03:00
Drakmor 3d6a300742 Update README.md 2026-06-02 11:51:43 +03:00
Roman Tarasov 51d01011ff Fixed mounting of exfat 2026-06-02 10:14:47 +03:00
Roman Tarasov 9e803cff7f Added support for compressed PFS 2026-06-01 21:28:57 +03:00
Roman Tarasov dee175e2bd Added copying of trophy data 2026-06-01 01:12:30 +03:00
Roman Tarasov 860e9de3b1 Fix duplicate uninstall requests during scan cleanup 2026-06-01 00:05:13 +03:00
Roman Tarasov e0b41b3ca6 Fix cleanup for non-uninstallable app.db entries 2026-05-30 17:00:11 +03:00
Roman Tarasov 5c4470cdcd Merge branch 'main' of https://github.com/drakmor/ShadowMountPlus 2026-05-30 11:13:24 +03:00
Roman Tarasov 5b6be76272 Fix same-session manual reinstalls by clearing stale install/register state and refreshing app.db before manual scans 2026-05-30 11:12:44 +03:00
Roman Tarasov c66bff6d2a Prevent kstuff auto-resume during shutdown 2026-05-30 10:59:17 +03:00
Drakmor 2df61d7e2d Update README.md 2026-05-25 22:51:30 +03:00
Drakmor e960fc8079 Update README.md 2026-05-25 22:51:03 +03:00
Drakmor d1c6978e78 Update README.md 2026-05-25 22:44:36 +03:00
Roman Tarasov 4eadb06f57 Add manual install list support 2026-05-25 18:00:14 +03:00
Drakmor 78f536b41a maximum mounted images 64->256 2026-05-23 18:47:58 +03:00
Drakmor 99e000eed9 Update README.md
kstuff link
2026-05-23 16:17:25 +03:00
Drakmor 8622d8757f Update README.md 2026-05-14 11:55:28 +03:00
Roman Tarasov bcffa18997 Redice poll STOP file 2026-05-13 05:24:52 +03:00
Roman Tarasov e00dda0075 Lower default scan interval to 15 seconds 2026-05-13 05:21:47 +03:00
Roman Tarasov 315265f5db Code review 2026-05-13 05:19:22 +03:00
Drakmor 30b04358f4 Update README.md 2026-05-10 19:51:33 +03:00
Roman Tarasov a6ad4922a9 Support link 2026-05-10 19:50:17 +03:00
Roman Tarasov ebd649e0ea Add more detailed diagnostics when entering sleep 2026-05-10 19:45:07 +03:00
Roman Tarasov 4072ced15d Pause kstuff during sleep 2026-05-10 19:44:53 +03:00
Roman Tarasov fee1029b0a Enter sleep mode earlier 2026-05-10 19:18:32 +03:00
Roman Tarasov cbd0fce4f9 Fix snd0Info paths 2026-05-10 18:24:29 +03:00
Roman Tarasov 9fa6ad94de Handle suspend sleep mode and USB cleanup. Thx to @hadobedo 2026-05-10 16:01:59 +03:00
Roman TarasovandCopilot d4c4cda739 kstuff autopause 12.70-13.20
Co-authored-by: Copilot <copilot@github.com>
2026-05-10 12:31:15 +03:00
Roman Tarasov 954b3a7d17 Fix discovery after removable drive changes 2026-05-10 12:21:09 +03:00
Roman Tarasov 3652f506c0 Add global fakelib overlay support 2026-05-10 11:50:18 +03:00
Roman Tarasov 6c188a03bb typo 2026-05-10 10:09:47 +03:00
Roman Tarasov 2680ed28d7 ci/cd sdk 2026-04-17 02:44:55 +03:00
Roman Tarasov f401660113 stub path 2026-04-17 02:39:45 +03:00
Roman Tarasov 11a135cf1c typo 2026-04-17 02:35:15 +03:00
Roman Tarasov 146942c138 mdbg: simplify crash monitor and log snapshot handling 2026-04-17 02:30:07 +03:00
Roman Tarasov a8f1a216bd mdbg: switch klog monitoring to debug log snapshots 2026-04-17 01:51:45 +03:00
Roman Tarasov c37088543d /dev/klog reopen 2026-04-16 02:36:49 +03:00
Roman Tarasov 059f8b40f1 Rework crash detection with sm_mdbg and post-pause autotune 2026-04-16 01:50:51 +03:00
Roman Tarasov 6f61e8d29e Merge branch 'main' of https://github.com/drakmor/ShadowMountPlus 2026-04-15 00:09:45 +03:00
Roman Tarasov a5471da456 Stop background full scans while a game is running 2026-04-15 00:09:39 +03:00
Roman Tarasov 0abfecd3e7 Rework backport overlay resolution and mount handling 2026-04-14 23:52:09 +03:00
Drakmor 9f1786905c Use SDK ps5-payload-dev/pacbrew-repo 2026-04-05 10:50:53 +03:00
Roman Tarasov fa714f868f Dynamically load AppInstallTitleDir on FW below 12 2026-04-05 10:40:50 +03:00
Roman Tarasov 17bb561e8d Only log pre-scan cleanup during the initial startup scan 2026-04-04 23:23:47 +03:00
Roman Tarasov 0efeec16fa Merge branch 'main' of https://github.com/drakmor/ShadowMountPlus 2026-04-04 22:55:09 +03:00
Roman Tarasov a584121d76 Create config.ini from bundled template when missing 2026-04-04 22:55:03 +03:00
Roman Tarasov 45da514549 Increase default scan interval to 60 seconds and clarify config example 2026-04-04 22:42:32 +03:00
Roman Tarasov e272c9736c Remove kstuff noautomount sentinel files on shutdown 2026-04-04 21:57:48 +03:00
Roman Tarasov 902a6917df Removed the extra log messages 2026-04-04 21:55:07 +03:00
Roman Tarasov 6df8a472e3 Add optional AppInstallAll mode with FW 12+ override 2026-04-04 21:53:27 +03:00
Roman Tarasov 4e9afa7885 Fast shutdown 2026-04-04 21:27:10 +03:00
Drakmor 40967215b4 Update README.md 2026-03-30 01:39:04 +03:00
Roman Tarasov a23822a26f Disabled crash detection by default, increased kstuff pause timeouts 2026-03-30 01:25:26 +03:00
Roman Tarasov 97ffedf0df Create target /system_ex/app mount directory before nullfs mount 2026-03-29 21:38:09 +03:00
Roman Tarasov bdd3083d65 Temporary use of the EchoStretch repository 2026-03-29 20:15:50 +03:00
Roman Tarasov bcb906e5b6 Add per-image sector-size overrides with autotune on cluster-size mount errors 2026-03-29 16:32:28 +03:00
Roman Tarasov 47a033b50b Make kstuff crash detection optional and reset heuristic state on config reload 2026-03-29 16:05:33 +03:00
Roman Tarasov 47bded61da Restore missing appmeta on remount and replace access checks with stat 2026-03-29 15:49:03 +03:00
Roman Tarasov 5d0ca6d5d1 Add kstuff crash autotune with autotune.ini pause-delay overrides 2026-03-29 03:27:43 +03:00
Roman Tarasov ebfe0376c6 Detect early post-kstuff game crashes 2026-03-29 03:01:38 +03:00
Roman Tarasov a6a5437a42 Reduce install retry log spam 2026-03-29 00:08:31 +03:00
Roman Tarasov 8c1ebbdf28 Wait for sce_sys stabilization before folder install 2026-03-28 23:19:54 +03:00
Roman Tarasov 853ff28b85 Typo and move release archive contents to a manifest file 2026-03-28 01:42:38 +03:00
Roman Tarasov e9098631c1 Add GitHub Actions workflow for PS5 build 2026-03-28 01:34:06 +03:00
Roman Tarasov bf24df419b Added 10.20 to 12.00 kstuff pause support 2026-03-28 01:04:03 +03:00
Roman Tarasov ff70cb1bc8 Deduplicate device node wait helper across image and mount code 2026-03-24 00:23:52 +03:00
Roman Tarasov 9254525a6c Tune release build flags 2026-03-23 23:54:29 +03:00
Roman Tarasov 26ba93c9aa Streamline sm_log synchronization and reuse the open log file 2026-03-23 23:50:40 +03:00
Roman Tarasov 43d61dce37 Move scan work buffers off the stack into reusable scanner workspace 2026-03-23 23:35:49 +03:00
Roman Tarasov 7759b83293 Fix scan-now request synchronization and simplify pending-state handling 2026-03-23 23:14:33 +03:00
Roman Tarasov 0eca3fa138 Fix kstuff auto-pause focus handling for initially-disabled state 2026-03-23 22:47:45 +03:00
Roman Tarasov e30a296a53 Add autopause.txt per-game delay overrides for kstuff auto-pause 2026-03-22 20:49:28 +03:00
Roman Tarasov b374c3163e Add runtime config hot-reload 2026-03-22 20:25:08 +03:00
Roman Tarasov a71a03d0db Probe kstuff availability at auto-pause time instead of startup 2026-03-22 19:20:21 +03:00
Roman Tarasov 56e8ba2185 Prefer valid installed sources when resolving duplicate title IDs 2026-03-22 19:04:55 +03:00
Roman Tarasov 18a97ecca9 Refactor directory candidate collection into focused scan helpers 2026-03-22 18:43:53 +03:00
Roman Tarasov 270264cc75 Detect unreadable image mounts and report them as damaged images 2026-03-22 18:28:36 +03:00
Roman Tarasov ef814ad56b Unify scan and watcher tree traversal with a shared scan walker 2026-03-22 17:54:58 +03:00
Roman Tarasov cec94656c6 Cache owning scan roots for game entries and simplify targeted backport reconciliation 2026-03-22 17:29:47 +03:00
Roman Tarasov 4ff9a3dffe Optimize scanner watcher registry with fd indexing and per-root watch lists 2026-03-22 17:21:15 +03:00
Roman Tarasov fe7dad099c Optimize scanner watcher rebuilds to refresh only affected subtrees 2026-03-22 16:20:26 +03:00
Roman Tarasov c02f237ddf Cache mounted games when app.db title list is temporarily unavailable 2026-03-22 15:53:02 +03:00
Roman Tarasov d318b5e607 Refactor scanning to use kevent-driven dirty roots, debounce filesystem events, run targeted per-root cleanup and discovery, and support bounded scan_depth traversal for nested directories and image files 2026-03-22 15:39:31 +03:00
Roman Tarasov 6f97cde602 Remove redundant internal guard checks 2026-03-22 13:56:09 +03:00
Roman Tarasov edcac6bdd7 Refactor scanning into a scanner service, replace main loop sleep polling with kevent/wake-pipe waiting, and keep polling as a runtime fallback 2026-03-22 13:13:52 +03:00
Roman Tarasov 27feba2085 Add bounded scan_depth with recursive_scan compatibility remap
Add scan_depth config, remap legacy recursive_scan=1 to scan_depth=2, and avoid duplicate traversal of nested scan roots
2026-03-22 12:43:55 +03:00
Roman Tarasov 82603fce7d Fixed waiting for images to stabilize 2026-03-22 12:09:05 +03:00
Roman Tarasov 01b64346c8 Strip all from elf 2026-03-22 12:07:43 +03:00
Roman Tarasov 7e48103149 Updated readme 2026-03-16 22:04:49 +03:00
Roman Tarasov 124e757199 Disable kstuff auto-pause when kstuff is not installed 2026-03-16 20:44:34 +03:00
Roman Tarasov 0fcc970a20 Create /data/.kstuff_noautomount 2026-03-16 01:18:56 +03:00
Roman Tarasov 7684e8431d Increased block size to 64kb for UFS 2026-03-15 14:28:24 +03:00
Roman Tarasov 3166434db1 The start notification has been fixed and its sound has been disabled. 2026-03-15 12:27:11 +03:00
Roman Tarasov c785daaddf Changed image mount directory to /mnt/shadowmnt 2026-03-14 23:41:28 +03:00
Roman Tarasov 2db6b49439 Fix /dev/lvd attach 2026-03-14 23:18:19 +03:00
Roman Tarasov aff2fc0e6c Remove stale duplicate-tracking entries when sources disappear 2026-03-14 20:08:29 +03:00
Roman Tarasov ae36dd5871 unionfs options 2026-03-13 02:57:57 +03:00
Roman Tarasov cc3e07d201 Update compatibility note in README.md 2026-03-10 10:24:30 +03:00
Roman Tarasov 2c03ede25c Update README.md 2026-03-10 02:43:18 +03:00
Roman Tarasov a212a883b3 1.6b4 2026-03-10 02:24:03 +03:00
Roman Tarasov 99a2956c28 upd .gitingore 2026-03-10 02:23:13 +03:00
Roman Tarasov 93071924b5 wait /dev/lvd1 -> /dev/lvd2 2026-03-04 23:47:40 +03:00
Roman Tarasov 47e47477d1 Add macOS exFAT image builder and improve LVD startup wait handling 2026-02-28 20:21:46 +03:00
Roman Tarasov 31c28e1896 Block mounting of images with too small a cluster size. 2026-02-27 22:20:16 +03:00
Roman Tarasov c1f34ffa22 Version bump 2026-02-27 01:09:34 +03:00
86 changed files with 22566 additions and 3812 deletions

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
custom:
- https://github.com/drakmor/shadowMountPlus#support-development
+12
View File
@@ -0,0 +1,12 @@
# Files included in the release archive.
# One path per line, relative to the repository root.
shadowmountplus.elf
config.ini.example
LICENSE
make_image.bat
mkexfat.sh
mkexfat_macos.sh
mkufs2.sh
New-OsfExfatImage.ps1
README.md
+130
View File
@@ -0,0 +1,130 @@
name: PS5 Build and Release
on:
push:
paths-ignore:
- "**/*.md"
pull_request:
paths-ignore:
- "**/*.md"
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
cancel-in-progress: false
permissions:
contents: read
jobs:
build:
outputs:
archive_name: ${{ steps.package.outputs.archive_name }}
runs-on: ubuntu-latest
steps:
- name: Checkout Payload
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Checkout pacbrew-repo
uses: actions/checkout@v4
with:
repository: ps5-payload-dev/pacbrew-repo
path: pacbrew-repo
- name: Checkout SDK stubs
uses: actions/checkout@v4
with:
repository: ps5-payload-dev/sdk
path: ps5-sdk
- name: Setup build environment
run: |
sudo apt update
sudo apt install -y \
autoconf \
automake \
build-essential \
clang-18 \
curl \
git \
libarchive-tools \
libtool \
lld-18 \
makepkg \
meson \
pacman-package-manager \
pkg-config \
xxd \
zip
- name: Build PS5 payload SDK
working-directory: pacbrew-repo/sdk
run: |
makepkg -c -f
sudo pacman --noconfirm -U ./ps5-payload-*.pkg.tar.gz
- name: Build PS5 sqlite package
working-directory: pacbrew-repo/sqlite
run: |
makepkg -c -f
sudo pacman --noconfirm -U ./ps5-payload-*.pkg.tar.gz
- name: Build payload
run: |
make clean all PS5_SCE_STUBS_DIR="$GITHUB_WORKSPACE/ps5-sdk/sce_stubs"
- name: Create release archive
id: package
shell: bash
run: |
set -euo pipefail
version_tag="$(git describe --abbrev=6 --dirty --always --tags 2>/dev/null || echo "${GITHUB_SHA::7}")"
archive_name="ShadowMountPlus_${version_tag}.zip"
manifest_file=".github/release-files.txt"
mapfile -t release_files < <(grep -Ev '^[[:space:]]*(#|$)' "${manifest_file}")
if [ "${#release_files[@]}" -eq 0 ]; then
echo "Release manifest is empty: ${manifest_file}" >&2
exit 1
fi
for release_file in "${release_files[@]}"; do
if [ ! -f "${release_file}" ]; then
echo "Missing release file: ${release_file}" >&2
exit 1
fi
done
zip -9 "${archive_name}" "${release_files[@]}"
echo "archive_name=${archive_name}" >> "$GITHUB_OUTPUT"
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ steps.package.outputs.archive_name }}
path: ${{ steps.package.outputs.archive_name }}
if-no-files-found: error
release:
needs: build
permissions:
contents: write
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: ${{ needs.build.outputs.archive_name }}
path: dist
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
generate_release_notes: true
files: dist/${{ needs.build.outputs.archive_name }}
+6
View File
@@ -1,5 +1,11 @@
*.o
*.elf
*.so
*.pyc
__pycache__/
daemon.lock
debug.log
notify.txt
src/notify_icon_asset.c
refs/*
docs/*
+36 -7
View File
@@ -1,22 +1,51 @@
PS5_PAYLOAD_SDK ?= /opt/ps5-payload-sdk
include $(PS5_PAYLOAD_SDK)/toolchain/prospero.mk
VERSION_TAG := $(shell git describe --abbrev=6 --dirty --always --tags 2>/dev/null || echo unknown)
# Standard Flags (No extra libraries)
CFLAGS := -O2 -Wall -D_BSD_SOURCE -std=gnu11 -Isrc
CFLAGS := -O3 -flto=thin -DNDEBUG -ffunction-sections -fdata-sections -Wall -Wextra -Wstrict-prototypes -Wmissing-prototypes -Werror=strict-prototypes -Werror=missing-prototypes -D_BSD_SOURCE -std=gnu11 -Iinclude -Isrc
CFLAGS += -DSHADOWMOUNT_VERSION=\"$(VERSION_TAG)\"
# Linker
LDFLAGS :=
LDFLAGS := -flto=thin -Wl,--gc-sections
# Standard Libraries Only
LIBS := -lkernel_sys -lSceSystemService -lSceUserService -lSceAppInstUtil -lsqlite3
LIBS := -lSceNotification -lSceSystemService -lSceUserService -lSceAppInstUtil -lsqlite3
PS5_SCE_STUBS_DIR ?= $(PS5_PAYLOAD_SDK)/src/sce_stubs
KERNEL_SYS_STUB_SO := src/libkernel_sys_ext.so
KERNEL_SYS_STUB_SRCS := $(PS5_SCE_STUBS_DIR)/libkernel_sys.c src/libkernel_sys_ext.c
ASSET_SRCS := src/notify_icon_asset.c src/config_ini_example_asset.c
SRCS := src/main.c $(wildcard src/sm_*.c) $(ASSET_SRCS)
ASM_SRCS := src/sm_shellcore_bridge.S
OBJS := $(SRCS:.c=.o) $(ASM_SRCS:.S=.o)
HEADERS := $(wildcard include/*.h)
# Targets
all: shadowmountplus.elf
# Build Daemon
shadowmountplus.elf: src/main.c
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $< $(LIBS)
$(PS5_PAYLOAD_SDK)/bin/prospero-strip --strip-debug $@
shadowmountplus.elf: $(OBJS) $(KERNEL_SYS_STUB_SO)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJS) $(KERNEL_SYS_STUB_SO) $(LIBS)
$(PS5_PAYLOAD_SDK)/bin/prospero-strip --strip-all $@
rm -f src/notify_icon_asset.c src/config_ini_example_asset.c
$(KERNEL_SYS_STUB_SO): $(KERNEL_SYS_STUB_SRCS)
test -f "$(PS5_SCE_STUBS_DIR)/libkernel_sys.c"
$(CC) -shared -Wl,-soname=libkernel_sys.sprx -o $@ $^
src/notify_icon_asset.c: smp_icon.png
xxd -i $< > $@
src/config_ini_example_asset.c: config.ini.example
xxd -i $< > $@
src/%.o: src/%.c $(HEADERS)
$(CC) $(CFLAGS) -c -o $@ $<
src/%.o: src/%.S
$(CC) $(CFLAGS) -c -o $@ $<
clean:
rm -f shadowmountplus.elf kill.elf src/*.o
rm -f shadowmountplus.elf kill.elf src/*.o $(KERNEL_SYS_STUB_SO) src/notify_icon_asset.c src/config_ini_example_asset.c
+253 -45
View File
@@ -1,16 +1,26 @@
# ShadowMountPlus (PS5)
**Version:** `1.6beta1`
**Repository:** https://github.com/drakmor/shadowMountPlus
**Discord:** https://discord.gg/x2Ppvzwjhm
**Warning! Mounting images can cause shutdown problems and data corruption on internal drives! This depends on many factors, but is more common with older firmware versions. Please take this into account when testing.**
**ShadowMountPlus** is a fully automated, background "Auto-Mounter" payload for Jailbroken PlayStation 5 consoles. It streamlines the game mounting process by eliminating the need for manual configuration or external tools (such as DumpRunner or Itemzflow). ShadowMountPlus automatically detects, mounts, and installs game dumps from both **internal and external storage**.
**Compatibility:** Supports all Jailbroken PS5 firmwares running **Kstuff v1.6.7**.
**Compatibility:** Supports all Jailbroken PS5 firmwares running **[Kstuff-lite v1.07+](https://github.com/EchoStretch/kstuff-lite)**.
## 💜 Support Development
If you want to support this project, you can donate
- USDT (TRC-20): **`TKaUGEwMm9KBXzEoiaaKYBX2yCHAKASW3p`**
- USDT (ERC-20): **`0x313dD245dBA957A5560618eA882d08e66aaFb430`**
- USDC (Solana): **`5kv7j2RbUGaSP1kU1cZWj9jHH7d6rfvxmK6YXTYbH4um`**
## Current image support
@@ -19,36 +29,58 @@
| Extension | Mounted FS | Attach backend | Status |
| --- | --- | --- | --- |
| `.exfat` | `exfatfs` | `LVD` or `MD` (configurable) | Optimal |
| `.ffpkg` | `ufs` | `LVD` or `MD` (configurable) | Legacy |
| `.ffpkg` | `ufs` | `LVD` or `MD` (configurable) | Recommended |
| `.exfat` | `exfatfs` | `LVD` or `MD` (configurable) | Compatibility / external-drive-only titles |
| `.ffpfs` | `pfs` | `LVD` | Experimental |
| `.ffpfsc` | `pfs` container | `LVD` | Experimental container for nested images |
Notes:
- Backend, read-only mode, and sector size can be configured via `/data/shadowmount/config.ini`.
- Debug logging is enabled by default (`debug=1`) and writes to console plus `/data/shadowmount/debug.log` (set `debug=0` to disable).
- **exFAT is the preferred image filesystem, including on 4.xx firmware, with no known reboot/shutdown issues in typical use.**
- **UFS (`.ffpkg`) is the recommended image format for normal use.**
- **Use exFAT (`.exfat`) only for titles that need external-drive-style compatibility.**
- **When building exFAT images manually, keep the cluster size at `64 KB`; smaller clusters can reduce performance.**
## Recommended FS choice
- Prefer **exFAT (`.exfat`)** in most cases: it is generally more performant, case-insensitive, and does not have reboot/shutdown issues in typical use.
- Use **UFS (`.ffpkg`)** only when specifically needed for compatibility with your game/setup.
- Prefer **UFS (`.ffpkg`)** in most cases: this is the recommended default image format for ShadowMountPlus.
- Use **exFAT (`.exfat`)** only for games that do not work correctly unless they are handled like external-drive content.
- If you create an **exFAT (`.exfat`)** image manually, use a **`64 KB` cluster size**. Smaller clusters can cause a noticeable performance loss.
## Runtime config (`/data/shadowmount/config.ini`)
This file is optional. If it does not exist, built-in defaults are used.
This file is optional. If it does not exist, ShadowMountPlus creates it from the bundled `config.ini.example` template on startup and uses built-in defaults until you uncomment overrides.
Supported keys (all optional):
- `debug=1|0` (`1` enables `log_debug` output to console + `/data/shadowmount/debug.log`; default is `1`)
- `quiet_mode=1|0` (`1` suppresses plain informational popups but keeps rich toasts; default is `0`)
- `mount_read_only=1|0` (default: `1`)
- `force_mount=1|0` (mounting even damaged file systems; default: `0`)
- `app_install_all=1|0` (`1` stages new titles and submits them through the stock batch `sceAppInstUtilAppInstallAll`; default: `0` on every firmware. On FW `12.00+`, the default per-title path is provided by the SceShellCore TitleDir bridge.)
- `image_ro=<image_filename>` (repeatable; force read-only mode for this image filename)
- `image_rw=<image_filename>` (repeatable; force read-write mode for this image filename)
- `recursive_scan=1|0` (`0` = scan only first-level subfolders, `1` = recursive scan without depth limit; default: `0`)
- `backports_path=<absolute_path>` (default: `/data/backports`; if `<backports_path>/<TITLE_ID>` exists, it is overlaid on mounted image via `unionfs`)
- `scan_interval_seconds=<1..3600>` (full scan loop interval; default: `10`)
- `image_sector=<image_filename>:<sector_size>` (repeatable; force sector size for this image filename)
- `scan_depth=<1..2>` (`1` = scan only first-level subfolders, `2` = also scan one additional nested level; default: `1`)
- `recursive_scan=1|0` (deprecated compatibility key; `1` forces `scan_depth=2`)
- `scan_interval_seconds=<1..3600>` (full scan loop interval; default: `15`)
- `stability_wait_seconds=<0..3600>` (minimum source age before processing; default: `10`)
- `exfat_backend=lvd|md` (default: `lvd`)
- `ufs_backend=lvd|md` (default: `lvd`)
- `backport_fakelib=1|0` (`1` mounts sandbox `fakelib` overlays for running games; default: `1`)
- `global_fakelib=1|0` (`1` enables the global fakelib overlay when the folder exists; default: `1`)
- `global_fakelib_path=<absolute_path>` (global fakelib folder; default: `/data/shadowmount/fakelib`)
- `global_fakelib_priority=game|global` (overlay priority when both global and game fakelib exist; default: `game`)
- `global_fakelib_exclude=<TITLE_ID>` (repeatable; disables the global fakelib overlay for matching titles)
- `kstuff_game_auto_toggle=1|0` (`1` pauses kstuff after tracked game launches and resumes it on stop; default: `1`)
- `kstuff_crash_detection=1|0` (`1` enables crash monitoring and pause-delay autotune updates; default: `1`)
- `kstuff_pause_delay_image_seconds=<0..3600>` (delay before pausing kstuff for image-backed launches; default: `25`)
- `kstuff_pause_delay_direct_seconds=<0..3600>` (delay before pausing kstuff for direct/non-image launches; default: `15`)
- `kstuff_no_pause=<TITLE_ID>` (repeatable; keeps kstuff enabled for matching titles)
- `kstuff_delay=<TITLE_ID>:<0..3600>` (repeatable; per-title pause delay override, last matching rule wins)
- `/data/shadowmount/autotune.ini` may also provide per-title pause-delay overrides with highest priority:
- `kstuff_delay=<TITLE_ID>:<0..3600>`
- `<TITLE_ID>=<0..3600>`
- `image_sector=<image_filename>:<sector_size>`
- `scanpath=<absolute_path>` (can be repeated on multiple lines; default: built-in scan path list below)
- `lvd_exfat_sector_size=<value>` (default: `512`)
- `lvd_ufs_sector_size=<value>` (default: `4096`)
@@ -67,19 +99,58 @@ mount_read_only=1
image_rw=PPSA1234-my-image.ffpfs
image_rw=MYGame 123.exfat
image_ro=legacy_dump.ffpkg
image_sector=MYGame 123.exfat:65536
```
Per-image sector override behavior:
- Match is done by image file name (without path).
- `image_sector` in `/data/shadowmount/autotune.ini` has the highest priority for matching image files.
- If no per-image rule matches, the backend-specific global sector size defaults are used.
- When image validation fails because the mounted file-system cluster size is smaller than the selected device sector size, ShadowMountPlus writes `image_sector=<image_filename>:<cluster_size>` into `/data/shadowmount/autotune.ini` and asks you to try mounting again.
Scan path behavior:
- If at least one `scanpath=...` is present, only those custom paths are used.
- `/data/imgmnt/{ufsmnt,exfatmnt,pfsmnt}` are always added automatically, even with custom paths.
- With `recursive_scan=0` (default), only first-level subfolders are checked.
- With `recursive_scan=1`, subfolders are scanned recursively.
- Full scan loop runs every `scan_interval_seconds` (default: `10`).
- `/mnt/shadowmnt/pfsc` and `/mnt/shadowmnt` are always added automatically, even with custom paths.
- With `scan_depth=1` (default), only first-level subfolders are checked.
- With `scan_depth=2`, one additional nested level is checked.
- If `recursive_scan=1` is set, ShadowMount+ forces `scan_depth=2`.
- Full scan loop runs every `scan_interval_seconds` (default: `15`).
- Sources newer than `stability_wait_seconds` are deferred until stable (default: `10`).
- Direct folder installs use `<game>/sce_sys` for this check; image and backport sources use the target path itself.
Backport overlay behavior:
- If `${backports_path}/${TITLE_ID}` exists, ShadowMount+ mounts it over the game root with `unionfs`.
- Files from backport folder override files from the mounted image.
- For each `scanpath`, use:
- `<scanpath>/backports/<TITLE_ID>/`
- The `backports` folder is ignored during normal game scanning.
- A backport is applied automatically to the matching mounted game from any configured scan path.
- If multiple scan paths provide the same title backport, the game's own scan path wins; otherwise scan path order is used.
- If `/mnt/sandbox/<TITLE_ID>_XXX/app0/fakelib2` exists while the game is running, ShadowMount+ mounts it into that game's sandbox `common/lib`; otherwise it falls back to `app0/fakelib`.
- If `global_fakelib=1` and `global_fakelib_path` exists as a directory, ShadowMount+ also mounts that folder into the same sandbox `common/lib`.
- When both global and per-game fakelib exist, the default gives the game's own `fakelib` priority by mounting `/data/shadowmount/fakelib` first and then the game-specific fakelib.
- `global_fakelib_priority=global` reverses that priority.
- Use repeatable `global_fakelib_exclude=<TITLE_ID>` entries to skip the global fakelib for specific games without disabling per-game fakelib.
- `backport_fakelib=0` disables the sandbox `fakelib` watcher, including global fakelib.
- For `backport_fakelib` to work correctly, the standalone `BackPork` payload must be disabled. Running both at the same time will conflict.
Kstuff game lifecycle behavior:
- When `kstuff_game_auto_toggle=1`, ShadowMount watches game `exec/exit` events in the background.
- Image-backed launches use `kstuff_pause_delay_image_seconds`; direct/non-image launches use `kstuff_pause_delay_direct_seconds`.
- `kstuff_crash_detection=0` disables crash monitoring and the automatic pause-delay tuning logic, while leaving normal kstuff auto-pause/auto-resume behavior intact.
- `kstuff_no_pause` skips auto-pause entirely for matching title IDs.
- `kstuff_delay` overrides the pause delay for matching title IDs, regardless of image/direct launch type.
- `/data/shadowmount/autotune.ini` overrides both `config.ini` and `autopause.txt` for matching title IDs.
- `/data/shadowmount/autotune.ini` also overrides `image_sector` rules from `config.ini` for matching image file names.
- A game source folder may optionally contain `autopause.txt`; it is read once at launch time.
- Priority order is: `autotune.ini` -> `kstuff_delay` from `config.ini` -> `autopause.txt` -> global direct/image default delay.
- If `autopause.txt` contains only a number, that value is used for direct launches and doubled for image-backed launches.
- `autopause.txt` may also use:
- `direct=<seconds>`
- `image=<seconds>`
- If both kinds of rule target the same title, `kstuff_no_pause` takes priority.
- When crash monitoring detects an app crash before kstuff was paused, ShadowMountPlus only notifies that the app crashed and kstuff is not to blame.
- When crash monitoring detects an app crash within 2 minutes after kstuff auto-pause, ShadowMountPlus doubles the applied pause delay for that title and upserts it into `/data/shadowmount/autotune.ini` (up to `3600` seconds), then prompts you to launch the game again.
- When the last tracked game stops, ShadowMount immediately enables kstuff again if it was the component that disabled it.
Validation:
- See `config.ini.example` for a ready-to-use template.
@@ -88,9 +159,11 @@ Validation:
Image mountpoints are created under:
`/data/imgmnt/<fs>/<image_name>`
`/mnt/shadowmnt/<image_name>_<hash>`
Where `<fs>` is one of: `ufsmnt`, `exfatmnt`, `pfsmnt`.
PFSC container mountpoints are created under:
`/mnt/shadowmnt/pfsc/<image_name>_<hash>`
Image layout requirement (`.ffpkg`, `.exfat`, `.ffpfs`):
- Game files must be placed at the image root.
@@ -98,6 +171,92 @@ Image layout requirement (`.ffpkg`, `.exfat`, `.ffpfs`):
- Valid example: `/sce_sys/param.json` exists directly from image root.
- Invalid example: `/GAME_FOLDER/sce_sys/param.json` (extra nesting level).
PFSC container layout requirement (`.ffpfsc`):
- Do not place game files directly in the container root.
- Place supported nested image files inside the container; ShadowMountPlus mounts those nested images and scans them for the game.
- A nested `pfs_image.dat` file inside a PFSC container is treated as a PFS image.
- `.ffpfsc` uses the nested outer PFS profile (`img_type=0x02`); `.ffpfs` and `pfs_image.dat` files mounted from inside it use the nested inner profile (`img_type=0x82`). Signature verification and GDDR5 cache setup are kept in code but currently disabled.
## Compressed PFS containers (`.ffpfsc`)
Compressed PFS mode is intended only for nested images. During packing, data is
zero-padded to a `64 KB` sector boundary, so the compressed PFS should be used
as an outer container for another image rather than as a direct game-file
layout.
Recommended layouts:
- exFAT image inside compressed PFS.
- Uncompressed PFS image inside compressed PFS.
MkPFS uses zLib compression. Decompression is hardware-assisted, but throughput
is limited to roughly `150-250 MB/s`. This is about one third of the speed of an
external USB drive, FFPKG/exFAT images, or about one tenth of the internal drive
speed. Keep this in mind when choosing which games to pack. Games that read large
amounts of data or stream textures continuously may stutter.
Use the official [PSBrew/MkPFS](https://github.com/PSBrew/MkPFS) tool to pack
PFS images.
### Packing an uncompressed PFS image into compressed PFS
First create an uncompressed nested PFS image:
```bash
mkpfs pack folder --verify --no-compress --no-adjust-output-file-extension --version PS5 --inode-bits 32 \
'./PPSA07923/PPSA07923-app' \
'./pfs_image.dat'
```
Then pack the nested image (**pfs_image.dat**) into a compressed PFS container:
```bash
mkpfs pack file --verify --version PS5 --inode-bits 32 \
'./pfs_image.dat' \
'./PPSA12345.ffpfsc'
```
After successful packing, the temporary nested image can be removed:
```bash
rm './pfs_image.dat'
```
### Packing an exFAT image into compressed PFS
First create a normal exFAT image using one of the methods from
`Creating an exFAT image`. The nested exFAT image name must keep the `.exfat`
extension, for example `PPSA12345.exfat`.
Linux:
```bash
chmod +x mkexfat.sh
./mkexfat.sh ./PPSA12345-app ./PPSA12345.exfat
```
Windows:
```cmd
make_image.bat "C:\images\PPSA12345.exfat" "C:\payload\PPSA12345-app"
```
The exFAT image must contain the game files at the image root, without an extra
top-level folder.
Then pack the exFAT image into a compressed PFS container:
```bash
mkpfs pack file --verify --version PS5 --inode-bits 32 \
'./PPSA12345.exfat' \
'./PPSA12345.ffpfsc'
```
After successful packing, the temporary exFAT image can be removed:
```bash
rm './PPSA12345.exfat'
```
## Scan paths
Default scan locations:
@@ -112,22 +271,66 @@ Default scan locations:
- `/mnt/usb0` .. `/mnt/usb7`
- `/mnt/ext0`
- `/mnt/ext1`
- `/data/imgmnt/ufsmnt`, `/data/imgmnt/exfatmnt`, `/data/imgmnt/pfsmnt` (mounted image content scan)
- `/mnt/shadowmnt/pfsc` (mounted PFSC container scan)
- `/mnt/shadowmnt` (mounted image content scan)
You can override scan roots with `scanpath=...` entries in `/data/shadowmount/config.ini`.
## Manual install list
For games that should not live under the normal scan paths, ShadowMountPlus also
checks:
`/data/shadowmount/manual.lst`
Add one source per line:
- Path to a game folder, where `sce_sys/param.json` exists inside that folder.
- Path to a supported image file: `.ffpkg`, `.exfat`, `.ffpfs`, or `.ffpfsc`.
- Empty lines and lines starting with `#` are ignored.
Example:
```text
/mnt/usb0/MyGames/PPSA12345
/mnt/usb0/images/PPSA54321.ffpkg
# /mnt/usb0/disabled/PPSA00000
```
`manual.lst` is watched for changes. When you add a new line, ShadowMountPlus
rescans shortly after the write, mounts images when needed, and installs the
game through the same pipeline as normal scan path discoveries.
Manual install state is tracked in:
`/data/shadowmount/manual.status`
This status file is managed by ShadowMountPlus. If a manually installed game is
later removed by the user and disappears from `app.db`, ShadowMountPlus marks it
as deleted in `manual.status` and removes the matching source line from
`manual.lst` so it is not installed again automatically. If you later add the
same source path back to `manual.lst`, it will be processed again and the status
will be updated after installation.
You can use **Dump Installer** to install or prepare game dumps for this manual
workflow, then add the resulting game folder path or image file path to
`manual.lst`.
Recommended folder structure:
- Default mode (`recursive_scan=0`):
- Default mode (`scan_depth=1`):
- `/data/homebrew/<TITLE_ID>/`
- `/data/etaHEN/games/<TITLE_ID>/`
- `/data/homebrew/backports/<TITLE_ID>/`
- `/data/etaHEN/games/backports/<TITLE_ID>/`
- Recursive mode (`recursive_scan=1`):
- Nested mode (`scan_depth=2`):
- `/data/homebrew/PS5/<AnyFolder>/<TITLE_ID>/`
- `/mnt/ext0/etaHEN/games/<Collection>/<TITLE_ID>/`
- `/mnt/ext0/etaHEN/games/backports/<TITLE_ID>/`
## Creating an exFAT image
Recommended only for titles that need external-drive-style compatibility. For general use, prefer `.ffpkg`.
Linux (Ubuntu/Debian):
- Required components installation:
- `sudo apt-get update && sudo apt-get install -y exfatprogs exfat-fuse fuse3 rsync`
@@ -139,6 +342,7 @@ Linux (Ubuntu/Debian):
- Notes:
- Source folder must be the game root and contain `eboot.bin`.
- Auto-calculates image size using rounded file allocation + metadata + safety margin.
- For manual exFAT builds, keep the cluster size at `64 KB` or you may lose performance.
- Automatically selects exFAT cluster profile:
- Large-file profile: `64K`
- Small/mixed-file profile: `32K`
@@ -149,6 +353,7 @@ Windows:
- Install OSFMount: https://www.osforensics.com/tools/mount-disk-images.html.
- Keep `make_image.bat` and `New-OsfExfatImage.ps1` in the same folder.
- Run `cmd.exe` as Administrator.
- If you build an exFAT image manually instead of using the script, keep the cluster size at `64 KB` or you may lose performance.
- Usage:
- `make_image.bat "C:\images\game.exfat" "C:\payload\APPXXXX"`
- Behavior:
@@ -170,14 +375,20 @@ FreeBSD:
- Source folder must be the game root and contain `eboot.bin`.
- The script auto-calculates image size using rounded file allocation + metadata + safety margin.
- Recommended `newfs` parameters for UFS2:
- Large-file profile: `newfs -O 2 -b 65536 -f 4096 -m 0 -i 262144`
- Small/mixed-file profile: `newfs -O 2 -b 32768 -f 4096 -m 0 -i 262144`
- `mkufs2.sh` selects between these two profiles automatically (based on average file size).
- `newfs -O 2 -b 65536 -f 65536 -m 0 -S 4096`
- `mkufs2.sh` keeps this fixed block/fragment/sector profile and auto-tunes `-i` based on source file/directory count.
- Rough manual `-i` estimate for manual builds:
- `target_inodes ~= file_count + dir_count + 2048`
- `bytes_per_inode ~= image_size_bytes / target_inodes`
- Round `bytes_per_inode` down to a multiple of `4096`, then keep it in the practical range `65536..262144`.
- Practical rule of thumb: use `262144` for normal game dumps, `131072` for tens of thousands of files, and `65536` only for very file-dense images.
- Example: for an `8 GiB` image with `60000` files and `4000` directories, `-i ~= 8*1024^3 / (60000 + 4000 + 2048) ~= 130312`, so use `-i 131072`.
Windows:
- You can create UFS2 images with **UFS2Tool** https://github.com/SvenGDK/UFS2Tool.
- Example:
- `UFS2Tool.exe newfs -O 2 -b 32768 -f 4096 -m 0 -i 262144 -D ./APPXXXX ./PPSA12345.ffpkg`
- `UFS2Tool.exe newfs -O 2 -b 65536 -f 65536 -m 0 -S 4096 -i 262144 -D ./APPXXXX ./PPSA12345.ffpkg`
- For manual builds, use `-i 262144` as the baseline and lower it for images with many small files.
## Installation and usage
@@ -186,23 +397,16 @@ Windows:
### Method 1: Manual Payload Injection (Port 9021)
Use a payload sender (such as NetCat GUI or a web-based loader) to send the files to **Port 9021**.
1. Send `notify.elf` (Optional).
* *Only send this if you want graphical pop-ups. Skip if you prefer standard notifications.*
2. Send `shadowmountplus.elf`.
3. Wait for the notification: *"ShadowMount+"*.
1. Send `shadowmountplus.elf`.
2. Wait for the notification: *"ShadowMount+"*.
### Method 2: PLK Autoloader (Recommended)
Add ShadowMountPlus to your `autoload.txt` for **plk-autoloader** to ensure it starts automatically on every boot.
**Sample Configuration:**
```ini
!1000
notify.elf ; Optional - Remove this line if you do not want Rich Toasts
!1000
shadowmountplus.elf
!3000
ps5-backpork.elf
!1000
kstuff.elf
```
@@ -215,16 +419,18 @@ If a game is not mounted:
- Check `/data/shadowmount/debug.log` and system notifications from ShadowMount+.
- Verify scan roots:
- if `scanpath=...` is set, only these paths are scanned;
- `/data/imgmnt/{ufsmnt,exfatmnt,pfsmnt}` are always scanned.
- `/mnt/shadowmnt/pfsc` and `/mnt/shadowmnt` are always scanned.
- Verify scan depth:
- `recursive_scan=0` scans only first-level subfolders;
- `recursive_scan=1` scans recursively.
- `scan_depth=1` scans only first-level subfolders;
- `scan_depth=2` scans one additional nested level;
- `recursive_scan=1` is treated as deprecated compatibility mode and forces `scan_depth=2`.
- If logs show `source not stable yet`, adjust `stability_wait_seconds` (or wait for source copy/write to finish).
- Verify game structure:
- folder game: `<GAME_DIR>/sce_sys/param.json`;
- image game (`.ffpkg` / `.exfat` / `.ffpfs`): `sce_sys/param.json` must be at image root (no extra top-level folder).
- If you see `missing/invalid param.json` for an image, check via FTP that files are present under `/data/imgmnt/<fs>/<image_name>/` and include `sce_sys/param.json`.
- If you see image mount failure, check image integrity and filesystem type (`.ffpkg`=UFS, `.exfat`=exFAT, `.ffpfs`=PFS).
- image game (`.ffpkg` / `.exfat` / `.ffpfs`): `sce_sys/param.json` must be at image root (no extra top-level folder);
- PFSC container (`.ffpfsc`): nested supported image files are scanned; direct game files inside the container are ignored.
- If you see `missing/invalid param.json` for an image, check via FTP that files are present under `/mnt/shadowmnt/<image_name>_<hash>/` and include `sce_sys/param.json`.
- If you see image mount failure, check image integrity and filesystem type (`.ffpkg`=UFS, `.exfat`=exFAT, `.ffpfs`=PFS, `.ffpfsc`=PFS container).
- If you see duplicate titleId notification, keep only one source per `<TITLE_ID>`.
If a game is mounted but does not start:
@@ -240,10 +446,12 @@ If a game is mounted but does not start:
* **Drakmor** - Evolution of ShadowMount to ShadowMountPlus
* **Special Thanks:**
* VoidWhisper for ShadowMount [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/voidwhisper)
* EchoStretch
* VoidWhisper for ShadowMount
* BestPig for BackPort
* EchoStretch for kstuff-toggle and etc
* Gezine
* earthonion
* LightningMods
* john-tornblom
* RenanGBarreto for his excellent https://github.com/PSBrew/MkPFS
* john-tornblom for SDK
* PS5 R&D Community
+3583
View File
File diff suppressed because it is too large. Load diff
+112 -27
View File
@@ -1,53 +1,117 @@
# ShadowMount runtime config example
# Place as: /data/shadowmount/config.ini
#
# All keys are optional.
# If a key is missing, built-in defaults are used.
# This file is a template.
# Nothing below is active until you remove the leading '#'.
#
# How to use this file:
# 1. Uncomment only the parameters you really want to override.
# 2. Lines that start with '# key=value' are real parameters you can uncomment directly.
# 3. Lines that start with '##' are examples/templates only. Edit them before use.
# Debug logging:
# 1/true/yes/on -> enable log_debug output to console and /data/shadowmount/debug.log
# 0/false/no/off -> disable log_debug output
debug=1
# Default: 1
# debug=1
# Quiet mode:
# 1/true/yes/on -> only show the startup version popup and runtime errors
# 0/false/no/off -> also show normal informational popups
# Default: 0
# quiet_mode=0
# Global mount mode:
# 1/true/yes/on/ro -> read-only mounts
# 0/false/no/off/rw -> read-write mounts
mount_read_only=1
# Default: 1
# mount_read_only=1
# Mounting even damaged file systems
# Mounting even damaged file systems:
# 1/true/yes/on -> include force option
# 0/false/no/off -> default mount options (default)
force_mount=0
# 0/false/no/off -> default mount options
# Default: 0
# force_mount=0
# Per-image mount mode override by file name (can be repeated many times):
# image_ro=<image_filename>
# image_rw=<image_filename>
# image_ro=PPSA12345.ffpfs
# image_rw=My Game Dump.exfat
# Use batch registration for newly staged titles:
# 1/true/yes/on -> submit queued installs through sceAppInstUtilAppInstallAll
# 0/false/no/off -> register each title directly
# Default: 0 on FW below 12.00, forced to 1 on FW 12.00 and newer
# app_install_all=0
# Recursive scan mode:
# 1/true/yes/on -> scan subdirectories recursively (no depth limit)
# 0/false/no/off -> scan only first-level subdirectories (default)
recursive_scan=0
# Scan depth relative to each scan root:
# 1 -> scan only first-level subdirectories
# 2 -> also scan one additional nested level
# Default: 1
# scan_depth=1
# Optional backport overlay root:
# If "<backports_path>/<TITLE_ID>" exists, it is mounted over image root via unionfs.
backports_path=/data/backports
# Legacy compatibility:
# recursive_scan=1 forces scan_depth=2
# Default: disabled
# recursive_scan=0
# Full scan loop interval (seconds), range: 1..3600
scan_interval_seconds=10
# Default: 15
# scan_interval_seconds=15
# Source stabilization wait window (seconds), range: 0..3600
# If source mtime is newer than this window, it will be deferred to next scan.
stability_wait_seconds=10
# If source mtime/ctime is newer than this window, it will be deferred to next scan.
# Default: 10
# stability_wait_seconds=10
# Backend selection per filesystem:
# lvd -> /dev/lvdctl -> /dev/lvdN
# md -> /dev/mdctl -> /dev/mdN
exfat_backend=lvd
ufs_backend=lvd
# Defaults: exfat=lvd, ufs=lvd
# exfat_backend=lvd
# ufs_backend=lvd
# Backend-specific overrides (optional):
# Sandbox fakelib backport watcher:
# 1/true/yes/on -> mount app0/fakelib2 when present, otherwise app0/fakelib, into common/lib
# 0/false/no/off -> disable fakelib watcher
# Default: 1
# backport_fakelib=1
# Global fakelib overlay:
# 1/true/yes/on -> also mount global fakelib for every tracked game when present
# 0/false/no/off -> do not use the global fakelib folder
# Default: 1
# global_fakelib=1
#
# Default global fakelib folder:
# global_fakelib_path=/data/shadowmount/fakelib
#
# Priority when both global and per-game fakelib exist:
# game -> mount global first, then game-specific fakelib (game wins)
# global -> mount game-specific fakelib first, then global fakelib (global wins)
# Default: game
# global_fakelib_priority=game
#
## Exclude specific title IDs from the global fakelib overlay (repeatable):
## global_fakelib_exclude=PPSA12345
# Auto-manage kstuff around game launches:
# 1/true/yes/on -> pause kstuff after game launch, resume immediately on stop
# 0/false/no/off -> leave kstuff state untouched
# Default: 1
# kstuff_game_auto_toggle=1
# Detect crash-candidate state and autotune pause delay:
# 1/true/yes/on -> enable crash monitoring and autotune updates
# 0/false/no/off -> disable crash detection/autotune logic
# Default: 1
# kstuff_crash_detection=1
# Delay before pausing kstuff for image-backed titles.
# Default: 25
# kstuff_pause_delay_image_seconds=25
# Delay before pausing kstuff for direct/non-image launches.
# Default: 15
# kstuff_pause_delay_direct_seconds=15
# Backend-specific sector size overrides:
# Defaults: lvd_exfat=512, lvd_ufs=4096, lvd_pfs=32768, md_exfat=512, md_ufs=512
# lvd_exfat_sector_size=512
# lvd_ufs_sector_size=4096
# lvd_pfs_sector_size=32768
@@ -56,6 +120,27 @@ ufs_backend=lvd
# Optional custom scan roots (can be repeated):
# If at least one scanpath=... is present, only these paths are used.
# "/data/imgmnt/{ufsmnt,exfatmnt,pfsmnt}" are always added automatically.
# "/mnt/shadowmnt/pfsc" and "/mnt/shadowmnt" are always added automatically.
# Default: use built-in scan path list
# scanpath=/data/homebrew
# scanpath=/data/etaHEN/games
## Per-image mount mode override by file name (repeatable):
## image_ro=<image_filename>
## image_rw=<image_filename>
## image_sector=<image_filename>:<sector_size>
##
## Examples:
## image_ro=PPSA12345.ffpfs
## image_rw=My Game Dump.exfat
## image_sector=My Game Dump.exfat:65536
## Keep kstuff enabled for specific title IDs (repeatable):
## kstuff_no_pause=PPSA12345
## Override pause delay for specific title IDs (repeatable):
## Format: TITLEID:SECONDS
## kstuff_delay=PPSA12345:30
## More custom scan root examples:
## scanpath=/data/homebrew
## scanpath=/data/etaHEN/games
+28
View File
@@ -0,0 +1,28 @@
#ifndef SM_APPDB_H
#define SM_APPDB_H
#include <stdbool.h>
struct AppDbTitleList;
// Close the cached app.db handle and release related resources.
void shutdown_app_db(void);
// Normalize existing snd0info rows during process startup.
bool app_db_run_startup_maintenance(void);
// Update snd0 metadata for a registered title.
int update_snd0info(const char *title_id);
// Normalize an existing snd0Info path for one title.
int normalize_snd0info_for_title(const char *title_id);
// Check whether a title ID exists in a cached app.db title list.
bool app_db_title_list_contains(const struct AppDbTitleList *list,
const char *title_id);
// Release a title-list snapshot returned by get_app_db_title_list_cached().
void free_app_db_title_list(struct AppDbTitleList *list);
// Drop the cached app.db title list so it is reloaded on next access.
void invalidate_app_db_title_cache(void);
// Copy the cached app.db title list into caller-owned storage, loading it if needed.
bool get_app_db_title_list_cached(struct AppDbTitleList *list_out);
// Copy the cached PPSA titles that app.db marks as not uninstallable.
bool get_app_db_blocked_uninstall_ppsa_list(struct AppDbTitleList *list_out);
#endif
+47
View File
@@ -0,0 +1,47 @@
#ifndef SM_CONFIG_MOUNT_H
#define SM_CONFIG_MOUNT_H
#include <stdbool.h>
#include <stdint.h>
typedef struct runtime_config runtime_config_t;
// Ensure runtime configuration is loaded before use.
void ensure_runtime_config_ready(void);
// Load runtime configuration from disk and apply defaults.
bool load_runtime_config(void);
// Reload runtime configuration from disk when config.ini changed.
bool reload_runtime_config_if_changed(bool *reloaded_out);
// Return the current runtime configuration.
const runtime_config_t *runtime_config(void);
// Return the number of configured scan roots.
int get_scan_path_count(void);
// Return a scan root by index, or NULL if out of range.
const char *get_scan_path(int index);
// Return scan depth for a root, including managed container-root expansion.
uint32_t get_scan_depth_for_root(const char *scan_path);
// Resolve a per-image read-only override from the file name.
bool get_image_mode_override(const char *filename, bool *mount_read_only_out);
// Resolve a per-image sector-size override from autotune.ini or config.ini.
bool get_image_sector_size_override(const char *filename,
uint32_t *sector_size_out);
// Return true when kstuff auto-pause is disabled for the given title ID.
bool is_kstuff_pause_disabled_for_title(const char *title_id);
// Resolve a per-title kstuff pause-delay override in seconds.
bool get_kstuff_pause_delay_override_for_title(const char *title_id,
uint32_t *delay_seconds_out);
// Resolve a per-title autotuned kstuff pause-delay override in seconds.
bool get_kstuff_autotune_pause_delay_for_title(const char *title_id,
uint32_t *delay_seconds_out);
// Upsert an autotuned pause-delay override for the title.
bool upsert_kstuff_autotune_pause_delay(const char *title_id,
uint32_t current_delay_seconds,
uint32_t *delay_seconds_out);
// Upsert an autotuned per-image sector-size override.
bool upsert_image_sector_size_autotune(const char *filename,
uint32_t sector_size,
uint32_t *sector_size_out);
// Return true when the global fakelib overlay is disabled for this title.
bool is_global_fakelib_excluded_for_title(const char *title_id);
#endif
+17
View File
@@ -0,0 +1,17 @@
#ifndef SM_FAKELIB_H
#define SM_FAKELIB_H
#include <stdbool.h>
#include <stdint.h>
#include <sys/types.h>
// Return true when per-game fakelib backport automation is enabled.
bool sm_fakelib_game_feature_enabled(void);
// Track a supported game launch and mount its fakelib overlay immediately.
void sm_fakelib_game_on_exec(pid_t pid, const char *title_id);
// Remove a tracked fakelib overlay for a stopped game.
void sm_fakelib_game_on_exit(pid_t pid);
// Unmount all tracked overlays during watcher shutdown.
void sm_fakelib_game_shutdown(void);
#endif
+70
View File
@@ -0,0 +1,70 @@
#ifndef SM_FILESYSTEM_H
#define SM_FILESYSTEM_H
#include <dirent.h>
#include <stdbool.h>
#include <stddef.h>
#include "sm_limits.h"
// Check whether a title is present in the installed app set.
bool is_installed(const char *title_id);
// Check whether /user/appmeta/<TITLE_ID>/param.json exists.
bool has_appmeta_data(const char *title_id);
// Check whether a title currently has mounted data.
bool is_data_mounted(const char *title_id);
// Read the mount.lnk source path for a title tracked under /user/app.
bool read_mount_link(const char *title_id, char *out, size_t out_size);
// Atomically persist the mount.lnk source path for a title.
bool write_mount_link(const char *title_id, const char *source_path);
// Read the optional backing image path retained for an image-backed title.
bool read_mount_image_link(const char *title_id, char *out, size_t out_size);
// Persist the outer backing-image path for an already staged title.
bool write_mount_image_link(const char *title_id, const char *image_path);
bool read_mount_image_chain(
const char *title_id,
char paths[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH], size_t *count_out);
bool write_mount_image_chain(
const char *title_id,
const char paths[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH], size_t count);
// Validate a /user/app/<TITLE_ID> tracker entry and optionally build its path.
bool resolve_title_app_dir(const struct dirent *entry, char *app_dir,
size_t app_dir_size);
// Recover staged mount links and warm image source mappings on startup.
void cleanup_staged_mount_links(void);
// Remove duplicated managed mount layers left from previous runs.
void cleanup_duplicate_title_mounts(void);
// Remount /system_ex with the expected flags.
int remount_system_ex(void);
// Mount a title source into /system_ex/app/<title_id> via nullfs.
bool mount_title_nullfs(const char *title_id, const char *src_path);
// Roll back a just-mounted title nullfs layer when publication is aborted.
bool rollback_title_nullfs_mount(const char *title_id, const char *src_path);
// Drop runtime layers for a title while preserving its metadata/link files.
bool unmount_title_runtime_layers(const char *title_id);
// Release every managed title layer while preserving link metadata.
void unmount_all_title_runtime_layers(void);
// Reconcile the title mount stack against the expected source/backport state.
bool reconcile_title_backport_mount(const char *title_id, const char *src_path,
const char *expected_backport_path,
bool *overlay_active_out);
// Mount a prepared backport overlay on top of an already mounted title.
// Returns false only when nmount() itself fails.
bool mount_backport_overlay(const char *mount_point,
const char *backport_path,
const char *title_id);
// Unmount all managed /system_ex/app/<title_id> mount stacks on shutdown.
void shutdown_title_mounts(void);
// Remove stale mount links and optionally restore image-backed mounts.
void cleanup_mount_links(const char *removed_source_root,
bool unmount_system_ex_bind);
// Unmount and remove title links that point under a source root.
void cleanup_mount_links_for_source_unmount(const char *source_root);
// Unmount and remove title links backed by USB sources or USB-backed images.
void cleanup_usb_mount_links_for_suspend(void);
// Recursively copy a directory tree.
int copy_dir(const char *src, const char *dst);
// Copy a single file.
int copy_file(const char *src, const char *dst);
#endif
+26
View File
@@ -0,0 +1,26 @@
#ifndef SM_GAME_CACHE_H
#define SM_GAME_CACHE_H
#include <stdbool.h>
typedef bool (*game_cache_iter_fn)(const char *path, const char *title_id,
const char *title_name,
const char *owning_scan_root, void *ctx);
// Cache resolved metadata for a mounted or discovered game.
void cache_game_entry(const char *path, const char *title_id,
const char *title_name);
// Drop invalid or stale entries from the game cache.
void prune_game_cache(void);
// Drop invalid or stale entries that belong to a specific scan root.
void prune_game_cache_for_root(const char *root);
// Look up a cached game entry by path or title ID.
bool find_cached_game(const char *path, const char *title_id,
const char **existing_path_out);
// Visit cached games, optionally limited to one source root.
void for_each_cached_game_entry(const char *root, game_cache_iter_fn fn,
void *ctx);
// Remove a game cache entry by path.
void clear_cached_game(const char *path);
#endif
+17
View File
@@ -0,0 +1,17 @@
#ifndef SM_GAME_LIFECYCLE_H
#define SM_GAME_LIFECYCLE_H
#include <stdbool.h>
// Start the shared game lifecycle watcher used by runtime game features.
bool start_game_lifecycle_watcher(void);
// Wake the shared watcher so pending lifecycle work is processed immediately.
void wake_game_lifecycle_watcher(void);
// Stop the shared watcher and let lifecycle modules clean up tracked state.
void stop_game_lifecycle_watcher(void);
// Return true when a supported game is currently being tracked as running.
bool sm_game_lifecycle_has_active_game(void);
// Ensure the shared watcher is running for runtime game-state tracking.
bool refresh_game_lifecycle_watcher(void);
#endif
+13
View File
@@ -0,0 +1,13 @@
#ifndef SM_GAMEINFO_H
#define SM_GAMEINFO_H
#include <stdbool.h>
#include <sys/stat.h>
// Read title ID and title name from a mounted game directory.
bool get_game_info(const char *base_path, const struct stat *param_st,
char *out_id, char *out_name);
// Check whether a directory contains sce_sys/param.json and stat it.
bool directory_has_param_json(const char *dir_path, struct stat *param_st_out);
#endif
+16
View File
@@ -0,0 +1,16 @@
#ifndef SM_HASH_H
#define SM_HASH_H
#include <stdint.h>
// Return a stable 32-bit FNV-1a hash for a NUL-terminated string.
static inline uint32_t sm_fnv1a32(const char *s) {
uint32_t h = 2166136261u;
while (s && *s) {
h ^= (uint8_t)(*s++);
h *= 16777619u;
}
return h;
}
#endif
+42
View File
@@ -0,0 +1,42 @@
#ifndef SM_IMAGE_H
#define SM_IMAGE_H
#include <stdbool.h>
#include "sm_types.h"
// Log filesystem statistics for a mounted path.
void log_fs_stats(const char *tag, const char *path, const char *type_hint);
// Attach and mount an image file to its runtime mount point.
bool mount_image(const char *file_path, image_fs_type_t fs_type);
// Unmount an image mount point and detach its backing device.
bool unmount_image(const char *file_path, int unit_id, attach_backend_t backend);
// Unmount a runtime image without deleting persistent title metadata links.
bool unmount_runtime_image(const char *file_path, int unit_id,
attach_backend_t backend);
// Release one cached runtime image mount. An absent live mount is harmless.
bool release_runtime_image_mount(const char *file_path);
// Release all discovery/runtime image mounts without deleting title metadata.
bool release_runtime_image_mounts(void);
// Reconcile cached image mounts with current sources and remount if needed.
void cleanup_stale_image_mounts(void);
// Reconcile cached image mounts that belong to a specific scan root.
void cleanup_stale_image_mounts_for_root(const char *root);
// Unmount cached image mounts backed by USB storage during suspend.
bool unmount_usb_image_mounts_for_suspend(void);
// Unmount every cached image mount during shutdown.
bool shutdown_image_mounts(void);
// Remove empty directories left under the image mount root.
void cleanup_mount_dirs(void);
// Mount an image file if it is stable and not currently rate-limited.
bool maybe_mount_image_file(const char *full_path, const char *name,
bool *unstable_out);
// Build the deterministic runtime mount point for an image source path.
void get_image_mount_point_for_source(const char *file_path,
char mount_point[MAX_PATH]);
// Return true when the filename has a supported image extension.
bool is_supported_image_file_name(const char *name);
// Return true when the source path is a supported image, including nested names.
bool is_supported_image_file_path(const char *full_path, const char *name);
#endif
+48
View File
@@ -0,0 +1,48 @@
#ifndef SM_IMAGE_CACHE_H
#define SM_IMAGE_CACHE_H
#include <stdbool.h>
#include "sm_types.h"
typedef struct {
char path[MAX_PATH];
char mount_point[MAX_PATH];
int unit_id;
attach_backend_t backend;
} image_cache_entry_t;
// Cache a successful image mount and its attached device.
// Returns false when no free cache slot is available.
bool cache_image_mount(const char *path, const char *mount_point,
int unit_id, attach_backend_t backend);
// Cache an image source mapping without attach metadata.
bool cache_image_source_mapping(const char *path, const char *mount_point);
// Return a cached image mount entry by index.
bool get_image_cache_entry(int index, image_cache_entry_t *entry_out);
// Find a cached source path with one cache lock acquisition.
bool find_image_cache_entry(const char *path, image_cache_entry_t *entry_out,
int *index_out);
// Mark a cached image mount entry as invalid.
void invalidate_image_cache_entry(int index);
// Resolve a device mapping from the in-memory mount cache.
bool resolve_device_from_mount_cache(const char *mount_point,
attach_backend_t *backend_out,
int *unit_out);
// Resolve the original image file path for a cached runtime mount point.
bool resolve_image_source_from_mount_cache(const char *mount_point,
char *path_out,
size_t path_out_size);
// Resolve the image whose mount point owns a mounted child path.
bool resolve_owning_image_source_from_mount_cache(const char *path,
char *path_out,
size_t path_out_size);
// Resolve every owning image from the outermost source to the innermost one.
size_t resolve_image_source_chain_from_mount_cache(
const char *path,
char chain[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH]);
// Resolve only the outermost owning image source.
bool resolve_outermost_image_source_from_mount_cache(
const char *path, char *path_out, size_t path_out_size);
#endif
+25
View File
@@ -0,0 +1,25 @@
#ifndef SM_IMAGE_INDEX_H
#define SM_IMAGE_INDEX_H
#include <stdbool.h>
#include <sys/stat.h>
struct AppDbTitleList;
// Return true when an image must be opened to discover/install its contents.
bool sm_image_index_needs_scan(const char *path, const struct stat *st,
const struct AppDbTitleList *app_db_titles,
bool app_db_titles_ready);
// Start a new fingerprint generation before mounting an image.
void sm_image_index_begin_scan(const char *path, const struct stat *st);
// Associate a discovered mounted game with its outer backing image. Returns
// false when an image-backed title could not be retained in the index.
bool sm_image_index_record_game(const char *game_path, const char *title_id);
// Commit one fingerprint after its mounted content was scanned.
void sm_image_index_complete_scan(const char *path);
// Atomically persist accumulated index changes at the scan-cycle boundary.
void sm_image_index_flush(void);
// Drop persistent entries whose backing image disappeared.
void sm_image_index_prune(void);
#endif
+10
View File
@@ -0,0 +1,10 @@
#ifndef SM_INSTALL_H
#define SM_INSTALL_H
typedef struct scan_candidate scan_candidate_t;
// Install or remount the collected scan candidates.
void process_scan_candidates(const scan_candidate_t *candidates,
int candidate_count);
#endif
+30
View File
@@ -0,0 +1,30 @@
#ifndef SM_INSTALL_QUEUE_H
#define SM_INSTALL_QUEUE_H
#include <stdbool.h>
#include <stdint.h>
typedef struct scan_candidate scan_candidate_t;
// Return whether a title is already waiting for async batch installation.
bool is_title_install_pending(const char *title_id);
// Drop queued/submitted async install state for a title after confirmed removal.
void sm_install_forget_pending_title(const char *title_id);
// Return the next pending-install wake deadline in monotonic microseconds.
uint64_t sm_install_next_wake_us(uint64_t now_us);
// Poll async batch installs for success/timeout completion only.
void sm_install_poll_pending(void);
// Poll async batch installs for success/timeout completion and auto-submit
// queued installs when possible.
void sm_install_service_pending(void);
// Queue a staged title for the next batch install request.
bool sm_install_queue_candidate(const scan_candidate_t *candidate,
bool has_src_snd0);
// Submit all queued titles when no batch install is currently pending.
bool sm_install_submit_queued(void);
// Record retry state for every queued title after a batch submit failure.
void sm_install_note_submit_failure(void);
// Return true while an install still needs its staged mounts.
bool sm_install_has_pending_work(void);
#endif
+41
View File
@@ -0,0 +1,41 @@
#ifndef SM_KSTUFF_H
#define SM_KSTUFF_H
#include <stdbool.h>
#include <stdint.h>
#include <sys/types.h>
// Resolve firmware-specific kstuff sysentvec addresses and initialize state.
void sm_kstuff_init(void);
// Shut down runtime kstuff control state.
void sm_kstuff_shutdown(void);
// Return true when firmware-specific kstuff control is available.
bool sm_kstuff_is_supported(void);
// Return true when both tracked kstuff sysentvecs are enabled.
bool sm_kstuff_is_enabled(void);
// Set both tracked kstuff sysentvecs and return the resulting enabled state.
bool sm_kstuff_set_enabled(bool enabled, bool notify_user);
// Return true when game-driven kstuff automation should run.
bool sm_kstuff_game_feature_enabled(void);
// Track a supported game launch for delayed auto-pause handling.
void sm_kstuff_game_on_exec(pid_t pid, const char *title_id, uint32_t app_id,
uint64_t exec_time_us);
// Publish an AppFocus change for processing on the lifecycle/kstuff thread.
void sm_kstuff_note_app_focus(uint32_t app_id);
// Return the next wake deadline in monotonic microseconds, or 0 when idle.
uint64_t sm_kstuff_game_next_wake_us(uint64_t now_us);
// Forget a tracked game and restore kstuff when no paused entries remain.
void sm_kstuff_game_on_exit(pid_t pid);
// Apply any delayed kstuff pause whose deadline has elapsed.
void sm_kstuff_game_poll(void);
// Clear tracked game state and restore kstuff if the watcher paused it.
void sm_kstuff_game_shutdown(void);
// Pause kstuff for runtime sleep without restoring game-driven pauses.
void sm_kstuff_sleep_enter(void);
// Restore kstuff after runtime sleep when sleep entry disabled it.
void sm_kstuff_sleep_leave(void);
// Re-apply runtime config changes to the currently tracked game, if any.
void sm_kstuff_on_config_reload(void);
#endif
+53
View File
@@ -0,0 +1,53 @@
#ifndef SM_LIMITS_H
#define SM_LIMITS_H
#define DEFAULT_SCAN_INTERVAL_US 15000000u
#define DEFAULT_SCAN_DEPTH 1u
#define DEFAULT_STABILITY_WAIT_SECONDS 10u
#define DEFAULT_KSTUFF_PAUSE_DELAY_IMAGE_SECONDS 25u
#define DEFAULT_KSTUFF_PAUSE_DELAY_DIRECT_SECONDS 15u
#define MAX_PENDING 512
#define MAX_IMAGE_MOUNTS 2048
#define MAX_IMAGE_TITLES 8192
#define MAX_IMAGE_MODE_RULES 128
#define MAX_KSTUFF_TITLE_RULES 128
#define MAX_FAKELIB_EXCLUDE_RULES 128
#define PATH_STATE_CAPACITY MAX_PENDING
#define TITLE_STATE_CAPACITY MAX_PENDING
#define STATE_HASH_SIZE 1024u
#define MAX_SCAN_PATHS 256
#define MAX_FAILED_MOUNT_ATTEMPTS 2
#define MAX_REGISTER_ATTEMPTS 2
#define MAX_MISSING_PARAM_SCAN_ATTEMPTS 3
#define MAX_IMAGE_MOUNT_ATTEMPTS 3
#define MAX_LAYERED_UNMOUNT_ATTEMPTS 4
#define MAX_IMAGE_CHAIN_DEPTH 8
#define LVD_RELEASE_WAIT_MAX_US 15000000u
#define LVD_RELEASE_WAIT_POLL_US 500000u
#define GAME_LIFECYCLE_POLL_INTERVAL_US 250000u
#define GAME_APPINFO_LOOKUP_TIMEOUT_US 1000000u
#define GAME_SLEEP_EXIT_TIMEOUT_US 5000000u
#define KSTUFF_RESTORE_RETRY_US 1000000u
#define SHELLCORE_FLAG_POLL_INTERVAL_US 100000u
#define MIN_SCAN_DEPTH 1u
#define MAX_SCAN_DEPTH 2u
#define MIN_SCAN_INTERVAL_SECONDS 1u
#define MAX_SCAN_INTERVAL_SECONDS 3600u
#define MAX_STABILITY_WAIT_SECONDS 3600u
#define MAX_KSTUFF_PAUSE_DELAY_SECONDS 3600u
#define APP_DB_QUERY_BUSY_RETRIES 3
#define APP_DB_UPDATE_BUSY_RETRIES 25
#define APP_DB_PREPARE_BUSY_RETRIES 25
#define APP_DB_BUSY_RETRY_SLEEP_US 200000u
#define APP_DB_BUSY_TIMEOUT_MS 5000
#define MAX_PATH 1024
#define MAX_TITLE_ID 32
#define MAX_TITLE_NAME 256
#define MAX_PARAM_JSON_SIZE (1024u * 1024u)
#endif
+36
View File
@@ -0,0 +1,36 @@
#ifndef SM_LOG_H
#define SM_LOG_H
#include <stdbool.h>
typedef struct sm_error sm_error_t;
// Write a formatted line to the debug log.
void log_debug(const char *fmt, ...);
// Prepare notification assets such as the packaged icon file.
void sm_notifications_init(void);
// Flush and close persistent log resources.
void sm_log_shutdown(void);
// Send a rich toast notification with packaged icon and version header.
void notify_system_rich(bool allow_in_quiet_mode, const char *fmt, ...);
// Send the "game installed" rich toast for the given title ID.
void notify_game_installed_rich(const char *title_id);
// Send an informational system notification (suppressed in quiet mode).
void notify_system_info(const char *fmt, ...);
// Send a system notification with formatted text.
void notify_system(const char *fmt, ...);
// Clear the last recorded subsystem error.
void sm_error_clear(void);
// Store the last subsystem error with formatted details.
void sm_error_set(const char *subsystem, int code, const char *path,
const char *fmt, ...);
// Return the last recorded subsystem error.
const sm_error_t *sm_last_error(void);
// Return whether the current error was already notified.
bool sm_error_notified(void);
// Mark the current error as already shown to the user.
void sm_error_mark_notified(void);
// Notify the user about an image mount failure once per error state.
void notify_image_mount_failed(const char *path, int mount_err);
#endif
+20
View File
@@ -0,0 +1,20 @@
#ifndef SM_MANUAL_H
#define SM_MANUAL_H
#include <stdbool.h>
struct AppDbTitleList;
// Visit each manual source path from manual.lst.
bool sm_manual_for_each_path(bool (*visit)(const char *path, void *ctx),
void *ctx);
// Mark previously installed manual titles as deleted when they disappear from
// app.db, and remove their source lines from manual.lst.
bool sm_manual_reconcile_deleted_titles(const struct AppDbTitleList *app_db_titles,
bool app_db_titles_ready);
// Record that a manual source is installed and associated with a title.
void sm_manual_note_installed(const char *manual_source_path,
const char *title_id,
const char *title_name);
#endif
+26
View File
@@ -0,0 +1,26 @@
#ifndef SM_MDBG_H
#define SM_MDBG_H
#include <stdbool.h>
#include <stdint.h>
#include <sys/types.h>
// Initialize mdbg-backed crash monitoring state.
void sm_mdbg_init(void);
// Shut down mdbg-backed crash monitoring state.
void sm_mdbg_shutdown(void);
// Start monitoring the tracked game process for crash-candidate state.
void sm_mdbg_game_on_exec(pid_t pid, const char *title_id, uint32_t app_id);
// Record that kstuff auto-pause was actually applied to the tracked game.
void sm_mdbg_game_on_kstuff_pause(pid_t pid, uint64_t pause_time_us,
uint32_t pause_delay_seconds);
// Stop monitoring the tracked game process on exit.
void sm_mdbg_game_on_exit(pid_t pid);
// Clear tracked game monitoring state unconditionally.
void sm_mdbg_game_shutdown(void);
// Return the next wake deadline in monotonic microseconds, or 0 when idle.
uint64_t sm_mdbg_next_wake_us(uint64_t now_us);
// Poll the tracked process for mdbg crash-candidate state.
void sm_mdbg_poll(void);
#endif
+83
View File
@@ -0,0 +1,83 @@
#ifndef SM_MOUNT_DEFS_H
#define SM_MOUNT_DEFS_H
#define IMAGE_MOUNT_READ_ONLY 1
// 1 = use legacy /dev/mdctl backend for .exfat images, 0 = use LVD for all.
#define EXFAT_ATTACH_USE_MDCTL 0
// 1 = allow mounting .ffpkg images via /dev/mdctl, 0 = keep UFS on LVD.
#define UFS_ATTACH_USE_MDCTL 0
// --- LVD Definitions ---
// Kernel exposes base attach (V0), extended attach (V1/Attach2) and detach.
// This code only builds V0 layer descriptors, so it uses the base attach path.
// Wrapper-side raw attach presets:
// - 0x8/0x9 -> single-image/save-data family (normalized 0x14/0x1C)
// - 0xC/0xD -> download-data/LWFS family (normalized 0x16/0x1E)
// raw bit3 (0x8) sets the DONT_MAP_BM-style mode, raw bit2 (0x4) switches to
// the DD/LWFS family, and raw bit0 (0x1) selects the alternate preset inside a
// family. Firmware pairs bit0 with read-only mount variants, but this is not a
// standalone LVD readonly bit by itself.
// image_type values accepted by validator: 0..0xC (13 values total).
// layer source_type observed: 1=file, 2=device/special source (/dev/sbram0, char/block).
// layer descriptor flag bit0 is "no bitmap file specified".
#define LVD_CTRL_PATH "/dev/lvdctl"
#define MD_CTRL_PATH "/dev/mdctl"
#define SCE_LVD_IOC_ATTACH_V0 0xC0286D00
#define SCE_LVD_IOC_ATTACH_V1 0xC0286D09
#define SCE_LVD_IOC_DETACH 0xC0286D01
#define LVD_ATTACH_IO_VERSION_V0 0u
#define LVD_ATTACH_IO_VERSION_V1 1u
#define LVD_ATTACH_RAW_FLAGS_SINGLE_RO 0x9
#define LVD_ATTACH_RAW_FLAGS_SINGLE_RW 0x8
#define LVD_ATTACH_RAW_FLAGS_DD_RO 0xD
#define LVD_ATTACH_RAW_FLAGS_DD_RW 0xC
#
#define LVD_SECTOR_SIZE_EXFAT 512u
#define LVD_SECTOR_SIZE_UFS 4096u
#define LVD_SECTOR_SIZE_PFS 4096u
#define LVD_SECONDARY_UNIT_SINGLE_IMAGE 0x10000u
#define MD_SECTOR_SIZE_EXFAT 512u
#define MD_SECTOR_SIZE_UFS 512u
// Raw option bits are normalized by sceFsLvdAttachCommon before validation:
// raw:0x1->norm:0x08, raw:0x2->norm:0x80, raw:0x4->norm:0x02, raw:0x8->norm:0x10.
// The normalized masks are then checked against validator constraints (0x82/0x92).
#define LVD_ATTACH_IMAGE_TYPE_SINGLE 0
#define LVD_ATTACH_IMAGE_TYPE_UFS_DOWNLOAD_DATA 7
#define LVD_ATTACH_IMAGE_TYPE_PFS_SAVE_DATA 5
#define PFS_NESTED_OUTER_IMG_TYPE 0x02u
#define PFS_NESTED_INNER_IMG_TYPE 0x82u
#define LVD_ATTACH_LAYER_COUNT 1
#define LVD_ATTACH_LAYER_ARRAY_SIZE 3
#define LVD_ENTRY_TYPE_FILE 1
#define LVD_ENTRY_TYPE_SPECIAL 2
#define LVD_ENTRY_FLAG_NO_BITMAP 0x1
#define LVD_NODE_WAIT_US 100000
#define LVD_NODE_WAIT_RETRIES 100
#define UFS_NMOUNT_FLAG_RW 0x10000000u
#define UFS_NMOUNT_FLAG_RO 0x10000001u
// --- devpfs/pfs option defaults ---
// PFS nmount key/value variants observed in refs:
// - fstype: "pfs", "transaction_pfs", "ppr_pfs"
// - mkeymode: "SD"
// - budgetid: "game"/"system" in init paths
// - sigverify/playgo/disc: "0" or "1"
// - optional keys in specific flows: ekpfs/eekpfs, eekc, pubkey_ver, key_ver,
// finalized, ppkg_opt, sblock_offset, maxpkgszingib
#define DEVPFS_BUDGET_GAME "game"
#define DEVPFS_BUDGET_SYSTEM "system"
#define DEVPFS_MKEYMODE_SD "SD"
#define DEVPFS_MKEYMODE_GD "GD"
#define DEVPFS_MKEYMODE_AC "AC"
#define PFS_MOUNT_BUDGET_ID DEVPFS_BUDGET_GAME
#define PFS_MOUNT_MKEYMODE DEVPFS_MKEYMODE_AC
#define PFS_MOUNT_SIGVERIFY 0
#define PFS_MOUNT_PLAYGO 0
#define PFS_MOUNT_DISC 0
#define PFS_GDDR5_CACHE_IOCTL 0x80209168
// 4x64-bit PFS key encoded as 64 hex chars.
#define PFS_ZERO_EKPFS_KEY_HEX \
"0000000000000000000000000000000000000000000000000000000000000000"
#endif
+25
View File
@@ -0,0 +1,25 @@
#ifndef SM_MOUNT_DEVICE_H
#define SM_MOUNT_DEVICE_H
#include <stdbool.h>
#include "sm_types.h"
// Return a printable name for an attach backend enum value.
const char *attach_backend_name(attach_backend_t backend);
// Check whether a source path has aged enough to be mounted safely.
bool is_source_stable_for_mount(const char *path, const char *name,
const char *tag);
// Wait until a device node appears or disappears.
bool wait_for_dev_node_state(const char *devname, bool should_exist);
// Resolve backend and unit ID for a mounted image path.
bool resolve_device_from_mount(const char *mount_point,
attach_backend_t *backend_out, int *unit_out);
// Return true if a path is an active mount point.
bool is_active_image_mount_point(const char *path);
// Wait until /dev/lvd2 is no longer mounted during startup.
bool wait_for_lvd_release(void);
// Detach a previously attached MD or LVD unit.
bool detach_attached_unit(attach_backend_t backend, int unit_id);
#endif
+42
View File
@@ -0,0 +1,42 @@
#ifndef SM_PATH_STATE_H
#define SM_PATH_STATE_H
#include <stdbool.h>
#include <stdint.h>
#include <sys/stat.h>
// Load cached game info if the param file state still matches.
bool load_cached_game_info(const char *path, const struct stat *param_st,
char *out_id, char *out_name, bool *valid_out);
// Store parsed game info together with the current param file state.
void store_cached_game_info(const char *path, const struct stat *param_st,
bool valid, const char *title_id,
const char *title_name);
// Drop expired entries from path-based retry and metadata state.
void prune_path_state(void);
// Drop expired entries that belong to a specific scan root.
void prune_path_state_for_root(const char *root);
// Return whether scans for a path are temporarily suppressed after failures.
bool is_missing_param_scan_limited(const char *path);
// Record a missing param.json failure for scan backoff.
void record_missing_param_failure(const char *path);
// Clear missing-param backoff state for a path.
void clear_missing_param_entry(const char *path);
// Return whether image mount retries are temporarily suppressed for a path.
bool is_image_mount_limited(const char *path);
// Increment and return the image mount retry counter for a path.
uint8_t bump_image_mount_attempts(const char *path);
// Clear image mount retry state for a path.
void clear_image_mount_attempts(const char *path);
// Return whether backport overlay remounts are suppressed for a path.
bool is_backport_mount_blocked(const char *path);
// Record the first failed backport overlay mount for a path.
void note_backport_mount_failure(const char *path);
// Clear failed backport overlay mount state for a path.
void clear_backport_mount_failure(const char *path);
// Return true once for a missing manual source until the path appears again.
bool note_manual_missing_source_once(const char *path);
// Allow a manual source to be reported missing again after it becomes visible.
void clear_manual_missing_source(const char *path);
#endif
+24
View File
@@ -0,0 +1,24 @@
#ifndef SM_PATH_UTILS_H
#define SM_PATH_UTILS_H
#include <stdbool.h>
#include <stddef.h>
#include "sm_types.h"
// Return the last path component without modifying the input string.
const char *get_filename_component(const char *path);
// Return true when stat() can see the path.
bool path_exists(const char *path);
// Return true when a path lives under the image mount root.
bool is_under_image_mount_base(const char *path);
// Return true when a path is the PFSC container mount root or lives under it.
bool is_pfsc_image_mount_base_or_child(const char *path);
// Return true when path is equal to root or is under it.
bool path_matches_root_or_child(const char *path, const char *root);
// Return true when a path lives on USB-backed storage.
bool is_usb_storage_path(const char *path);
// Build "<scan_path>/backports" for a managed scan root.
bool build_backports_root_path(const char *scan_path, char out[MAX_PATH]);
#endif
+48
View File
@@ -0,0 +1,48 @@
#ifndef SM_PATHS_H
#define SM_PATHS_H
#define IMAGE_MOUNT_BASE "/mnt/shadowmnt"
#define PFSC_IMAGE_MOUNT_BASE "/mnt/shadowmnt/pfsc"
#define DEFAULT_BACKPORTS_DIR_NAME "backports"
#define DEFAULT_GLOBAL_FAKELIB_PATH "/data/shadowmount/fakelib"
#define LOG_DIR "/data/shadowmount"
#define LOG_FILE "/data/shadowmount/debug.log"
#define LOG_FILE_PREV "/data/shadowmount/debug.log.1"
#define CONFIG_FILE "/data/shadowmount/config.ini"
#define AUTOTUNE_FILE "/data/shadowmount/autotune.ini"
#define MANUAL_LIST_FILE "/data/shadowmount/manual.lst"
#define MANUAL_STATUS_FILE "/data/shadowmount/manual.status"
#define IMAGE_INDEX_FILE_PATH "/data/shadowmount/image_index.bin"
#define APPMETA_BASE "/user/appmeta"
#define APP_BASE "/user/app"
#define KSTUFF_NOAUTOMOUNT_FILE "/data/.kstuff_noautomount"
#define KILL_FILE "/data/shadowmount/STOP"
#define TOAST_FILE "/data/shadowmount/notify.txt"
#define NOTIFY_ICON_DIR "/user/data/shadowmount"
#define NOTIFY_ICON_FILE "/user/data/shadowmount/smp_icon.png"
#define APP_DB_PATH "/system_data/priv/mms/app.db"
/* Compile-time default scan roots (used when config has no scanpath entries). */
#define SM_DEFAULT_SCAN_PATHS_INITIALIZER \
{ \
/* Internal */ \
"/data/homebrew", "/data/etaHEN/games", \
/* Extended Storage */ \
"/mnt/ext0/homebrew", "/mnt/ext0/etaHEN/games", \
/* M.2 Drive */ \
"/mnt/ext1/homebrew", "/mnt/ext1/etaHEN/games", \
/* USB Subfolders */ \
"/mnt/usb0/homebrew", "/mnt/usb1/homebrew", "/mnt/usb2/homebrew", \
"/mnt/usb3/homebrew", "/mnt/usb4/homebrew", "/mnt/usb5/homebrew", \
"/mnt/usb6/homebrew", "/mnt/usb7/homebrew", \
"/mnt/usb0/etaHEN/games", "/mnt/usb1/etaHEN/games", \
"/mnt/usb2/etaHEN/games", "/mnt/usb3/etaHEN/games", \
"/mnt/usb4/etaHEN/games", "/mnt/usb5/etaHEN/games", \
"/mnt/usb6/etaHEN/games", "/mnt/usb7/etaHEN/games", \
/* USB Root Paths */ \
"/mnt/usb0", "/mnt/usb1", "/mnt/usb2", "/mnt/usb3", "/mnt/usb4", \
"/mnt/usb5", "/mnt/usb6", "/mnt/usb7", "/mnt/ext0", "/mnt/ext1", NULL \
}
#endif
+72
View File
@@ -0,0 +1,72 @@
#ifndef SM_PLATFORM_H
#define SM_PLATFORM_H
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/file.h>
#include <sys/ioctl.h>
#include <sys/mdioctl.h>
#include <sys/mount.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <time.h>
#include <unistd.h>
#include <ps5/kernel.h>
typedef struct {
uint32_t app_id;
uint64_t unknown1;
char title_id[14];
char unknown2[0x3c];
} app_info_t;
static inline uint32_t sm_firmware_major_version(void) {
uint32_t fw = kernel_get_fw_version();
uint32_t major_bcd = (fw >> 24) & 0xFFu;
return ((major_bcd >> 4) & 0xFu) * 10u + (major_bcd & 0xFu);
}
static inline bool sm_set_fd_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags < 0)
return false;
return fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0;
}
// --- SDK Imports ---
int sceAppInstUtilInitialize(void);
int sceAppInstUtilAppInstallAll(void *reserved);
int sceAppInstUtilAppUnInstall(const char *title_id);
int sceKernelGetAppInfo(pid_t pid, app_info_t *info);
int sceKernelUsleep(unsigned int microseconds);
int sceUserServiceInitialize(void *);
void sceUserServiceTerminate(void);
// Standard Notification
typedef struct notify_request {
char unused[45];
char message[3075];
} notify_request_t;
int sceKernelSendNotificationRequest(int, notify_request_t *, size_t, int);
#define IOVEC_ENTRY(x) \
{(void *)(x), \
((const char *)(x) == NULL ? 0u : (size_t)(strlen((const char *)(x)) + 1u))}
#define IOVEC_SIZE(x) (sizeof(x) / sizeof(struct iovec))
#endif
+31
View File
@@ -0,0 +1,31 @@
#ifndef SM_RUNTIME_H
#define SM_RUNTIME_H
#include <stdbool.h>
#include <stddef.h>
#include <sys/types.h>
// Install process signal handlers used for graceful shutdown.
void install_signal_handlers(void);
// Return process pid with the given name, or 0 if not found.
pid_t find_pid_by_name(const char *name, bool exclude_self);
// Return true when shutdown was requested by signal or kill file.
bool should_stop_requested(void);
// Request graceful shutdown with a descriptive source string.
void request_shutdown_stop(const char *reason);
// Return true when the payload is paused for system suspend/resume.
bool runtime_sleep_mode_active(void);
// Enter or leave sleep mode with a descriptive source string.
// Returns true when the runtime sleep state changed.
bool request_runtime_sleep_mode(bool active, const char *reason);
// Serialize mount/link/cache publication against suspend cleanup.
void runtime_mount_state_lock(void);
void runtime_mount_state_unlock(void);
// Request an immediate scan cycle with a descriptive source string.
void request_scan_now(const char *reason);
// Consume a pending immediate scan request and copy its reason into caller storage.
bool consume_scan_now_request(char *reason_out, size_t reason_out_size);
// Sleep in chunks and stop early if shutdown was requested.
bool sleep_with_stop_check(unsigned int total_us);
#endif
+32
View File
@@ -0,0 +1,32 @@
#ifndef SM_SCAN_H
#define SM_SCAN_H
#include <stdbool.h>
typedef struct scan_candidate scan_candidate_t;
// Unmount and clean up mounts whose backing sources disappeared.
void cleanup_lost_sources_before_scan(void);
// Unmount and clean up mounts whose backing sources disappeared under one root.
void cleanup_lost_sources_for_scan_root(const char *scan_root);
// Immediately unmount runtime mounts backed by USB storage for suspend.
void unmount_usb_sources_for_suspend(void);
// Scan configured roots and collect install candidates.
int collect_scan_candidates(scan_candidate_t *candidates, int max_candidates,
int *total_found_out,
bool *unstable_found_out);
// Scan a single configured root and collect install candidates.
int collect_scan_candidates_for_scan_root(const char *scan_root,
scan_candidate_t *candidates,
int max_candidates,
int *total_found_out,
bool *unstable_found_out);
// Mount the stable backport overlay, when present, for one runtime title.
bool mount_backport_overlay_for_title(const char *source_path,
const char *title_id,
const char *owning_scan_root,
bool *unstable_found_out);
// Remove discovery title/image mounts while preserving persistent metadata.
bool release_scan_runtime_mounts(void);
#endif
+31
View File
@@ -0,0 +1,31 @@
#ifndef SM_SCAN_TREE_H
#define SM_SCAN_TREE_H
#include <stdbool.h>
typedef enum {
SM_SCAN_TREE_DIR_DESCEND = 0,
SM_SCAN_TREE_DIR_SKIP_DESCEND,
SM_SCAN_TREE_DIR_ABORT,
} sm_scan_tree_dir_visit_t;
typedef sm_scan_tree_dir_visit_t (*sm_scan_tree_dir_fn)(const char *dir_path,
unsigned int depth_from_root,
void *ctx);
typedef bool (*sm_scan_tree_image_fn)(const char *image_path,
const char *image_name,
unsigned int depth_from_root,
void *ctx);
typedef struct {
sm_scan_tree_dir_fn on_directory;
sm_scan_tree_image_fn on_image_file;
} sm_scan_tree_callbacks_t;
// Walk a managed scan subtree using the shared scan-depth and path filtering rules.
bool sm_scan_tree_walk(const char *scan_root, const char *dir_path,
unsigned int depth_from_root,
unsigned int remaining_depth,
const sm_scan_tree_callbacks_t *callbacks, void *ctx);
#endif
+19
View File
@@ -0,0 +1,19 @@
#ifndef SM_SCANNER_H
#define SM_SCANNER_H
#include <stdbool.h>
// Initialize scanner service resources such as wake pipe and file watches.
bool sm_scanner_init(void);
// Wake a blocked scanner wait. Safe to call from signal context.
void sm_scanner_wake(void);
// Return true after scanner-owned USB directory descriptors are closed.
bool sm_scanner_usb_watches_suspended(void);
// Run the initial full synchronization cycle.
bool sm_scanner_run_startup_sync(void);
// Run the steady-state scanner wait/scan loop until shutdown is requested.
void sm_scanner_run_loop(void);
// Release scanner service resources.
void sm_scanner_shutdown(void);
#endif
+9
View File
@@ -0,0 +1,9 @@
#ifndef SM_SHELLCORE_FLAGS_H
#define SM_SHELLCORE_FLAGS_H
#include <stdbool.h>
bool sm_shellcore_flags_start(void);
void sm_shellcore_flags_stop(void);
#endif
+19
View File
@@ -0,0 +1,19 @@
#ifndef SM_SHELLCORE_HOOKS_H
#define SM_SHELLCORE_HOOKS_H
#include <stdbool.h>
// Install/remove the fail-open SceShellCore lifecycle bridge.
bool sm_shellcore_hooks_start(void);
void sm_shellcore_hooks_stop(void);
// Return whether this firmware uses the internal TitleDir bridge.
bool sm_shellcore_install_bridge_enabled(void);
// Invoke the internal TitleDir path through one armed AppInstallAll dispatch.
// Returns false when the bridge is unavailable or was not consumed.
bool sm_shellcore_install_title_dir(const char *title_id,
const char *install_dir,
int *result_out);
#endif
+29
View File
@@ -0,0 +1,29 @@
#ifndef SM_SHELLCORE_OFFSETS_H
#define SM_SHELLCORE_OFFSETS_H
#include <stdint.h>
typedef enum {
SM_SHELLCORE_TARGET_LAUNCH_APP = 0,
SM_SHELLCORE_TARGET_UNMOUNT_WORKSPACE,
SM_SHELLCORE_TARGET_INSTALL_TITLE_DIR,
SM_SHELLCORE_TARGET_INSTALL_ALL,
SM_SHELLCORE_TARGET_COUNT
} sm_shellcore_target_t;
typedef struct {
uintptr_t offset;
uint8_t patch_size;
} sm_shellcore_target_offset_t;
typedef struct {
uint16_t firmware;
const char *name;
sm_shellcore_target_offset_t targets[SM_SHELLCORE_TARGET_COUNT];
} sm_shellcore_firmware_offsets_t;
const sm_shellcore_firmware_offsets_t *
sm_shellcore_offsets_for_firmware(uint32_t raw_firmware);
const char *sm_shellcore_target_name(sm_shellcore_target_t target);
#endif
+28
View File
@@ -0,0 +1,28 @@
#ifndef SM_SHELLCORE_PROTOCOL_H
#define SM_SHELLCORE_PROTOCOL_H
#include <stdint.h>
#include "sm_limits.h"
#define SM_SHELLCORE_SOCKET_PATH "/system_tmp/shadowmount.sock"
#define SM_SHELLCORE_PROTOCOL_MAGIC 0x534d5343u
#define SM_SHELLCORE_PROTOCOL_VERSION 1u
typedef enum {
SM_SHELLCORE_REQUEST_LAUNCH = 1,
SM_SHELLCORE_REQUEST_LAUNCH_FAILED = 2
} sm_shellcore_request_op_t;
typedef struct {
uint32_t magic;
uint16_t version;
uint16_t operation;
char title_id[MAX_TITLE_ID];
} sm_shellcore_request_t;
typedef struct {
int32_t status;
} sm_shellcore_response_t;
#endif
+26
View File
@@ -0,0 +1,26 @@
#ifndef SM_SHELLCORE_REMOTE_H
#define SM_SHELLCORE_REMOTE_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include "sm_shellcore_offsets.h"
typedef struct {
pid_t pid;
uintptr_t image_base;
uintptr_t targets[SM_SHELLCORE_TARGET_COUNT];
const sm_shellcore_firmware_offsets_t *offsets;
} sm_shellcore_remote_t;
// Resolve and validate every lifecycle target in a running SceShellCore.
bool sm_shellcore_remote_resolve(pid_t pid, sm_shellcore_remote_t *remote_out);
// Read or write another process while handling post-8.20 write restrictions.
bool sm_remote_process_read(pid_t pid, uintptr_t address, void *buffer,
size_t size);
bool sm_remote_process_write(pid_t pid, uintptr_t address, const void *buffer,
size_t size);
#endif
+19
View File
@@ -0,0 +1,19 @@
#ifndef SM_SHELLCORE_SERVICE_H
#define SM_SHELLCORE_SERVICE_H
#include <stdbool.h>
// Start/stop the local Unix-socket owner used by the SceShellCore bridge.
bool sm_shellcore_service_start(void);
void sm_shellcore_service_stop(void);
// Release transient title/image mounts after the lifecycle watcher confirms
// that the game process exited.
bool sm_shellcore_release_title_runtime(const char *title_id);
// Ensure that a managed title has its image/nullfs/backport runtime stack.
// Unmanaged stock titles are treated as already ready.
bool sm_shellcore_ensure_title_runtime(const char *title_id);
// Return true while ShellCore owns a title mount prepared for launch or exit.
bool sm_shellcore_service_has_prepared_mount(void);
#endif
+12
View File
@@ -0,0 +1,12 @@
#ifndef SM_STABILITY_H
#define SM_STABILITY_H
#include <stdbool.h>
// Check whether a path is old enough since its latest mtime/ctime change.
bool is_path_stable_now(const char *path, double *root_diff_out,
int *stat_errno_out);
// Wait briefly for a path to become stable before giving up.
bool wait_for_stability_fast(const char *path, const char *name);
#endif
+9
View File
@@ -0,0 +1,9 @@
#ifndef SM_TIME_H
#define SM_TIME_H
#include <stdint.h>
// Return a monotonic timestamp in microseconds, or 0 on failure.
uint64_t monotonic_time_us(void);
#endif
+27
View File
@@ -0,0 +1,27 @@
#ifndef SM_TITLE_STATE_H
#define SM_TITLE_STATE_H
#include <stdbool.h>
#include <stdint.h>
// Return whether registration was already attempted for a title.
bool was_register_attempted(const char *title_id);
// Return the number of registration attempts for a title.
uint8_t get_register_attempts(const char *title_id);
// Record a registration attempt for a title.
void mark_register_attempted(const char *title_id);
// Clear registration attempt state after success or confirmed user removal.
void clear_register_attempts(const char *title_id);
// Notify once about duplicate title IDs from different paths.
void notify_duplicate_title_once(const char *title_id, const char *path_a,
const char *path_b);
// Clear the one-shot duplicate notification state for a title.
void clear_duplicate_title_notification(const char *title_id);
// Return the number of failed install/remount attempts for a title.
uint8_t get_failed_mount_attempts(const char *title_id);
// Clear failed install/remount attempts for a title.
void clear_failed_mount_attempts(const char *title_id);
// Increment and return failed install/remount attempts for a title.
uint8_t bump_failed_mount_attempts(const char *title_id);
#endif
+152
View File
@@ -0,0 +1,152 @@
#ifndef SM_TYPES_H
#define SM_TYPES_H
#include <stdbool.h>
#include <stdint.h>
#include "sm_limits.h"
typedef struct {
// +0x00: 1 -> mount read-only, 0 -> allow write.
uint32_t ro;
// +0x04: reserved in observed Shell/FSMP callers.
uint32_t reserved0;
// +0x08: logical budget/domain string, usually "game" or "system".
const char *budget_id;
// +0x10: reserved in observed Shell/FSMP callers.
uint32_t reserved1;
// +0x14: bitmask consumed by devpfs mount logic.
uint32_t flags;
// +0x18: optional "maxpkgszingib" value (GiB), 0 means not set.
uint64_t max_pkg_gib;
} devpfs_mount_opt_t;
typedef struct {
// Human-readable profile id for logs.
const char *name;
// Raw option payload mapped to FSMP mount behavior.
devpfs_mount_opt_t opt;
} devpfs_mount_profile_t;
typedef struct {
// Source object class (observed: 1=file, 2=device-like/special source).
uint16_t source_type;
// Layer descriptor flags (observed bit0 = no bitmap file path).
uint16_t flags;
// Must be zero.
uint32_t reserved0;
// Backing file or device path.
const char *path;
// Data start offset in backing object (bytes).
uint64_t offset;
// Data size exposed via this layer (bytes).
uint64_t size;
// Optional bitmap file path.
const char *bitmap_path;
// Bitmap offset in bitmap file (bytes).
uint64_t bitmap_offset;
// Bitmap size (bytes), 0 when bitmap is unused.
uint64_t bitmap_size;
} lvd_ioctl_layer_v0_t;
typedef struct {
// Protocol version for /dev/lvdctl ioctl payload (0 = V0/base attach).
uint32_t io_version;
// Input: usually -1 for auto-assign. Output: created lvd unit id.
int32_t device_id;
// User-visible sector size exported by /dev/lvdN.
uint32_t sector_size;
// Secondary unit/granularity validated against sector_size.
uint32_t secondary_unit;
// Normalized attach flags produced from wrapper raw options.
uint16_t flags;
// LVD image type id (validator accepts 0..0xC).
uint16_t image_type;
// Number of valid entries in layers_ptr.
uint32_t layer_count;
// Total exported virtual size (bytes).
uint64_t device_size;
// Pointer to V0 layer array in user payload.
lvd_ioctl_layer_v0_t *layers_ptr;
} lvd_ioctl_attach_v0_t;
typedef struct {
// Must be zero.
uint32_t reserved0;
// Target lvd unit id to detach.
int32_t device_id;
// Reserved padding required by kernel ABI.
uint8_t reserved[0x20];
} lvd_ioctl_detach_t;
struct AppDbTitleList {
char(*ids)[MAX_TITLE_ID];
int count;
int capacity;
};
typedef struct scan_candidate {
char path[MAX_PATH];
char title_id[MAX_TITLE_ID];
char title_name[MAX_TITLE_NAME];
char manual_source_path[MAX_PATH];
bool installed;
bool in_app_db;
bool manual;
} scan_candidate_t;
typedef enum {
ATTACH_BACKEND_NONE = 0,
// /dev/lvdctl -> /dev/lvdN
ATTACH_BACKEND_LVD,
// /dev/mdctl -> /dev/mdN
ATTACH_BACKEND_MD,
} attach_backend_t;
typedef struct runtime_config {
bool debug_enabled;
bool quiet_mode;
bool mount_read_only;
bool force_mount;
bool app_install_all_enabled;
bool backport_fakelib_enabled;
bool global_fakelib_enabled;
bool global_fakelib_mount_first;
bool kstuff_game_auto_toggle;
bool kstuff_crash_detection_enabled;
bool legacy_recursive_scan_forced;
char global_fakelib_path[MAX_PATH];
uint32_t global_fakelib_exclude_title_count;
char global_fakelib_exclude_title_ids[MAX_FAKELIB_EXCLUDE_RULES][MAX_TITLE_ID];
uint32_t scan_depth;
uint32_t scan_interval_us;
uint32_t stability_wait_seconds;
uint32_t kstuff_pause_delay_image_seconds;
uint32_t kstuff_pause_delay_direct_seconds;
attach_backend_t exfat_backend;
attach_backend_t ufs_backend;
uint32_t lvd_sector_exfat;
uint32_t lvd_sector_ufs;
uint32_t lvd_sector_pfs;
uint32_t md_sector_exfat;
uint32_t md_sector_ufs;
} runtime_config_t;
typedef enum {
IMAGE_FS_UNKNOWN = 0,
IMAGE_FS_UFS,
IMAGE_FS_EXFAT,
IMAGE_FS_PFS,
IMAGE_FS_PFSC_CONTAINER,
} image_fs_type_t;
typedef struct sm_error {
int code;
char subsystem[16];
char message[256];
char path[MAX_PATH];
bool valid;
bool notified;
} sm_error_t;
#endif
+137
View File
@@ -0,0 +1,137 @@
#!/bin/sh
# macOS exFAT image builder — requires macOS 10.6.5+ (exFAT and rsync built-in)
# Usage: mkexfat_macos.sh <input_dir> [output_file]
# (c) @drakmor,@betmoar
set -e
# ── argument validation ───────────────────────────────────────────────────────
if [ -z "$1" ]; then
echo "Usage: $0 <input_dir> [output_file]"
exit 1
fi
INPUT_DIR="$1"
OUTPUT="${2:-test.exfat}"
# newfs_exfat interprets relative paths as device names under /dev/ — resolve to absolute
OUTPUT="$(cd "$(dirname "$OUTPUT")" && pwd)/$(basename "$OUTPUT")"
if [ ! -d "$INPUT_DIR" ]; then
echo "Error: input directory not found: $INPUT_DIR"
exit 1
fi
if [ ! -f "$INPUT_DIR/eboot.bin" ]; then
echo "Error: eboot.bin not found in source directory: $INPUT_DIR"
exit 1
fi
# ── sizing constants ──────────────────────────────────────────────────────────
CLUSTER_SIZE=32768
LARGE_FILE_THRESHOLD=$((1024 * 1024))
META_FIXED=$((32 * 1024 * 1024)) # boot region, upcase, root and misc
MIN_SLACK=$((64 * 1024 * 1024)) # minimum copy/runtime safety margin
SPARE_MIN=$((64 * 1024 * 1024)) # lower bound for dynamic headroom
SPARE_MAX=$((512 * 1024 * 1024)) # upper bound for dynamic headroom
ENTRY_META_BYTES=256
# Single filesystem traversal: collect all file sizes into a temp file.
# FILE_COUNT and RAW_FILE_BYTES are derived from it in one awk pass.
# DIR_COUNT needs a separate traversal (different -type).
SIZES_FILE=$(mktemp)
MOUNT_POINT=""
LOOP_DEVICE=""
cleanup() {
if [ -n "$MOUNT_POINT" ] && mount | grep -q "on $MOUNT_POINT "; then
umount "$MOUNT_POINT" 2>/dev/null || true
fi
if [ -n "$LOOP_DEVICE" ]; then
hdiutil detach "$LOOP_DEVICE" 2>/dev/null || true
fi
if [ -n "$MOUNT_POINT" ]; then
rmdir "$MOUNT_POINT" 2>/dev/null || true
fi
rm -f "$SIZES_FILE"
}
trap cleanup EXIT INT TERM
find "$INPUT_DIR" -type f -exec stat -f '%z' {} \; > "$SIZES_FILE"
read -r FILE_COUNT RAW_FILE_BYTES <<EOF
$(awk '{c++; s += $1} END {print c+0, s+0}' "$SIZES_FILE")
EOF
DIR_COUNT=$(find "$INPUT_DIR" -type d | wc -l | tr -d ' ')
AVG_FILE_BYTES=0
if [ "$FILE_COUNT" -gt 0 ]; then
AVG_FILE_BYTES=$((RAW_FILE_BYTES / FILE_COUNT))
fi
# exFAT profile selection:
# - large-file sets: 64K cluster
# - small/mixed-file sets: 32K cluster
if [ "$AVG_FILE_BYTES" -ge "$LARGE_FILE_THRESHOLD" ]; then
CLUSTER_SIZE=65536
fi
# DATA_BYTES re-reads the already-collected sizes from the temp file — no second traversal
DATA_BYTES=$(awk -v cls="$CLUSTER_SIZE" \
'{s += int(($1 + cls - 1) / cls) * cls} END {print s + 0}' "$SIZES_FILE")
rm -f "$SIZES_FILE"
DATA_CLUSTERS=$(( (DATA_BYTES + CLUSTER_SIZE - 1) / CLUSTER_SIZE ))
FAT_BYTES=$((DATA_CLUSTERS * 4))
BITMAP_BYTES=$(( (DATA_CLUSTERS + 7) / 8 ))
ENTRY_BYTES=$(( (FILE_COUNT + DIR_COUNT) * ENTRY_META_BYTES ))
BASE_TOTAL=$((DATA_BYTES + FAT_BYTES + BITMAP_BYTES + ENTRY_BYTES + META_FIXED))
SPARE_BYTES=$((BASE_TOTAL / 200)) # ~0.5%
if [ "$SPARE_BYTES" -lt "$SPARE_MIN" ]; then SPARE_BYTES=$SPARE_MIN; fi
if [ "$SPARE_BYTES" -gt "$SPARE_MAX" ]; then SPARE_BYTES=$SPARE_MAX; fi
TOTAL=$((BASE_TOTAL + SPARE_BYTES))
MIN_TOTAL=$((RAW_FILE_BYTES + MIN_SLACK))
if [ "$TOTAL" -lt "$MIN_TOTAL" ]; then TOTAL=$MIN_TOTAL; fi
# Round up to nearest MB
MB=$(( (TOTAL + 1024*1024 - 1) / (1024*1024) ))
echo "Input size (raw files): $RAW_FILE_BYTES bytes"
echo "Input size (exFAT alloc): $DATA_BYTES bytes"
echo "Files: $FILE_COUNT, Dirs: $DIR_COUNT"
echo "exFAT profile: -b ${CLUSTER_SIZE} (avg file=$AVG_FILE_BYTES bytes)"
echo "Image size: ${MB}MB"
# ── create, attach, format, mount ────────────────────────────────────────────
MOUNT_POINT=$(mktemp -d)
# mkfile -n creates a sparse file without writing zeros (equivalent to Linux truncate)
mkfile -n "${MB}m" "$OUTPUT"
# Attach as a block device first — newfs_exfat requires real sector geometry via ioctl
# and cannot format a raw file directly
ATTACH_OUTPUT=$(hdiutil attach \
-imagekey diskimage-class=CRawDiskImage \
-nomount "$OUTPUT")
LOOP_DEVICE=$(printf '%s\n' "$ATTACH_OUTPUT" | awk 'NR==1 {print $1; exit}')
if [ -z "$LOOP_DEVICE" ]; then
echo "Error: failed to determine attached device: $OUTPUT"
exit 1
fi
# newfs_exfat -b takes bytes-per-cluster directly
newfs_exfat -b "$CLUSTER_SIZE" "$LOOP_DEVICE"
mount -t exfat "$LOOP_DEVICE" "$MOUNT_POINT"
rsync -r --progress "$INPUT_DIR"/ "$MOUNT_POINT"/
umount "$MOUNT_POINT"
hdiutil detach "$LOOP_DEVICE"
LOOP_DEVICE="" # prevent double-detach in trap
echo "Created $OUTPUT"
+25 -18
View File
@@ -23,14 +23,17 @@ if [ ! -f "$INPUT_DIR/eboot.bin" ]; then
fi
# More accurate sizing for UFS2:
# - file payload rounded to fragment size (-f)
# - file payload rounded to the actual allocation unit (-f)
# - minimal per-directory allocation
# - inode table estimate
# - fixed filesystem metadata headroom
BLOCK_SIZE=32768
FRAG_SIZE=4096
BLOCK_SIZE=65536
FRAG_SIZE=65536
SECTOR_SIZE=4096
MINFREE_PERCENT=0
BYTES_PER_INODE=262144
DEFAULT_BYTES_PER_INODE=$((4 * FRAG_SIZE))
MIN_BYTES_PER_INODE=$FRAG_SIZE
INODE_SIZE=256
INODE_SPARE=2048
META_FIXED=$((64 * 1024 * 1024)) # superblocks, cg metadata, allocator slack
@@ -43,18 +46,6 @@ DIR_COUNT=$(find "$INPUT_DIR" -type d | wc -l | tr -d ' ')
RAW_FILE_BYTES=$(find "$INPUT_DIR" -type f -exec stat -f '%z' {} + | \
awk '{s += $1} END {print s + 0}')
AVG_FILE_BYTES=0
if [ "$FILE_COUNT" -gt 0 ]; then
AVG_FILE_BYTES=$((RAW_FILE_BYTES / FILE_COUNT))
fi
# UFS profile selection:
# - large-file sets: 64K block
# - small/mixed-file sets: 32K block (safer/denser default)
if [ "$AVG_FILE_BYTES" -ge $((1024 * 1024)) ]; then
BLOCK_SIZE=65536
fi
DATA_BYTES=$(find "$INPUT_DIR" -type f -exec stat -f '%z' {} + | \
awk -v frag="$FRAG_SIZE" '{s += int(($1 + frag - 1) / frag) * frag} END {print s + 0}')
DIR_BYTES=$((DIR_COUNT * FRAG_SIZE))
@@ -77,17 +68,33 @@ fi
# Round up to nearest MB
MB=$(( (TOTAL + 1024*1024 - 1) / (1024*1024) ))
IMAGE_BYTES=$((MB * 1024 * 1024))
# Tune inode density for the actual tree size. Keep the documented default
# unless the source contains enough files/directories to require denser inodes.
BYTES_PER_INODE=$DEFAULT_BYTES_PER_INODE
if [ "$INODE_COUNT" -gt 0 ]; then
AUTO_BYTES_PER_INODE=$((IMAGE_BYTES / INODE_COUNT))
AUTO_BYTES_PER_INODE=$(( (AUTO_BYTES_PER_INODE / SECTOR_SIZE) * SECTOR_SIZE ))
if [ "$AUTO_BYTES_PER_INODE" -lt "$MIN_BYTES_PER_INODE" ]; then
AUTO_BYTES_PER_INODE=$MIN_BYTES_PER_INODE
fi
if [ "$AUTO_BYTES_PER_INODE" -lt "$BYTES_PER_INODE" ]; then
BYTES_PER_INODE=$AUTO_BYTES_PER_INODE
fi
fi
echo "Input size (raw files): $RAW_FILE_BYTES bytes"
echo "Input size (UFS alloc): $DATA_BYTES bytes"
echo "Files: $FILE_COUNT, Dirs: $DIR_COUNT"
echo "UFS profile: -b $BLOCK_SIZE -f $FRAG_SIZE -m $MINFREE_PERCENT -i $BYTES_PER_INODE (avg file=$AVG_FILE_BYTES bytes)"
echo "Inode target: $INODE_COUNT"
echo "UFS profile: -b $BLOCK_SIZE -f $FRAG_SIZE -m $MINFREE_PERCENT -S $SECTOR_SIZE -i $BYTES_PER_INODE"
echo "Image size: ${MB}MB"
truncate -s "${MB}M" "$OUTPUT"
MD=$(mdconfig -a -t vnode -f "$(realpath "$OUTPUT")")
newfs -O 2 -b "$BLOCK_SIZE" -f "$FRAG_SIZE" -m "$MINFREE_PERCENT" -i "$BYTES_PER_INODE" /dev/${MD}
newfs -O 2 -b "$BLOCK_SIZE" -f "$FRAG_SIZE" -m "$MINFREE_PERCENT" -S "$SECTOR_SIZE" -i "$BYTES_PER_INODE" /dev/${MD}
mkdir -p /mnt
mount /dev/${MD} /mnt
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+3
View File
@@ -0,0 +1,3 @@
asm(".global \"C49jelxiaVE\"\n"
".type \"C49jelxiaVE\" @function\n"
"\"C49jelxiaVE\":\n");
+455 -3715
View File
File diff suppressed because it is too large. Load diff
+538
View File
@@ -0,0 +1,538 @@
#include "sm_platform.h"
#include <pthread.h>
#include <sqlite3.h>
#include "sm_runtime.h"
#include "sm_types.h"
#include "sm_appdb.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_paths.h"
static sqlite3 *g_app_db;
static sqlite3_stmt *g_app_db_stmt_update_snd0;
static sqlite3_stmt *g_app_db_stmt_normalize_snd0;
static struct AppDbTitleList g_app_db_title_cache;
static struct AppDbTitleList g_app_db_blocked_uninstall_ppsa_cache;
static bool g_app_db_title_cache_ready = false;
static bool g_app_db_blocked_uninstall_ppsa_cache_ready = false;
static time_t g_app_db_title_cache_mtime = 0;
static time_t g_app_db_blocked_uninstall_ppsa_cache_mtime = 0;
static pthread_mutex_t g_app_db_mutex = PTHREAD_MUTEX_INITIALIZER;
#define APP_DB_STARTUP_MAINTENANCE_RETRIES 3
#define APP_DB_STARTUP_MAINTENANCE_BUSY_TIMEOUT_MS 250
static void close_app_db(void) {
if (g_app_db_stmt_update_snd0) {
sqlite3_finalize(g_app_db_stmt_update_snd0);
g_app_db_stmt_update_snd0 = NULL;
}
if (g_app_db_stmt_normalize_snd0) {
sqlite3_finalize(g_app_db_stmt_normalize_snd0);
g_app_db_stmt_normalize_snd0 = NULL;
}
if (g_app_db) {
sqlite3_close(g_app_db);
g_app_db = NULL;
}
}
void free_app_db_title_list(struct AppDbTitleList *list) {
free(list->ids);
list->ids = NULL;
list->count = 0;
list->capacity = 0;
}
void shutdown_app_db(void) {
pthread_mutex_lock(&g_app_db_mutex);
free_app_db_title_list(&g_app_db_title_cache);
free_app_db_title_list(&g_app_db_blocked_uninstall_ppsa_cache);
g_app_db_title_cache_ready = false;
g_app_db_blocked_uninstall_ppsa_cache_ready = false;
g_app_db_title_cache_mtime = 0;
g_app_db_blocked_uninstall_ppsa_cache_mtime = 0;
close_app_db();
pthread_mutex_unlock(&g_app_db_mutex);
}
static bool ensure_app_db_open(void) {
if (!g_app_db) {
if (sqlite3_open_v2(APP_DB_PATH, &g_app_db, SQLITE_OPEN_READWRITE, NULL) !=
SQLITE_OK) {
log_debug(" [DB] open failed: %s",
(g_app_db ? sqlite3_errmsg(g_app_db) : APP_DB_PATH));
close_app_db();
return false;
}
(void)sqlite3_busy_timeout(g_app_db, APP_DB_BUSY_TIMEOUT_MS);
}
return true;
}
static bool app_db_wait_retry(int rc, int attempt, int max_attempts,
bool close_before_retry) {
if (rc != SQLITE_BUSY && rc != SQLITE_LOCKED)
return false;
if (attempt + 1 >= max_attempts || should_stop_requested())
return false;
if (close_before_retry)
close_app_db();
sceKernelUsleep(APP_DB_BUSY_RETRY_SLEEP_US);
return true;
}
static int app_db_prepare_with_retry(const char *sql, sqlite3_stmt **stmt_out,
int max_attempts, const char *label) {
for (int attempt = 0; attempt < max_attempts; attempt++) {
if (!ensure_app_db_open())
return -1;
int rc = sqlite3_prepare_v2(g_app_db, sql, -1, stmt_out, NULL);
if (rc == SQLITE_OK)
return SQLITE_OK;
if (app_db_wait_retry(rc, attempt, max_attempts, true))
continue;
log_debug(" [DB] prepare failed for %s: rc=%d err=%s", label, rc,
(g_app_db ? sqlite3_errmsg(g_app_db) : "unknown"));
close_app_db();
return rc;
}
close_app_db();
return -1;
}
static bool app_db_exec_with_retry(const char *sql, int max_attempts,
const char *label, int *changes_out) {
if (changes_out)
*changes_out = 0;
for (int attempt = 0; attempt < max_attempts; attempt++) {
if (!ensure_app_db_open())
return false;
char *err_msg = NULL;
int rc = sqlite3_exec(g_app_db, sql, NULL, NULL, &err_msg);
if (rc == SQLITE_OK) {
if (changes_out)
*changes_out = sqlite3_changes(g_app_db);
sqlite3_free(err_msg);
return true;
}
if (app_db_wait_retry(rc, attempt, max_attempts, false)) {
sqlite3_free(err_msg);
continue;
}
if (err_msg) {
log_debug(" [DB] exec failed for %s: rc=%d err=%s", label, rc, err_msg);
} else {
log_debug(" [DB] exec failed for %s: rc=%d err=%s", label, rc,
sqlite3_errmsg(g_app_db));
}
sqlite3_free(err_msg);
close_app_db();
return false;
}
close_app_db();
return false;
}
static int app_db_query_int(const char *sql, const char *label) {
sqlite3_stmt *stmt = NULL;
int prep_rc = app_db_prepare_with_retry(sql, &stmt,
APP_DB_STARTUP_MAINTENANCE_RETRIES,
label);
if (prep_rc != SQLITE_OK)
return -1;
int result = -1;
for (int attempt = 0; attempt < APP_DB_QUERY_BUSY_RETRIES; attempt++) {
int rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
result = sqlite3_column_int(stmt, 0);
break;
}
if (app_db_wait_retry(rc, attempt, APP_DB_QUERY_BUSY_RETRIES, false))
continue;
log_debug(" [DB] query failed for %s: rc=%d err=%s", label, rc,
sqlite3_errmsg(g_app_db));
break;
}
sqlite3_finalize(stmt);
return result;
}
bool app_db_run_startup_maintenance(void) {
bool ok = false;
pthread_mutex_lock(&g_app_db_mutex);
if (!ensure_app_db_open())
goto out;
(void)sqlite3_busy_timeout(g_app_db,
APP_DB_STARTUP_MAINTENANCE_BUSY_TIMEOUT_MS);
const char *backfill_count_sql =
"SELECT COUNT(*) FROM tbl_contentinfo "
"WHERE instr(snd0Info, '/user/app/') > 0 "
"OR instr(snd0Info, '/sce_sys') > 0;";
int pending_backfill =
app_db_query_int(backfill_count_sql, "snd0info normalize backfill check");
if (pending_backfill < 0) {
close_app_db();
goto out;
}
int changes = 0;
if (pending_backfill > 0) {
const char *backfill_sql =
"UPDATE tbl_contentinfo "
"SET snd0Info = replace(replace(snd0Info, '/user/app/', "
"'/user/appmeta/'), '/sce_sys', '') "
"WHERE instr(snd0Info, '/user/app/') > 0 "
"OR instr(snd0Info, '/sce_sys') > 0;";
if (!app_db_exec_with_retry(backfill_sql,
APP_DB_STARTUP_MAINTENANCE_RETRIES,
"snd0info startup normalize", &changes))
goto out;
}
log_debug(" [DB] snd0info startup maintenance done rows=%d pending=%d",
changes, pending_backfill);
close_app_db();
ok = true;
out:
pthread_mutex_unlock(&g_app_db_mutex);
return ok;
}
int update_snd0info(const char *title_id) {
int result = -1;
pthread_mutex_lock(&g_app_db_mutex);
if (!g_app_db_stmt_update_snd0) {
const char *sql =
"UPDATE tbl_contentinfo "
"SET snd0info = '/user/appmeta/' || ?1 || '/snd0.at9' "
"WHERE titleId = ?1;";
int prep_rc = app_db_prepare_with_retry(sql, &g_app_db_stmt_update_snd0,
APP_DB_PREPARE_BUSY_RETRIES,
"snd0info update");
if (prep_rc != SQLITE_OK)
goto out;
}
for (int attempt = 0; attempt < APP_DB_UPDATE_BUSY_RETRIES; attempt++) {
sqlite3_reset(g_app_db_stmt_update_snd0);
sqlite3_clear_bindings(g_app_db_stmt_update_snd0);
if (sqlite3_bind_text(g_app_db_stmt_update_snd0, 1, title_id, -1,
SQLITE_TRANSIENT) != SQLITE_OK) {
log_debug(" [DB] bind failed for snd0info update: %s",
sqlite3_errmsg(g_app_db));
close_app_db();
goto out;
}
int rc = sqlite3_step(g_app_db_stmt_update_snd0);
if (rc == SQLITE_DONE) {
int changes = sqlite3_changes(g_app_db);
close_app_db();
result = changes;
goto out;
}
if (app_db_wait_retry(rc, attempt, APP_DB_UPDATE_BUSY_RETRIES, false))
continue;
log_debug(" [DB] step failed for snd0info update: rc=%d err=%s", rc,
sqlite3_errmsg(g_app_db));
close_app_db();
goto out;
}
close_app_db();
out:
pthread_mutex_unlock(&g_app_db_mutex);
return result;
}
int normalize_snd0info_for_title(const char *title_id) {
int result = -1;
pthread_mutex_lock(&g_app_db_mutex);
if (!g_app_db_stmt_normalize_snd0) {
const char *sql =
"UPDATE tbl_contentinfo "
"SET snd0Info = replace(replace(snd0Info, '/user/app/', "
"'/user/appmeta/'), '/sce_sys', '') "
"WHERE titleId = ?1 "
"AND (instr(snd0Info, '/user/app/') > 0 "
"OR instr(snd0Info, '/sce_sys') > 0);";
int prep_rc = app_db_prepare_with_retry(sql, &g_app_db_stmt_normalize_snd0,
APP_DB_PREPARE_BUSY_RETRIES,
"snd0info normalize");
if (prep_rc != SQLITE_OK)
goto out;
}
for (int attempt = 0; attempt < APP_DB_UPDATE_BUSY_RETRIES; attempt++) {
sqlite3_reset(g_app_db_stmt_normalize_snd0);
sqlite3_clear_bindings(g_app_db_stmt_normalize_snd0);
if (sqlite3_bind_text(g_app_db_stmt_normalize_snd0, 1, title_id, -1,
SQLITE_TRANSIENT) != SQLITE_OK) {
log_debug(" [DB] bind failed for snd0info normalize: %s",
sqlite3_errmsg(g_app_db));
close_app_db();
goto out;
}
int rc = sqlite3_step(g_app_db_stmt_normalize_snd0);
if (rc == SQLITE_DONE) {
int changes = sqlite3_changes(g_app_db);
close_app_db();
result = changes;
goto out;
}
if (app_db_wait_retry(rc, attempt, APP_DB_UPDATE_BUSY_RETRIES, false))
continue;
log_debug(" [DB] step failed for snd0info normalize: rc=%d err=%s", rc,
sqlite3_errmsg(g_app_db));
close_app_db();
goto out;
}
close_app_db();
out:
pthread_mutex_unlock(&g_app_db_mutex);
return result;
}
static bool append_app_db_title(struct AppDbTitleList *list,
const char *title_id) {
if (!list || !title_id || title_id[0] == '\0')
return true;
if (list->count >= list->capacity) {
int new_capacity = (list->capacity > 0) ? (list->capacity * 2) : 1024;
char(*new_ids)[MAX_TITLE_ID] =
realloc(list->ids, (size_t)new_capacity * sizeof(*list->ids));
if (!new_ids)
return false;
list->ids = new_ids;
list->capacity = new_capacity;
}
(void)strlcpy(list->ids[list->count], title_id, MAX_TITLE_ID);
list->count++;
return true;
}
static int compare_title_id_str(const void *a, const void *b) {
return strcmp((const char *)a, (const char *)b);
}
bool app_db_title_list_contains(const struct AppDbTitleList *list,
const char *title_id) {
if (list->count <= 0)
return false;
return bsearch(title_id, list->ids, (size_t)list->count, sizeof(*list->ids),
compare_title_id_str) != NULL;
}
static bool step_app_db_title_query(struct AppDbTitleList *list,
sqlite3_stmt *stmt,
const char *label) {
int busy_attempts = 0;
while (!should_stop_requested()) {
int rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
const char *title_id = (const char *)sqlite3_column_text(stmt, 0);
if (!append_app_db_title(list, title_id)) {
log_debug(" [DB] %s allocation failed", label);
sqlite3_finalize(stmt);
close_app_db();
free_app_db_title_list(list);
return false;
}
continue;
}
if (rc == SQLITE_DONE) {
sqlite3_finalize(stmt);
close_app_db();
return true;
}
if ((rc == SQLITE_BUSY || rc == SQLITE_LOCKED) &&
busy_attempts + 1 < APP_DB_QUERY_BUSY_RETRIES) {
busy_attempts++;
sceKernelUsleep(APP_DB_BUSY_RETRY_SLEEP_US);
continue;
}
log_debug(" [DB] %s query failed: rc=%d err=%s", label, rc,
sqlite3_errmsg(g_app_db));
sqlite3_finalize(stmt);
close_app_db();
free_app_db_title_list(list);
return false;
}
sqlite3_finalize(stmt);
close_app_db();
free_app_db_title_list(list);
return false;
}
static bool load_app_db_title_list(struct AppDbTitleList *list) {
free_app_db_title_list(list);
const char *sql =
"SELECT DISTINCT titleId "
"FROM tbl_contentinfo "
"WHERE titleId != '' "
"ORDER BY titleId;";
sqlite3_stmt *stmt = NULL;
int prep_rc = app_db_prepare_with_retry(sql, &stmt,
APP_DB_PREPARE_BUSY_RETRIES,
"title list query");
if (prep_rc != SQLITE_OK)
return false;
bool loaded = step_app_db_title_query(list, stmt, "title list");
if (loaded)
log_debug(" [DB] loaded app.db title list: %d entries", list->count);
return loaded;
}
static bool load_app_db_blocked_uninstall_ppsa_list(struct AppDbTitleList *list) {
free_app_db_title_list(list);
const char *sql =
"SELECT DISTINCT titleId "
"FROM tbl_contentinfo "
"WHERE titleId LIKE 'PPSA%' "
"AND uninstallable = 0 "
"ORDER BY titleId;";
sqlite3_stmt *stmt = NULL;
int prep_rc =
app_db_prepare_with_retry(sql, &stmt, APP_DB_PREPARE_BUSY_RETRIES,
"blocked PPSA uninstallable query");
if (prep_rc != SQLITE_OK)
return false;
bool loaded = step_app_db_title_query(list, stmt, "blocked PPSA uninstall");
if (loaded)
log_debug(" [DB] loaded blocked PPSA uninstall list: %d entries",
list->count);
return loaded;
}
void invalidate_app_db_title_cache(void) {
pthread_mutex_lock(&g_app_db_mutex);
g_app_db_title_cache_ready = false;
g_app_db_blocked_uninstall_ppsa_cache_ready = false;
g_app_db_title_cache_mtime = 0;
g_app_db_blocked_uninstall_ppsa_cache_mtime = 0;
pthread_mutex_unlock(&g_app_db_mutex);
}
static bool copy_app_db_title_list(struct AppDbTitleList *dst,
const struct AppDbTitleList *src) {
if (!dst || !src)
return false;
free_app_db_title_list(dst);
if (src->count <= 0)
return true;
char(*ids)[MAX_TITLE_ID] = malloc((size_t)src->count * sizeof(*ids));
if (!ids)
return false;
memcpy(ids, src->ids, (size_t)src->count * sizeof(*ids));
dst->ids = ids;
dst->count = src->count;
dst->capacity = src->count;
return true;
}
bool get_app_db_title_list_cached(struct AppDbTitleList *list_out) {
if (!list_out)
return false;
free_app_db_title_list(list_out);
pthread_mutex_lock(&g_app_db_mutex);
struct stat st;
int app_db_stat_rc = stat(APP_DB_PATH, &st);
if (!g_app_db_title_cache_ready ||
(app_db_stat_rc == 0 && g_app_db_title_cache_mtime != st.st_mtime)) {
struct AppDbTitleList fresh = {0};
if (app_db_stat_rc == 0 && load_app_db_title_list(&fresh)) {
free_app_db_title_list(&g_app_db_title_cache);
g_app_db_title_cache = fresh;
g_app_db_title_cache_mtime = st.st_mtime;
g_app_db_title_cache_ready = true;
} else {
free_app_db_title_list(&fresh);
if (!g_app_db_title_cache_ready) {
pthread_mutex_unlock(&g_app_db_mutex);
return false;
}
}
}
bool copied = copy_app_db_title_list(list_out, &g_app_db_title_cache);
pthread_mutex_unlock(&g_app_db_mutex);
return copied;
}
bool get_app_db_blocked_uninstall_ppsa_list(struct AppDbTitleList *list_out) {
if (!list_out)
return false;
free_app_db_title_list(list_out);
pthread_mutex_lock(&g_app_db_mutex);
struct stat st;
int app_db_stat_rc = stat(APP_DB_PATH, &st);
if (!g_app_db_blocked_uninstall_ppsa_cache_ready ||
(app_db_stat_rc == 0 &&
g_app_db_blocked_uninstall_ppsa_cache_mtime != st.st_mtime)) {
struct AppDbTitleList fresh = {0};
if (app_db_stat_rc == 0 &&
load_app_db_blocked_uninstall_ppsa_list(&fresh)) {
free_app_db_title_list(&g_app_db_blocked_uninstall_ppsa_cache);
g_app_db_blocked_uninstall_ppsa_cache = fresh;
g_app_db_blocked_uninstall_ppsa_cache_mtime = st.st_mtime;
g_app_db_blocked_uninstall_ppsa_cache_ready = true;
} else {
free_app_db_title_list(&fresh);
if (!g_app_db_blocked_uninstall_ppsa_cache_ready) {
pthread_mutex_unlock(&g_app_db_mutex);
return false;
}
}
}
bool loaded =
copy_app_db_title_list(list_out, &g_app_db_blocked_uninstall_ppsa_cache);
pthread_mutex_unlock(&g_app_db_mutex);
return loaded;
}
File diff suppressed because it is too large. Load diff
+311
View File
@@ -0,0 +1,311 @@
#include "sm_platform.h"
#include "sm_fakelib.h"
#include "sm_config_mount.h"
#include "sm_log.h"
#include "sm_types.h"
#include <pthread.h>
typedef struct {
char source_path[MAX_PATH];
char mount_path[MAX_PATH];
const char *label;
} fakelib_layer_t;
typedef struct {
pid_t pid;
char mount_path[MAX_PATH];
fakelib_layer_t layers[2];
size_t layer_count;
} fakelib_session_t;
static fakelib_session_t g_fakelib_mount;
static pthread_mutex_t g_fakelib_mutex = PTHREAD_MUTEX_INITIALIZER;
bool sm_fakelib_game_feature_enabled(void) {
return runtime_config()->backport_fakelib_enabled;
}
static bool mount_fakelib_overlay(const char *title_id,
const char *source_path,
const char *mount_path,
const char *label) {
struct iovec overlay_iov[] = {
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("unionfs"),
IOVEC_ENTRY("from"), IOVEC_ENTRY(source_path),
IOVEC_ENTRY("fspath"), IOVEC_ENTRY(mount_path),
IOVEC_ENTRY("copymode"), IOVEC_ENTRY("transparent"),
IOVEC_ENTRY("notime"), IOVEC_ENTRY(NULL),
IOVEC_ENTRY("fnodup"), IOVEC_ENTRY(NULL)};
if (nmount(overlay_iov, IOVEC_SIZE(overlay_iov), 0) == 0) {
log_debug(" [FAKELIB] %s libraries mounted for %s: %s -> %s", label,
title_id, source_path, mount_path);
return true;
}
log_debug(" [FAKELIB] %s mount failed for %s (%s -> %s): %s", label,
title_id, source_path, mount_path, strerror(errno));
return false;
}
static bool unmount_fakelib_overlay(const fakelib_layer_t *layer) {
const char *mount_path = layer->mount_path;
if (unmount(mount_path, MNT_FORCE) == 0 || errno == ENOENT ||
errno == EINVAL) {
log_debug(" [FAKELIB] %s libraries unmounted: %s -> %s", layer->label,
layer->source_path, mount_path);
return true;
}
log_debug(" [FAKELIB] %s unmount failed for %s: %s", layer->label,
mount_path, strerror(errno));
return false;
}
static bool track_fakelib_overlay(const char *title_id,
const char *source_path,
const char *mount_path,
const char *label) {
if (!mount_fakelib_overlay(title_id, source_path, mount_path, label))
return false;
fakelib_layer_t *layer = &g_fakelib_mount.layers[g_fakelib_mount.layer_count++];
layer->label = label;
(void)strlcpy(layer->source_path, source_path, sizeof(layer->source_path));
(void)strlcpy(layer->mount_path, mount_path, sizeof(layer->mount_path));
return true;
}
static bool resolve_sandbox_context(const char *title_id,
char game_source_path[MAX_PATH],
char mount_path[MAX_PATH]) {
game_source_path[0] = '\0';
mount_path[0] = '\0';
char sandbox_id[MAX_TITLE_ID];
DIR *d = opendir("/mnt/sandbox");
if (!d)
return false;
size_t title_len = strlen(title_id);
int best_index = -1;
bool found = false;
struct dirent *entry;
while ((entry = readdir(d)) != NULL) {
if (entry->d_name[0] == '.')
continue;
if (strncmp(entry->d_name, title_id, title_len) != 0)
continue;
if (entry->d_name[title_len] != '_')
continue;
const char *suffix = entry->d_name + title_len + 1u;
if (!isdigit((unsigned char)suffix[0]))
continue;
if (strlen(entry->d_name) >= sizeof(sandbox_id))
continue;
errno = 0;
long idx_long = strtol(suffix, NULL, 10);
if (errno == ERANGE || idx_long > INT_MAX)
continue;
int idx = (int)idx_long;
if (idx < best_index)
continue;
best_index = idx;
(void)strlcpy(sandbox_id, entry->d_name, sizeof(sandbox_id));
found = true;
}
closedir(d);
if (!found)
return false;
char source_path[MAX_PATH];
snprintf(source_path, sizeof(source_path), "/mnt/sandbox/%s/app0/fakelib2",
sandbox_id);
struct stat st;
if (stat(source_path, &st) == 0 && S_ISDIR(st.st_mode)) {
(void)strlcpy(game_source_path, source_path, MAX_PATH);
} else {
snprintf(source_path, sizeof(source_path), "/mnt/sandbox/%s/app0/fakelib",
sandbox_id);
if (stat(source_path, &st) == 0 && S_ISDIR(st.st_mode))
(void)strlcpy(game_source_path, source_path, MAX_PATH);
}
char sandbox_root[MAX_PATH];
snprintf(sandbox_root, sizeof(sandbox_root), "/mnt/sandbox/%s", sandbox_id);
d = opendir(sandbox_root);
if (!d)
return false;
found = false;
while ((entry = readdir(d)) != NULL) {
if (entry->d_name[0] == '.')
continue;
if (strcmp(entry->d_name, "app0") == 0)
continue;
snprintf(mount_path, MAX_PATH, "%s/%s/common/lib", sandbox_root,
entry->d_name);
if (stat(mount_path, &st) != 0 || !S_ISDIR(st.st_mode))
continue;
found = true;
break;
}
closedir(d);
return found;
}
static bool resolve_global_fakelib_source(const char *title_id,
char source_path[MAX_PATH]) {
source_path[0] = '\0';
const runtime_config_t *cfg = runtime_config();
if (!cfg->global_fakelib_enabled)
return false;
if (is_global_fakelib_excluded_for_title(title_id))
return false;
struct stat st;
if (stat(cfg->global_fakelib_path, &st) != 0) {
if (errno != ENOENT)
log_debug(" [FAKELIB] global path unavailable for %s: %s (%s)",
title_id, cfg->global_fakelib_path, strerror(errno));
return false;
}
if (!S_ISDIR(st.st_mode)) {
log_debug(" [FAKELIB] global path is not a directory for %s: %s",
title_id, cfg->global_fakelib_path);
return false;
}
(void)strlcpy(source_path, cfg->global_fakelib_path, MAX_PATH);
return true;
}
static bool cleanup_fakelib_mount(void) {
if (g_fakelib_mount.layer_count == 0) {
memset(&g_fakelib_mount, 0, sizeof(g_fakelib_mount));
return true;
}
while (g_fakelib_mount.layer_count > 0) {
fakelib_layer_t *layer =
&g_fakelib_mount.layers[g_fakelib_mount.layer_count - 1];
if (!unmount_fakelib_overlay(layer))
return false;
memset(layer, 0, sizeof(*layer));
g_fakelib_mount.layer_count--;
}
memset(&g_fakelib_mount, 0, sizeof(g_fakelib_mount));
return true;
}
void sm_fakelib_game_on_exec(pid_t pid, const char *title_id) {
pthread_mutex_lock(&g_fakelib_mutex);
if (!sm_fakelib_game_feature_enabled()) {
pthread_mutex_unlock(&g_fakelib_mutex);
return;
}
if (g_fakelib_mount.layer_count > 0 && g_fakelib_mount.pid == pid) {
log_debug(" [FAKELIB] already tracking pid=%ld for %s", (long)pid,
title_id);
pthread_mutex_unlock(&g_fakelib_mutex);
return;
}
if (g_fakelib_mount.layer_count > 0 && g_fakelib_mount.pid != pid) {
log_debug(" [FAKELIB] handoff active mount pid=%ld -> pid=%ld (%s)",
(long)g_fakelib_mount.pid, (long)pid, title_id);
if (!cleanup_fakelib_mount()) {
log_debug(" [FAKELIB] handoff cleanup failed for pid=%ld, skipping %s",
(long)g_fakelib_mount.pid, title_id);
pthread_mutex_unlock(&g_fakelib_mutex);
return;
}
}
char game_source_path[MAX_PATH];
char global_source_path[MAX_PATH];
char mount_path[MAX_PATH];
if (!resolve_sandbox_context(title_id, game_source_path, mount_path)) {
pthread_mutex_unlock(&g_fakelib_mutex);
return;
}
bool has_global =
resolve_global_fakelib_source(title_id, global_source_path);
bool has_game = (game_source_path[0] != '\0');
if (!has_global && !has_game) {
pthread_mutex_unlock(&g_fakelib_mutex);
return;
}
memset(&g_fakelib_mount, 0, sizeof(g_fakelib_mount));
g_fakelib_mount.pid = pid;
(void)strlcpy(g_fakelib_mount.mount_path, mount_path,
sizeof(g_fakelib_mount.mount_path));
bool global_first = runtime_config()->global_fakelib_mount_first;
bool same_source =
has_global && has_game &&
strcmp(global_source_path, game_source_path) == 0;
if (global_first && has_global && !same_source) {
if (!track_fakelib_overlay(title_id, global_source_path, mount_path,
"global")) {
(void)cleanup_fakelib_mount();
pthread_mutex_unlock(&g_fakelib_mutex);
return;
}
}
if (has_game) {
if (!track_fakelib_overlay(title_id, game_source_path, mount_path,
"game")) {
(void)cleanup_fakelib_mount();
pthread_mutex_unlock(&g_fakelib_mutex);
return;
}
}
if (!global_first && has_global && !same_source) {
if (!track_fakelib_overlay(title_id, global_source_path, mount_path,
"global")) {
(void)cleanup_fakelib_mount();
pthread_mutex_unlock(&g_fakelib_mutex);
return;
}
}
if (has_game)
notify_system_info("Game backported: %s", title_id);
pthread_mutex_unlock(&g_fakelib_mutex);
}
void sm_fakelib_game_on_exit(pid_t pid) {
pthread_mutex_lock(&g_fakelib_mutex);
if (g_fakelib_mount.layer_count == 0 || g_fakelib_mount.pid != pid) {
pthread_mutex_unlock(&g_fakelib_mutex);
return;
}
log_debug(" [FAKELIB] game stopped: pid=%ld mount=%s", (long)pid,
g_fakelib_mount.mount_path);
cleanup_fakelib_mount();
pthread_mutex_unlock(&g_fakelib_mutex);
}
void sm_fakelib_game_shutdown(void) {
pthread_mutex_lock(&g_fakelib_mutex);
(void)cleanup_fakelib_mount();
pthread_mutex_unlock(&g_fakelib_mutex);
}
+1340
View File
File diff suppressed because it is too large. Load diff
+210
View File
@@ -0,0 +1,210 @@
#include "sm_platform.h"
#include "sm_game_cache.h"
#include "sm_config_mount.h"
#include "sm_filesystem.h"
#include "sm_image_cache.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_path_utils.h"
#include "sm_title_state.h"
struct GameCache {
char path[MAX_PATH];
char title_id[MAX_TITLE_ID];
char title_name[MAX_TITLE_NAME];
char owning_scan_root[MAX_PATH];
bool valid;
};
static struct GameCache g_game_cache[MAX_PENDING];
static bool game_cache_source_exists(const struct GameCache *entry) {
if (path_exists(entry->path))
return true;
if (!is_under_image_mount_base(entry->path))
return false;
char image_path[MAX_PATH];
return read_mount_image_link(entry->title_id, image_path,
sizeof(image_path)) &&
path_exists(image_path);
}
static bool resolve_game_cache_owning_scan_root(const char *path,
char owning_scan_root[MAX_PATH]) {
char resolved_source_path[MAX_PATH];
const char *match_path = path;
owning_scan_root[0] = '\0';
if (is_under_image_mount_base(path) &&
resolve_image_source_from_mount_cache(path, resolved_source_path,
sizeof(resolved_source_path))) {
match_path = resolved_source_path;
}
size_t best_match_len = 0;
for (int i = 0; i < get_scan_path_count(); i++) {
const char *scan_path = get_scan_path(i);
if (!path_matches_root_or_child(match_path, scan_path))
continue;
size_t scan_path_len = strlen(scan_path);
if (scan_path_len <= best_match_len)
continue;
(void)strlcpy(owning_scan_root, scan_path, MAX_PATH);
best_match_len = scan_path_len;
}
return owning_scan_root[0] != '\0';
}
static void write_game_cache_slot(struct GameCache *entry, const char *path,
const char *title_id,
const char *title_name,
const char *owning_scan_root) {
(void)strlcpy(entry->path, path, sizeof(entry->path));
(void)strlcpy(entry->title_id, title_id, sizeof(entry->title_id));
(void)strlcpy(entry->title_name, title_name, sizeof(entry->title_name));
(void)strlcpy(entry->owning_scan_root, owning_scan_root,
sizeof(entry->owning_scan_root));
entry->valid = true;
}
static bool ensure_game_cache_owning_scan_root(struct GameCache *entry) {
if (entry->owning_scan_root[0] != '\0')
return true;
return resolve_game_cache_owning_scan_root(entry->path, entry->owning_scan_root);
}
static void clear_game_cache_slot(int index, const char *reason) {
if (index < 0 || index >= MAX_PENDING || !g_game_cache[index].valid)
return;
if (reason && reason[0] != '\0') {
if (g_game_cache[index].title_id[0] != '\0')
log_debug(" [CACHE] %s: %s (%s)", reason, g_game_cache[index].title_id,
g_game_cache[index].path);
else
log_debug(" [CACHE] %s: %s", reason, g_game_cache[index].path);
}
if (g_game_cache[index].title_id[0] != '\0')
clear_duplicate_title_notification(g_game_cache[index].title_id);
memset(&g_game_cache[index], 0, sizeof(g_game_cache[index]));
}
void cache_game_entry(const char *path, const char *title_id,
const char *title_name) {
char owning_scan_root[MAX_PATH];
(void)resolve_game_cache_owning_scan_root(path, owning_scan_root);
for (int k = 0; k < MAX_PENDING; k++) {
if (!g_game_cache[k].valid)
continue;
if (strcmp(g_game_cache[k].path, path) != 0 &&
strcmp(g_game_cache[k].title_id, title_id) != 0) {
continue;
}
write_game_cache_slot(&g_game_cache[k], path, title_id, title_name,
owning_scan_root);
return;
}
for (int k = 0; k < MAX_PENDING; k++) {
if (!g_game_cache[k].valid) {
write_game_cache_slot(&g_game_cache[k], path, title_id, title_name,
owning_scan_root);
return;
}
}
}
void prune_game_cache(void) {
for (int k = 0; k < MAX_PENDING; k++) {
if (!g_game_cache[k].valid)
continue;
if (game_cache_source_exists(&g_game_cache[k]))
continue;
clear_game_cache_slot(k, "source removed");
}
}
void prune_game_cache_for_root(const char *root) {
if (!root || root[0] == '\0') {
prune_game_cache();
return;
}
for (int k = 0; k < MAX_PENDING; k++) {
if (!g_game_cache[k].valid)
continue;
const char *entry_root =
ensure_game_cache_owning_scan_root(&g_game_cache[k])
? g_game_cache[k].owning_scan_root
: g_game_cache[k].path;
if (!path_matches_root_or_child(entry_root, root))
continue;
if (game_cache_source_exists(&g_game_cache[k]))
continue;
clear_game_cache_slot(k, "source removed");
}
}
void for_each_cached_game_entry(const char *root, game_cache_iter_fn fn,
void *ctx) {
if (!fn)
return;
for (int k = 0; k < MAX_PENDING; k++) {
if (!g_game_cache[k].valid)
continue;
bool has_owning_scan_root = ensure_game_cache_owning_scan_root(&g_game_cache[k]);
const char *entry_root =
has_owning_scan_root ? g_game_cache[k].owning_scan_root : g_game_cache[k].path;
if (root && root[0] != '\0' && strcmp(entry_root, root) != 0) {
continue;
}
if (!fn(g_game_cache[k].path, g_game_cache[k].title_id,
g_game_cache[k].title_name,
has_owning_scan_root ? g_game_cache[k].owning_scan_root : NULL, ctx)) {
break;
}
}
}
bool find_cached_game(const char *path, const char *title_id,
const char **existing_path_out) {
if (existing_path_out)
*existing_path_out = NULL;
for (int k = 0; k < MAX_PENDING; k++) {
if (!g_game_cache[k].valid)
continue;
if (path && strcmp(g_game_cache[k].path, path) == 0) {
if (existing_path_out)
*existing_path_out = g_game_cache[k].path;
return true;
}
if (title_id && title_id[0] != '\0' &&
strcmp(g_game_cache[k].title_id, title_id) == 0) {
if (existing_path_out)
*existing_path_out = g_game_cache[k].path;
return true;
}
}
return false;
}
void clear_cached_game(const char *path) {
for (int k = 0; k < MAX_PENDING; k++) {
if (!g_game_cache[k].valid)
continue;
if (strcmp(g_game_cache[k].path, path) != 0)
continue;
clear_game_cache_slot(k, "removed from duplicate tracking");
}
}
+741
View File
@@ -0,0 +1,741 @@
#include "sm_platform.h"
#include <pthread.h>
#include <stdatomic.h>
#include <sys/event.h>
#include <sys/select.h>
#include "sm_appdb.h"
#include "sm_fakelib.h"
#include "sm_filesystem.h"
#include "sm_game_lifecycle.h"
#include "sm_kstuff.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_mdbg.h"
#include "sm_path_utils.h"
#include "sm_runtime.h"
#include "sm_scan.h"
#include "sm_scanner.h"
#include "sm_shellcore_service.h"
#include "sm_time.h"
#define MAX_PENDING_GAME_EXEC_CANDIDATES 32
typedef struct {
bool active;
pid_t pid;
uint64_t exec_time_us;
uint64_t deadline_us;
} pending_game_launch_t;
// SysCore can emit multiple child exec events before the actual game title_id
// becomes visible, so keep a small raw exec candidate buffer here.
static pending_game_launch_t
g_pending_game_launches[MAX_PENDING_GAME_EXEC_CANDIDATES];
static pthread_t g_game_lifecycle_thread;
static bool g_game_lifecycle_thread_started = false;
static volatile sig_atomic_t g_game_lifecycle_stop_requested = 0;
static int g_game_lifecycle_wake_pipe[2] = {-1, -1};
static pthread_mutex_t g_game_lifecycle_start_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t g_game_lifecycle_start_cond = PTHREAD_COND_INITIALIZER;
static bool g_game_lifecycle_start_ready = false;
static bool g_game_lifecycle_start_success = false;
static _Atomic pid_t g_active_game_pid = 0;
// Title storage is owned exclusively by the lifecycle thread. Other threads
// consume only the atomically published pid through the public state helper.
static char g_active_game_title_id[MAX_TITLE_ID];
static bool register_game_exit_watch(int kq, pid_t pid);
static void handle_game_exit(pid_t pid);
static void publish_active_game_pid(pid_t pid) {
pid_t previous_pid = atomic_exchange(&g_active_game_pid, pid);
if (previous_pid == pid)
return;
sm_scanner_wake();
}
static void publish_active_game(pid_t pid, const char *title_id) {
if (pid > 0 && title_id && title_id[0] != '\0')
(void)strlcpy(g_active_game_title_id, title_id,
sizeof(g_active_game_title_id));
else
g_active_game_title_id[0] = '\0';
publish_active_game_pid(pid);
}
static bool consume_active_game_title(pid_t pid,
char title_id_out[MAX_TITLE_ID]) {
if (pid <= 0 || atomic_load(&g_active_game_pid) != pid ||
g_active_game_title_id[0] == '\0')
return false;
(void)strlcpy(title_id_out, g_active_game_title_id, MAX_TITLE_ID);
g_active_game_title_id[0] = '\0';
return true;
}
static bool snapshot_active_game(pid_t pid, char title_id[MAX_TITLE_ID]) {
title_id[0] = '\0';
if (pid <= 0 || g_active_game_title_id[0] == '\0')
return false;
(void)strlcpy(title_id, g_active_game_title_id, MAX_TITLE_ID);
return true;
}
static bool title_is_usb_backed(const char *title_id, const char *source_path,
bool has_mount_link) {
char image_path[MAX_PATH];
if (read_mount_image_link(title_id, image_path, sizeof(image_path)) &&
is_usb_storage_path(image_path)) {
return true;
}
return has_mount_link && is_usb_storage_path(source_path);
}
static bool is_supported_title_id(const char *title_id) {
return strncmp(title_id, "PPSA", 4) == 0 || strncmp(title_id, "CUSA", 4) == 0;
}
static uint64_t min_nonzero_u64(uint64_t a, uint64_t b) {
if (a == 0)
return b;
if (b == 0)
return a;
return a < b ? a : b;
}
/*
static void bytes_to_hex(const void *src, size_t src_len, char *dst,
size_t dst_size) {
static const char hex_chars[] = "0123456789abcdef";
const uint8_t *bytes = (const uint8_t *)src;
size_t out_pos = 0;
if (!dst || dst_size == 0)
return;
for (size_t i = 0; i < src_len && out_pos + 2 < dst_size; ++i) {
dst[out_pos++] = hex_chars[(bytes[i] >> 4) & 0x0f];
dst[out_pos++] = hex_chars[bytes[i] & 0x0f];
}
dst[out_pos] = '\0';
}
static void log_app_info_response(pid_t pid, int rc, const app_info_t *appinfo) {
if (rc != 0) {
log_debug(" [GAME] sceKernelGetAppInfo pid=%ld failed: rc=0x%08X",
(long)pid, (unsigned)rc);
return;
}
if (!appinfo)
return;
char title_id[MAX_TITLE_ID];
size_t title_len = strnlen(appinfo->title_id, sizeof(appinfo->title_id));
if (title_len >= sizeof(title_id))
title_len = sizeof(title_id) - 1u;
memcpy(title_id, appinfo->title_id, title_len);
title_id[title_len] = '\0';
char unknown2_hex[sizeof(appinfo->unknown2) * 2u + 1u];
bytes_to_hex(appinfo->unknown2, sizeof(appinfo->unknown2), unknown2_hex,
sizeof(unknown2_hex));
log_debug(" [GAME] sceKernelGetAppInfo pid=%ld app_id=0x%08X "
"unknown1=0x%016llX title_id=\"%s\"",
(long)pid, appinfo->app_id,
(unsigned long long)appinfo->unknown1, title_id);
log_debug(" [GAME] sceKernelGetAppInfo pid=%ld unknown2=%s", (long)pid,
unknown2_hex);
}
*/
static bool resolve_game_title_id(pid_t pid, char title_id[MAX_TITLE_ID],
uint32_t *app_id_out) {
app_info_t appinfo;
memset(&appinfo, 0, sizeof(appinfo));
int rc = sceKernelGetAppInfo(pid, &appinfo);
/* log_app_info_response(pid, rc, &appinfo); */
if (rc != 0)
return false;
size_t title_len = strnlen(appinfo.title_id, sizeof(appinfo.title_id));
if (title_len == 0 || title_len >= MAX_TITLE_ID)
return false;
memcpy(title_id, appinfo.title_id, title_len);
title_id[title_len] = '\0';
if (app_id_out)
*app_id_out = appinfo.app_id;
return true;
}
static bool is_process_alive(pid_t pid) {
if (kill(pid, 0) == 0)
return true;
return errno != ESRCH;
}
static bool dispatch_game_launch(int kq, pid_t pid, uint64_t exec_time_us,
const char *title_id, uint32_t app_id) {
if (!is_data_mounted(title_id) &&
!sm_shellcore_ensure_title_runtime(title_id)) {
return false;
}
if (!register_game_exit_watch(kq, pid)) {
log_debug(" [GAME] skipping launch tracking for %s pid=%ld without exit watch",
title_id, (long)pid);
return false;
}
log_debug(" [GAME] started: %s pid=%ld app_id=0x%08X", title_id,
(long)pid, app_id);
publish_active_game(pid, title_id);
sm_kstuff_game_on_exec(pid, title_id, app_id, exec_time_us);
sm_fakelib_game_on_exec(pid, title_id);
return true;
}
static void clear_pending_game_launch(pending_game_launch_t *entry) {
memset(entry, 0, sizeof(*entry));
}
static void clear_all_pending_game_launches(void) {
memset(g_pending_game_launches, 0, sizeof(g_pending_game_launches));
}
static void close_game_lifecycle_wake_pipe(void) {
if (g_game_lifecycle_wake_pipe[0] >= 0) {
close(g_game_lifecycle_wake_pipe[0]);
g_game_lifecycle_wake_pipe[0] = -1;
}
if (g_game_lifecycle_wake_pipe[1] >= 0) {
close(g_game_lifecycle_wake_pipe[1]);
g_game_lifecycle_wake_pipe[1] = -1;
}
}
static void drain_game_lifecycle_wake_pipe(void) {
char drain_buf[32];
while (read(g_game_lifecycle_wake_pipe[0], drain_buf, sizeof(drain_buf)) > 0) {
}
}
static bool drain_game_lifecycle_events_nowait(int kq) {
struct kevent events[16];
struct timespec timeout;
memset(&timeout, 0, sizeof(timeout));
while (true) {
int nev = kevent(kq, NULL, 0, events, sizeof(events) / sizeof(events[0]),
&timeout);
if (nev < 0) {
if (errno == EINTR)
continue;
log_debug(" [GAME] stale event drain failed: %s", strerror(errno));
return false;
}
if (nev == 0)
return true;
for (int i = 0; i < nev; ++i) {
if (events[i].filter == EVFILT_PROC &&
(events[i].fflags & NOTE_EXIT) != 0) {
handle_game_exit((pid_t)events[i].ident);
}
}
}
}
static void set_game_lifecycle_start_result(bool success) {
pthread_mutex_lock(&g_game_lifecycle_start_mutex);
g_game_lifecycle_start_success = success;
g_game_lifecycle_start_ready = true;
pthread_cond_broadcast(&g_game_lifecycle_start_cond);
pthread_mutex_unlock(&g_game_lifecycle_start_mutex);
}
static pending_game_launch_t *find_pending_game_launch(pid_t pid) {
for (size_t i = 0; i < MAX_PENDING_GAME_EXEC_CANDIDATES; ++i) {
pending_game_launch_t *entry = &g_pending_game_launches[i];
if (entry->active && entry->pid == pid)
return entry;
}
return NULL;
}
static pending_game_launch_t *reserve_pending_game_launch(uint64_t now_us) {
pending_game_launch_t *oldest_entry = NULL;
for (size_t i = 0; i < MAX_PENDING_GAME_EXEC_CANDIDATES; ++i) {
pending_game_launch_t *entry = &g_pending_game_launches[i];
if (!entry->active)
return entry;
if (now_us != 0 && entry->deadline_us != 0 && now_us >= entry->deadline_us)
return entry;
if (!oldest_entry ||
(entry->deadline_us != 0 &&
(oldest_entry->deadline_us == 0 ||
entry->deadline_us < oldest_entry->deadline_us))) {
oldest_entry = entry;
}
}
return oldest_entry;
}
static pending_game_launch_t *queue_pending_game_launch(pid_t pid,
uint64_t exec_time_us,
uint64_t now_us) {
pending_game_launch_t *entry = find_pending_game_launch(pid);
if (!entry)
entry = reserve_pending_game_launch(now_us);
if (entry->active && entry->pid != pid) {
log_debug(" [GAME] replacing pending exec pid=%ld with pid=%ld",
(long)entry->pid, (long)pid);
}
uint64_t base_time_us = exec_time_us != 0 ? exec_time_us : now_us;
entry->active = true;
entry->pid = pid;
entry->exec_time_us = base_time_us;
entry->deadline_us =
base_time_us == 0 ? 0 : base_time_us + GAME_APPINFO_LOOKUP_TIMEOUT_US;
return entry;
}
static void defer_confirmed_game_launch_retry(pid_t pid, uint64_t exec_time_us,
uint64_t now_us,
const char *title_id) {
clear_all_pending_game_launches();
queue_pending_game_launch(pid, exec_time_us, now_us);
log_debug(" [GAME] deferring launch retry for %s pid=%ld", title_id,
(long)pid);
}
static uint64_t next_pending_game_wake_us(uint64_t now_us) {
uint64_t next_wake_us = 0;
for (size_t i = 0; i < MAX_PENDING_GAME_EXEC_CANDIDATES; ++i) {
pending_game_launch_t *entry = &g_pending_game_launches[i];
if (!entry->active)
continue;
if (now_us == 0) {
next_wake_us = min_nonzero_u64(next_wake_us, 1);
continue;
}
uint64_t poll_wake_us = now_us + GAME_LIFECYCLE_POLL_INTERVAL_US;
uint64_t entry_wake_us = min_nonzero_u64(entry->deadline_us, poll_wake_us);
next_wake_us = min_nonzero_u64(next_wake_us, entry_wake_us);
}
return next_wake_us;
}
static const struct timespec *compute_game_wait_timeout(
struct timespec *timeout_out) {
uint64_t now_us = monotonic_time_us();
uint64_t next_wake_us = 0;
next_wake_us = min_nonzero_u64(next_wake_us, next_pending_game_wake_us(now_us));
next_wake_us = min_nonzero_u64(next_wake_us, sm_kstuff_game_next_wake_us(now_us));
next_wake_us = min_nonzero_u64(next_wake_us, sm_mdbg_next_wake_us(now_us));
if (next_wake_us == 0)
return NULL;
uint64_t wait_us = GAME_LIFECYCLE_POLL_INTERVAL_US;
if (now_us != 0 && next_wake_us > now_us)
wait_us = next_wake_us - now_us;
else if (now_us != 0)
wait_us = 0;
timeout_out->tv_sec = (time_t)(wait_us / 1000000ull);
timeout_out->tv_nsec = (long)((wait_us % 1000000ull) * 1000ull);
return timeout_out;
}
static void poll_game_modules(int kq) {
uint64_t now_us = monotonic_time_us();
for (size_t i = 0; i < MAX_PENDING_GAME_EXEC_CANDIDATES; ++i) {
pending_game_launch_t *entry = &g_pending_game_launches[i];
if (!entry->active)
continue;
char title_id[MAX_TITLE_ID];
uint32_t app_id = 0;
if (resolve_game_title_id(entry->pid, title_id, &app_id)) {
pid_t pid = entry->pid;
uint64_t exec_time_us = entry->exec_time_us;
if (is_supported_title_id(title_id)) {
if (dispatch_game_launch(kq, pid, exec_time_us, title_id, app_id)) {
clear_pending_game_launch(entry);
clear_all_pending_game_launches();
break;
}
defer_confirmed_game_launch_retry(pid, exec_time_us, now_us, title_id);
break;
} else {
clear_pending_game_launch(entry);
}
} else if (!is_process_alive(entry->pid)) {
clear_pending_game_launch(entry);
}
if (entry->active && now_us != 0 && entry->deadline_us != 0 &&
now_us >= entry->deadline_us) {
log_debug(" [GAME] title_id was not available within %uus for pid=%ld",
(unsigned)GAME_APPINFO_LOOKUP_TIMEOUT_US, (long)entry->pid);
clear_pending_game_launch(entry);
}
}
sm_kstuff_game_poll();
sm_mdbg_poll();
}
static bool register_game_exit_watch(int kq, pid_t pid) {
struct kevent kev;
EV_SET(&kev, (uintptr_t)pid, EVFILT_PROC, EV_ADD | EV_ENABLE | EV_CLEAR,
NOTE_EXIT, 0, NULL);
if (kevent(kq, &kev, 1, NULL, 0, NULL) != 0) {
log_debug(" [GAME] failed to register exit watch for pid=%ld: %s",
(long)pid, strerror(errno));
return false;
}
return true;
}
static void handle_game_exec(int kq, pid_t pid) {
if (find_pending_game_launch(pid))
return;
uint64_t now_us = monotonic_time_us();
char title_id[MAX_TITLE_ID];
uint32_t app_id = 0;
if (resolve_game_title_id(pid, title_id, &app_id)) {
if (is_supported_title_id(title_id)) {
if (dispatch_game_launch(kq, pid, now_us, title_id, app_id))
clear_all_pending_game_launches();
else
defer_confirmed_game_launch_retry(pid, now_us, now_us, title_id);
}
return;
}
if (!is_process_alive(pid))
return;
queue_pending_game_launch(pid, now_us, now_us);
}
static void handle_game_exit(pid_t pid) {
char title_id[MAX_TITLE_ID] = {0};
bool had_active_title = consume_active_game_title(pid, title_id);
pending_game_launch_t *entry = find_pending_game_launch(pid);
if (entry)
clear_pending_game_launch(entry);
if (had_active_title)
publish_active_game_pid(0);
sm_fakelib_game_on_exit(pid);
sm_kstuff_game_on_exit(pid);
if (had_active_title) {
if (!sm_shellcore_release_title_runtime(title_id))
log_debug(" [SHELLCORE] runtime release deferred/failed: %s", title_id);
int snd0_updates = normalize_snd0info_for_title(title_id);
if (snd0_updates >= 0)
log_debug(" [DB] snd0info normalized after game exit rows=%d title=%s",
snd0_updates, title_id);
}
}
static bool terminate_usb_game_for_sleep(int kq, pid_t pid,
const char *title_id) {
if (kill(pid, SIGKILL) != 0 && errno != ESRCH) {
log_debug(" [SLEEP] failed to kill USB game: %s pid=%ld: %s", title_id,
(long)pid, strerror(errno));
return false;
}
log_debug(" [SLEEP] killing USB game before suspend: %s pid=%ld", title_id,
(long)pid);
uint64_t now_us = monotonic_time_us();
uint64_t deadline_us =
now_us == 0 ? 0 : now_us + GAME_SLEEP_EXIT_TIMEOUT_US;
while (!g_game_lifecycle_stop_requested && !should_stop_requested()) {
struct kevent event;
struct timespec timeout = {
.tv_sec = 0,
.tv_nsec = (long)GAME_LIFECYCLE_POLL_INTERVAL_US * 1000L,
};
int nev = kevent(kq, NULL, 0, &event, 1, &timeout);
if (nev < 0) {
if (errno == EINTR)
continue;
log_debug(" [SLEEP] NOTE_EXIT wait failed for %s pid=%ld: %s",
title_id, (long)pid, strerror(errno));
return false;
}
if (nev > 0) {
if (event.filter == EVFILT_READ &&
event.ident == (uintptr_t)g_game_lifecycle_wake_pipe[0]) {
drain_game_lifecycle_wake_pipe();
} else if (event.filter == EVFILT_PROC &&
(event.fflags & NOTE_EXIT) != 0) {
pid_t exited_pid = (pid_t)event.ident;
handle_game_exit(exited_pid);
if (exited_pid == pid) {
log_debug(" [SLEEP] USB game stopped: %s pid=%ld", title_id,
(long)pid);
return true;
}
}
}
now_us = monotonic_time_us();
if (deadline_us != 0 && now_us != 0 && now_us >= deadline_us) {
log_debug(" [SLEEP] timed out waiting NOTE_EXIT: %s pid=%ld", title_id,
(long)pid);
return false;
}
}
return false;
}
static void *game_lifecycle_watcher_main(void *arg) {
(void)arg;
pid_t syscore_pid = find_pid_by_name("SceSysCore.elf", false);
if (syscore_pid <= 0) {
log_debug(" [GAME] failed to find SceSysCore.elf");
set_game_lifecycle_start_result(false);
return NULL;
}
int kq = kqueue();
if (kq < 0) {
log_debug(" [GAME] kqueue failed: %s", strerror(errno));
set_game_lifecycle_start_result(false);
return NULL;
}
struct kevent kev;
EV_SET(&kev, (uintptr_t)syscore_pid, EVFILT_PROC, EV_ADD | EV_ENABLE | EV_CLEAR,
NOTE_FORK | NOTE_EXEC | NOTE_TRACK, 0, NULL);
if (kevent(kq, &kev, 1, NULL, 0, NULL) != 0) {
log_debug(" [GAME] proc watch registration failed: %s",
strerror(errno));
close(kq);
set_game_lifecycle_start_result(false);
return NULL;
}
EV_SET(&kev, (uintptr_t)g_game_lifecycle_wake_pipe[0], EVFILT_READ,
EV_ADD | EV_ENABLE | EV_CLEAR, 0, 0, NULL);
if (kevent(kq, &kev, 1, NULL, 0, NULL) != 0) {
log_debug(" [GAME] wake pipe registration failed: %s", strerror(errno));
close(kq);
set_game_lifecycle_start_result(false);
return NULL;
}
set_game_lifecycle_start_result(true);
log_debug(" [GAME] lifecycle watcher started");
bool sleep_cleanup_done = false;
pid_t suspended_game_pid = 0;
while (!g_game_lifecycle_stop_requested && !should_stop_requested()) {
if (runtime_sleep_mode_active()) {
if (!sleep_cleanup_done && sm_scanner_usb_watches_suspended()) {
suspended_game_pid = atomic_load(&g_active_game_pid);
char usb_game_title_id[MAX_TITLE_ID];
bool active_game = snapshot_active_game(suspended_game_pid,
usb_game_title_id);
char source_path[MAX_PATH];
bool has_mount_link =
active_game && read_mount_link(usb_game_title_id, source_path,
sizeof(source_path));
bool usb_game =
active_game && title_is_usb_backed(
usb_game_title_id, source_path, has_mount_link);
clear_all_pending_game_launches();
sm_fakelib_game_shutdown();
sm_kstuff_sleep_enter();
bool usb_cleanup_allowed = true;
if (usb_game) {
usb_cleanup_allowed = terminate_usb_game_for_sleep(
kq, suspended_game_pid, usb_game_title_id);
if (usb_cleanup_allowed) {
suspended_game_pid = 0;
}
}
if (usb_cleanup_allowed) {
runtime_mount_state_lock();
unmount_usb_sources_for_suspend();
runtime_mount_state_unlock();
} else {
log_debug("[SLEEP] USB cleanup skipped while game process remains");
}
// Keep a live non-USB game published while it is suspended so scanner
// work cannot race its unchanged runtime mount.
sleep_cleanup_done = true;
}
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(g_game_lifecycle_wake_pipe[0], &readfds);
int rc = select(g_game_lifecycle_wake_pipe[0] + 1, &readfds, NULL, NULL,
NULL);
if (rc < 0) {
if (errno == EINTR)
continue;
log_debug(" [GAME] sleep wait failed: %s", strerror(errno));
break;
}
drain_game_lifecycle_wake_pipe();
continue;
}
if (sleep_cleanup_done) {
if (!drain_game_lifecycle_events_nowait(kq))
break;
sleep_cleanup_done = false;
if (runtime_sleep_mode_active())
continue;
if (suspended_game_pid > 0 && is_process_alive(suspended_game_pid))
handle_game_exec(kq, suspended_game_pid);
sm_kstuff_sleep_leave();
suspended_game_pid = 0;
}
struct kevent event;
struct timespec timeout;
const struct timespec *timeout_ptr = compute_game_wait_timeout(&timeout);
int nev = kevent(kq, NULL, 0, &event, 1, timeout_ptr);
if (nev < 0) {
if (errno == EINTR)
continue;
if (g_game_lifecycle_stop_requested)
break;
log_debug(" [GAME] kevent wait failed: %s", strerror(errno));
break;
}
if (nev > 0) {
if (event.filter == EVFILT_READ &&
event.ident == (uintptr_t)g_game_lifecycle_wake_pipe[0]) {
drain_game_lifecycle_wake_pipe();
} else if ((event.fflags & NOTE_TRACKERR) != 0) {
log_debug(" [GAME] NOTE_TRACKERR for pid=%ld", (long)event.ident);
} else {
if ((event.fflags & NOTE_EXEC) != 0)
handle_game_exec(kq, (pid_t)event.ident);
if ((event.fflags & NOTE_EXIT) != 0)
handle_game_exit((pid_t)event.ident);
}
}
if (runtime_sleep_mode_active())
continue;
poll_game_modules(kq);
}
clear_all_pending_game_launches();
sm_fakelib_game_shutdown();
sm_kstuff_game_shutdown();
publish_active_game(0, NULL);
close(kq);
log_debug(" [GAME] lifecycle watcher stopped");
return NULL;
}
bool start_game_lifecycle_watcher(void) {
if (g_game_lifecycle_thread_started)
return true;
if (pipe(g_game_lifecycle_wake_pipe) != 0) {
log_debug(" [GAME] wake pipe creation failed: %s", strerror(errno));
return false;
}
if (!sm_set_fd_nonblocking(g_game_lifecycle_wake_pipe[0]) ||
!sm_set_fd_nonblocking(g_game_lifecycle_wake_pipe[1])) {
log_debug(" [GAME] wake pipe nonblocking setup failed: %s",
strerror(errno));
close_game_lifecycle_wake_pipe();
return false;
}
g_game_lifecycle_stop_requested = 0;
pthread_mutex_lock(&g_game_lifecycle_start_mutex);
g_game_lifecycle_start_ready = false;
g_game_lifecycle_start_success = false;
pthread_mutex_unlock(&g_game_lifecycle_start_mutex);
int rc =
pthread_create(&g_game_lifecycle_thread, NULL, game_lifecycle_watcher_main,
NULL);
if (rc != 0) {
log_debug(" [GAME] watcher start failed: %s", strerror(rc));
close_game_lifecycle_wake_pipe();
return false;
}
pthread_mutex_lock(&g_game_lifecycle_start_mutex);
while (!g_game_lifecycle_start_ready)
pthread_cond_wait(&g_game_lifecycle_start_cond, &g_game_lifecycle_start_mutex);
bool start_success = g_game_lifecycle_start_success;
pthread_mutex_unlock(&g_game_lifecycle_start_mutex);
if (!start_success) {
(void)pthread_join(g_game_lifecycle_thread, NULL);
close_game_lifecycle_wake_pipe();
return false;
}
g_game_lifecycle_thread_started = true;
return true;
}
void wake_game_lifecycle_watcher(void) {
if (g_game_lifecycle_wake_pipe[1] < 0)
return;
char wake = 'x';
ssize_t written = write(g_game_lifecycle_wake_pipe[1], &wake, sizeof(wake));
if (written < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
log_debug(" [GAME] wake pipe write failed: %s", strerror(errno));
}
}
void stop_game_lifecycle_watcher(void) {
if (!g_game_lifecycle_thread_started)
return;
g_game_lifecycle_stop_requested = 1;
wake_game_lifecycle_watcher();
(void)pthread_join(g_game_lifecycle_thread, NULL);
g_game_lifecycle_thread_started = false;
g_game_lifecycle_stop_requested = 0;
publish_active_game(0, NULL);
close_game_lifecycle_wake_pipe();
}
bool sm_game_lifecycle_has_active_game(void) {
return atomic_load(&g_active_game_pid) > 0;
}
bool refresh_game_lifecycle_watcher(void) {
if (!g_game_lifecycle_thread_started)
return start_game_lifecycle_watcher();
wake_game_lifecycle_watcher();
return true;
}
+113
View File
@@ -0,0 +1,113 @@
#include "sm_platform.h"
#include "sm_gameinfo.h"
#include "sm_limits.h"
#include "sm_path_state.h"
// --- Game Metadata Parsing (param.json) ---
static int extract_json_string(const char *json, const char *key, char *out,
size_t out_size) {
char search[64];
snprintf(search, sizeof(search), "\"%s\"", key);
const char *p = strstr(json, search);
if (!p)
return -1;
p = strchr(p + strlen(search), ':');
if (!p)
return -2;
while (*++p && isspace(*p)) {
/* skip */
}
if (*p != '"')
return -3;
p++;
size_t i = 0;
while (i < out_size - 1 && p[i] && p[i] != '"') {
out[i] = p[i];
i++;
}
out[i] = '\0';
return 0;
}
bool get_game_info(const char *base_path, const struct stat *param_st,
char *out_id, char *out_name) {
out_id[0] = '\0';
out_name[0] = '\0';
if (!S_ISREG(param_st->st_mode))
return false;
bool cached_valid = false;
if (load_cached_game_info(base_path, param_st, out_id, out_name, &cached_valid))
return cached_valid;
if (param_st->st_size <= 0 || param_st->st_size > MAX_PARAM_JSON_SIZE) {
store_cached_game_info(base_path, param_st, false, "", "");
return false;
}
char path[MAX_PATH];
int written = snprintf(path, sizeof(path), "%s/sce_sys/param.json",
base_path);
if (written < 0 || (size_t)written >= sizeof(path)) {
store_cached_game_info(base_path, param_st, false, "", "");
return false;
}
FILE *f = fopen(path, "rb");
if (!f) {
store_cached_game_info(base_path, param_st, false, "", "");
return false;
}
size_t len = (size_t)param_st->st_size;
char *buf = (char *)malloc(len + 1);
if (!buf) {
fclose(f);
return false;
}
bool read_ok = (fread(buf, 1, len, f) == len);
fclose(f);
if (!read_ok) {
free(buf);
return false;
}
buf[len] = '\0';
bool valid = false;
int res = extract_json_string(buf, "titleId", out_id, MAX_TITLE_ID);
if (res != 0)
res = extract_json_string(buf, "title_id", out_id, MAX_TITLE_ID);
if (res == 0) {
const char *en_ptr = strstr(buf, "\"en-US\"");
const char *search_start = en_ptr ? en_ptr : buf;
if (extract_json_string(search_start, "titleName", out_name,
MAX_TITLE_NAME) != 0)
extract_json_string(buf, "titleName", out_name, MAX_TITLE_NAME);
if (out_name[0] == '\0')
(void)strlcpy(out_name, out_id, MAX_TITLE_NAME);
valid = true;
}
free(buf);
store_cached_game_info(base_path, param_st, valid, out_id, out_name);
return valid;
}
bool directory_has_param_json(const char *dir_path, struct stat *param_st_out) {
int dir_fd = open(dir_path, O_RDONLY | O_DIRECTORY);
if (dir_fd < 0)
return false;
struct stat st;
if (fstatat(dir_fd, "sce_sys", &st, 0) == 0 && S_ISDIR(st.st_mode) &&
fstatat(dir_fd, "sce_sys/param.json", &st, 0) == 0 &&
S_ISREG(st.st_mode)) {
if (param_st_out)
*param_st_out = st;
close(dir_fd);
return true;
}
close(dir_fd);
return false;
}
+1498
View File
File diff suppressed because it is too large. Load diff
+256
View File
@@ -0,0 +1,256 @@
#include "sm_platform.h"
#include <pthread.h>
#include "sm_image_cache.h"
#include "sm_limits.h"
struct ImageCache {
char path[MAX_PATH];
char mount_point[MAX_PATH];
int unit_id;
attach_backend_t backend;
bool valid;
};
static struct ImageCache g_image_cache[MAX_IMAGE_MOUNTS];
static pthread_mutex_t g_image_cache_mutex = PTHREAD_MUTEX_INITIALIZER;
static void copy_cache_entry(int index, image_cache_entry_t *entry_out) {
memset(entry_out, 0, sizeof(*entry_out));
(void)strlcpy(entry_out->path, g_image_cache[index].path,
sizeof(entry_out->path));
(void)strlcpy(entry_out->mount_point, g_image_cache[index].mount_point,
sizeof(entry_out->mount_point));
entry_out->unit_id = g_image_cache[index].unit_id;
entry_out->backend = g_image_cache[index].backend;
}
static int find_cache_index(const char *path, const char *mount_point) {
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
if (!g_image_cache[k].valid)
continue;
if (mount_point && strcmp(g_image_cache[k].mount_point, mount_point) == 0) {
return k;
}
if (path && strcmp(g_image_cache[k].path, path) == 0)
return k;
}
return -1;
}
static int upsert_image_source_mapping(const char *path, const char *mount_point) {
int entry_index = find_cache_index(path, mount_point);
if (entry_index >= 0) {
struct ImageCache *entry = &g_image_cache[entry_index];
(void)strlcpy(entry->path, path, sizeof(entry->path));
(void)strlcpy(entry->mount_point, mount_point, sizeof(entry->mount_point));
return entry_index;
}
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
if (!g_image_cache[k].valid) {
(void)strlcpy(g_image_cache[k].path, path, sizeof(g_image_cache[k].path));
(void)strlcpy(g_image_cache[k].mount_point, mount_point,
sizeof(g_image_cache[k].mount_point));
g_image_cache[k].unit_id = -1;
g_image_cache[k].backend = ATTACH_BACKEND_NONE;
g_image_cache[k].valid = true;
return k;
}
}
return -1;
}
bool cache_image_source_mapping(const char *path, const char *mount_point) {
pthread_mutex_lock(&g_image_cache_mutex);
int entry_index = find_cache_index(path, mount_point);
if (entry_index >= 0) {
const struct ImageCache *entry = &g_image_cache[entry_index];
// A metadata-only path mapping must never retarget a live mount entry.
// The live entry owns its actual filesystem mount point until release.
if (entry->backend != ATTACH_BACKEND_NONE && entry->unit_id >= 0 &&
(strcmp(entry->path, path) != 0 ||
strcmp(entry->mount_point, mount_point) != 0)) {
pthread_mutex_unlock(&g_image_cache_mutex);
return true;
}
}
bool ok = upsert_image_source_mapping(path, mount_point) >= 0;
pthread_mutex_unlock(&g_image_cache_mutex);
return ok;
}
bool cache_image_mount(const char *path, const char *mount_point, int unit_id,
attach_backend_t backend) {
pthread_mutex_lock(&g_image_cache_mutex);
int entry_index = upsert_image_source_mapping(path, mount_point);
if (entry_index < 0) {
pthread_mutex_unlock(&g_image_cache_mutex);
return false;
}
struct ImageCache *entry = &g_image_cache[entry_index];
entry->unit_id = unit_id;
entry->backend = backend;
pthread_mutex_unlock(&g_image_cache_mutex);
return true;
}
bool get_image_cache_entry(int index, image_cache_entry_t *entry_out) {
pthread_mutex_lock(&g_image_cache_mutex);
if (index < 0 || index >= MAX_IMAGE_MOUNTS || !g_image_cache[index].valid) {
pthread_mutex_unlock(&g_image_cache_mutex);
return false;
}
copy_cache_entry(index, entry_out);
pthread_mutex_unlock(&g_image_cache_mutex);
return true;
}
bool find_image_cache_entry(const char *path, image_cache_entry_t *entry_out,
int *index_out) {
if (!path || !entry_out)
return false;
pthread_mutex_lock(&g_image_cache_mutex);
int index = find_cache_index(path, NULL);
if (index < 0) {
pthread_mutex_unlock(&g_image_cache_mutex);
return false;
}
copy_cache_entry(index, entry_out);
if (index_out)
*index_out = index;
pthread_mutex_unlock(&g_image_cache_mutex);
return true;
}
void invalidate_image_cache_entry(int index) {
pthread_mutex_lock(&g_image_cache_mutex);
if (index < 0 || index >= MAX_IMAGE_MOUNTS) {
pthread_mutex_unlock(&g_image_cache_mutex);
return;
}
memset(&g_image_cache[index], 0, sizeof(g_image_cache[index]));
pthread_mutex_unlock(&g_image_cache_mutex);
}
bool resolve_device_from_mount_cache(const char *mount_point,
attach_backend_t *backend_out,
int *unit_out) {
pthread_mutex_lock(&g_image_cache_mutex);
int entry_index = find_cache_index(NULL, mount_point);
if (entry_index < 0) {
pthread_mutex_unlock(&g_image_cache_mutex);
return false;
}
const struct ImageCache *entry = &g_image_cache[entry_index];
if (entry->backend == ATTACH_BACKEND_NONE || entry->unit_id < 0) {
pthread_mutex_unlock(&g_image_cache_mutex);
return false;
}
*backend_out = entry->backend;
*unit_out = entry->unit_id;
pthread_mutex_unlock(&g_image_cache_mutex);
return true;
}
bool resolve_image_source_from_mount_cache(const char *mount_point,
char *path_out,
size_t path_out_size) {
pthread_mutex_lock(&g_image_cache_mutex);
int entry_index = find_cache_index(NULL, mount_point);
if (entry_index < 0) {
pthread_mutex_unlock(&g_image_cache_mutex);
return false;
}
const struct ImageCache *entry = &g_image_cache[entry_index];
path_out[0] = '\0';
(void)strlcpy(path_out, entry->path, path_out_size);
pthread_mutex_unlock(&g_image_cache_mutex);
return true;
}
static int find_owning_cache_index(const char *path) {
int best_index = -1;
size_t best_length = 0;
for (int i = 0; i < MAX_IMAGE_MOUNTS; ++i) {
if (!g_image_cache[i].valid)
continue;
size_t length = strlen(g_image_cache[i].mount_point);
if (length <= best_length || strncmp(path, g_image_cache[i].mount_point,
length) != 0) {
continue;
}
if (path[length] != '\0' && path[length] != '/')
continue;
best_index = i;
best_length = length;
}
return best_index;
}
static size_t find_owning_chain_indices(const char *path,
int indices[MAX_IMAGE_CHAIN_DEPTH]) {
char current[MAX_PATH];
(void)strlcpy(current, path, sizeof(current));
size_t count = 0;
while (count < MAX_IMAGE_CHAIN_DEPTH) {
int index = find_owning_cache_index(current);
if (index < 0)
break;
indices[count++] = index;
(void)strlcpy(current, g_image_cache[index].path, sizeof(current));
}
return count;
}
bool resolve_owning_image_source_from_mount_cache(const char *path,
char *path_out,
size_t path_out_size) {
if (!path || !path_out || path_out_size == 0)
return false;
pthread_mutex_lock(&g_image_cache_mutex);
int index = find_owning_cache_index(path);
if (index >= 0)
(void)strlcpy(path_out, g_image_cache[index].path, path_out_size);
pthread_mutex_unlock(&g_image_cache_mutex);
return index >= 0;
}
size_t resolve_image_source_chain_from_mount_cache(
const char *path,
char chain[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH]) {
if (!path || !chain)
return 0;
int indices[MAX_IMAGE_CHAIN_DEPTH];
pthread_mutex_lock(&g_image_cache_mutex);
size_t count = find_owning_chain_indices(path, indices);
for (size_t i = 0; i < count; ++i)
(void)strlcpy(chain[i], g_image_cache[indices[count - i - 1u]].path,
MAX_PATH);
pthread_mutex_unlock(&g_image_cache_mutex);
return count;
}
bool resolve_outermost_image_source_from_mount_cache(
const char *path, char *path_out, size_t path_out_size) {
if (!path || !path_out || path_out_size == 0)
return false;
int indices[MAX_IMAGE_CHAIN_DEPTH];
pthread_mutex_lock(&g_image_cache_mutex);
size_t count = find_owning_chain_indices(path, indices);
if (count > 0)
(void)strlcpy(path_out, g_image_cache[indices[count - 1u]].path,
path_out_size);
pthread_mutex_unlock(&g_image_cache_mutex);
return count > 0;
}
+310
View File
@@ -0,0 +1,310 @@
#include "sm_platform.h"
#include "sm_appdb.h"
#include "sm_filesystem.h"
#include "sm_image_cache.h"
#include "sm_image_index.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_path_utils.h"
#include "sm_paths.h"
#define IMAGE_INDEX_FILE IMAGE_INDEX_FILE_PATH
#define IMAGE_INDEX_TEMP_FILE LOG_DIR "/image_index.bin.tmp"
#define IMAGE_INDEX_MAGIC 0x58444953u
#define IMAGE_INDEX_VERSION 3u
typedef struct {
char path[MAX_PATH];
int64_t size;
int64_t mtime_sec;
int32_t mtime_nsec;
uint8_t complete;
uint8_t valid;
uint8_t reserved[2];
} image_index_entry_t;
typedef struct {
uint16_t image_index;
uint8_t valid;
uint8_t reserved;
char title_id[MAX_TITLE_ID];
} image_index_title_t;
typedef struct {
uint32_t magic;
uint32_t version;
uint32_t entry_capacity;
uint32_t title_capacity;
} image_index_header_t;
static image_index_entry_t g_image_index[MAX_IMAGE_MOUNTS];
static image_index_title_t g_image_titles[MAX_IMAGE_TITLES];
static bool g_image_index_loaded;
static bool g_image_index_dirty;
static bool stamp_matches(const image_index_entry_t *entry,
const struct stat *st) {
return entry->size == (int64_t)st->st_size &&
entry->mtime_sec == (int64_t)st->st_mtim.tv_sec &&
entry->mtime_nsec == (int32_t)st->st_mtim.tv_nsec;
}
static int find_entry(const char *path) {
for (int i = 0; i < MAX_IMAGE_MOUNTS; ++i) {
if (g_image_index[i].valid && strcmp(g_image_index[i].path, path) == 0)
return i;
}
return -1;
}
static void clear_entry_titles(int entry_index) {
for (int i = 0; i < MAX_IMAGE_TITLES; ++i) {
if (g_image_titles[i].valid &&
g_image_titles[i].image_index == (uint16_t)entry_index) {
memset(&g_image_titles[i], 0, sizeof(g_image_titles[i]));
}
}
}
static void clear_entry(int entry_index) {
clear_entry_titles(entry_index);
memset(&g_image_index[entry_index], 0, sizeof(g_image_index[entry_index]));
g_image_index_dirty = true;
}
static bool save_index(void) {
if (!g_image_index_dirty)
return true;
FILE *file = fopen(IMAGE_INDEX_TEMP_FILE, "wb");
if (!file)
return false;
image_index_header_t header = {
.magic = IMAGE_INDEX_MAGIC,
.version = IMAGE_INDEX_VERSION,
.entry_capacity = MAX_IMAGE_MOUNTS,
.title_capacity = MAX_IMAGE_TITLES,
};
bool ok = fwrite(&header, sizeof(header), 1, file) == 1 &&
fwrite(g_image_index, sizeof(g_image_index), 1, file) == 1 &&
fwrite(g_image_titles, sizeof(g_image_titles), 1, file) == 1 &&
fflush(file) == 0;
if (ok && fsync(fileno(file)) != 0)
ok = false;
if (fclose(file) != 0)
ok = false;
if (!ok || rename(IMAGE_INDEX_TEMP_FILE, IMAGE_INDEX_FILE) != 0) {
int saved_errno = errno;
if (saved_errno == 0)
saved_errno = EIO;
(void)unlink(IMAGE_INDEX_TEMP_FILE);
errno = saved_errno;
return false;
}
g_image_index_dirty = false;
return true;
}
static bool loaded_index_is_valid(void) {
for (int i = 0; i < MAX_IMAGE_MOUNTS; ++i) {
const image_index_entry_t *entry = &g_image_index[i];
if (!entry->valid)
continue;
if (entry->complete > 1 || entry->path[0] == '\0' ||
memchr(entry->path, '\0', sizeof(entry->path)) == NULL) {
return false;
}
}
for (int i = 0; i < MAX_IMAGE_TITLES; ++i) {
const image_index_title_t *title = &g_image_titles[i];
if (!title->valid)
continue;
if (title->image_index >= MAX_IMAGE_MOUNTS ||
!g_image_index[title->image_index].valid ||
title->title_id[0] == '\0' ||
memchr(title->title_id, '\0', sizeof(title->title_id)) == NULL) {
return false;
}
}
return true;
}
static void load_index(void) {
if (g_image_index_loaded)
return;
g_image_index_loaded = true;
FILE *file = fopen(IMAGE_INDEX_FILE, "rb");
if (!file)
return;
image_index_header_t header;
bool ok = fread(&header, sizeof(header), 1, file) == 1 &&
header.magic == IMAGE_INDEX_MAGIC &&
header.version == IMAGE_INDEX_VERSION &&
header.entry_capacity == MAX_IMAGE_MOUNTS &&
header.title_capacity == MAX_IMAGE_TITLES &&
fread(g_image_index, sizeof(g_image_index), 1, file) == 1 &&
fread(g_image_titles, sizeof(g_image_titles), 1, file) == 1;
(void)fclose(file);
if (ok)
ok = loaded_index_is_valid();
if (!ok) {
memset(g_image_index, 0, sizeof(g_image_index));
memset(g_image_titles, 0, sizeof(g_image_titles));
(void)unlink(IMAGE_INDEX_FILE);
log_debug(" [IMGIDX] invalid cache discarded: %s", IMAGE_INDEX_FILE);
}
}
static int reserve_entry(const char *path) {
int entry_index = find_entry(path);
if (entry_index >= 0)
return entry_index;
for (int i = 0; i < MAX_IMAGE_MOUNTS; ++i) {
if (!g_image_index[i].valid) {
g_image_index[i].valid = 1;
(void)strlcpy(g_image_index[i].path, path, sizeof(g_image_index[i].path));
return i;
}
}
return -1;
}
static bool cached_titles_ready(int entry_index,
const struct AppDbTitleList *app_db_titles,
bool app_db_titles_ready) {
const char *image_path = g_image_index[entry_index].path;
for (int i = 0; i < MAX_IMAGE_TITLES; ++i) {
if (!g_image_titles[i].valid ||
g_image_titles[i].image_index != (uint16_t)entry_index) {
continue;
}
const char *title_id = g_image_titles[i].title_id;
if (!is_installed(title_id) || !has_appmeta_data(title_id))
return false;
char runtime_path[MAX_PATH];
char linked_image[MAX_PATH];
if (!read_mount_link(title_id, runtime_path, sizeof(runtime_path)) ||
!is_under_image_mount_base(runtime_path) ||
!read_mount_image_link(title_id, linked_image, sizeof(linked_image)) ||
strcmp(linked_image, image_path) != 0) {
return false;
}
if (app_db_titles_ready &&
!app_db_title_list_contains(app_db_titles, title_id)) {
return false;
}
}
return true;
}
bool sm_image_index_needs_scan(const char *path, const struct stat *st,
const struct AppDbTitleList *app_db_titles,
bool app_db_titles_ready) {
load_index();
int entry_index = find_entry(path);
if (entry_index < 0 || !g_image_index[entry_index].complete ||
!stamp_matches(&g_image_index[entry_index], st)) {
return true;
}
return !cached_titles_ready(entry_index, app_db_titles, app_db_titles_ready);
}
void sm_image_index_begin_scan(const char *path, const struct stat *st) {
load_index();
int entry_index = reserve_entry(path);
if (entry_index < 0) {
log_debug(" [IMGIDX] cache full, scanning without fingerprint: %s", path);
return;
}
clear_entry_titles(entry_index);
image_index_entry_t *entry = &g_image_index[entry_index];
entry->size = (int64_t)st->st_size;
entry->mtime_sec = (int64_t)st->st_mtim.tv_sec;
entry->mtime_nsec = (int32_t)st->st_mtim.tv_nsec;
entry->complete = 0;
entry->valid = 1;
g_image_index_dirty = true;
}
bool sm_image_index_record_game(const char *game_path, const char *title_id) {
load_index();
char image_path[MAX_PATH];
if (!resolve_outermost_image_source_from_mount_cache(
game_path, image_path, sizeof(image_path))) {
return true;
}
int entry_index = find_entry(image_path);
if (entry_index < 0) {
struct stat st;
if (stat(image_path, &st) != 0)
return false;
sm_image_index_begin_scan(image_path, &st);
entry_index = find_entry(image_path);
if (entry_index < 0)
return false;
}
char tracked_path[MAX_PATH];
char linked_image[MAX_PATH];
if (is_installed(title_id) &&
read_mount_link(title_id, tracked_path, sizeof(tracked_path)) &&
strcmp(tracked_path, game_path) == 0 &&
(!read_mount_image_link(title_id, linked_image, sizeof(linked_image)) ||
strcmp(linked_image, image_path) != 0) &&
!write_mount_image_link(title_id, image_path)) {
log_debug(" [IMGIDX] failed to repair image link: %s -> %s", title_id,
image_path);
}
for (int i = 0; i < MAX_IMAGE_TITLES; ++i) {
if (g_image_titles[i].valid &&
g_image_titles[i].image_index == (uint16_t)entry_index &&
strcmp(g_image_titles[i].title_id, title_id) == 0) {
return true;
}
}
for (int i = 0; i < MAX_IMAGE_TITLES; ++i) {
if (g_image_titles[i].valid)
continue;
g_image_titles[i].valid = 1;
g_image_titles[i].image_index = (uint16_t)entry_index;
(void)strlcpy(g_image_titles[i].title_id, title_id,
sizeof(g_image_titles[i].title_id));
g_image_index_dirty = true;
return true;
}
log_debug(" [IMGIDX] title cache full: %s", title_id);
return false;
}
void sm_image_index_complete_scan(const char *path) {
load_index();
int entry_index = find_entry(path);
if (entry_index < 0)
return;
image_index_entry_t *entry = &g_image_index[entry_index];
if (!entry->complete) {
entry->complete = 1;
g_image_index_dirty = true;
}
}
void sm_image_index_flush(void) {
load_index();
if (!save_index())
log_debug(" [IMGIDX] failed to persist scan index: %s", strerror(errno));
}
void sm_image_index_prune(void) {
struct stat st;
load_index();
for (int i = 0; i < MAX_IMAGE_MOUNTS; ++i) {
if (g_image_index[i].valid && stat(g_image_index[i].path, &st) != 0 &&
errno == ENOENT) {
clear_entry(i);
}
}
if (!save_index())
log_debug(" [IMGIDX] failed to persist prune: %s", strerror(errno));
}
+454
View File
@@ -0,0 +1,454 @@
#include "sm_platform.h"
#include "sm_config_mount.h"
#include "sm_runtime.h"
#include "sm_install.h"
#include "sm_install_queue.h"
#include "sm_types.h"
#include "sm_game_cache.h"
#include "sm_log.h"
#include "sm_filesystem.h"
#include "sm_limits.h"
#include "sm_path_utils.h"
#include "sm_appdb.h"
#include "sm_title_state.h"
#include "sm_image_cache.h"
#include "sm_paths.h"
#include "sm_manual.h"
#include "sm_shellcore_hooks.h"
#include <dlfcn.h>
typedef int (*app_install_title_dir_fn_t)(const char *title_id,
const char *install_path,
void *reserved);
static const char *const k_app_inst_util_sprx_path =
"/system/common/lib/libSceAppInstUtil.sprx";
static const char *const k_app_install_title_dir_symbol =
"sceAppInstUtilAppInstallTitleDir";
static app_install_title_dir_fn_t g_app_install_title_dir_fn = NULL;
static bool g_app_install_title_dir_resolve_attempted = false;
static app_install_title_dir_fn_t resolve_app_install_title_dir(void) {
if (g_app_install_title_dir_fn)
return g_app_install_title_dir_fn;
if (g_app_install_title_dir_resolve_attempted)
return NULL;
if (sm_firmware_major_version() >= 12u) {
g_app_install_title_dir_resolve_attempted = true;
return NULL;
}
g_app_install_title_dir_resolve_attempted = true;
void *app_inst_util_handle = dlopen(k_app_inst_util_sprx_path, RTLD_LAZY);
if (!app_inst_util_handle) {
const char *error = dlerror();
log_debug(" [REG] dlopen(%s) failed: %s", k_app_inst_util_sprx_path,
error ? error : "unknown");
return NULL;
}
dlerror();
g_app_install_title_dir_fn = (app_install_title_dir_fn_t)dlsym(
app_inst_util_handle, k_app_install_title_dir_symbol);
if (g_app_install_title_dir_fn)
return g_app_install_title_dir_fn;
const char *error = dlerror();
log_debug(" [REG] %s lookup in %s failed: %s",
k_app_install_title_dir_symbol, k_app_inst_util_sprx_path,
error ? error : "unknown");
return NULL;
}
static bool is_appmeta_file(const char *name) {
if (!name)
return false;
if (strcasecmp(name, "param.json") == 0 || strcasecmp(name, "param.sfo") == 0)
return true;
const char *ext = strrchr(name, '.');
if (!ext)
return false;
return (strcasecmp(ext, ".png") == 0 || strcasecmp(ext, ".dds") == 0 ||
strcasecmp(ext, ".at9") == 0);
}
static bool copy_sce_sys_to_appmeta(const char *src_sce_sys,
const char *user_appmeta_dir) {
DIR *d = opendir(src_sce_sys);
if (!d)
return false;
bool ok = true;
struct dirent *e;
char src_path[MAX_PATH];
char dst_path[MAX_PATH];
struct stat st;
while ((e = readdir(d)) != NULL) {
if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0)
continue;
if (!is_appmeta_file(e->d_name))
continue;
snprintf(src_path, sizeof(src_path), "%s/%s", src_sce_sys, e->d_name);
if (stat(src_path, &st) != 0 || !S_ISREG(st.st_mode))
continue;
snprintf(dst_path, sizeof(dst_path), "%s/%s", user_appmeta_dir, e->d_name);
if (copy_file(src_path, dst_path) != 0) {
ok = false;
break;
}
}
if (closedir(d) != 0)
ok = false;
return ok;
}
static bool copy_optional_trophy_metadata_file(const char *src_sce_sys,
const char *dst_base,
const char *relative_path) {
char src_path[MAX_PATH];
char dst_path[MAX_PATH];
snprintf(src_path, sizeof(src_path), "%s/%s", src_sce_sys, relative_path);
if (!path_exists(src_path))
return true;
snprintf(dst_path, sizeof(dst_path), "%s/%s", dst_base, relative_path);
if (path_exists(dst_path))
return true;
if (copy_file(src_path, dst_path) == 0)
return true;
log_debug(" [COPY] Failed to copy trophy metadata: %s -> %s", src_path,
dst_path);
return false;
}
static bool update_trophy_metadata(const char *title_id,
const char *src_sce_sys) {
char dst_base[MAX_PATH];
char dst_dir[MAX_PATH];
bool ok = true;
snprintf(dst_base, sizeof(dst_base), "/system_data/priv/appmeta/%s",
title_id);
mkdir("/system_data/priv/appmeta", 0755);
mkdir(dst_base, 0755);
snprintf(dst_dir, sizeof(dst_dir), "%s/trophy2", dst_base);
mkdir(dst_dir, 0755);
snprintf(dst_dir, sizeof(dst_dir), "%s/uds", dst_base);
mkdir(dst_dir, 0755);
if (!copy_optional_trophy_metadata_file(src_sce_sys, dst_base,
"trophy2/npbind.dat"))
ok = false;
if (!copy_optional_trophy_metadata_file(src_sce_sys, dst_base,
"uds/npbind.dat"))
ok = false;
if (!copy_optional_trophy_metadata_file(src_sce_sys, dst_base, "param.json"))
ok = false;
return ok;
}
// --- Install/Remount Action ---
static bool mount_and_install(const char *src_path, const char *title_id,
const char *title_name, bool is_remount,
bool should_register,
bool use_app_install_all,
bool *has_src_snd0_out) {
char user_appmeta_dir[MAX_PATH];
char user_app_dir[MAX_PATH];
char user_sce_sys[MAX_PATH];
char src_sce_sys[MAX_PATH];
char src_snd0[MAX_PATH];
char image_source_chain[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH];
size_t image_source_count = 0;
bool has_image_source = false;
bool appmeta_missing = false;
bool metadata_restaged = false;
bool restage_staging = false;
bool restage_appmeta = false;
bool has_src_snd0 = false;
snprintf(user_appmeta_dir, sizeof(user_appmeta_dir), "%s/%s", APPMETA_BASE,
title_id);
snprintf(user_app_dir, sizeof(user_app_dir), "%s/%s", APP_BASE, title_id);
if (is_under_image_mount_base(src_path)) {
image_source_count = resolve_image_source_chain_from_mount_cache(
src_path, image_source_chain);
has_image_source = image_source_count > 0;
if (!has_image_source) {
log_debug(" [LINK] image source lookup failed for %s: %s", title_id,
src_path);
}
}
snprintf(src_sce_sys, sizeof(src_sce_sys), "%s/sce_sys", src_path);
appmeta_missing = !has_appmeta_data(title_id);
if (is_remount && appmeta_missing) {
log_debug(" [REG] appmeta missing, restaging metadata only: %s", title_id);
}
restage_staging = (!is_remount || should_register);
restage_appmeta = (!is_remount || appmeta_missing);
if (should_stop_requested() || runtime_sleep_mode_active())
return false;
// COPY FILES
snprintf(src_snd0, sizeof(src_snd0), "%s/snd0.at9", src_sce_sys);
has_src_snd0 = path_exists(src_snd0);
if (!restage_staging && !restage_appmeta)
log_debug(" [SPEED] Skipping file copy (Assets already exist)");
if (restage_staging) {
mkdir(APP_BASE, 0777);
mkdir(user_app_dir, 0777);
snprintf(user_sce_sys, sizeof(user_sce_sys), "%s/sce_sys", user_app_dir);
mkdir(user_sce_sys, 0777);
if (copy_dir(src_sce_sys, user_sce_sys) != 0) {
log_debug(" [COPY] Failed to copy sce_sys staging: %s -> %s", src_sce_sys,
user_sce_sys);
return false;
}
char icon_src[MAX_PATH];
char icon_dst[MAX_PATH];
snprintf(icon_src, sizeof(icon_src), "%s/icon0.png", src_sce_sys);
snprintf(icon_dst, sizeof(icon_dst), "%s/icon0.png", user_app_dir);
if (copy_file(icon_src, icon_dst) != 0) {
log_debug(" [COPY] Failed to copy staged icon: %s -> %s", icon_src,
icon_dst);
return false;
}
}
if (restage_appmeta) {
mkdir(APPMETA_BASE, 0777);
mkdir(user_appmeta_dir, 0777);
if (!copy_sce_sys_to_appmeta(src_sce_sys, user_appmeta_dir)) {
log_debug(" [COPY] Failed to copy appmeta files: %s -> %s", src_sce_sys,
user_appmeta_dir);
return false;
}
metadata_restaged = true;
}
if (!update_trophy_metadata(title_id, src_sce_sys))
return false;
if (should_stop_requested() || runtime_sleep_mode_active())
return false;
if (!mount_title_nullfs(title_id, src_path)) {
log_debug(" [LINK] nullfs mount failed: title=%s src=%s", title_id,
src_path);
return false;
}
// WRITE TRACKER
char lnk_path[MAX_PATH];
runtime_mount_state_lock();
if (runtime_sleep_mode_active()) {
(void)rollback_title_nullfs_mount(title_id, src_path);
runtime_mount_state_unlock();
return false;
}
mkdir(APP_BASE, 0777);
mkdir(user_app_dir, 0777);
snprintf(lnk_path, sizeof(lnk_path), "%s/mount.lnk", user_app_dir);
if (!write_mount_link(title_id, src_path)) {
log_debug(" [LINK] write failed for %s: %s", lnk_path, strerror(errno));
(void)rollback_title_nullfs_mount(title_id, src_path);
runtime_mount_state_unlock();
return false;
}
log_debug(" [LINK] mount.lnk created: %s -> %s", lnk_path, src_path);
char img_lnk_path[MAX_PATH];
snprintf(img_lnk_path, sizeof(img_lnk_path), "%s/mount_img.lnk",
user_app_dir);
if (has_image_source) {
if (!write_mount_image_chain(title_id, image_source_chain,
image_source_count)) {
(void)unlink(lnk_path);
(void)rollback_title_nullfs_mount(title_id, src_path);
runtime_mount_state_unlock();
return false;
}
log_debug(" [LINK] mount_img.lnk created: %s -> %s layers=%zu",
img_lnk_path, image_source_chain[0], image_source_count);
if (!cache_image_source_mapping(image_source_chain[0], src_path)) {
log_debug(" [LINK] image source cache update failed: %s -> %s",
image_source_chain[0], src_path);
}
} else if (unlink(img_lnk_path) != 0 && errno != ENOENT) {
log_debug(" [LINK] remove failed for %s: %s", img_lnk_path,
strerror(errno));
}
bool sleep_started = runtime_sleep_mode_active();
runtime_mount_state_unlock();
if (sleep_started || runtime_sleep_mode_active())
return true;
if (!should_register) {
if (metadata_restaged && has_src_snd0) {
int snd0_updates = update_snd0info(title_id);
if (snd0_updates >= 0)
log_debug(" [DB] snd0info force-updated after appmeta refresh rows=%d",
snd0_updates);
}
log_debug(" [REG] Skip (already present in app.db)");
return true;
}
if (use_app_install_all) {
if (has_src_snd0_out)
*has_src_snd0_out = has_src_snd0;
log_debug(" [REG] Prepared for batch install: %s (%s)", title_name,
title_id);
return true;
}
// REGISTER
int res = 0;
if (sm_shellcore_install_bridge_enabled()) {
if (!sm_shellcore_install_title_dir(title_id, APP_BASE "/", &res)) {
log_debug(" [REG] internal AppInstallTitleDir bridge unavailable");
notify_system("Register failed: %s (%s)\nTitleDir bridge unavailable",
title_name, title_id);
return false;
}
mark_register_attempted(title_id);
} else {
app_install_title_dir_fn_t app_install_title_dir_fn =
resolve_app_install_title_dir();
if (!app_install_title_dir_fn) {
log_debug(" [REG] sceAppInstUtilAppInstallTitleDir unavailable");
notify_system("Register failed: %s (%s)\nAppInstallTitleDir unavailable",
title_name, title_id);
return false;
}
mark_register_attempted(title_id);
res = app_install_title_dir_fn(title_id, APP_BASE "/", NULL);
}
sceKernelUsleep(200000);
if (res == 0) {
invalidate_app_db_title_cache();
clear_register_attempts(title_id);
log_debug(" [REG] Installed NEW!");
notify_game_installed_rich(title_id);
if (has_src_snd0) {
int snd0_updates = update_snd0info(title_id);
if (snd0_updates >= 0)
log_debug(" [DB] snd0info updated rows=%d", snd0_updates);
}
} else if ((uint32_t)res == 0x80990002u) {
invalidate_app_db_title_cache();
clear_register_attempts(title_id);
log_debug(" [REG] Restored.");
if (has_src_snd0) {
int snd0_updates = update_snd0info(title_id);
if (snd0_updates >= 0)
log_debug(" [DB] snd0info updated rows=%d", snd0_updates);
}
// Silent on restore/remount to avoid spam
} else {
log_debug(" [REG] FAIL: 0x%x", res);
notify_system("Register failed: %s (%s)\ncode=0x%08X", title_name, title_id,
(uint32_t)res);
return false;
}
return true;
}
// --- Execution (per discovered candidate) ---
void process_scan_candidates(const scan_candidate_t *candidates,
int candidate_count) {
if (runtime_sleep_mode_active())
return;
sm_install_poll_pending();
for (int i = 0; i < candidate_count; i++) {
if (should_stop_requested() || runtime_sleep_mode_active())
return;
const scan_candidate_t *c = &candidates[i];
bool has_src_snd0 = false;
bool use_app_install_all =
runtime_config()->app_install_all_enabled && !c->in_app_db;
if (c->installed) {
log_debug(" [ACTION] Remounting: %s", c->title_name);
} else {
log_debug(" [ACTION] Installing: %s (%s)", c->title_name, c->title_id);
if (!use_app_install_all) {
notify_system_info("Installing: %s (%s)...", c->title_name,
c->title_id);
}
}
bool mounted = mount_and_install(c->path, c->title_id, c->title_name,
c->installed, !c->in_app_db,
use_app_install_all, &has_src_snd0);
if (runtime_sleep_mode_active())
return;
if (mounted) {
if (c->manual && !use_app_install_all) {
sm_manual_note_installed(c->manual_source_path, c->title_id,
c->title_name);
}
if (use_app_install_all) {
if (!sm_install_queue_candidate(c, has_src_snd0)) {
log_debug(" [REG] Failed to queue staged install: %s (%s)",
c->title_name, c->title_id);
if (should_stop_requested() || runtime_sleep_mode_active())
return;
uint8_t failed_attempts = bump_failed_mount_attempts(c->title_id);
if (failed_attempts == MAX_FAILED_MOUNT_ATTEMPTS) {
log_debug(" [RETRY] limit reached (%u/%u): %s (%s)",
(unsigned)failed_attempts,
(unsigned)MAX_FAILED_MOUNT_ATTEMPTS, c->title_name,
c->title_id);
}
continue;
}
clear_failed_mount_attempts(c->title_id);
} else {
clear_failed_mount_attempts(c->title_id);
cache_game_entry(c->path, c->title_id, c->title_name);
}
} else {
if (should_stop_requested() || runtime_sleep_mode_active())
return;
uint8_t failed_attempts = bump_failed_mount_attempts(c->title_id);
if (failed_attempts == MAX_FAILED_MOUNT_ATTEMPTS) {
log_debug(" [RETRY] limit reached (%u/%u): %s (%s)",
(unsigned)failed_attempts,
(unsigned)MAX_FAILED_MOUNT_ATTEMPTS, c->title_name,
c->title_id);
}
}
}
if (!sm_install_submit_queued())
sm_install_note_submit_failure();
}
+473
View File
@@ -0,0 +1,473 @@
#include "sm_platform.h"
#include "sm_install_queue.h"
#include "sm_types.h"
#include "sm_appdb.h"
#include "sm_config_mount.h"
#include "sm_game_cache.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_manual.h"
#include "sm_runtime.h"
#include "sm_scan.h"
#include "sm_time.h"
#include "sm_title_state.h"
#define INSTALL_WAIT_TIMEOUT_US (300ull * 1000000ull)
#define INSTALL_POLL_INTERVAL_US (2ull * 1000000ull)
#define INSTALL_SUBMIT_RETRY_US (2ull * 1000000ull)
typedef enum {
INSTALL_TRACK_NONE = 0,
INSTALL_TRACK_QUEUED,
INSTALL_TRACK_SUBMITTED,
} install_track_state_t;
typedef struct {
char title_id[MAX_TITLE_ID];
char title_name[MAX_TITLE_NAME];
char source_path[MAX_PATH];
char manual_source_path[MAX_PATH];
uint64_t requested_at_us;
bool has_src_snd0;
bool manual;
install_track_state_t state;
} pending_install_entry_t;
static pending_install_entry_t g_pending_installs[MAX_PENDING];
static uint64_t g_pending_install_poll_due_us = 0;
static uint64_t g_queued_install_submit_due_us = 0;
static int g_tracked_install_count = 0;
static int g_submitted_install_count = 0;
static bool g_queued_install_batch_announced = false;
static bool g_queued_install_submit_failure_notified = false;
static int count_queued_installs(void);
static uint8_t note_pending_install_failure(const pending_install_entry_t *entry);
static void drop_queued_install_entry(pending_install_entry_t *entry);
static bool install_queue_active(void) {
return runtime_config()->app_install_all_enabled || g_tracked_install_count > 0;
}
bool sm_install_has_pending_work(void) {
return g_tracked_install_count > 0;
}
static pending_install_entry_t *find_pending_install_entry(
const char *title_id) {
if (!title_id || title_id[0] == '\0')
return NULL;
for (int i = 0; i < MAX_PENDING; i++) {
if (g_pending_installs[i].state == INSTALL_TRACK_NONE)
continue;
if (strcmp(g_pending_installs[i].title_id, title_id) == 0)
return &g_pending_installs[i];
}
return NULL;
}
static pending_install_entry_t *reserve_pending_install_entry(
const char *title_id) {
pending_install_entry_t *entry = find_pending_install_entry(title_id);
if (entry)
return entry;
for (int i = 0; i < MAX_PENDING; i++) {
if (g_pending_installs[i].state == INSTALL_TRACK_NONE)
return &g_pending_installs[i];
}
return NULL;
}
static uint64_t next_pending_install_timeout_us(void) {
if (g_submitted_install_count <= 0)
return 0;
uint64_t next_deadline = 0;
for (int i = 0; i < MAX_PENDING; i++) {
const pending_install_entry_t *entry = &g_pending_installs[i];
if (entry->state != INSTALL_TRACK_SUBMITTED || entry->requested_at_us == 0)
continue;
uint64_t deadline_us = entry->requested_at_us + INSTALL_WAIT_TIMEOUT_US;
if (next_deadline == 0 || deadline_us < next_deadline)
next_deadline = deadline_us;
}
return next_deadline;
}
static void schedule_pending_install_poll(uint64_t now_us) {
if (g_submitted_install_count <= 0) {
g_pending_install_poll_due_us = 0;
return;
}
g_pending_install_poll_due_us =
now_us == 0 ? 1 : now_us + INSTALL_POLL_INTERVAL_US;
}
static void schedule_queued_install_submit(uint64_t now_us) {
if (count_queued_installs() <= 0 || g_submitted_install_count > 0) {
g_queued_install_submit_due_us = 0;
return;
}
g_queued_install_submit_due_us = now_us == 0 ? 1 : now_us;
}
static void schedule_queued_install_submit_retry(uint64_t now_us) {
if (count_queued_installs() <= 0 || g_submitted_install_count > 0) {
g_queued_install_submit_due_us = 0;
return;
}
g_queued_install_submit_due_us =
now_us == 0 ? 1 : now_us + INSTALL_SUBMIT_RETRY_US;
}
static void clear_pending_install_entry(pending_install_entry_t *entry) {
if (!entry)
return;
if (entry->state == INSTALL_TRACK_SUBMITTED && g_submitted_install_count > 0)
g_submitted_install_count--;
if (entry->state != INSTALL_TRACK_NONE && g_tracked_install_count > 0)
g_tracked_install_count--;
memset(entry, 0, sizeof(*entry));
if (g_submitted_install_count <= 0)
g_pending_install_poll_due_us = 0;
if (g_tracked_install_count <= 0) {
g_queued_install_submit_due_us = 0;
g_queued_install_batch_announced = false;
g_queued_install_submit_failure_notified = false;
}
}
bool is_title_install_pending(const char *title_id) {
return find_pending_install_entry(title_id) != NULL;
}
void sm_install_forget_pending_title(const char *title_id) {
pending_install_entry_t *entry = find_pending_install_entry(title_id);
if (entry)
clear_pending_install_entry(entry);
}
static void append_batch_install_line(char *message, size_t message_size,
const char *title_name,
const char *title_id, int *shown_count) {
if (!message || message_size == 0 || !title_name || !title_id || !shown_count)
return;
size_t used = strlen(message);
if (used >= message_size)
return;
int written = snprintf(message + used, message_size - used, "\n%s (%s)",
title_name, title_id);
if (written < 0 || (size_t)written >= message_size - used)
return;
(*shown_count)++;
}
bool sm_install_queue_candidate(const scan_candidate_t *candidate,
bool has_src_snd0) {
if (!candidate || candidate->title_id[0] == '\0' ||
candidate->title_name[0] == '\0' || candidate->path[0] == '\0') {
return false;
}
if (!runtime_config()->app_install_all_enabled)
return false;
bool was_queue_empty = (count_queued_installs() <= 0);
pending_install_entry_t *entry =
reserve_pending_install_entry(candidate->title_id);
if (!entry) {
log_debug(" [REG] install queue full, cannot queue %s (%s)",
candidate->title_name, candidate->title_id);
return false;
}
install_track_state_t previous_state = entry->state;
if (previous_state == INSTALL_TRACK_SUBMITTED)
return true;
memset(entry, 0, sizeof(*entry));
(void)strlcpy(entry->title_id, candidate->title_id, sizeof(entry->title_id));
(void)strlcpy(entry->title_name, candidate->title_name,
sizeof(entry->title_name));
(void)strlcpy(entry->source_path, candidate->path, sizeof(entry->source_path));
(void)strlcpy(entry->manual_source_path, candidate->manual_source_path,
sizeof(entry->manual_source_path));
entry->manual = candidate->manual;
entry->has_src_snd0 = has_src_snd0;
entry->state = INSTALL_TRACK_QUEUED;
if (previous_state == INSTALL_TRACK_NONE)
g_tracked_install_count++;
if (was_queue_empty) {
g_queued_install_batch_announced = false;
g_queued_install_submit_failure_notified = false;
}
schedule_queued_install_submit(monotonic_time_us());
return true;
}
static void finalize_pending_install_success(pending_install_entry_t *entry) {
if (!entry)
return;
log_debug(" [REG] Installed: %s (%s)", entry->title_name, entry->title_id);
notify_game_installed_rich(entry->title_id);
if (entry->has_src_snd0) {
int snd0_updates = update_snd0info(entry->title_id);
if (snd0_updates >= 0)
log_debug(" [DB] snd0info updated rows=%d", snd0_updates);
}
cache_game_entry(entry->source_path, entry->title_id, entry->title_name);
if (entry->manual)
sm_manual_note_installed(entry->manual_source_path, entry->title_id,
entry->title_name);
clear_register_attempts(entry->title_id);
clear_failed_mount_attempts(entry->title_id);
clear_pending_install_entry(entry);
}
static void finalize_pending_install_timeout(pending_install_entry_t *entry) {
if (!entry)
return;
log_debug(" [REG] Install timeout after 5 minutes: %s (%s)",
entry->title_name, entry->title_id);
notify_system("Install failed: %s (%s)\nTimed out after 5 minutes",
entry->title_name, entry->title_id);
(void)note_pending_install_failure(entry);
clear_pending_install_entry(entry);
}
static void poll_pending_installs(void) {
if (g_submitted_install_count <= 0)
return;
invalidate_app_db_title_cache();
struct AppDbTitleList app_db_titles = {0};
bool app_db_titles_ready = get_app_db_title_list_cached(&app_db_titles);
uint64_t now_us = monotonic_time_us();
if (!app_db_titles_ready)
log_debug(" [DB] app.db title list unavailable while polling installs");
for (int i = 0; i < MAX_PENDING; i++) {
pending_install_entry_t *entry = &g_pending_installs[i];
if (entry->state != INSTALL_TRACK_SUBMITTED)
continue;
if (app_db_titles_ready &&
app_db_title_list_contains(&app_db_titles, entry->title_id)) {
finalize_pending_install_success(entry);
continue;
}
if (now_us != 0 && entry->requested_at_us != 0 &&
now_us >= entry->requested_at_us &&
now_us - entry->requested_at_us >= INSTALL_WAIT_TIMEOUT_US) {
finalize_pending_install_timeout(entry);
}
}
schedule_pending_install_poll(now_us);
if (g_submitted_install_count == 0)
schedule_queued_install_submit(now_us);
free_app_db_title_list(&app_db_titles);
if (!sm_install_has_pending_work() && !release_scan_runtime_mounts())
log_debug(" [IMG] deferred release after install completion");
}
uint64_t sm_install_next_wake_us(uint64_t now_us) {
if (runtime_sleep_mode_active())
return 0;
if (!install_queue_active())
return 0;
if (g_submitted_install_count <= 0)
return g_queued_install_submit_due_us;
uint64_t next_wake_us = next_pending_install_timeout_us();
if (g_pending_install_poll_due_us != 0 &&
(next_wake_us == 0 || g_pending_install_poll_due_us < next_wake_us)) {
next_wake_us = g_pending_install_poll_due_us;
}
if (g_queued_install_submit_due_us != 0 &&
(next_wake_us == 0 || g_queued_install_submit_due_us < next_wake_us)) {
next_wake_us = g_queued_install_submit_due_us;
}
if (next_wake_us == 0)
next_wake_us = now_us == 0 ? 1 : now_us + INSTALL_POLL_INTERVAL_US;
return next_wake_us;
}
void sm_install_poll_pending(void) {
if (runtime_sleep_mode_active())
return;
poll_pending_installs();
}
void sm_install_service_pending(void) {
if (runtime_sleep_mode_active())
return;
if (!install_queue_active())
return;
sm_install_poll_pending();
if (count_queued_installs() > 0 && !sm_install_submit_queued())
sm_install_note_submit_failure();
}
static void log_batch_submit_retry_limit(const pending_install_entry_t *entry) {
uint8_t failed_attempts = get_failed_mount_attempts(entry->title_id);
if (failed_attempts == MAX_FAILED_MOUNT_ATTEMPTS) {
log_debug(" [RETRY] limit reached (%u/%u): %s (%s)",
(unsigned)failed_attempts,
(unsigned)MAX_FAILED_MOUNT_ATTEMPTS, entry->title_name,
entry->title_id);
}
}
static int count_queued_installs(void) {
int queued_count = g_tracked_install_count - g_submitted_install_count;
return queued_count > 0 ? queued_count : 0;
}
static void log_queued_install_batch(void) {
int queued_count = count_queued_installs();
if (queued_count <= 0)
return;
log_debug(" [REG] Batch install request contains %d title(s):", queued_count);
for (int i = 0; i < MAX_PENDING; i++) {
const pending_install_entry_t *entry = &g_pending_installs[i];
if (entry->state != INSTALL_TRACK_QUEUED)
continue;
log_debug(" - %s (%s)", entry->title_name, entry->title_id);
}
}
static void notify_queued_install_batch(void) {
int queued_count = count_queued_installs();
if (queued_count <= 0 || g_queued_install_batch_announced)
return;
char message[3075];
int shown_count = 0;
snprintf(message, sizeof(message), "Batch install queued (%d):", queued_count);
for (int i = 0; i < MAX_PENDING; i++) {
const pending_install_entry_t *entry = &g_pending_installs[i];
if (entry->state != INSTALL_TRACK_QUEUED)
continue;
append_batch_install_line(message, sizeof(message), entry->title_name,
entry->title_id, &shown_count);
}
if (shown_count < queued_count) {
size_t used = strlen(message);
if (used < sizeof(message)) {
(void)snprintf(message + used, sizeof(message) - used, "\n... and %d more",
queued_count - shown_count);
}
}
notify_system_info("%s", message);
g_queued_install_batch_announced = true;
}
static uint8_t note_pending_install_failure(const pending_install_entry_t *entry) {
if (!entry)
return 0;
uint8_t failed_attempts = bump_failed_mount_attempts(entry->title_id);
log_batch_submit_retry_limit(entry);
return failed_attempts;
}
static void drop_queued_install_entry(pending_install_entry_t *entry) {
if (!entry || entry->state != INSTALL_TRACK_QUEUED)
return;
log_debug(" [REG] Dropping queued install after retry limit: %s (%s)",
entry->title_name, entry->title_id);
clear_pending_install_entry(entry);
}
bool sm_install_submit_queued(void) {
if (runtime_sleep_mode_active())
return true;
int queued_count = count_queued_installs();
if (queued_count <= 0)
return true;
if (g_submitted_install_count > 0)
return true;
uint64_t now_us = monotonic_time_us();
if (g_queued_install_submit_due_us != 0 && now_us != 0 &&
now_us < g_queued_install_submit_due_us) {
return true;
}
log_queued_install_batch();
notify_queued_install_batch();
int res = sceAppInstUtilAppInstallAll(NULL);
if (res != 0) {
log_debug(" [REG] Batch install request failed: 0x%x", res);
if (!g_queued_install_submit_failure_notified) {
notify_system("Batch install failed for %d app(s).\ncode=0x%08X",
queued_count, (uint32_t)res);
g_queued_install_submit_failure_notified = true;
}
schedule_queued_install_submit_retry(now_us);
return false;
}
g_queued_install_submit_due_us = 0;
g_queued_install_submit_failure_notified = false;
for (int i = 0; i < MAX_PENDING; i++) {
pending_install_entry_t *entry = &g_pending_installs[i];
if (entry->state != INSTALL_TRACK_QUEUED)
continue;
entry->state = INSTALL_TRACK_SUBMITTED;
entry->requested_at_us = now_us;
g_submitted_install_count++;
clear_failed_mount_attempts(entry->title_id);
}
invalidate_app_db_title_cache();
schedule_pending_install_poll(now_us);
return true;
}
void sm_install_note_submit_failure(void) {
for (int i = 0; i < MAX_PENDING; i++) {
pending_install_entry_t *entry = &g_pending_installs[i];
if (entry->state != INSTALL_TRACK_QUEUED)
continue;
if (note_pending_install_failure(entry) >= MAX_FAILED_MOUNT_ATTEMPTS)
drop_queued_install_entry(entry);
}
}
+920
View File
@@ -0,0 +1,920 @@
#include "sm_platform.h"
#include "sm_config_mount.h"
#include "sm_filesystem.h"
#include "sm_kstuff.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_mdbg.h"
#include "sm_path_utils.h"
#include "sm_runtime.h"
#include "sm_time.h"
#include "sm_types.h"
#include <stdatomic.h>
#define KSTUFF_SYSENTVEC_TOGGLE_OFFSET 14
#define KSTUFF_SYSENTVEC_ENABLED 0xdeb7u
#define KSTUFF_SYSENTVEC_DISABLED 0xffffu
typedef struct {
bool active;
bool image_backed;
bool autopause_delay_valid;
bool pause_applied;
bool focus_override_active;
pid_t pid;
uint32_t app_id;
uint32_t autopause_delay_seconds;
uint64_t launch_time_us;
uint64_t pause_deadline_us;
char title_id[MAX_TITLE_ID];
} kstuff_game_entry_t;
typedef struct {
intptr_t sysentvec_ps5;
intptr_t sysentvec_ps4;
bool probe_available;
bool supported;
bool restore_on_empty;
bool sleep_pause_restore;
bool current_app_focus_valid;
uint32_t current_app_focus_id;
uint64_t restore_retry_us;
kstuff_game_entry_t game;
} kstuff_state_t;
static kstuff_state_t g_kstuff;
static _Atomic uint32_t g_pending_app_focus_id;
static _Atomic bool g_pending_app_focus_valid;
static _Atomic bool g_pending_config_reload;
static uint32_t get_effective_pause_delay_seconds(const kstuff_game_entry_t *entry);
static bool refresh_kstuff_support_state(void);
static uint32_t get_pause_delay_seconds_for_title(const char *title_id,
bool *image_backed_out,
uint32_t *autopause_delay_out,
bool *autopause_delay_valid_out);
static void restore_kstuff_if_needed(const char *reason);
static char *trim_ascii_inplace(char *s) {
while (*s == ' ' || *s == '\t' || *s == '\r' || *s == '\n')
s++;
size_t len = strlen(s);
while (len > 0) {
char ch = s[len - 1u];
if (ch != ' ' && ch != '\t' && ch != '\r' && ch != '\n')
break;
s[len - 1u] = '\0';
len--;
}
return s;
}
static char *strip_inline_comment(char *s) {
char *comment = strpbrk(s, "#;");
if (comment)
*comment = '\0';
return trim_ascii_inplace(s);
}
static bool parse_pause_delay_seconds(const char *value,
uint32_t *delay_seconds_out) {
if (!value || !delay_seconds_out)
return false;
errno = 0;
char *end = NULL;
unsigned long parsed = strtoul(value, &end, 10);
if (errno != 0 || end == value || *end != '\0' ||
parsed > MAX_KSTUFF_PAUSE_DELAY_SECONDS) {
return false;
}
*delay_seconds_out = (uint32_t)parsed;
return true;
}
static bool get_autopause_delay_for_source(const char *source_path,
bool image_backed,
uint32_t *delay_seconds_out) {
if (!source_path || source_path[0] == '\0' || !delay_seconds_out)
return false;
char autopause_path[MAX_PATH];
int written = snprintf(autopause_path, sizeof(autopause_path), "%s/%s",
source_path, "autopause.txt");
if (written <= 0 || (size_t)written >= sizeof(autopause_path))
return false;
FILE *f = fopen(autopause_path, "r");
if (!f)
return false;
char line[128];
bool have_scalar = false;
bool have_direct = false;
bool have_image = false;
uint32_t scalar_delay = 0;
uint32_t direct_delay = 0;
uint32_t image_delay = 0;
while (fgets(line, sizeof(line), f)) {
char *value = strip_inline_comment(line);
if (value[0] == '\0')
continue;
char *eq = strchr(value, '=');
if (!eq) {
if (!have_scalar && parse_pause_delay_seconds(value, &scalar_delay))
have_scalar = true;
continue;
}
*eq = '\0';
char *key = trim_ascii_inplace(value);
char *arg = trim_ascii_inplace(eq + 1);
uint32_t parsed_delay = 0;
if (!parse_pause_delay_seconds(arg, &parsed_delay))
continue;
if (strcasecmp(key, "direct") == 0) {
direct_delay = parsed_delay;
have_direct = true;
} else if (strcasecmp(key, "image") == 0) {
image_delay = parsed_delay;
have_image = true;
}
}
fclose(f);
if (image_backed && have_image) {
*delay_seconds_out = image_delay;
return true;
}
if (!image_backed && have_direct) {
*delay_seconds_out = direct_delay;
return true;
}
if (have_scalar) {
uint64_t delay_seconds = scalar_delay;
if (image_backed)
delay_seconds *= 2u;
if (delay_seconds > MAX_KSTUFF_PAUSE_DELAY_SECONDS)
delay_seconds = MAX_KSTUFF_PAUSE_DELAY_SECONDS;
*delay_seconds_out = (uint32_t)delay_seconds;
return true;
}
return false;
}
static bool resolve_title_pause_source(const char *title_id,
char source_path[MAX_PATH],
bool *image_backed_out) {
source_path[0] = '\0';
*image_backed_out = false;
if (!title_id)
return false;
if (!read_mount_link(title_id, source_path, MAX_PATH))
return false;
*image_backed_out = is_under_image_mount_base(source_path);
return true;
}
static uint32_t get_configured_pause_delay_seconds(const char *title_id,
bool image_backed,
bool *override_applied_out) {
const runtime_config_t *cfg = runtime_config();
uint32_t delay_seconds =
image_backed ? cfg->kstuff_pause_delay_image_seconds
: cfg->kstuff_pause_delay_direct_seconds;
uint32_t override_delay_seconds = 0;
if (get_kstuff_pause_delay_override_for_title(title_id,
&override_delay_seconds)) {
*override_applied_out = true;
return override_delay_seconds;
}
*override_applied_out = false;
return delay_seconds;
}
static bool resolve_kstuff_sysentvec_addrs(intptr_t *ps5_out, intptr_t *ps4_out) {
if (!ps5_out || !ps4_out)
return false;
*ps5_out = 0;
*ps4_out = 0;
switch (kernel_get_fw_version() & 0xffff0000u) {
case 0x1000000:
case 0x1010000:
case 0x1020000:
case 0x1050000:
case 0x1100000:
case 0x1110000:
case 0x1120000:
case 0x1130000:
case 0x1140000:
case 0x2000000:
case 0x2200000:
case 0x2250000:
case 0x2260000:
case 0x2300000:
case 0x2500000:
case 0x2700000:
// Older jailbreak chains usually rely on Byepervisor instead of kstuff.
return false;
case 0x3000000:
case 0x3100000:
case 0x3200000:
case 0x3210000:
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xca0cd8;
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xca0e50;
return true;
case 0x4000000:
case 0x4020000:
case 0x4030000:
case 0x4500000:
case 0x4510000:
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xd11bb8;
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xd11d30;
return true;
case 0x5000000:
case 0x5020000:
case 0x5100000:
case 0x5500000:
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xe00be8;
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xe00d60;
return true;
case 0x6000000:
case 0x6020000:
case 0x6500000:
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xe210a8;
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xe21220;
return true;
case 0x7000000:
case 0x7010000:
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xe21ab8;
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xe21c30;
return true;
case 0x7200000:
case 0x7400000:
case 0x7600000:
case 0x7610000:
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xe21b78;
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xe21cf0;
return true;
case 0x8000000:
case 0x8200000:
case 0x8400000:
case 0x8600000:
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xe21ca8;
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xe21e20;
return true;
case 0x9000000:
case 0x9050000:
case 0x9200000:
case 0x9400000:
case 0x9600000:
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xdba648;
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xdba7c0;
return true;
case 0x10000000:
case 0x10010000:
case 0x10200000:
case 0x10400000:
case 0x10600000:
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xdba6d8;
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xdba850;
return true;
case 0x11000000:
case 0x11200000:
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xdcbc78;
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xdcbdf0;
return true;
case 0x11400000:
case 0x11600000:
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xdcbc98;
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xdcbe10;
return true;
case 0x12000000:
case 0x12020000:
case 0x12200000:
case 0x12400000:
case 0x12600000:
case 0x12700000:
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xdcc978;
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xdccaf0;
return true;
case 0x13000000:
case 0x13200000:
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xddd8a8;
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xddda20;
return true;
default:
return false;
}
}
static bool kstuff_sysentvec_is_enabled(intptr_t sysentvec_addr) {
return kernel_getshort(sysentvec_addr + KSTUFF_SYSENTVEC_TOGGLE_OFFSET) !=
KSTUFF_SYSENTVEC_DISABLED;
}
static uint16_t read_kstuff_sysentvec_toggle(intptr_t sysentvec_addr) {
return (uint16_t)kernel_getshort(sysentvec_addr +
KSTUFF_SYSENTVEC_TOGGLE_OFFSET);
}
static bool kstuff_sysentvec_toggle_is_known(uint16_t value) {
return value == KSTUFF_SYSENTVEC_ENABLED ||
value == KSTUFF_SYSENTVEC_DISABLED;
}
static void set_kstuff_sysentvec_enabled(intptr_t sysentvec_addr, bool enabled) {
(void)kernel_setshort(sysentvec_addr + KSTUFF_SYSENTVEC_TOGGLE_OFFSET,
enabled ? KSTUFF_SYSENTVEC_ENABLED
: KSTUFF_SYSENTVEC_DISABLED);
}
static bool read_kstuff_enabled_state(bool *ps5_out, bool *ps4_out) {
if (!refresh_kstuff_support_state())
return false;
bool ps5_enabled = kstuff_sysentvec_is_enabled(g_kstuff.sysentvec_ps5);
bool ps4_enabled = kstuff_sysentvec_is_enabled(g_kstuff.sysentvec_ps4);
if (ps5_out)
*ps5_out = ps5_enabled;
if (ps4_out)
*ps4_out = ps4_enabled;
return ps5_enabled && ps4_enabled;
}
static bool refresh_kstuff_support_state(void) {
if (!g_kstuff.probe_available) {
g_kstuff.supported = false;
return false;
}
uint16_t ps5_toggle = read_kstuff_sysentvec_toggle(g_kstuff.sysentvec_ps5);
uint16_t ps4_toggle = read_kstuff_sysentvec_toggle(g_kstuff.sysentvec_ps4);
g_kstuff.supported = kstuff_sysentvec_toggle_is_known(ps5_toggle) &&
kstuff_sysentvec_toggle_is_known(ps4_toggle);
return g_kstuff.supported;
}
static bool kstuff_state_matches(bool enabled, bool ps5_enabled,
bool ps4_enabled) {
return ps5_enabled == enabled && ps4_enabled == enabled;
}
static bool apply_kstuff_enabled_state(bool enabled, bool notify_user,
bool *fully_applied_out);
static void mark_restore_needed(void) {
g_kstuff.restore_on_empty = true;
g_kstuff.restore_retry_us = 0;
}
static bool tracked_game_requires_restore(void) {
return g_kstuff.restore_on_empty;
}
static void finish_tracked_game_clear(const char *reason) {
if (!g_kstuff.restore_on_empty)
return;
mark_restore_needed();
restore_kstuff_if_needed(reason);
}
static void apply_kstuff_config_reload(void) {
if (!runtime_config()->kstuff_crash_detection_enabled)
sm_mdbg_game_shutdown();
if (!runtime_config()->kstuff_game_auto_toggle) {
sm_kstuff_game_shutdown();
return;
}
if (!g_kstuff.game.active)
return;
if (is_kstuff_pause_disabled_for_title(g_kstuff.game.title_id)) {
bool restore_needed = tracked_game_requires_restore();
log_debug(" [KSTUFF] cleared tracked game by config reload: %s",
g_kstuff.game.title_id);
sm_mdbg_game_shutdown();
memset(&g_kstuff.game, 0, sizeof(g_kstuff.game));
if (restore_needed)
finish_tracked_game_clear("config reload");
return;
}
if (g_kstuff.game.pause_applied)
return;
bool image_backed = g_kstuff.game.image_backed;
bool autopause_delay_valid = false;
uint32_t autopause_delay_seconds = 0;
uint32_t delay_seconds = get_pause_delay_seconds_for_title(
g_kstuff.game.title_id, &image_backed, &autopause_delay_seconds,
&autopause_delay_valid);
g_kstuff.game.image_backed = image_backed;
g_kstuff.game.autopause_delay_valid = autopause_delay_valid;
g_kstuff.game.autopause_delay_seconds = autopause_delay_seconds;
g_kstuff.game.pause_deadline_us =
g_kstuff.game.launch_time_us == 0
? 0
: g_kstuff.game.launch_time_us + (uint64_t)delay_seconds * 1000000ull;
log_debug(" [KSTUFF] updated tracked game config: %s source=%s pause_delay=%us",
g_kstuff.game.title_id,
g_kstuff.game.image_backed ? "image" : "direct",
delay_seconds);
}
static uint32_t get_pause_delay_seconds_for_title(const char *title_id,
bool *image_backed_out,
uint32_t *autopause_delay_out,
bool *autopause_delay_valid_out) {
char source_path[MAX_PATH];
bool image_backed = false;
(void)resolve_title_pause_source(title_id, source_path, &image_backed);
*image_backed_out = image_backed;
*autopause_delay_out = 0;
*autopause_delay_valid_out = false;
uint32_t autotuned_delay_seconds = 0;
if (get_kstuff_autotune_pause_delay_for_title(title_id,
&autotuned_delay_seconds)) {
return autotuned_delay_seconds;
}
bool override_applied = false;
uint32_t delay_seconds =
get_configured_pause_delay_seconds(title_id, image_backed,
&override_applied);
uint32_t autopause_delay_seconds = 0;
if (get_autopause_delay_for_source(source_path, image_backed,
&autopause_delay_seconds)) {
*autopause_delay_out = autopause_delay_seconds;
*autopause_delay_valid_out = true;
if (!override_applied)
return autopause_delay_seconds;
}
return delay_seconds;
}
static void restore_kstuff_if_needed(const char *reason) {
if (g_kstuff.game.active && g_kstuff.game.pause_applied)
return;
if (!g_kstuff.restore_on_empty)
return;
uint64_t now_us = monotonic_time_us();
if (now_us != 0 && g_kstuff.restore_retry_us != 0 &&
now_us < g_kstuff.restore_retry_us) {
return;
}
if (g_kstuff.game.active && !g_kstuff.game.pause_applied) {
if (g_kstuff.game.pause_deadline_us == 0 ||
(now_us != 0 && now_us >= g_kstuff.game.pause_deadline_us)) {
return;
}
}
if (!sm_kstuff_is_supported()) {
g_kstuff.restore_retry_us =
now_us == 0 ? 1 : now_us + GAME_LIFECYCLE_POLL_INTERVAL_US;
return;
}
bool enabled = sm_kstuff_is_enabled();
if (!enabled)
enabled = sm_kstuff_set_enabled(true, true);
if (enabled) {
log_debug(" [KSTUFF] auto-resumed (%s)%s",
reason ? reason : "no reason",
g_kstuff.game.active ? ", pending delay for another tracked game"
: "");
} else {
log_debug(" [KSTUFF] failed to auto-resume (%s)",
reason ? reason : "no reason");
g_kstuff.restore_retry_us =
now_us == 0 ? 0 : now_us + KSTUFF_RESTORE_RETRY_US;
return;
}
g_kstuff.restore_on_empty = false;
g_kstuff.restore_retry_us = 0;
}
static void maybe_apply_kstuff_pause_for_slot(kstuff_game_entry_t *entry) {
if (!entry->active || entry->pause_applied)
return;
uint64_t now_us = monotonic_time_us();
if (now_us != 0 && now_us < entry->pause_deadline_us)
return;
if (g_kstuff.current_app_focus_valid && entry->app_id != 0 &&
g_kstuff.current_app_focus_id != entry->app_id) {
return;
}
if (!sm_kstuff_is_supported()) {
log_debug(" [KSTUFF] skipping auto-pause for %s pid=%ld: kstuff not "
"available",
entry->title_id, (long)entry->pid);
sm_mdbg_game_shutdown();
memset(entry, 0, sizeof(*entry));
return;
}
bool was_enabled = sm_kstuff_is_enabled();
bool fully_disabled = false;
(void)apply_kstuff_enabled_state(false, was_enabled, &fully_disabled);
if (!fully_disabled) {
log_debug(" [KSTUFF] failed to auto-pause for %s pid=%ld",
entry->title_id, (long)entry->pid);
return;
}
if (was_enabled) {
mark_restore_needed();
sm_mdbg_game_on_kstuff_pause(entry->pid, now_us,
get_effective_pause_delay_seconds(entry));
log_debug(" [KSTUFF] auto-paused for %s pid=%ld (%s launch)",
entry->title_id, (long)entry->pid,
entry->image_backed ? "image" : "direct");
} else {
log_debug(" [KSTUFF] pause deadline reached for %s pid=%ld but kstuff "
"was already disabled", entry->title_id, (long)entry->pid);
}
entry->pause_applied = true;
}
static void handle_kstuff_app_focus_change(uint32_t focused_app_id) {
g_kstuff.current_app_focus_valid = true;
g_kstuff.current_app_focus_id = focused_app_id;
if (!g_kstuff.game.active || g_kstuff.game.app_id == 0)
return;
if (focused_app_id != g_kstuff.game.app_id) {
if (!g_kstuff.game.pause_applied)
return;
if (!g_kstuff.restore_on_empty)
return;
if (g_kstuff.game.focus_override_active)
return;
bool enabled = sm_kstuff_is_enabled();
if (!enabled)
enabled = sm_kstuff_set_enabled(true, false);
if (!enabled) {
log_debug(" [KSTUFF] failed to enable on app focus leave "
"(tracked=0x%08X current=0x%08X)",
g_kstuff.game.app_id, focused_app_id);
return;
}
g_kstuff.game.focus_override_active = true;
log_debug(" [KSTUFF] temporarily enabled on app focus leave "
"(tracked=0x%08X current=0x%08X)",
g_kstuff.game.app_id, focused_app_id);
return;
}
if (!g_kstuff.game.focus_override_active)
return;
bool fully_disabled = false;
(void)apply_kstuff_enabled_state(false, false, &fully_disabled);
if (!fully_disabled) {
log_debug(" [KSTUFF] failed to restore disabled state on app focus return "
"(app_id=0x%08X)",
focused_app_id);
return;
}
g_kstuff.game.focus_override_active = false;
log_debug(" [KSTUFF] restored disabled state on app focus return "
"(app_id=0x%08X)",
focused_app_id);
}
uint64_t sm_kstuff_game_next_wake_us(uint64_t now_us) {
if (!sm_kstuff_game_feature_enabled())
return 0;
uint64_t next_wake_us = 0;
if (g_kstuff.game.active && !g_kstuff.game.pause_applied) {
if (g_kstuff.current_app_focus_valid && g_kstuff.game.app_id != 0 &&
g_kstuff.current_app_focus_id != g_kstuff.game.app_id) {
next_wake_us = 0;
} else if (g_kstuff.game.pause_deadline_us != 0) {
next_wake_us = g_kstuff.game.pause_deadline_us;
} else if (now_us == 0) {
next_wake_us = 1;
} else {
next_wake_us = now_us + GAME_LIFECYCLE_POLL_INTERVAL_US;
}
}
if (g_kstuff.restore_on_empty) {
uint64_t restore_wake_us = g_kstuff.restore_retry_us;
if (restore_wake_us == 0) {
if (g_kstuff.game.active && !g_kstuff.game.pause_applied) {
if (g_kstuff.game.pause_deadline_us != 0) {
restore_wake_us = g_kstuff.game.pause_deadline_us;
} else if (now_us == 0) {
restore_wake_us = 1;
} else {
restore_wake_us = now_us + GAME_LIFECYCLE_POLL_INTERVAL_US;
}
} else {
restore_wake_us = now_us == 0 ? 1 : now_us;
}
}
if (next_wake_us == 0 || restore_wake_us < next_wake_us)
next_wake_us = restore_wake_us;
}
return next_wake_us;
}
bool sm_kstuff_is_supported(void) {
return refresh_kstuff_support_state();
}
bool sm_kstuff_is_enabled(void) {
return read_kstuff_enabled_state(NULL, NULL);
}
static bool apply_kstuff_enabled_state(bool enabled, bool notify_user,
bool *fully_applied_out) {
bool ps5_enabled = false;
bool ps4_enabled = false;
bool is_enabled = read_kstuff_enabled_state(&ps5_enabled, &ps4_enabled);
if (!g_kstuff.supported)
return false;
bool fully_applied = kstuff_state_matches(enabled, ps5_enabled, ps4_enabled);
if (fully_applied) {
if (fully_applied_out)
*fully_applied_out = true;
return is_enabled;
}
if (ps5_enabled != enabled)
set_kstuff_sysentvec_enabled(g_kstuff.sysentvec_ps5, enabled);
if (ps4_enabled != enabled)
set_kstuff_sysentvec_enabled(g_kstuff.sysentvec_ps4, enabled);
is_enabled = read_kstuff_enabled_state(&ps5_enabled, &ps4_enabled);
fully_applied = kstuff_state_matches(enabled, ps5_enabled, ps4_enabled);
if (fully_applied_out)
*fully_applied_out = fully_applied;
if (fully_applied) {
log_debug(" [KSTUFF] %s both sysentvecs", enabled ? "enabled" : "disabled");
if (notify_user) {
notify_system_info(enabled ? "KStuff active again"
: "KStuff paused while the game is running");
}
} else {
log_debug(" [KSTUFF] %s request incomplete (ps5=%s ps4=%s)",
enabled ? "enable" : "disable", ps5_enabled ? "on" : "off",
ps4_enabled ? "on" : "off");
}
return is_enabled;
}
bool sm_kstuff_set_enabled(bool enabled, bool notify_user) {
return apply_kstuff_enabled_state(enabled, notify_user, NULL);
}
bool sm_kstuff_game_feature_enabled(void) {
return runtime_config()->kstuff_game_auto_toggle && g_kstuff.probe_available;
}
void sm_kstuff_game_on_exec(pid_t pid, const char *title_id, uint32_t app_id,
uint64_t exec_time_us) {
if (!sm_kstuff_game_feature_enabled())
return;
if (g_kstuff.game.active) {
if (g_kstuff.game.pid == pid) {
log_debug(" [KSTUFF] already tracking pid=%ld for %s", (long)pid,
title_id);
return;
}
bool restore_needed = tracked_game_requires_restore();
log_debug(" [KSTUFF] handoff tracked game pid=%ld (%s) -> pid=%ld (%s)",
(long)g_kstuff.game.pid, g_kstuff.game.title_id, (long)pid,
title_id);
sm_mdbg_game_shutdown();
memset(&g_kstuff.game, 0, sizeof(g_kstuff.game));
if (restore_needed)
finish_tracked_game_clear("tracked game handoff");
}
if (is_kstuff_pause_disabled_for_title(title_id)) {
log_debug(" [KSTUFF] auto-pause disabled by config for %s pid=%ld "
"app_id=0x%08X", title_id, (long)pid, app_id);
return;
}
bool image_backed = false;
bool autopause_delay_valid = false;
uint32_t autopause_delay_seconds = 0;
uint32_t delay_seconds = get_pause_delay_seconds_for_title(
title_id, &image_backed, &autopause_delay_seconds,
&autopause_delay_valid);
uint64_t now_us = monotonic_time_us();
uint64_t base_time_us = exec_time_us != 0 ? exec_time_us : now_us;
g_kstuff.game.active = true;
g_kstuff.game.image_backed = image_backed;
g_kstuff.game.autopause_delay_valid = autopause_delay_valid;
g_kstuff.game.pause_applied = false;
g_kstuff.game.focus_override_active = false;
g_kstuff.game.pid = pid;
g_kstuff.game.app_id = app_id;
g_kstuff.game.autopause_delay_seconds = autopause_delay_seconds;
g_kstuff.game.launch_time_us = base_time_us;
g_kstuff.game.pause_deadline_us =
base_time_us == 0 ? 0 : base_time_us + (uint64_t)delay_seconds * 1000000ull;
(void)strlcpy(g_kstuff.game.title_id, title_id, sizeof(g_kstuff.game.title_id));
sm_mdbg_game_on_exec(pid, title_id, app_id);
log_debug(" [KSTUFF] tracking game launch: %s pid=%ld app_id=0x%08X source=%s "
"pause_delay=%us", title_id, (long)pid, app_id,
image_backed ? "image" : "direct", delay_seconds);
}
void sm_kstuff_note_app_focus(uint32_t app_id) {
atomic_store(&g_pending_app_focus_id, app_id);
atomic_store(&g_pending_app_focus_valid, true);
}
void sm_kstuff_game_on_exit(pid_t pid) {
if (!g_kstuff.game.active || g_kstuff.game.pid != pid)
return;
bool restore_needed = tracked_game_requires_restore();
sm_mdbg_game_on_exit(pid);
log_debug(" [KSTUFF] game stopped: %s pid=%ld",
g_kstuff.game.title_id, (long)pid);
memset(&g_kstuff.game, 0, sizeof(g_kstuff.game));
if (restore_needed)
finish_tracked_game_clear("tracked game exit");
}
void sm_kstuff_game_poll(void) {
if (atomic_exchange(&g_pending_config_reload, false))
apply_kstuff_config_reload();
if (!sm_kstuff_game_feature_enabled())
return;
if (atomic_exchange(&g_pending_app_focus_valid, false)) {
handle_kstuff_app_focus_change(atomic_load(&g_pending_app_focus_id));
}
restore_kstuff_if_needed("pending auto-resume");
maybe_apply_kstuff_pause_for_slot(&g_kstuff.game);
}
void sm_kstuff_game_shutdown(void) {
bool restore_needed = tracked_game_requires_restore();
if (restore_needed && !g_kstuff.sleep_pause_restore &&
runtime_sleep_mode_active() && should_stop_requested()) {
restore_needed = false;
g_kstuff.restore_on_empty = false;
g_kstuff.restore_retry_us = 0;
} else if (g_kstuff.sleep_pause_restore) {
restore_needed = true;
g_kstuff.sleep_pause_restore = false;
}
sm_mdbg_game_shutdown();
memset(&g_kstuff.game, 0, sizeof(g_kstuff.game));
if (restore_needed) {
mark_restore_needed();
restore_kstuff_if_needed("watcher shutdown");
}
}
void sm_kstuff_sleep_enter(void) {
bool was_enabled = false;
bool fully_disabled = false;
sm_mdbg_game_shutdown();
memset(&g_kstuff.game, 0, sizeof(g_kstuff.game));
g_kstuff.sleep_pause_restore = false;
if (!sm_kstuff_is_supported())
return;
was_enabled = sm_kstuff_is_enabled();
(void)apply_kstuff_enabled_state(false, false, &fully_disabled);
if (!fully_disabled) {
log_debug(" [KSTUFF] failed to pause for system sleep");
return;
}
g_kstuff.sleep_pause_restore = was_enabled;
if (was_enabled)
log_debug(" [KSTUFF] paused for system sleep");
}
void sm_kstuff_sleep_leave(void) {
if (!g_kstuff.sleep_pause_restore)
return;
g_kstuff.sleep_pause_restore = false;
mark_restore_needed();
restore_kstuff_if_needed("system sleep");
}
void sm_kstuff_on_config_reload(void) {
atomic_store(&g_pending_config_reload, true);
}
void sm_kstuff_init(void) {
memset(&g_kstuff, 0, sizeof(g_kstuff));
atomic_store(&g_pending_app_focus_id, 0);
atomic_store(&g_pending_app_focus_valid, false);
atomic_store(&g_pending_config_reload, false);
if (!resolve_kstuff_sysentvec_addrs(&g_kstuff.sysentvec_ps5,
&g_kstuff.sysentvec_ps4)) {
log_debug(" [KSTUFF] runtime control unavailable on firmware 0x%08X",
kernel_get_fw_version());
return;
}
g_kstuff.probe_available = true;
if (!refresh_kstuff_support_state()) {
log_debug(" [KSTUFF] runtime control probe ready; kstuff not present at "
"startup");
return;
}
log_debug(" [KSTUFF] runtime control ready (image_delay=%u "
"direct_delay=%u)",
runtime_config()->kstuff_pause_delay_image_seconds,
runtime_config()->kstuff_pause_delay_direct_seconds);
}
void sm_kstuff_shutdown(void) {
sm_kstuff_game_shutdown();
atomic_store(&g_pending_app_focus_id, 0);
atomic_store(&g_pending_app_focus_valid, false);
atomic_store(&g_pending_config_reload, false);
memset(&g_kstuff, 0, sizeof(g_kstuff));
}
static uint32_t get_effective_pause_delay_seconds(const kstuff_game_entry_t *entry) {
if (!entry || entry->launch_time_us == 0 || entry->pause_deadline_us == 0 ||
entry->pause_deadline_us <= entry->launch_time_us) {
return 0;
}
uint64_t delay_us = entry->pause_deadline_us - entry->launch_time_us;
uint64_t delay_seconds = delay_us / 1000000ull;
if (delay_seconds > UINT32_MAX)
delay_seconds = UINT32_MAX;
return (uint32_t)delay_seconds;
}
+405
View File
@@ -0,0 +1,405 @@
#include "sm_platform.h"
#include <pthread.h>
#include "sm_log.h"
#include "sm_config_mount.h"
#include "sm_types.h"
#include "sm_paths.h"
static sm_error_t g_last_error;
static bool g_notifications_initialized = false;
static pthread_mutex_t g_notifications_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t g_log_mutex = PTHREAD_MUTEX_INITIALIZER;
static FILE *g_log_file = NULL;
extern unsigned char smp_icon_png[];
extern unsigned int smp_icon_png_len;
#define SCE_NOTIFICATION_LOCAL_USER_ID_SYSTEM 0xFE
int sceNotificationSend(int userId, bool isLogged, const char *payload);
int sceNotificationSendById(int userid, bool logged_in, const char *useCaseId,
const char *message);
static void notify_system_plain_message(const char *message) {
notify_request_t req;
memset(&req, 0, sizeof(req));
(void)strlcpy(req.message, message, sizeof(req.message));
sceKernelSendNotificationRequest(0, &req, sizeof(req), 0);
log_debug("NOTIFY: %s", req.message);
}
static bool ensure_notification_icon_present(void) {
struct stat st;
if (stat(NOTIFY_ICON_FILE, &st) == 0 && st.st_size > 0)
return true;
(void)mkdir("/user/data", 0777);
(void)mkdir(NOTIFY_ICON_DIR, 0777);
FILE *fp = fopen(NOTIFY_ICON_FILE, "wb");
if (!fp) {
log_debug(" [NOTIFY] icon create failed: %s (%s)", NOTIFY_ICON_FILE,
strerror(errno));
return false;
}
size_t written = fwrite(smp_icon_png, 1, smp_icon_png_len, fp);
int saved_errno = 0;
if (written != smp_icon_png_len)
saved_errno = ferror(fp) ? errno : EIO;
if (fflush(fp) != 0 && saved_errno == 0)
saved_errno = errno;
if (fclose(fp) != 0 && saved_errno == 0)
saved_errno = errno;
if (saved_errno != 0) {
errno = saved_errno;
log_debug(" [NOTIFY] icon write failed: %s (%s)", NOTIFY_ICON_FILE,
strerror(errno));
(void)unlink(NOTIFY_ICON_FILE);
return false;
}
log_debug(" [NOTIFY] icon created: %s", NOTIFY_ICON_FILE);
return true;
}
void sm_notifications_init(void) {
pthread_mutex_lock(&g_notifications_mutex);
if (!g_notifications_initialized)
g_notifications_initialized = ensure_notification_icon_present();
pthread_mutex_unlock(&g_notifications_mutex);
}
static void append_json_escaped(char *dst, size_t dst_size, const char *src) {
size_t used = strlen(dst);
if (used >= dst_size)
return;
for (; *src != '\0' && used + 1 < dst_size; ++src) {
const char *escape = NULL;
char single[2] = {0};
switch (*src) {
case '\\':
escape = "\\\\";
break;
case '"':
escape = "\\\"";
break;
case '\n':
escape = "\\n";
break;
case '\r':
escape = "\\r";
break;
case '\t':
escape = "\\t";
break;
default:
single[0] = *src;
escape = single;
break;
}
size_t escape_len = strlen(escape);
if (used + escape_len >= dst_size)
break;
memcpy(dst + used, escape, escape_len);
used += escape_len;
dst[used] = '\0';
}
}
static bool build_notification_timestamp(time_t timestamp, char out[32]) {
struct tm tm_utc;
if (gmtime_r(&timestamp, &tm_utc) == NULL)
return false;
return strftime(out, 32, "%Y-%m-%dT%H:%M:%S.000Z", &tm_utc) != 0;
}
static bool build_notification_metadata(char *created_at,
char *notification_id,
size_t notification_id_size) {
time_t now;
now = time(NULL);
if (!build_notification_timestamp(now, created_at))
return false;
snprintf(notification_id, notification_id_size, "%u",
(unsigned)((uint32_t)now ^ (uint32_t)getpid()));
return true;
}
static bool send_rich_notification(const char *message) {
char escaped_message[4096];
char escaped_version[128];
char payload[8192];
char created_at[32];
char notification_id[32];
if (!message || message[0] == '\0')
return false;
sm_notifications_init();
pthread_mutex_lock(&g_notifications_mutex);
bool notifications_ready = g_notifications_initialized;
pthread_mutex_unlock(&g_notifications_mutex);
if (!notifications_ready)
return false;
escaped_message[0] = '\0';
escaped_version[0] = '\0';
append_json_escaped(escaped_message, sizeof(escaped_message), message);
append_json_escaped(escaped_version, sizeof(escaped_version),
SHADOWMOUNT_VERSION);
if (!build_notification_metadata(created_at, notification_id,
sizeof(notification_id))) {
return false;
}
int len = snprintf(
payload, sizeof(payload),
"{\n"
" \"rawData\": {\n"
" \"viewTemplateType\": \"InteractiveToastTemplateB\",\n"
" \"channelType\": \"ServiceFeedback\",\n"
" \"bundleName\": \"ShadowMountPlusWelcome\",\n"
" \"useCaseId\": \"IDC\",\n"
" \"soundEffect\": \"none\",\n"
" \"toastOverwriteType\": \"InQueue\",\n"
" \"isImmediate\": true,\n"
" \"priority\": 100,\n"
" \"viewData\": {\n"
" \"icon\": {\n"
" \"type\": \"Url\",\n"
" \"parameters\": {\n"
" \"url\": \"" NOTIFY_ICON_FILE "\"\n"
" }\n"
" },\n"
" \"message\": {\n"
" \"body\": \"%s\"\n"
" },\n"
" \"subMessage\": {\n"
" \"body\": \"ShadowMountPlus %s\"\n"
" },\n"
" \"actions\": [\n"
" {\n"
" \"actionName\": \"Go to Debug Settings\",\n"
" \"actionType\": \"DeepLink\",\n"
" \"defaultFocus\": true,\n"
" \"parameters\": {\n"
" \"actionUrl\": \"pssettings:play?function=debug_settings\"\n"
" }\n"
" }\n"
" ]\n"
" },\n"
" \"platformViews\": {\n"
" \"previewDisabled\": {\n"
" \"viewData\": {\n"
" \"icon\": {\n"
" \"type\": \"Predefined\",\n"
" \"parameters\": {\n"
" \"icon\": \"community\"\n"
" }\n"
" },\n"
" \"message\": {\n"
" \"body\": \"%s\"\n"
" }\n"
" }\n"
" }\n"
" }\n"
" },\n"
" \"createdDateTime\": \"%s\",\n"
" \"localNotificationId\": \"%s\"\n"
"}",
escaped_message, escaped_version, escaped_message, created_at,
notification_id);
if (len < 0 || (size_t)len >= sizeof(payload))
return false;
int rc = sceNotificationSend(SCE_NOTIFICATION_LOCAL_USER_ID_SYSTEM, true,
payload);
return rc == 0;
}
static bool send_game_installed_rich_notification(const char *title_id) {
char escaped_title_id[128];
char params[256];
if (!title_id || title_id[0] == '\0')
return false;
escaped_title_id[0] = '\0';
append_json_escaped(escaped_title_id, sizeof(escaped_title_id), title_id);
int len = snprintf(params, sizeof(params),
"{\"npTitleId\":\"%s_00\"}", escaped_title_id);
if (len < 0 || (size_t)len >= sizeof(params))
return false;
int rc = sceNotificationSendById(SCE_NOTIFICATION_LOCAL_USER_ID_SYSTEM, true,
"NUC240", params);
return rc == 0;
}
static void notify_system_plain_v(const char *fmt, va_list args) {
char message[3075];
vsnprintf(message, sizeof(message), fmt, args);
notify_system_plain_message(message);
}
static FILE *ensure_log_file_open_locked(void) {
if (g_log_file)
return g_log_file;
(void)mkdir(LOG_DIR, 0777);
g_log_file = fopen(LOG_FILE, "a");
if (!g_log_file)
return NULL;
return g_log_file;
}
static void log_to_file_locked(const char *fmt, va_list args) {
FILE *fp = ensure_log_file_open_locked();
if (!fp)
return;
va_list args_file;
time_t rawtime;
struct tm tm_local;
char buffer[80];
time(&rawtime);
(void)localtime_r(&rawtime, &tm_local);
(void)strftime(buffer, sizeof(buffer), "%H:%M:%S", &tm_local);
va_copy(args_file, args);
fprintf(fp, "[%s] ", buffer);
vfprintf(fp, fmt, args_file);
fputc('\n', fp);
fflush(fp);
va_end(args_file);
}
void log_debug(const char *fmt, ...) {
if (runtime_config()->debug_enabled == false)
return;
va_list args;
va_list args_copy;
va_start(args, fmt);
va_copy(args_copy, args);
pthread_mutex_lock(&g_log_mutex);
vfprintf(stdout, fmt, args);
fputc('\n', stdout);
fflush(stdout);
log_to_file_locked(fmt, args_copy);
pthread_mutex_unlock(&g_log_mutex);
va_end(args_copy);
va_end(args);
}
void sm_log_shutdown(void) {
pthread_mutex_lock(&g_log_mutex);
if (g_log_file) {
fclose(g_log_file);
g_log_file = NULL;
}
pthread_mutex_unlock(&g_log_mutex);
}
void notify_system_rich(bool allow_in_quiet_mode, const char *fmt, ...) {
char message[3075];
va_list args;
if (runtime_config()->quiet_mode && !allow_in_quiet_mode)
return;
va_start(args, fmt);
vsnprintf(message, sizeof(message), fmt, args);
va_end(args);
if (!send_rich_notification(message)) {
notify_system_plain_message(message);
return;
}
log_debug("NOTIFY: %s", message);
}
void notify_game_installed_rich(const char *title_id) {
if (!send_game_installed_rich_notification(title_id)) {
notify_system_info("Installed: %s",
(title_id && title_id[0] != '\0') ? title_id : "Unknown");
return;
}
log_debug("NOTIFY: installed game %s", title_id ? title_id : "unknown");
}
void notify_system_info(const char *fmt, ...) {
if (runtime_config()->quiet_mode)
return;
va_list args;
va_start(args, fmt);
notify_system_plain_v(fmt, args);
va_end(args);
}
void sm_error_clear(void) {
memset(&g_last_error, 0, sizeof(g_last_error));
}
void sm_error_set(const char *subsystem, int code, const char *path,
const char *fmt, ...) {
memset(&g_last_error, 0, sizeof(g_last_error));
g_last_error.valid = true;
g_last_error.code = code;
if (subsystem && subsystem[0] != '\0')
(void)strlcpy(g_last_error.subsystem, subsystem,
sizeof(g_last_error.subsystem));
if (path && path[0] != '\0')
(void)strlcpy(g_last_error.path, path, sizeof(g_last_error.path));
if (fmt && fmt[0] != '\0') {
va_list args;
va_start(args, fmt);
vsnprintf(g_last_error.message, sizeof(g_last_error.message), fmt, args);
va_end(args);
}
}
const sm_error_t *sm_last_error(void) {
return &g_last_error;
}
bool sm_error_notified(void) {
return g_last_error.notified;
}
void sm_error_mark_notified(void) {
g_last_error.notified = true;
}
void notify_system(const char *fmt, ...) {
va_list args;
va_start(args, fmt);
notify_system_plain_v(fmt, args);
va_end(args);
}
void notify_image_mount_failed(const char *path, int mount_err) {
if (g_last_error.valid && g_last_error.message[0] != '\0') {
const char *error_path =
(g_last_error.path[0] != '\0') ? g_last_error.path : path;
notify_system("%s\n%s", g_last_error.message, error_path);
g_last_error.notified = true;
return;
}
notify_system("Image mount failed: 0x%08X (%s)\n%s", (uint32_t)mount_err,
strerror(mount_err), path);
}
+420
View File
@@ -0,0 +1,420 @@
#include "sm_platform.h"
#include "sm_manual.h"
#include "sm_appdb.h"
#include "sm_install_queue.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_paths.h"
#include "sm_title_state.h"
#define MANUAL_STATUS_CAPACITY 1024
typedef enum {
MANUAL_STATUS_UNKNOWN = 0,
MANUAL_STATUS_INSTALLED,
MANUAL_STATUS_DELETED,
} manual_status_t;
typedef struct {
char path[MAX_PATH];
char title_id[MAX_TITLE_ID];
char title_name[MAX_TITLE_NAME];
manual_status_t status;
bool remove_from_list;
} manual_status_entry_t;
static manual_status_entry_t g_manual_status_entries[MANUAL_STATUS_CAPACITY];
static void trim_manual_line(char *line) {
if (!line)
return;
char *start = line;
while (*start != '\0' && isspace((unsigned char)*start))
start++;
if (start != line)
memmove(line, start, strlen(start) + 1u);
size_t len = strlen(line);
while (len > 0 && isspace((unsigned char)line[len - 1u])) {
line[len - 1u] = '\0';
len--;
}
while (len > 1 && line[len - 1u] == '/') {
line[len - 1u] = '\0';
len--;
}
}
static void drain_manual_file_line(FILE *f) {
int ch;
while ((ch = fgetc(f)) != EOF && ch != '\n') {
}
}
static void copy_manual_file_line_remainder(FILE *in, FILE *out,
int *saved_errno) {
int ch;
while ((ch = fgetc(in)) != EOF) {
if (saved_errno && *saved_errno == 0 && fputc(ch, out) == EOF)
*saved_errno = errno;
if (ch == '\n')
break;
}
}
bool sm_manual_for_each_path(bool (*visit)(const char *path, void *ctx),
void *ctx) {
if (!visit)
return false;
FILE *f = fopen(MANUAL_LIST_FILE, "r");
if (!f) {
if (errno != ENOENT)
log_debug(" [MANUAL] open failed for %s: %s", MANUAL_LIST_FILE,
strerror(errno));
return false;
}
char line[MAX_PATH + 64];
bool completed = true;
while (fgets(line, sizeof(line), f) != NULL) {
bool truncated =
strchr(line, '\n') == NULL && strlen(line) == sizeof(line) - 1u;
trim_manual_line(line);
if (line[0] == '\0' || line[0] == '#') {
if (truncated)
drain_manual_file_line(f);
continue;
}
if (truncated || strlen(line) >= MAX_PATH) {
log_debug(" [MANUAL] path too long, ignored: %.80s", line);
if (truncated)
drain_manual_file_line(f);
continue;
}
if (!visit(line, ctx)) {
completed = false;
break;
}
}
if (ferror(f)) {
log_debug(" [MANUAL] read failed for %s: %s", MANUAL_LIST_FILE,
strerror(errno));
completed = false;
}
fclose(f);
return completed;
}
static const char *manual_status_name(manual_status_t status) {
switch (status) {
case MANUAL_STATUS_INSTALLED:
return "installed";
case MANUAL_STATUS_DELETED:
return "deleted";
default:
return "unknown";
}
}
static manual_status_t parse_manual_status_name(const char *name) {
if (!name)
return MANUAL_STATUS_UNKNOWN;
if (strcasecmp(name, "installed") == 0)
return MANUAL_STATUS_INSTALLED;
if (strcasecmp(name, "deleted") == 0)
return MANUAL_STATUS_DELETED;
return MANUAL_STATUS_UNKNOWN;
}
static bool parse_manual_status_line(char *line, manual_status_entry_t *entry) {
char *status = strtok(line, "\t");
char *title_id = strtok(NULL, "\t");
char *path = strtok(NULL, "\t");
char *title_name = strtok(NULL, "\n");
if (!status || !title_id || !path)
return false;
trim_manual_line(status);
trim_manual_line(title_id);
trim_manual_line(path);
if (title_name)
trim_manual_line(title_name);
manual_status_t parsed_status = parse_manual_status_name(status);
if (parsed_status == MANUAL_STATUS_UNKNOWN || title_id[0] == '\0' ||
path[0] == '\0') {
return false;
}
memset(entry, 0, sizeof(*entry));
entry->status = parsed_status;
(void)strlcpy(entry->title_id, title_id, sizeof(entry->title_id));
(void)strlcpy(entry->path, path, sizeof(entry->path));
if (title_name && title_name[0] != '\0')
(void)strlcpy(entry->title_name, title_name, sizeof(entry->title_name));
return true;
}
static int load_manual_status(void) {
FILE *f = fopen(MANUAL_STATUS_FILE, "r");
if (!f) {
if (errno != ENOENT)
log_debug(" [MANUAL] status open failed for %s: %s",
MANUAL_STATUS_FILE, strerror(errno));
return 0;
}
int count = 0;
char line[MAX_PATH + MAX_TITLE_NAME + MAX_TITLE_ID + 64];
while (fgets(line, sizeof(line), f) != NULL) {
if (count >= MANUAL_STATUS_CAPACITY) {
log_debug(" [MANUAL] status limit reached (%d)",
MANUAL_STATUS_CAPACITY);
break;
}
manual_status_entry_t entry;
if (!parse_manual_status_line(line, &entry))
continue;
g_manual_status_entries[count++] = entry;
}
if (ferror(f))
log_debug(" [MANUAL] status read failed for %s: %s",
MANUAL_STATUS_FILE, strerror(errno));
fclose(f);
return count;
}
static bool write_manual_status(int entry_count) {
mkdir(LOG_DIR, 0777);
char tmp_path[MAX_PATH];
snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", MANUAL_STATUS_FILE);
FILE *f = fopen(tmp_path, "w");
if (!f) {
log_debug(" [MANUAL] status write open failed for %s: %s", tmp_path,
strerror(errno));
return false;
}
int saved_errno = 0;
for (int i = 0; i < entry_count; i++) {
const manual_status_entry_t *entry = &g_manual_status_entries[i];
if (entry->status == MANUAL_STATUS_UNKNOWN || entry->path[0] == '\0' ||
entry->title_id[0] == '\0') {
continue;
}
if (fprintf(f, "%s\t%s\t%s\t%s\n", manual_status_name(entry->status),
entry->title_id, entry->path, entry->title_name) < 0) {
saved_errno = errno;
break;
}
}
if (fflush(f) != 0 && saved_errno == 0)
saved_errno = errno;
if (fclose(f) != 0 && saved_errno == 0)
saved_errno = errno;
if (saved_errno != 0) {
errno = saved_errno;
log_debug(" [MANUAL] status write failed for %s: %s", tmp_path,
strerror(errno));
(void)unlink(tmp_path);
return false;
}
if (rename(tmp_path, MANUAL_STATUS_FILE) != 0) {
log_debug(" [MANUAL] status rename failed for %s: %s",
MANUAL_STATUS_FILE, strerror(errno));
(void)unlink(tmp_path);
return false;
}
return true;
}
static int find_manual_status_entry(int entry_count, const char *path,
const char *title_id) {
for (int i = 0; i < entry_count; i++) {
if (path && path[0] != '\0' &&
strcmp(g_manual_status_entries[i].path, path) == 0) {
return i;
}
if (title_id && title_id[0] != '\0' &&
strcmp(g_manual_status_entries[i].title_id, title_id) == 0) {
return i;
}
}
return -1;
}
static bool rewrite_manual_list_without_deleted_entries(int entry_count) {
FILE *in = fopen(MANUAL_LIST_FILE, "r");
if (!in) {
if (errno == ENOENT)
return true;
log_debug(" [MANUAL] list rewrite open failed for %s: %s",
MANUAL_LIST_FILE, strerror(errno));
return false;
}
char tmp_path[MAX_PATH];
snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", MANUAL_LIST_FILE);
FILE *out = fopen(tmp_path, "w");
if (!out) {
log_debug(" [MANUAL] list rewrite temp open failed for %s: %s", tmp_path,
strerror(errno));
fclose(in);
return false;
}
bool changed = false;
int saved_errno = 0;
char line[MAX_PATH + 64];
while (fgets(line, sizeof(line), in) != NULL) {
bool truncated =
strchr(line, '\n') == NULL && strlen(line) == sizeof(line) - 1u;
char normalized[MAX_PATH + 64];
(void)strlcpy(normalized, line, sizeof(normalized));
trim_manual_line(normalized);
if (!truncated && normalized[0] != '\0' && normalized[0] != '#') {
bool remove_line = false;
for (int i = 0; i < entry_count; i++) {
if (g_manual_status_entries[i].remove_from_list &&
strcmp(g_manual_status_entries[i].path, normalized) == 0) {
remove_line = true;
break;
}
}
if (remove_line) {
changed = true;
if (truncated)
drain_manual_file_line(in);
continue;
}
}
if (fputs(line, out) == EOF) {
saved_errno = errno;
break;
}
if (truncated)
copy_manual_file_line_remainder(in, out, &saved_errno);
}
if (ferror(in) && saved_errno == 0)
saved_errno = errno;
if (fflush(out) != 0 && saved_errno == 0)
saved_errno = errno;
if (fclose(out) != 0 && saved_errno == 0)
saved_errno = errno;
fclose(in);
if (saved_errno != 0) {
errno = saved_errno;
log_debug(" [MANUAL] list rewrite failed for %s: %s", MANUAL_LIST_FILE,
strerror(errno));
(void)unlink(tmp_path);
return false;
}
if (!changed) {
(void)unlink(tmp_path);
return true;
}
if (rename(tmp_path, MANUAL_LIST_FILE) != 0) {
log_debug(" [MANUAL] list rewrite rename failed for %s: %s",
MANUAL_LIST_FILE, strerror(errno));
(void)unlink(tmp_path);
return false;
}
return true;
}
bool sm_manual_reconcile_deleted_titles(
const struct AppDbTitleList *app_db_titles, bool app_db_titles_ready) {
if (!app_db_titles_ready || !app_db_titles)
return true;
int entry_count = load_manual_status();
if (entry_count <= 0)
return true;
bool changed = false;
for (int i = 0; i < entry_count; i++) {
manual_status_entry_t *entry = &g_manual_status_entries[i];
if (entry->status != MANUAL_STATUS_INSTALLED ||
entry->title_id[0] == '\0') {
continue;
}
if (app_db_title_list_contains(app_db_titles, entry->title_id))
continue;
entry->status = MANUAL_STATUS_DELETED;
entry->remove_from_list = true;
sm_install_forget_pending_title(entry->title_id);
clear_register_attempts(entry->title_id);
clear_failed_mount_attempts(entry->title_id);
changed = true;
log_debug(" [MANUAL] title removed from app.db, disabling manual entry: "
"%s (%s)",
entry->title_name, entry->title_id);
}
if (!changed)
return true;
if (!rewrite_manual_list_without_deleted_entries(entry_count))
return false;
return write_manual_status(entry_count);
}
void sm_manual_note_installed(const char *manual_source_path,
const char *title_id,
const char *title_name) {
if (!manual_source_path || manual_source_path[0] == '\0' || !title_id ||
title_id[0] == '\0') {
return;
}
int entry_count = load_manual_status();
int index =
find_manual_status_entry(entry_count, manual_source_path, title_id);
if (index < 0) {
if (entry_count >= MANUAL_STATUS_CAPACITY) {
log_debug(" [MANUAL] status full, cannot record %s (%s)",
manual_source_path, title_id);
return;
}
index = entry_count++;
memset(&g_manual_status_entries[index], 0,
sizeof(g_manual_status_entries[index]));
}
manual_status_entry_t *entry = &g_manual_status_entries[index];
manual_status_t previous_status = entry->status;
bool unchanged = previous_status == MANUAL_STATUS_INSTALLED &&
strcmp(entry->path, manual_source_path) == 0 &&
strcmp(entry->title_id, title_id) == 0 &&
(!title_name || title_name[0] == '\0' ||
strcmp(entry->title_name, title_name) == 0);
if (unchanged)
return;
entry->status = MANUAL_STATUS_INSTALLED;
(void)strlcpy(entry->path, manual_source_path, sizeof(entry->path));
(void)strlcpy(entry->title_id, title_id, sizeof(entry->title_id));
if (title_name && title_name[0] != '\0')
(void)strlcpy(entry->title_name, title_name, sizeof(entry->title_name));
if (!write_manual_status(entry_count))
return;
if (previous_status != MANUAL_STATUS_INSTALLED) {
log_debug(" [MANUAL] installed status recorded: %s (%s) from %s",
entry->title_name, entry->title_id, entry->path);
}
}
+664
View File
@@ -0,0 +1,664 @@
#include "sm_platform.h"
#include <inttypes.h>
#include <stdlib.h>
#include "sm_config_mount.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_mdbg.h"
#include "sm_time.h"
#include "sm_types.h"
int sceKernelDebugGetPrivateLogText(void *buffer, size_t buffer_size,
char **text, uint64_t *text_size);
int sceKernelDebugGetSdkLogText(void *buffer, size_t buffer_size,
char **text, uint64_t *text_size);
uint64_t sceKernelDebugGetLogBufferSize(void) __asm__("C49jelxiaVE");
int mdbg_call(void *cmd, void *req, void *res);
#define MDBG_CMD_TYPE_SERVICE 1ull
#define MDBG_CMD_PROCESS_STATE 30ull
#define MDBG_SUBCMD_FLAGS 2ull
#define MDBG_FLAG_EXCEPTION_STOP 0x00080000ull
#define MDBG_AUTOTUNE_WINDOW_US (300ull * 1000000ull)
#define MDBG_LOG_LINE_BUFFER_SIZE 512u
#define MDBG_RTLD_ERROR_PREFIX_SIZE 32u
#ifndef MDBG_USE_PRIVATE_LOG_TEXT
#define MDBG_USE_PRIVATE_LOG_TEXT 0
#endif
#ifndef MDBG_SKIP_PRIVILEGE_ELEVATION
#define MDBG_SKIP_PRIVILEGE_ELEVATION 0
#endif
#if MDBG_USE_PRIVATE_LOG_TEXT
#define MDBG_FETCH_LOG_TEXT sceKernelDebugGetPrivateLogText
#else
#define MDBG_FETCH_LOG_TEXT sceKernelDebugGetSdkLogText
#endif
typedef struct {
uint64_t type;
uint64_t cmd;
} mdbg_cmd_t;
typedef struct {
int64_t pid;
int64_t subcmd;
uint64_t arg;
uint64_t reserved[5];
} mdbg_req_t;
typedef struct {
int64_t status;
uint64_t value;
uint64_t reserved[2];
} mdbg_res_t;
typedef struct {
bool active;
bool log_monitoring_active;
bool pause_seen;
pid_t pid;
uint32_t pause_delay_seconds;
uint64_t monitor_deadline_us;
uint64_t pause_time_us;
uint64_t next_poll_us;
char title_id[MAX_TITLE_ID];
char rtld_error_prefix[MDBG_RTLD_ERROR_PREFIX_SIZE];
} mdbg_game_state_t;
typedef struct {
bool privilege_probe_done;
bool privilege_ready;
size_t log_buffer_size;
size_t log_snapshot_length;
size_t log_line_length;
char *log_snapshot;
char *log_storage;
char log_line[MDBG_LOG_LINE_BUFFER_SIZE];
mdbg_game_state_t game;
} sm_mdbg_state_t;
static sm_mdbg_state_t g_mdbg;
static bool sm_mdbg_enabled(void);
#if !MDBG_SKIP_PRIVILEGE_ELEVATION
static int elevate_to_coredump(void);
#endif
static bool ensure_mdbg_privileges(void);
static int mdbg_call_raw(int64_t pid, uint64_t subcmd, uint64_t arg,
int64_t *status_out, uint64_t *value_out);
static int query_mdbg_flags(pid_t pid, uint64_t *flags_out);
static bool is_mdbg_process_gone_error(int ret);
static void reset_log_line_buffer(void);
static void reset_log_snapshot(void);
static void free_log_buffers(void);
static bool ensure_log_buffers(void);
static int fetch_log_text(const char **text_out, size_t *text_len_out);
static size_t find_log_overlap(const char *current, size_t current_len);
static void update_log_snapshot(const char *text, size_t text_len);
static void clear_tracked_game(void);
static bool matches_tracked_rtld_error(const char *text);
static void summarize_failure_reason(const char *reason, char *summary_out,
size_t summary_out_size,
bool is_rtld_error);
static void handle_pre_pause_failure(const char *reason);
static void handle_post_pause_failure(const char *reason, uint64_t now_us);
static void process_log_line(const char *line, uint64_t now_us);
static void append_log_char(char ch, uint64_t now_us);
static void poll_log_monitor(uint64_t now_us);
static void start_log_monitoring(void);
static void handle_crash_candidate(uint64_t flags, uint64_t now_us);
static bool sm_mdbg_enabled(void) {
return runtime_config()->kstuff_crash_detection_enabled &&
runtime_config()->kstuff_game_auto_toggle;
}
#if !MDBG_SKIP_PRIVILEGE_ELEVATION
#define SCE_AUTHID_COREDUMP 0x4800000000000006ull
static int elevate_to_coredump(void) {
static const uint8_t k_priv_caps[16] = {
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
pid_t pid = getpid();
if (kernel_set_ucred_authid(pid, SCE_AUTHID_COREDUMP) < 0)
return -1;
if (kernel_set_ucred_caps(pid, k_priv_caps) < 0)
return -1;
return 0;
}
#endif
static bool ensure_mdbg_privileges(void) {
if (g_mdbg.privilege_probe_done)
return g_mdbg.privilege_ready;
g_mdbg.privilege_probe_done = true;
#if MDBG_SKIP_PRIVILEGE_ELEVATION
g_mdbg.privilege_ready = true;
log_debug(" [MDBG] coredump privilege probe skipped");
#else
g_mdbg.privilege_ready = elevate_to_coredump() == 0;
if (g_mdbg.privilege_ready) {
log_debug(" [MDBG] coredump privileges enabled");
} else {
log_debug(" [MDBG] failed to enable coredump privileges; mdbg polling "
"disabled, log monitoring disabled");
}
#endif
return g_mdbg.privilege_ready;
}
static int mdbg_call_raw(int64_t pid, uint64_t subcmd, uint64_t arg,
int64_t *status_out, uint64_t *value_out) {
mdbg_cmd_t cmd = {MDBG_CMD_TYPE_SERVICE, MDBG_CMD_PROCESS_STATE};
mdbg_req_t req = {
.pid = pid,
.subcmd = (int64_t)subcmd,
.arg = arg,
};
mdbg_res_t res = {0};
int ret = mdbg_call(&cmd, &req, &res);
if (ret != 0)
return ret;
if (status_out)
*status_out = res.status;
if (value_out)
*value_out = res.value;
return 0;
}
static int query_mdbg_flags(pid_t pid, uint64_t *flags_out) {
int64_t status;
uint64_t value;
int ret = mdbg_call_raw(pid, MDBG_SUBCMD_FLAGS, 0, &status, &value);
if (ret < 0)
return ret;
if (status != 0)
return (int)status;
if (flags_out)
*flags_out = value;
return 0;
}
static bool is_mdbg_process_gone_error(int ret) {
return ret == -ESRCH || ret == -ENOENT || ret == ESRCH || ret == ENOENT;
}
static void reset_log_line_buffer(void) {
g_mdbg.log_line_length = 0;
g_mdbg.log_line[0] = '\0';
}
static void reset_log_snapshot(void) {
g_mdbg.log_snapshot_length = 0;
reset_log_line_buffer();
}
static void free_log_buffers(void) {
free(g_mdbg.log_snapshot);
free(g_mdbg.log_storage);
g_mdbg.log_snapshot = NULL;
g_mdbg.log_storage = NULL;
g_mdbg.log_buffer_size = 0;
reset_log_snapshot();
}
static bool ensure_log_buffers(void) {
if (g_mdbg.log_storage && g_mdbg.log_snapshot && g_mdbg.log_buffer_size != 0)
return true;
free_log_buffers();
const char *title_id =
g_mdbg.game.title_id[0] != '\0' ? g_mdbg.game.title_id : "?";
uint64_t raw_size = sceKernelDebugGetLogBufferSize();
if (raw_size == 0) {
log_debug(" [MDBG] invalid log buffer size for %s: 0x%016" PRIx64,
title_id,
raw_size);
return false;
}
size_t buffer_size = (size_t)raw_size;
char *storage = malloc(buffer_size);
char *snapshot = malloc(buffer_size);
if (!storage || !snapshot) {
free(storage);
free(snapshot);
log_debug(" [MDBG] failed to allocate log buffers for %s: size=0x%zx",
title_id,
buffer_size);
return false;
}
g_mdbg.log_storage = storage;
g_mdbg.log_snapshot = snapshot;
g_mdbg.log_buffer_size = buffer_size;
reset_log_snapshot();
log_debug(" [MDBG] log monitor ready: %s buffer_size=0x%zx", title_id,
buffer_size);
return true;
}
static int fetch_log_text(const char **text_out, size_t *text_len_out) {
char *raw_text = NULL;
uint64_t raw_len = 0;
*text_out = NULL;
*text_len_out = 0;
int ret =
MDBG_FETCH_LOG_TEXT(g_mdbg.log_storage, g_mdbg.log_buffer_size, &raw_text,
&raw_len);
if (ret < 0)
return ret;
if (!raw_text || raw_len == 0) {
return 0;
}
if (raw_len > g_mdbg.log_buffer_size)
return -EOVERFLOW;
*text_out = raw_text;
*text_len_out = (size_t)raw_len;
return 0;
}
static size_t find_log_overlap(const char *current, size_t current_len) {
if (!g_mdbg.log_snapshot || g_mdbg.log_snapshot_length == 0 || !current ||
current_len == 0) {
return 0;
}
size_t max_overlap = g_mdbg.log_snapshot_length < current_len
? g_mdbg.log_snapshot_length
: current_len;
const char *search = g_mdbg.log_snapshot + g_mdbg.log_snapshot_length -
max_overlap;
const char *end = g_mdbg.log_snapshot + g_mdbg.log_snapshot_length;
while (search < end) {
const char *candidate =
memchr(search, current[0], (size_t)(end - search));
if (!candidate)
break;
size_t overlap = (size_t)(end - candidate);
if (!memcmp(candidate, current, overlap))
return overlap;
search = candidate + 1;
}
return 0;
}
static void update_log_snapshot(const char *text, size_t text_len) {
if (!g_mdbg.log_snapshot || g_mdbg.log_buffer_size == 0)
return;
if (!text || text_len == 0) {
g_mdbg.log_snapshot_length = 0;
return;
}
if (text_len > g_mdbg.log_buffer_size)
text_len = g_mdbg.log_buffer_size;
memcpy(g_mdbg.log_snapshot, text, text_len);
g_mdbg.log_snapshot_length = text_len;
}
static void clear_tracked_game(void) {
memset(&g_mdbg.game, 0, sizeof(g_mdbg.game));
free_log_buffers();
}
static bool matches_tracked_rtld_error(const char *text) {
return text &&
g_mdbg.game.rtld_error_prefix[0] != '\0' &&
strstr(text, g_mdbg.game.rtld_error_prefix) != NULL;
}
static void summarize_failure_reason(const char *reason, char *summary_out,
size_t summary_out_size,
bool is_rtld_error) {
if (!summary_out || summary_out_size == 0)
return;
summary_out[0] = '\0';
if (!reason || reason[0] == '\0')
return;
if (is_rtld_error) {
const char *open_paren = strrchr(reason, '(');
const char *close_paren =
open_paren ? strchr(open_paren + 1, ')') : NULL;
if (open_paren && close_paren && close_paren > open_paren + 1) {
int written = snprintf(summary_out, summary_out_size,
"can't load module %.*s after KStuff pause",
(int)(close_paren - open_paren - 1),
open_paren + 1);
if (written > 0 && (size_t)written < summary_out_size)
return;
}
(void)strlcpy(summary_out, "can't load module after KStuff pause",
summary_out_size);
return;
}
(void)strlcpy(summary_out, reason, summary_out_size);
}
static void handle_pre_pause_failure(const char *reason) {
log_debug(" [MDBG] %s crashed before kstuff auto-pause%s%s",
g_mdbg.game.title_id, reason ? ": " : "", reason ? reason : "");
notify_system_info("App crashed before KStuff pause: %s. KStuff is not to "
"blame.",
g_mdbg.game.title_id);
clear_tracked_game();
}
static void handle_post_pause_failure(const char *reason, uint64_t now_us) {
if (!g_mdbg.game.pause_seen || g_mdbg.game.pause_time_us == 0 ||
now_us < g_mdbg.game.pause_time_us) {
handle_pre_pause_failure(reason);
return;
}
uint64_t post_pause_us = now_us - g_mdbg.game.pause_time_us;
if (post_pause_us > MDBG_AUTOTUNE_WINDOW_US) {
log_debug(" [MDBG] %s crashed %us after kstuff pause; autotune skipped%s%s",
g_mdbg.game.title_id, (unsigned)(post_pause_us / 1000000ull),
reason ? ": " : "", reason ? reason : "");
notify_system_info("App crashed after KStuff pause: %s. Autotune skipped.",
g_mdbg.game.title_id);
clear_tracked_game();
return;
}
char reason_summary[128];
bool is_rtld_error = matches_tracked_rtld_error(reason);
summarize_failure_reason(reason, reason_summary, sizeof(reason_summary),
is_rtld_error);
uint32_t tuned_delay_seconds = 0;
if (upsert_kstuff_autotune_pause_delay(g_mdbg.game.title_id,
g_mdbg.game.pause_delay_seconds,
&tuned_delay_seconds)) {
log_debug(" [MDBG] autotune pause delay updated: %s=%us",
g_mdbg.game.title_id, tuned_delay_seconds);
if (reason_summary[0] != '\0')
log_debug(" [MDBG] autotune trigger: %s", reason_summary);
if (is_rtld_error) {
notify_system_info("%s: %s. Delay increased to %us.\nLaunch the game again.",
g_mdbg.game.title_id,
reason_summary[0] != '\0' ? reason_summary
: "Can't load module after "
"KStuff pause",
tuned_delay_seconds);
} else {
notify_system_info("Crash detected after KStuff pause: pause delay for %s "
"increased to %us. Launch the game again.",
g_mdbg.game.title_id, tuned_delay_seconds);
}
clear_tracked_game();
return;
}
log_debug(" [MDBG] failed to persist autotune pause delay for %s%s%s",
g_mdbg.game.title_id, reason ? ": " : "", reason ? reason : "");
notify_system_info("Crash detected after KStuff pause: %s. Failed to update "
"autotune delay.",
g_mdbg.game.title_id);
clear_tracked_game();
}
static void process_log_line(const char *line, uint64_t now_us) {
if (!matches_tracked_rtld_error(line))
return;
log_debug(" [MDBG] log load error for %s: %s", g_mdbg.game.title_id, line);
handle_post_pause_failure(line, now_us);
}
static void append_log_char(char ch, uint64_t now_us) {
if (!g_mdbg.game.active)
return;
if (ch == '\r' || ch == '\n') {
if (g_mdbg.log_line_length == 0)
return;
g_mdbg.log_line[g_mdbg.log_line_length] = '\0';
process_log_line(g_mdbg.log_line, now_us);
reset_log_line_buffer();
return;
}
if (g_mdbg.log_line_length + 1u >= sizeof(g_mdbg.log_line)) {
g_mdbg.log_line[g_mdbg.log_line_length] = '\0';
process_log_line(g_mdbg.log_line, now_us);
reset_log_line_buffer();
}
g_mdbg.log_line[g_mdbg.log_line_length++] = ch;
}
static void poll_log_monitor(uint64_t now_us) {
if (!g_mdbg.game.active || !g_mdbg.game.log_monitoring_active) {
return;
}
if (!ensure_log_buffers()) {
g_mdbg.game.log_monitoring_active = false;
return;
}
const char *text = NULL;
size_t text_len = 0;
int ret = fetch_log_text(&text, &text_len);
if (ret < 0) {
log_debug(" [MDBG] log snapshot failed for %s: 0x%08x",
g_mdbg.game.title_id, ret);
g_mdbg.game.log_monitoring_active = false;
reset_log_snapshot();
return;
}
size_t skip = 0;
if (g_mdbg.log_snapshot_length != 0 && text_len >= g_mdbg.log_snapshot_length &&
!memcmp(g_mdbg.log_snapshot, text, g_mdbg.log_snapshot_length)) {
skip = g_mdbg.log_snapshot_length;
} else {
skip = find_log_overlap(text, text_len);
if (skip == 0 && g_mdbg.log_snapshot_length != 0) {
log_debug(" [MDBG] log stream reset or truncated for %s",
g_mdbg.game.title_id);
reset_log_line_buffer();
}
}
for (size_t i = skip; i < text_len; ++i) {
append_log_char(text[i], now_us);
if (!g_mdbg.game.active)
return;
}
update_log_snapshot(text, text_len);
}
static void start_log_monitoring(void) {
if (!g_mdbg.game.active)
return;
if (!ensure_mdbg_privileges()) {
g_mdbg.game.log_monitoring_active = false;
return;
}
if (!ensure_log_buffers()) {
g_mdbg.game.log_monitoring_active = false;
return;
}
g_mdbg.game.log_monitoring_active = true;
const char *text = NULL;
size_t text_len = 0;
int ret = fetch_log_text(&text, &text_len);
if (ret < 0) {
log_debug(" [MDBG] initial log snapshot failed for %s: 0x%08x",
g_mdbg.game.title_id, ret);
g_mdbg.game.log_monitoring_active = false;
reset_log_snapshot();
return;
}
update_log_snapshot(text, text_len);
reset_log_line_buffer();
}
static void handle_crash_candidate(uint64_t flags, uint64_t now_us) {
if (!g_mdbg.game.active)
return;
log_debug(" [MDBG] crash-candidate: %s pid=%ld flags=0x%08" PRIx64,
g_mdbg.game.title_id, (long)g_mdbg.game.pid, flags);
handle_post_pause_failure("crash-candidate", now_us);
}
void sm_mdbg_init(void) {
memset(&g_mdbg, 0, sizeof(g_mdbg));
}
void sm_mdbg_shutdown(void) {
free_log_buffers();
memset(&g_mdbg, 0, sizeof(g_mdbg));
}
void sm_mdbg_game_on_exec(pid_t pid, const char *title_id, uint32_t app_id) {
if (pid <= 0 || !title_id || title_id[0] == '\0')
return;
if (!sm_mdbg_enabled()) {
clear_tracked_game();
return;
}
if (g_mdbg.game.active && g_mdbg.game.pid != pid) {
log_debug(" [MDBG] replacing tracked game pid=%ld (%s) with pid=%ld (%s)",
(long)g_mdbg.game.pid, g_mdbg.game.title_id, (long)pid, title_id);
}
clear_tracked_game();
uint64_t now_us = monotonic_time_us();
g_mdbg.game.active = true;
g_mdbg.game.pid = pid;
g_mdbg.game.next_poll_us = now_us;
(void)strlcpy(g_mdbg.game.title_id, title_id, sizeof(g_mdbg.game.title_id));
int rtld_prefix_len =
snprintf(g_mdbg.game.rtld_error_prefix,
sizeof(g_mdbg.game.rtld_error_prefix),
"[rtld] <%ld> ERROR", (long)pid);
if (rtld_prefix_len <= 0 ||
(size_t)rtld_prefix_len >= sizeof(g_mdbg.game.rtld_error_prefix)) {
g_mdbg.game.rtld_error_prefix[0] = '\0';
}
log_debug(" [MDBG] tracking crash-candidate state: %s pid=%ld app_id=0x%08X",
g_mdbg.game.title_id, (long)pid, app_id);
}
void sm_mdbg_game_on_kstuff_pause(pid_t pid, uint64_t pause_time_us,
uint32_t pause_delay_seconds) {
if (!g_mdbg.game.active || g_mdbg.game.pid != pid)
return;
g_mdbg.game.pause_seen = true;
g_mdbg.game.pause_time_us = pause_time_us;
g_mdbg.game.monitor_deadline_us =
g_mdbg.game.pause_time_us + MDBG_AUTOTUNE_WINDOW_US;
g_mdbg.game.pause_delay_seconds = pause_delay_seconds;
g_mdbg.game.next_poll_us = pause_time_us;
start_log_monitoring();
}
void sm_mdbg_game_on_exit(pid_t pid) {
if (!g_mdbg.game.active || g_mdbg.game.pid != pid)
return;
clear_tracked_game();
}
void sm_mdbg_game_shutdown(void) {
clear_tracked_game();
}
uint64_t sm_mdbg_next_wake_us(uint64_t now_us) {
(void)now_us;
if (!sm_mdbg_enabled())
return 0;
if (!g_mdbg.game.active)
return 0;
uint64_t next_wake_us = g_mdbg.game.next_poll_us;
if (g_mdbg.game.monitor_deadline_us != 0 &&
(next_wake_us == 0 || g_mdbg.game.monitor_deadline_us < next_wake_us)) {
next_wake_us = g_mdbg.game.monitor_deadline_us;
}
return next_wake_us;
}
void sm_mdbg_poll(void) {
if (!sm_mdbg_enabled())
return;
if (!g_mdbg.game.active)
return;
uint64_t now_us = monotonic_time_us();
if (now_us < g_mdbg.game.next_poll_us) {
return;
}
if (g_mdbg.game.pause_seen && g_mdbg.game.monitor_deadline_us != 0 &&
now_us >= g_mdbg.game.monitor_deadline_us) {
log_debug(" [MDBG] crash monitoring window expired for %s pid=%ld",
g_mdbg.game.title_id, (long)g_mdbg.game.pid);
clear_tracked_game();
return;
}
if (!ensure_mdbg_privileges()) {
g_mdbg.game.next_poll_us = 0;
return;
}
poll_log_monitor(now_us);
if (!g_mdbg.game.active)
return;
g_mdbg.game.next_poll_us = now_us + GAME_LIFECYCLE_POLL_INTERVAL_US;
uint64_t flags = 0;
int ret = query_mdbg_flags(g_mdbg.game.pid, &flags);
if (is_mdbg_process_gone_error(ret)) {
clear_tracked_game();
return;
}
if (ret != 0)
return;
if ((flags & MDBG_FLAG_EXCEPTION_STOP) == 0)
return;
handle_crash_candidate(flags, now_us);
}
+248
View File
@@ -0,0 +1,248 @@
#include "sm_platform.h"
#include "sm_runtime.h"
#include "sm_mount_device.h"
#include "sm_image_cache.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_config_mount.h"
#include "sm_mount_defs.h"
#include "sm_path_utils.h"
#include "sm_stability.h"
const char *attach_backend_name(attach_backend_t backend) {
if (backend == ATTACH_BACKEND_LVD)
return "LVD";
if (backend == ATTACH_BACKEND_MD)
return "MD";
return "UNKNOWN";
}
// --- Device Node Wait and Source Stability ---
bool wait_for_dev_node_state(const char *devname, bool should_exist) {
for (int i = 0; i < LVD_NODE_WAIT_RETRIES; i++) {
if (path_exists(devname) == should_exist)
return true;
sceKernelUsleep(LVD_NODE_WAIT_US);
}
return false;
}
bool is_source_stable_for_mount(const char *path, const char *name,
const char *tag) {
double age = 0.0;
int st_err = 0;
if (is_path_stable_now(path, &age, &st_err))
return true;
if (st_err != 0)
return false;
log_debug(" [%s] %s modified %.0fs ago, waiting...", tag, name, age);
return false;
}
// --- Mounted Device Resolution (/dev/lvdN, /dev/mdN) ---
static bool parse_unit_from_dev_path(const char *dev_path, const char *prefix,
int *unit_out) {
size_t prefix_len = strlen(prefix);
if (strncmp(dev_path, prefix, prefix_len) != 0)
return false;
char *end = NULL;
long unit = strtol(dev_path + prefix_len, &end, 10);
if (end == dev_path + prefix_len || *end != '\0' || unit < 0 ||
unit > INT_MAX)
return false;
*unit_out = (int)unit;
return true;
}
bool resolve_device_from_mount(const char *mount_point,
attach_backend_t *backend_out, int *unit_out) {
*backend_out = ATTACH_BACKEND_NONE;
*unit_out = -1;
if (resolve_device_from_mount_cache(mount_point, backend_out, unit_out))
return true;
struct statfs sfs;
if (statfs(mount_point, &sfs) != 0)
return false;
if (strcmp(sfs.f_mntonname, mount_point) != 0)
return false;
if (parse_unit_from_dev_path(sfs.f_mntfromname, "/dev/lvd", unit_out)) {
*backend_out = ATTACH_BACKEND_LVD;
return true;
}
if (parse_unit_from_dev_path(sfs.f_mntfromname, "/dev/md", unit_out)) {
*backend_out = ATTACH_BACKEND_MD;
return true;
}
struct statfs *mntbuf = NULL;
int mntcount = getmntinfo(&mntbuf, MNT_NOWAIT);
if (mntcount <= 0 || !mntbuf)
return false;
for (int i = 0; i < mntcount; i++) {
if (strcmp(mntbuf[i].f_mntonname, mount_point) != 0)
continue;
if (parse_unit_from_dev_path(mntbuf[i].f_mntfromname, "/dev/lvd", unit_out)) {
*backend_out = ATTACH_BACKEND_LVD;
return true;
}
if (parse_unit_from_dev_path(mntbuf[i].f_mntfromname, "/dev/md", unit_out)) {
*backend_out = ATTACH_BACKEND_MD;
return true;
}
}
return false;
}
static bool is_path_mountpoint(const char *path) {
struct statfs sfs;
return (statfs(path, &sfs) == 0 && strcmp(sfs.f_mntonname, path) == 0);
}
bool is_active_image_mount_point(const char *path) {
return is_path_mountpoint(path);
}
bool wait_for_lvd_release(void) {
for (unsigned int waited_us = 0;; waited_us += LVD_RELEASE_WAIT_POLL_US) {
struct statfs *mntbuf = NULL;
int mntcount = getmntinfo(&mntbuf, MNT_NOWAIT);
bool mounted = false;
for (int i = 0; i < mntcount && mntbuf; i++) {
if (strcmp(mntbuf[i].f_mntfromname, "/dev/lvd2") != 0)
continue;
mounted = true;
break;
}
if (!mounted) {
if (waited_us != 0)
log_debug(" [IMG][LVD] /dev/lvd2 released");
return true;
}
if (waited_us == 0) {
log_debug(" [IMG][LVD] waiting for /dev/lvd2 to be released...");
for (int i = 0; i < mntcount && mntbuf; i++) {
if (strncmp(mntbuf[i].f_mntfromname, "/dev/lvd", 8) != 0)
continue;
log_debug(" [IMG][LVD] mounted: from=%s path=%s type=%s "
"bsize=%llu iosize=%llu blocks=%llu bfree=%llu "
"bavail=%llu files=%llu ffree=%llu flags=0x%lX",
mntbuf[i].f_mntfromname, mntbuf[i].f_mntonname,
mntbuf[i].f_fstypename,
(unsigned long long)(uint64_t)mntbuf[i].f_bsize,
(unsigned long long)(uint64_t)mntbuf[i].f_iosize,
(unsigned long long)(uint64_t)mntbuf[i].f_blocks,
(unsigned long long)(uint64_t)mntbuf[i].f_bfree,
(unsigned long long)(uint64_t)mntbuf[i].f_bavail,
(unsigned long long)(uint64_t)mntbuf[i].f_files,
(unsigned long long)(uint64_t)mntbuf[i].f_ffree,
(unsigned long)mntbuf[i].f_flags);
}
}
if (should_stop_requested())
return false;
if (waited_us >= LVD_RELEASE_WAIT_MAX_US) {
log_debug(" [IMG][LVD] /dev/lvd2 wait timeout reached (%u ms), "
"continuing startup", LVD_RELEASE_WAIT_MAX_US / 1000u);
return true;
}
sceKernelUsleep(LVD_RELEASE_WAIT_POLL_US);
}
}
// --- Device Detach Helpers ---
static bool detach_lvd_unit(int unit_id) {
if (unit_id < 0)
return true;
int fd = open(LVD_CTRL_PATH, O_RDWR);
if (fd < 0) {
log_debug(" [IMG][%s] open %s for detach failed: %s",
attach_backend_name(ATTACH_BACKEND_LVD), LVD_CTRL_PATH,
strerror(errno));
return false;
}
lvd_ioctl_detach_t req;
memset(&req, 0, sizeof(req));
req.device_id = unit_id;
bool ok = true;
if (ioctl(fd, SCE_LVD_IOC_DETACH, &req) != 0) {
log_debug(" [IMG][%s] detach %d failed: %s",
attach_backend_name(ATTACH_BACKEND_LVD), unit_id, strerror(errno));
ok = false;
}
close(fd);
char devname[64];
snprintf(devname, sizeof(devname), "/dev/lvd%d", unit_id);
if (!wait_for_dev_node_state(devname, false)) {
log_debug(" [IMG][%s] device node still present after detach: /dev/lvd%d",
attach_backend_name(ATTACH_BACKEND_LVD), unit_id);
ok = false;
}
return ok;
}
static bool detach_md_unit(int unit_id) {
if (unit_id < 0)
return true;
int fd = open(MD_CTRL_PATH, O_RDWR);
if (fd < 0) {
log_debug(" [IMG][%s] open %s for detach failed: %s",
attach_backend_name(ATTACH_BACKEND_MD), MD_CTRL_PATH,
strerror(errno));
return false;
}
struct md_ioctl req;
memset(&req, 0, sizeof(req));
req.md_version = MDIOVERSION;
req.md_unit = (unsigned int)unit_id;
bool ok = true;
if (ioctl(fd, MDIOCDETACH, &req) != 0) {
int err = errno;
req.md_options = MD_FORCE;
if (ioctl(fd, MDIOCDETACH, &req) != 0) {
log_debug(" [IMG][%s] detach %d failed: %s",
attach_backend_name(ATTACH_BACKEND_MD), unit_id,
strerror(errno));
ok = false;
} else {
log_debug(" [IMG][%s] detach %d forced after error: %s",
attach_backend_name(ATTACH_BACKEND_MD), unit_id,
strerror(err));
}
}
close(fd);
char devname[64];
snprintf(devname, sizeof(devname), "/dev/md%d", unit_id);
if (!wait_for_dev_node_state(devname, false)) {
log_debug(" [IMG][%s] device node still present after detach: /dev/md%d",
attach_backend_name(ATTACH_BACKEND_MD), unit_id);
ok = false;
}
return ok;
}
bool detach_attached_unit(attach_backend_t backend, int unit_id) {
if (backend == ATTACH_BACKEND_MD)
return detach_md_unit(unit_id);
else if (backend == ATTACH_BACKEND_LVD)
return detach_lvd_unit(unit_id);
return true;
}
+306
View File
@@ -0,0 +1,306 @@
#include "sm_platform.h"
#include "sm_path_state.h"
#include "sm_filesystem.h"
#include "sm_hash.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_path_utils.h"
struct PathStateEntry {
char path[MAX_PATH];
uint8_t missing_param_attempts;
uint8_t image_mount_attempts;
bool missing_param_limit_logged;
bool image_mount_limit_logged;
bool backport_mount_blocked;
bool manual_missing_source_logged;
bool game_info_cached;
bool game_info_valid;
time_t game_info_mtime;
off_t game_info_size;
ino_t game_info_ino;
char game_title_id[MAX_TITLE_ID];
char game_title_name[MAX_TITLE_NAME];
bool valid;
};
static struct PathStateEntry g_path_state[PATH_STATE_CAPACITY];
static uint16_t g_path_state_hash[STATE_HASH_SIZE];
static void rebuild_path_state_hash(void) {
memset(g_path_state_hash, 0, sizeof(g_path_state_hash));
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
if (!g_path_state[k].valid || g_path_state[k].path[0] == '\0')
continue;
uint32_t slot = sm_fnv1a32(g_path_state[k].path) & (STATE_HASH_SIZE - 1u);
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
if (g_path_state_hash[slot] == 0) {
g_path_state_hash[slot] = (uint16_t)(k + 1);
break;
}
slot = (slot + 1u) & (STATE_HASH_SIZE - 1u);
}
}
}
static struct PathStateEntry *find_path_state(const char *path) {
uint32_t slot = sm_fnv1a32(path) & (STATE_HASH_SIZE - 1u);
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
uint16_t idx = g_path_state_hash[slot];
if (idx == 0)
return NULL;
struct PathStateEntry *entry = &g_path_state[idx - 1u];
if (entry->valid && strcmp(entry->path, path) == 0)
return entry;
slot = (slot + 1u) & (STATE_HASH_SIZE - 1u);
}
return NULL;
}
static struct PathStateEntry *create_path_state(const char *path) {
int slot_k = -1;
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
if (!g_path_state[k].valid) {
slot_k = k;
break;
}
}
if (slot_k < 0) {
int evict_k = -1;
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
if (!g_path_state[k].valid)
continue;
if (!path_exists(g_path_state[k].path) &&
!g_path_state[k].manual_missing_source_logged) {
evict_k = k;
break;
}
}
if (evict_k < 0) {
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
if (!g_path_state[k].valid)
continue;
if (g_path_state[k].missing_param_attempts == 0 &&
g_path_state[k].image_mount_attempts == 0 &&
!g_path_state[k].backport_mount_blocked &&
!g_path_state[k].manual_missing_source_logged &&
!g_path_state[k].game_info_cached) {
evict_k = k;
break;
}
}
}
if (evict_k < 0)
evict_k = 0;
memset(&g_path_state[evict_k], 0, sizeof(g_path_state[evict_k]));
rebuild_path_state_hash();
slot_k = evict_k;
}
memset(&g_path_state[slot_k], 0, sizeof(g_path_state[slot_k]));
g_path_state[slot_k].valid = true;
(void)strlcpy(g_path_state[slot_k].path, path,
sizeof(g_path_state[slot_k].path));
uint32_t slot = sm_fnv1a32(path) & (STATE_HASH_SIZE - 1u);
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
if (g_path_state_hash[slot] == 0) {
g_path_state_hash[slot] = (uint16_t)(slot_k + 1);
return &g_path_state[slot_k];
}
slot = (slot + 1u) & (STATE_HASH_SIZE - 1u);
}
g_path_state[slot_k].valid = false;
g_path_state[slot_k].path[0] = '\0';
rebuild_path_state_hash();
return NULL;
}
static struct PathStateEntry *get_or_create_path_state(const char *path) {
struct PathStateEntry *entry = find_path_state(path);
return entry ? entry : create_path_state(path);
}
bool load_cached_game_info(const char *path, const struct stat *param_st,
char *out_id, char *out_name, bool *valid_out) {
*valid_out = false;
struct PathStateEntry *entry = find_path_state(path);
if (!entry || !entry->game_info_cached ||
entry->game_info_mtime != param_st->st_mtime ||
entry->game_info_size != param_st->st_size ||
entry->game_info_ino != param_st->st_ino) {
return false;
}
*valid_out = entry->game_info_valid;
if (!entry->game_info_valid)
return true;
(void)strlcpy(out_id, entry->game_title_id, MAX_TITLE_ID);
(void)strlcpy(out_name, entry->game_title_name, MAX_TITLE_NAME);
return true;
}
void store_cached_game_info(const char *path, const struct stat *param_st,
bool valid, const char *title_id,
const char *title_name) {
struct PathStateEntry *entry = get_or_create_path_state(path);
entry->game_info_cached = true;
entry->game_info_valid = valid;
entry->game_info_mtime = param_st->st_mtime;
entry->game_info_size = param_st->st_size;
entry->game_info_ino = param_st->st_ino;
(void)strlcpy(entry->game_title_id, valid ? title_id : "", MAX_TITLE_ID);
(void)strlcpy(entry->game_title_name, valid ? title_name : "", MAX_TITLE_NAME);
}
void prune_path_state(void) {
bool changed = false;
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
if (!g_path_state[k].valid || g_path_state[k].path[0] == '\0')
continue;
if (path_exists(g_path_state[k].path))
continue;
if (g_path_state[k].manual_missing_source_logged)
continue;
memset(&g_path_state[k], 0, sizeof(g_path_state[k]));
changed = true;
}
if (changed)
rebuild_path_state_hash();
}
void prune_path_state_for_root(const char *root) {
if (!root || root[0] == '\0') {
prune_path_state();
return;
}
bool changed = false;
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
if (!g_path_state[k].valid || g_path_state[k].path[0] == '\0')
continue;
if (!path_matches_root_or_child(g_path_state[k].path, root))
continue;
if (path_exists(g_path_state[k].path))
continue;
if (g_path_state[k].manual_missing_source_logged)
continue;
memset(&g_path_state[k], 0, sizeof(g_path_state[k]));
changed = true;
}
if (changed)
rebuild_path_state_hash();
}
bool is_missing_param_scan_limited(const char *path) {
if (!is_under_image_mount_base(path))
return false;
struct PathStateEntry *entry = find_path_state(path);
if (!entry)
return false;
return entry->missing_param_attempts >= MAX_MISSING_PARAM_SCAN_ATTEMPTS;
}
void record_missing_param_failure(const char *path) {
if (!is_under_image_mount_base(path))
return;
struct PathStateEntry *entry = get_or_create_path_state(path);
if (entry->missing_param_attempts < UINT8_MAX)
entry->missing_param_attempts++;
log_debug(" [SCAN] missing/invalid param.json: %s", path);
if (entry->missing_param_attempts == 1)
notify_system("Missing/invalid param.json:\n%s", path);
if (entry->missing_param_attempts >= MAX_MISSING_PARAM_SCAN_ATTEMPTS &&
!entry->missing_param_limit_logged) {
log_debug(" [SCAN] attempt limit reached (%u), skipping path: %s",
(unsigned)MAX_MISSING_PARAM_SCAN_ATTEMPTS, path);
entry->missing_param_limit_logged = true;
}
}
void clear_missing_param_entry(const char *path) {
struct PathStateEntry *entry = find_path_state(path);
if (!entry)
return;
entry->missing_param_attempts = 0;
entry->missing_param_limit_logged = false;
}
bool is_image_mount_limited(const char *path) {
struct PathStateEntry *entry = find_path_state(path);
return entry ? entry->image_mount_attempts >= MAX_IMAGE_MOUNT_ATTEMPTS
: false;
}
uint8_t bump_image_mount_attempts(const char *path) {
struct PathStateEntry *entry = get_or_create_path_state(path);
if (entry->image_mount_attempts < UINT8_MAX)
entry->image_mount_attempts++;
if (entry->image_mount_attempts >= MAX_IMAGE_MOUNT_ATTEMPTS &&
!entry->image_mount_limit_logged) {
log_debug(" [IMG] retry limit reached (%u/%u), skipping image: %s",
(unsigned)entry->image_mount_attempts,
(unsigned)MAX_IMAGE_MOUNT_ATTEMPTS, path);
entry->image_mount_limit_logged = true;
}
return entry->image_mount_attempts;
}
void clear_image_mount_attempts(const char *path) {
struct PathStateEntry *entry = find_path_state(path);
if (!entry)
return;
entry->image_mount_attempts = 0;
entry->image_mount_limit_logged = false;
}
bool is_backport_mount_blocked(const char *path) {
if (!path || path[0] == '\0')
return false;
struct PathStateEntry *entry = find_path_state(path);
return entry ? entry->backport_mount_blocked : false;
}
void note_backport_mount_failure(const char *path) {
if (!path || path[0] == '\0')
return;
struct PathStateEntry *entry = get_or_create_path_state(path);
if (!entry)
return;
if (entry->backport_mount_blocked)
return;
entry->backport_mount_blocked = true;
}
void clear_backport_mount_failure(const char *path) {
if (!path || path[0] == '\0')
return;
struct PathStateEntry *entry = find_path_state(path);
if (!entry)
return;
entry->backport_mount_blocked = false;
}
bool note_manual_missing_source_once(const char *path) {
if (!path || path[0] == '\0')
return false;
struct PathStateEntry *entry = get_or_create_path_state(path);
if (!entry || entry->manual_missing_source_logged)
return false;
entry->manual_missing_source_logged = true;
return true;
}
void clear_manual_missing_source(const char *path) {
if (!path || path[0] == '\0')
return;
struct PathStateEntry *entry = find_path_state(path);
if (!entry)
return;
entry->manual_missing_source_logged = false;
}
+60
View File
@@ -0,0 +1,60 @@
#include "sm_platform.h"
#include "sm_path_utils.h"
#include "sm_paths.h"
const char *get_filename_component(const char *path) {
const char *base = strrchr(path, '/');
if (!base)
base = strrchr(path, '\\');
return base ? base + 1 : path;
}
bool path_exists(const char *path) {
struct stat st;
return (path && path[0] != '\0' && stat(path, &st) == 0);
}
bool is_under_image_mount_base(const char *path) {
size_t image_prefix_len = strlen(IMAGE_MOUNT_BASE);
return (strncmp(path, IMAGE_MOUNT_BASE, image_prefix_len) == 0 &&
path[image_prefix_len] == '/');
}
bool is_pfsc_image_mount_base_or_child(const char *path) {
return path_matches_root_or_child(path, PFSC_IMAGE_MOUNT_BASE);
}
bool path_matches_root_or_child(const char *path, const char *root) {
if (!path || !root || root[0] == '\0')
return false;
size_t root_len = strlen(root);
if (strncmp(path, root, root_len) != 0)
return false;
return path[root_len] == '\0' || path[root_len] == '/';
}
bool is_usb_storage_path(const char *path) {
static const char *usb_roots[] = {
"/mnt/usb0", "/mnt/usb1", "/mnt/usb2", "/mnt/usb3",
"/mnt/usb4", "/mnt/usb5", "/mnt/usb6", "/mnt/usb7",
"/mnt/ext0",
};
if (!path || path[0] == '\0')
return false;
for (size_t i = 0; i < sizeof(usb_roots) / sizeof(usb_roots[0]); i++) {
if (path_matches_root_or_child(path, usb_roots[i]))
return true;
}
return false;
}
bool build_backports_root_path(const char *scan_path, char out[MAX_PATH]) {
if (is_under_image_mount_base(scan_path))
return false;
int written = snprintf(out, MAX_PATH, "%s/%s", scan_path,
DEFAULT_BACKPORTS_DIR_NAME);
return written >= 0 && (size_t)written < MAX_PATH;
}
+977
View File
@@ -0,0 +1,977 @@
#include "sm_platform.h"
#include "sm_runtime.h"
#include "sm_scan.h"
#include "sm_scan_tree.h"
#include "sm_types.h"
#include "sm_game_cache.h"
#include "sm_game_lifecycle.h"
#include "sm_gameinfo.h"
#include "sm_log.h"
#include "sm_config_mount.h"
#include "sm_mount_device.h"
#include "sm_filesystem.h"
#include "sm_appdb.h"
#include "sm_paths.h"
#include "sm_path_state.h"
#include "sm_path_utils.h"
#include "sm_stability.h"
#include "sm_title_state.h"
#include "sm_image_cache.h"
#include "sm_image_index.h"
#include "sm_image.h"
#include "sm_install_queue.h"
#include "sm_manual.h"
#include "sm_shellcore_service.h"
typedef struct {
char discovered_param_roots[MAX_PENDING][MAX_PATH];
char checked_appmeta_titles[MAX_PENDING][MAX_TITLE_ID];
char blocked_ppsa_uninstall_titles[MAX_PENDING][MAX_TITLE_ID];
bool checked_appmeta_present[MAX_PENDING];
int checked_appmeta_count;
int blocked_ppsa_uninstall_count;
} scan_workspace_t;
// Reuse the largest transient scan buffer instead of placing ~512 KiB of path
// state on the stack each cycle.
static scan_workspace_t g_scan_workspace;
static void reset_scan_workspace(void) {
g_scan_workspace.checked_appmeta_count = 0;
g_scan_workspace.blocked_ppsa_uninstall_count = 0;
}
static bool blocked_ppsa_uninstall_requested(const char *title_id) {
if (!title_id || title_id[0] == '\0')
return false;
for (int i = 0; i < g_scan_workspace.blocked_ppsa_uninstall_count; i++) {
if (strcmp(g_scan_workspace.blocked_ppsa_uninstall_titles[i], title_id) == 0)
return true;
}
return false;
}
static void remember_blocked_ppsa_uninstall(const char *title_id) {
if (!title_id || title_id[0] == '\0' ||
blocked_ppsa_uninstall_requested(title_id))
return;
if (g_scan_workspace.blocked_ppsa_uninstall_count >= MAX_PENDING)
return;
int slot = g_scan_workspace.blocked_ppsa_uninstall_count++;
(void)strlcpy(g_scan_workspace.blocked_ppsa_uninstall_titles[slot], title_id,
sizeof(g_scan_workspace.blocked_ppsa_uninstall_titles[slot]));
}
static bool is_blocked_ppsa_title(
const struct AppDbTitleList *blocked_ppsa_titles,
bool blocked_ppsa_titles_ready, const char *title_id) {
return blocked_ppsa_titles_ready &&
app_db_title_list_contains(blocked_ppsa_titles, title_id);
}
static bool request_blocked_ppsa_uninstall(const char *title_id,
const char *source_path) {
if (!title_id || title_id[0] == '\0' ||
blocked_ppsa_uninstall_requested(title_id))
return false;
remember_blocked_ppsa_uninstall(title_id);
int res = sceAppInstUtilAppUnInstall(title_id);
if (res == 0) {
sm_install_forget_pending_title(title_id);
invalidate_app_db_title_cache();
if (source_path && source_path[0] != '\0') {
log_debug(" [REG] requested blocked PPSA uninstall: %s source=%s",
title_id, source_path);
} else {
log_debug(" [REG] requested blocked PPSA uninstall: %s", title_id);
}
return true;
}
log_debug(" [REG] blocked PPSA uninstall failed: %s code=0x%08X", title_id,
(uint32_t)res);
return false;
}
static bool get_appmeta_present_for_scan_cycle(const char *title_id) {
for (int i = 0; i < g_scan_workspace.checked_appmeta_count; i++) {
if (strcmp(g_scan_workspace.checked_appmeta_titles[i], title_id) == 0)
return g_scan_workspace.checked_appmeta_present[i];
}
bool present = has_appmeta_data(title_id);
if (g_scan_workspace.checked_appmeta_count < MAX_PENDING) {
int slot = g_scan_workspace.checked_appmeta_count++;
(void)strlcpy(g_scan_workspace.checked_appmeta_titles[slot], title_id,
sizeof(g_scan_workspace.checked_appmeta_titles[slot]));
g_scan_workspace.checked_appmeta_present[slot] = present;
}
return present;
}
static bool is_under_discovered_param_root(
const char *path, char discovered_param_roots[][MAX_PATH],
int discovered_count) {
for (int i = 0; i < discovered_count; i++) {
const char *root = discovered_param_roots[i];
size_t root_len = strlen(root);
if (strncmp(path, root, root_len) != 0)
continue;
if (path[root_len] == '\0' || path[root_len] == '/')
return true;
}
return false;
}
// --- Candidate Discovery ---
typedef struct {
char title_id[MAX_TITLE_ID];
char title_name[MAX_TITLE_NAME];
} directory_candidate_info_t;
typedef enum {
DIRECTORY_CANDIDATE_DESCEND = 0,
DIRECTORY_CANDIDATE_SKIP_DESCEND,
DIRECTORY_CANDIDATE_READY,
} directory_candidate_probe_t;
typedef enum {
EXISTING_DIRECTORY_CONTINUE = 0,
EXISTING_DIRECTORY_HANDLED,
EXISTING_DIRECTORY_PREFER_CURRENT,
EXISTING_DIRECTORY_PREFER_CACHED,
} existing_directory_result_t;
typedef struct {
const struct AppDbTitleList *titles;
const struct AppDbTitleList *blocked_ppsa_titles;
bool titles_ready;
bool blocked_ppsa_titles_ready;
} scan_app_db_context_t;
static directory_candidate_probe_t probe_directory_candidate(
const char *full_path, char discovered_param_roots[][MAX_PATH],
int *discovered_param_root_count, bool allow_known_param_root,
directory_candidate_info_t *info_out) {
struct stat param_st;
if (is_pfsc_image_mount_base_or_child(full_path))
return DIRECTORY_CANDIDATE_DESCEND;
if (!allow_known_param_root &&
is_under_discovered_param_root(full_path, discovered_param_roots,
*discovered_param_root_count)) {
return DIRECTORY_CANDIDATE_SKIP_DESCEND;
}
if (is_under_image_mount_base(full_path) && !is_active_image_mount_point(full_path)) {
log_debug(" [SKIP] inactive mount path: %s", full_path);
return DIRECTORY_CANDIDATE_SKIP_DESCEND;
}
if (!directory_has_param_json(full_path, &param_st)) {
if (is_missing_param_scan_limited(full_path)) {
log_debug(" [SKIP] param.json retry limit reached: %s", full_path);
} else {
record_missing_param_failure(full_path);
}
return DIRECTORY_CANDIDATE_DESCEND;
}
if (!get_game_info(full_path, &param_st, info_out->title_id,
info_out->title_name)) {
record_missing_param_failure(full_path);
log_debug(" [SKIP] game info unavailable: %s", full_path);
return DIRECTORY_CANDIDATE_SKIP_DESCEND;
}
if (*discovered_param_root_count < MAX_PENDING &&
!is_under_discovered_param_root(full_path, discovered_param_roots,
*discovered_param_root_count)) {
(void)strlcpy(discovered_param_roots[*discovered_param_root_count], full_path,
MAX_PATH);
(*discovered_param_root_count)++;
}
clear_missing_param_entry(full_path);
return DIRECTORY_CANDIDATE_READY;
}
static int find_scan_candidate_index_by_title_id(scan_candidate_t *candidates,
int candidate_count,
const char *title_id) {
for (int i = 0; i < candidate_count; i++) {
if (strcmp(candidates[i].title_id, title_id) == 0)
return i;
}
return -1;
}
static void mark_scan_candidate_manual(scan_candidate_t *candidate,
const char *manual_source_path) {
if (!candidate || !manual_source_path || manual_source_path[0] == '\0')
return;
candidate->manual = true;
(void)strlcpy(candidate->manual_source_path, manual_source_path,
sizeof(candidate->manual_source_path));
}
static void remove_scan_candidate_at(scan_candidate_t *candidates,
int *candidate_count, int index) {
int trailing_count = *candidate_count - index - 1;
if (trailing_count > 0) {
memmove(&candidates[index], &candidates[index + 1],
(size_t)trailing_count * sizeof(candidates[0]));
}
(*candidate_count)--;
}
static void notify_duplicate_scan_candidate(const char *title_id,
const char *ignored_path,
const char *existing_path) {
notify_duplicate_title_once(title_id, ignored_path, existing_path);
}
static bool defer_full_candidate_batch(int candidate_count,
int max_candidates,
bool *incomplete_found_out) {
if (candidate_count < max_candidates)
return false;
if (incomplete_found_out)
*incomplete_found_out = true;
return true;
}
static existing_directory_result_t handle_existing_directory_candidate(
const char *full_path, const scan_app_db_context_t *app_db,
const directory_candidate_info_t *info,
bool *installed_out, bool *in_app_db_out,
char preferred_existing_path_out[MAX_PATH]) {
char tracked_path[MAX_PATH];
preferred_existing_path_out[0] = '\0';
bool has_tracked_path =
read_mount_link(info->title_id, tracked_path, sizeof(tracked_path));
bool link_matches_source =
has_tracked_path && strcmp(tracked_path, full_path) == 0;
if (is_blocked_ppsa_title(app_db->blocked_ppsa_titles,
app_db->blocked_ppsa_titles_ready,
info->title_id)) {
request_blocked_ppsa_uninstall(info->title_id, full_path);
return EXISTING_DIRECTORY_HANDLED;
}
if (!app_db->titles_ready) {
if (link_matches_source && is_data_mounted(info->title_id)) {
cache_game_entry(full_path, info->title_id, info->title_name);
return EXISTING_DIRECTORY_PREFER_CURRENT;
}
return EXISTING_DIRECTORY_HANDLED;
}
bool in_app_db = app_db_title_list_contains(app_db->titles, info->title_id);
bool installed = in_app_db && is_installed(info->title_id);
bool appmeta_present =
installed ? get_appmeta_present_for_scan_cycle(info->title_id) : false;
link_matches_source = installed && link_matches_source;
bool source_valid = false;
if (link_matches_source) {
char eboot_path[MAX_PATH];
int written = snprintf(eboot_path, sizeof(eboot_path), "%s/eboot.bin",
full_path);
source_valid = written > 0 && (size_t)written < sizeof(eboot_path) &&
path_exists(eboot_path);
}
if (source_valid && appmeta_present) {
cache_game_entry(full_path, info->title_id, info->title_name);
return EXISTING_DIRECTORY_PREFER_CURRENT;
}
if (installed && appmeta_present && has_tracked_path &&
strcmp(tracked_path, full_path) != 0 &&
is_data_mounted(info->title_id)) {
(void)strlcpy(preferred_existing_path_out, tracked_path, MAX_PATH);
cache_game_entry(tracked_path, info->title_id, info->title_name);
return EXISTING_DIRECTORY_PREFER_CACHED;
}
if (!in_app_db && is_title_install_pending(info->title_id))
return EXISTING_DIRECTORY_HANDLED;
if (!in_app_db && was_register_attempted(info->title_id)) {
return EXISTING_DIRECTORY_HANDLED;
}
*installed_out = installed;
*in_app_db_out = in_app_db;
return EXISTING_DIRECTORY_CONTINUE;
}
static bool enqueue_directory_candidate(
const char *full_path, scan_candidate_t *candidates, int max_candidates,
int *candidate_count, const directory_candidate_info_t *info, bool installed,
bool in_app_db, const char *manual_source_path,
bool *unstable_found_out) {
char metadata_path[MAX_PATH];
uint8_t failed_attempts = get_failed_mount_attempts(info->title_id);
if (failed_attempts >= MAX_FAILED_MOUNT_ATTEMPTS) {
log_debug(" [SKIP] mount/register retry limit reached (%u/%u): %s (%s)",
(unsigned)failed_attempts, (unsigned)MAX_FAILED_MOUNT_ATTEMPTS,
info->title_name, info->title_id);
return true;
}
int written =
snprintf(metadata_path, sizeof(metadata_path), "%s/sce_sys", full_path);
if (written < 0 || (size_t)written >= sizeof(metadata_path)) {
log_debug(" [SKIP] metadata path too long: %s (%s)", info->title_name,
full_path);
return true;
}
if (!wait_for_stability_fast(metadata_path, info->title_name)) {
if (unstable_found_out)
*unstable_found_out = true;
log_debug(" [SKIP] source not stable yet: %s (%s)", info->title_name,
full_path);
return true;
}
if (defer_full_candidate_batch(*candidate_count, max_candidates,
unstable_found_out)) {
log_debug(" [SKIP] candidate queue full (%d), deferring: %s (%s)",
max_candidates, info->title_name, info->title_id);
return true;
}
(void)strlcpy(candidates[*candidate_count].path, full_path,
sizeof(candidates[*candidate_count].path));
(void)strlcpy(candidates[*candidate_count].title_id, info->title_id,
sizeof(candidates[*candidate_count].title_id));
(void)strlcpy(candidates[*candidate_count].title_name, info->title_name,
sizeof(candidates[*candidate_count].title_name));
candidates[*candidate_count].manual_source_path[0] = '\0';
candidates[*candidate_count].installed = installed;
candidates[*candidate_count].in_app_db = in_app_db;
candidates[*candidate_count].manual = false;
mark_scan_candidate_manual(&candidates[*candidate_count], manual_source_path);
(*candidate_count)++;
return true;
}
static bool try_collect_candidate_for_directory(
const char *full_path, scan_candidate_t *candidates, int max_candidates,
int *candidate_count, const scan_app_db_context_t *app_db,
char discovered_param_roots[][MAX_PATH],
int *discovered_param_root_count, bool allow_known_param_root,
const char *manual_source_path, bool *unstable_found_out) {
directory_candidate_info_t info;
directory_candidate_probe_t probe_result =
probe_directory_candidate(full_path, discovered_param_roots,
discovered_param_root_count,
allow_known_param_root, &info);
if (probe_result == DIRECTORY_CANDIDATE_SKIP_DESCEND)
return true;
if (probe_result == DIRECTORY_CANDIDATE_DESCEND)
return false;
if (!sm_image_index_record_game(full_path, info.title_id) &&
unstable_found_out) {
*unstable_found_out = true;
}
int duplicate_candidate_index = find_scan_candidate_index_by_title_id(
candidates, *candidate_count, info.title_id);
const char *duplicate_candidate_path =
duplicate_candidate_index >= 0 ? candidates[duplicate_candidate_index].path
: NULL;
bool duplicate_candidate_same_path =
duplicate_candidate_path && strcmp(duplicate_candidate_path, full_path) == 0;
bool installed = false;
bool in_app_db = false;
char preferred_existing_path[MAX_PATH];
existing_directory_result_t existing_result =
handle_existing_directory_candidate(full_path, app_db, &info,
&installed, &in_app_db,
preferred_existing_path);
if (existing_result == EXISTING_DIRECTORY_PREFER_CURRENT) {
if (manual_source_path)
sm_manual_note_installed(manual_source_path, info.title_id,
info.title_name);
if (duplicate_candidate_index >= 0) {
if (!duplicate_candidate_same_path)
notify_duplicate_scan_candidate(info.title_id, duplicate_candidate_path,
full_path);
remove_scan_candidate_at(candidates, candidate_count,
duplicate_candidate_index);
}
return true;
}
if (existing_result == EXISTING_DIRECTORY_PREFER_CACHED) {
notify_duplicate_scan_candidate(info.title_id, full_path,
preferred_existing_path);
if (duplicate_candidate_index >= 0)
remove_scan_candidate_at(candidates, candidate_count,
duplicate_candidate_index);
return true;
}
if (existing_result == EXISTING_DIRECTORY_HANDLED) {
if (duplicate_candidate_index >= 0 && !duplicate_candidate_same_path)
notify_duplicate_scan_candidate(info.title_id, full_path,
duplicate_candidate_path);
if (duplicate_candidate_index >= 0)
mark_scan_candidate_manual(&candidates[duplicate_candidate_index],
manual_source_path);
return true;
}
if (duplicate_candidate_index >= 0) {
if (!duplicate_candidate_same_path)
notify_duplicate_scan_candidate(info.title_id, full_path,
duplicate_candidate_path);
mark_scan_candidate_manual(&candidates[duplicate_candidate_index],
manual_source_path);
return true;
}
return enqueue_directory_candidate(full_path, candidates, max_candidates,
candidate_count, &info, installed,
in_app_db, manual_source_path,
unstable_found_out);
}
typedef struct {
scan_candidate_t *candidates;
int max_candidates;
int *candidate_count;
const scan_app_db_context_t *app_db;
char (*discovered_param_roots)[MAX_PATH];
int *discovered_param_root_count;
const char *manual_source_path;
bool *unstable_found_out;
} collect_candidates_walk_ctx_t;
static void collect_scan_candidates_from_manual_root(
const char *scan_path, const char *manual_source_path,
scan_candidate_t *candidates, int max_candidates, int *candidate_count,
const scan_app_db_context_t *app_db,
char discovered_param_roots[][MAX_PATH], int *discovered_param_root_count,
bool *unstable_found_out);
static sm_scan_tree_dir_visit_t collect_candidate_directory_visit(
const char *dir_path, unsigned int depth_from_root, void *ctx_ptr) {
if (depth_from_root == 0u)
return SM_SCAN_TREE_DIR_DESCEND;
collect_candidates_walk_ctx_t *ctx = (collect_candidates_walk_ctx_t *)ctx_ptr;
if (defer_full_candidate_batch(*ctx->candidate_count, ctx->max_candidates,
ctx->unstable_found_out)) {
return SM_SCAN_TREE_DIR_ABORT;
}
if (try_collect_candidate_for_directory(
dir_path, ctx->candidates, ctx->max_candidates, ctx->candidate_count,
ctx->app_db, ctx->discovered_param_roots,
ctx->discovered_param_root_count, false, ctx->manual_source_path,
ctx->unstable_found_out)) {
return SM_SCAN_TREE_DIR_SKIP_DESCEND;
}
return SM_SCAN_TREE_DIR_DESCEND;
}
static bool collect_candidate_image_visit(const char *image_path,
const char *image_name,
unsigned int depth_from_root,
void *ctx_ptr) {
(void)depth_from_root;
collect_candidates_walk_ctx_t *ctx = (collect_candidates_walk_ctx_t *)ctx_ptr;
if (defer_full_candidate_batch(*ctx->candidate_count, ctx->max_candidates,
ctx->unstable_found_out)) {
return false;
}
struct stat image_st;
if (stat(image_path, &image_st) != 0)
return true;
bool persistent_image = !is_under_image_mount_base(image_path);
if (persistent_image &&
!sm_image_index_needs_scan(image_path, &image_st, ctx->app_db->titles,
ctx->app_db->titles_ready)) {
return true;
}
bool image_unstable = false;
if (!maybe_mount_image_file(image_path, image_name, &image_unstable)) {
if (image_unstable && ctx->unstable_found_out)
*ctx->unstable_found_out = true;
return true;
}
if (persistent_image)
sm_image_index_begin_scan(image_path, &image_st);
char mount_point[MAX_PATH];
get_image_mount_point_for_source(image_path, mount_point);
collect_scan_candidates_from_manual_root(
mount_point, ctx->manual_source_path, ctx->candidates,
ctx->max_candidates, ctx->candidate_count, ctx->app_db,
ctx->discovered_param_roots, ctx->discovered_param_root_count,
&image_unstable);
if (image_unstable && ctx->unstable_found_out)
*ctx->unstable_found_out = true;
if (persistent_image && !image_unstable && !should_stop_requested() &&
!runtime_sleep_mode_active()) {
sm_image_index_complete_scan(image_path);
}
return true;
}
static void collect_scan_candidates_from_manual_root(
const char *scan_path, const char *manual_source_path,
scan_candidate_t *candidates, int max_candidates, int *candidate_count,
const scan_app_db_context_t *app_db,
char discovered_param_roots[][MAX_PATH], int *discovered_param_root_count,
bool *unstable_found_out) {
if (should_stop_requested() || runtime_sleep_mode_active())
return;
bool pfsc_container_root = is_pfsc_image_mount_base_or_child(scan_path);
bool try_root_candidate = !pfsc_container_root;
if (try_root_candidate && is_under_image_mount_base(scan_path)) {
struct stat param_st;
try_root_candidate = directory_has_param_json(scan_path, &param_st);
}
if (try_root_candidate) {
if (try_collect_candidate_for_directory(
scan_path, candidates, max_candidates, candidate_count,
app_db, discovered_param_roots, discovered_param_root_count, true,
manual_source_path, unstable_found_out)) {
return;
}
}
unsigned int scan_depth = get_scan_depth_for_root(scan_path);
collect_candidates_walk_ctx_t ctx = {
.candidates = candidates,
.max_candidates = max_candidates,
.candidate_count = candidate_count,
.app_db = app_db,
.discovered_param_roots = discovered_param_roots,
.discovered_param_root_count = discovered_param_root_count,
.manual_source_path = manual_source_path,
.unstable_found_out = unstable_found_out,
};
sm_scan_tree_callbacks_t callbacks = {
.on_directory = collect_candidate_directory_visit,
.on_image_file = pfsc_container_root ? collect_candidate_image_visit
: NULL,
};
(void)sm_scan_tree_walk(scan_path, scan_path, 0u, scan_depth, &callbacks, &ctx);
}
static void collect_scan_candidates_from_manual_path(
const char *manual_path, scan_candidate_t *candidates, int max_candidates,
int *candidate_count, const scan_app_db_context_t *app_db,
char discovered_param_roots[][MAX_PATH],
int *discovered_param_root_count, bool *unstable_found_out) {
if (should_stop_requested() || runtime_sleep_mode_active())
return;
if (defer_full_candidate_batch(*candidate_count, max_candidates,
unstable_found_out)) {
return;
}
struct stat st;
if (stat(manual_path, &st) != 0) {
if (note_manual_missing_source_once(manual_path))
log_debug(" [MANUAL] source unavailable: %s", manual_path);
return;
}
clear_manual_missing_source(manual_path);
const char *name = get_filename_component(manual_path);
if (S_ISREG(st.st_mode) &&
is_supported_image_file_path(manual_path, name)) {
if (!sm_image_index_needs_scan(manual_path, &st, app_db->titles,
app_db->titles_ready))
return;
bool image_unstable = false;
if (!maybe_mount_image_file(manual_path, name, &image_unstable)) {
if (image_unstable && unstable_found_out)
*unstable_found_out = true;
return;
}
sm_image_index_begin_scan(manual_path, &st);
char mount_point[MAX_PATH];
get_image_mount_point_for_source(manual_path, mount_point);
collect_scan_candidates_from_manual_root(
mount_point, manual_path, candidates, max_candidates, candidate_count,
app_db, discovered_param_roots, discovered_param_root_count,
&image_unstable);
if (image_unstable && unstable_found_out)
*unstable_found_out = true;
if (!image_unstable && !should_stop_requested() &&
!runtime_sleep_mode_active()) {
sm_image_index_complete_scan(manual_path);
}
return;
}
struct stat param_st;
if (!S_ISDIR(st.st_mode) || !directory_has_param_json(manual_path, &param_st)) {
log_debug(" [MANUAL] expected game directory or image file: %s",
manual_path);
return;
}
collect_scan_candidates_from_manual_root(
manual_path, manual_path, candidates, max_candidates, candidate_count,
app_db, discovered_param_roots, discovered_param_root_count,
unstable_found_out);
}
typedef struct {
scan_candidate_t *candidates;
int max_candidates;
int *candidate_count;
const scan_app_db_context_t *app_db;
char (*discovered_param_roots)[MAX_PATH];
int *discovered_param_root_count;
bool *unstable_found_out;
int path_count;
} manual_scan_ctx_t;
static bool collect_manual_path_visit(const char *manual_path, void *ctx_ptr) {
manual_scan_ctx_t *ctx = (manual_scan_ctx_t *)ctx_ptr;
if (should_stop_requested() || runtime_sleep_mode_active())
return false;
if (*ctx->candidate_count >= ctx->max_candidates) {
log_debug(" [MANUAL] candidate queue full (%d), remaining sources deferred",
ctx->max_candidates);
return false;
}
ctx->path_count++;
collect_scan_candidates_from_manual_path(
manual_path, ctx->candidates, ctx->max_candidates, ctx->candidate_count,
ctx->app_db, ctx->discovered_param_roots,
ctx->discovered_param_root_count, ctx->unstable_found_out);
return true;
}
static bool build_backport_path_for_scan_root(const char *title_id,
const char *scan_path,
char backport_path[MAX_PATH]) {
if (!title_id || title_id[0] == '\0' || !scan_path || scan_path[0] == '\0')
return false;
char backport_root[MAX_PATH];
if (!build_backports_root_path(scan_path, backport_root))
return false;
int written = snprintf(backport_path, MAX_PATH, "%s/%s", backport_root,
title_id);
return written > 0 && (size_t)written < MAX_PATH;
}
static bool backport_path_is_dir(const char *backport_path) {
struct stat st;
return backport_path && stat(backport_path, &st) == 0 &&
S_ISDIR(st.st_mode);
}
static bool resolve_backport_path(const char *title_id,
const char *owning_scan_path,
char backport_path[MAX_PATH]) {
if (!title_id || title_id[0] == '\0')
return false;
backport_path[0] = '\0';
if (build_backport_path_for_scan_root(title_id, owning_scan_path,
backport_path) &&
backport_path_is_dir(backport_path)) {
return true;
}
for (int i = 0; i < get_scan_path_count(); i++) {
const char *scan_path = get_scan_path(i);
if (owning_scan_path && strcmp(scan_path, owning_scan_path) == 0)
continue;
char candidate_path[MAX_PATH];
if (!build_backport_path_for_scan_root(title_id, scan_path, candidate_path))
continue;
if (!backport_path_is_dir(candidate_path))
continue;
(void)strlcpy(backport_path, candidate_path, MAX_PATH);
return true;
}
backport_path[0] = '\0';
return false;
}
bool mount_backport_overlay_for_title(const char *source_path,
const char *title_id,
const char *owning_scan_root,
bool *unstable_found_out) {
if (should_stop_requested() || runtime_sleep_mode_active())
return false;
char backport_path[MAX_PATH];
if (!resolve_backport_path(title_id, owning_scan_root, backport_path)) {
return true;
}
if (is_backport_mount_blocked(backport_path))
return true;
bool overlay_active = false;
if (!reconcile_title_backport_mount(title_id, source_path, backport_path,
&overlay_active) ||
overlay_active) {
return true;
}
if (!wait_for_stability_fast(backport_path, "BKP")) {
if (unstable_found_out)
*unstable_found_out = true;
return false;
}
overlay_active = false;
if (!reconcile_title_backport_mount(title_id, source_path, backport_path,
&overlay_active) ||
overlay_active) {
return true;
}
char system_ex_path[MAX_PATH];
snprintf(system_ex_path, sizeof(system_ex_path), "/system_ex/app/%s",
title_id);
if (!mount_backport_overlay(system_ex_path, backport_path, title_id)) {
note_backport_mount_failure(backport_path);
return true;
}
clear_backport_mount_failure(backport_path);
return true;
}
bool release_scan_runtime_mounts(void) {
if (runtime_sleep_mode_active())
return false;
if (sm_game_lifecycle_has_active_game() ||
sm_shellcore_service_has_prepared_mount()) {
return true;
}
runtime_mount_state_lock();
if (sm_game_lifecycle_has_active_game() ||
sm_shellcore_service_has_prepared_mount()) {
runtime_mount_state_unlock();
return true;
}
unmount_all_title_runtime_layers();
bool released = release_runtime_image_mounts();
runtime_mount_state_unlock();
return released;
}
// --- Unified Scan Pass (images + game candidates) ---
void cleanup_lost_sources_before_scan(void) {
// 1) Drop stale game cache entries for deleted sources.
prune_game_cache();
// 2) Drop stale/broken mount links and unmount stale /system_ex stacks.
cleanup_mount_links(NULL, true);
// 3) Unmount stale image mounts for deleted image files.
cleanup_stale_image_mounts();
sm_image_index_prune();
// 4) Drop stale path-state entries.
prune_path_state();
}
void cleanup_lost_sources_for_scan_root(const char *scan_root) {
prune_game_cache_for_root(scan_root);
cleanup_mount_links(scan_root, true);
cleanup_stale_image_mounts_for_root(scan_root);
sm_image_index_prune();
prune_path_state_for_root(scan_root);
}
void unmount_usb_sources_for_suspend(void) {
log_debug("[SLEEP] USB-backed mount cleanup begin");
cleanup_usb_mount_links_for_suspend();
if (!unmount_usb_image_mounts_for_suspend()) {
log_debug("[SLEEP] some USB-backed image mounts remain busy");
}
cleanup_mount_dirs();
log_debug("[SLEEP] USB-backed mount cleanup done");
}
static void collect_scan_candidates_from_root(
const char *scan_path, scan_candidate_t *candidates, int max_candidates,
int *candidate_count, const scan_app_db_context_t *app_db,
char discovered_param_roots[][MAX_PATH],
int *discovered_param_root_count, bool *unstable_found_out) {
if (should_stop_requested() || runtime_sleep_mode_active())
return;
unsigned int scan_depth = get_scan_depth_for_root(scan_path);
collect_candidates_walk_ctx_t ctx = {
.candidates = candidates,
.max_candidates = max_candidates,
.candidate_count = candidate_count,
.app_db = app_db,
.discovered_param_roots = discovered_param_roots,
.discovered_param_root_count = discovered_param_root_count,
.manual_source_path = NULL,
.unstable_found_out = unstable_found_out,
};
sm_scan_tree_callbacks_t callbacks = {
.on_directory = collect_candidate_directory_visit,
.on_image_file = collect_candidate_image_visit,
};
(void)sm_scan_tree_walk(scan_path, scan_path, 0u, scan_depth, &callbacks, &ctx);
}
static void collect_scan_candidates_from_manual_list(
scan_candidate_t *candidates, int max_candidates, int *candidate_count,
const scan_app_db_context_t *app_db,
char discovered_param_roots[][MAX_PATH], int *discovered_param_root_count,
bool *unstable_found_out) {
if (should_stop_requested() || runtime_sleep_mode_active())
return;
(void)sm_manual_reconcile_deleted_titles(app_db->titles,
app_db->titles_ready);
manual_scan_ctx_t ctx = {
.candidates = candidates,
.max_candidates = max_candidates,
.candidate_count = candidate_count,
.app_db = app_db,
.discovered_param_roots = discovered_param_roots,
.discovered_param_root_count = discovered_param_root_count,
.unstable_found_out = unstable_found_out,
.path_count = 0,
};
if (!sm_manual_for_each_path(collect_manual_path_visit, &ctx) ||
ctx.path_count <= 0) {
return;
}
log_debug(" [MANUAL] scanned %d manual source(s)", ctx.path_count);
}
static void uninstall_blocked_ppsa_titles(
const struct AppDbTitleList *blocked_ppsa_titles,
bool blocked_ppsa_titles_ready) {
if (!blocked_ppsa_titles_ready || !blocked_ppsa_titles)
return;
int requested = 0;
for (int i = 0; i < blocked_ppsa_titles->count &&
!should_stop_requested() && !runtime_sleep_mode_active();
i++) {
const char *title_id = blocked_ppsa_titles->ids[i];
if (request_blocked_ppsa_uninstall(title_id, NULL))
requested++;
}
if (requested > 0)
log_debug(" [REG] requested %d blocked PPSA uninstall(s)", requested);
}
int collect_scan_candidates_for_scan_root(const char *scan_root,
scan_candidate_t *candidates,
int max_candidates,
int *total_found_out,
bool *unstable_found_out) {
reset_scan_workspace();
int candidate_count = 0;
struct AppDbTitleList app_db_titles = {0};
struct AppDbTitleList blocked_ppsa_titles = {0};
bool app_db_titles_ready = get_app_db_title_list_cached(&app_db_titles);
bool blocked_ppsa_titles_ready =
get_app_db_blocked_uninstall_ppsa_list(&blocked_ppsa_titles);
int discovered_param_root_count = 0;
if (!app_db_titles_ready)
log_debug(" [DB] app.db title list unavailable for this scan cycle");
if (!blocked_ppsa_titles_ready)
log_debug(" [DB] blocked PPSA uninstall list unavailable for this scan cycle");
scan_app_db_context_t app_db = {
.titles = &app_db_titles,
.blocked_ppsa_titles = &blocked_ppsa_titles,
.titles_ready = app_db_titles_ready,
.blocked_ppsa_titles_ready = blocked_ppsa_titles_ready,
};
collect_scan_candidates_from_root(scan_root, candidates, max_candidates,
&candidate_count, &app_db,
g_scan_workspace.discovered_param_roots,
&discovered_param_root_count,
unstable_found_out);
if (total_found_out)
*total_found_out = discovered_param_root_count;
sm_image_index_flush();
free_app_db_title_list(&blocked_ppsa_titles);
free_app_db_title_list(&app_db_titles);
return candidate_count;
}
int collect_scan_candidates(scan_candidate_t *candidates, int max_candidates,
int *total_found_out,
bool *unstable_found_out) {
reset_scan_workspace();
int candidate_count = 0;
struct AppDbTitleList app_db_titles = {0};
struct AppDbTitleList blocked_ppsa_titles = {0};
bool app_db_titles_ready = get_app_db_title_list_cached(&app_db_titles);
bool blocked_ppsa_titles_ready =
get_app_db_blocked_uninstall_ppsa_list(&blocked_ppsa_titles);
int discovered_param_root_count = 0;
if (!app_db_titles_ready)
log_debug(" [DB] app.db title list unavailable for this scan cycle");
if (!blocked_ppsa_titles_ready)
log_debug(" [DB] blocked PPSA uninstall list unavailable for this scan cycle");
scan_app_db_context_t app_db = {
.titles = &app_db_titles,
.blocked_ppsa_titles = &blocked_ppsa_titles,
.titles_ready = app_db_titles_ready,
.blocked_ppsa_titles_ready = blocked_ppsa_titles_ready,
};
for (int i = 0; i < get_scan_path_count(); i++) {
if (should_stop_requested() || runtime_sleep_mode_active())
break;
collect_scan_candidates_from_root(get_scan_path(i), candidates,
max_candidates,
&candidate_count, &app_db,
g_scan_workspace.discovered_param_roots,
&discovered_param_root_count,
unstable_found_out);
}
collect_scan_candidates_from_manual_list(
candidates, max_candidates, &candidate_count, &app_db,
g_scan_workspace.discovered_param_roots, &discovered_param_root_count,
unstable_found_out);
uninstall_blocked_ppsa_titles(&blocked_ppsa_titles,
blocked_ppsa_titles_ready);
if (total_found_out)
*total_found_out = discovered_param_root_count;
sm_image_index_flush();
free_app_db_title_list(&blocked_ppsa_titles);
free_app_db_title_list(&app_db_titles);
return candidate_count;
}
+124
View File
@@ -0,0 +1,124 @@
#include "sm_platform.h"
#include "sm_scan_tree.h"
#include "sm_config_mount.h"
#include "sm_filesystem.h"
#include "sm_image.h"
#include "sm_path_utils.h"
#include "sm_paths.h"
#include "sm_runtime.h"
static void classify_scan_tree_entry(const char *full_path, unsigned char d_type,
bool *is_dir_out, bool *is_regular_out) {
bool is_dir = false;
bool is_regular = false;
if (d_type == DT_DIR) {
is_dir = true;
} else if (d_type == DT_REG) {
is_regular = true;
} else if (d_type == DT_UNKNOWN) {
struct stat st;
if (lstat(full_path, &st) == 0) {
is_dir = S_ISDIR(st.st_mode);
is_regular = S_ISREG(st.st_mode);
}
}
*is_dir_out = is_dir;
*is_regular_out = is_regular;
}
static bool is_distinct_configured_scan_root(const char *current_scan_root,
const char *path) {
for (int i = 0; i < get_scan_path_count(); i++) {
const char *scan_path = get_scan_path(i);
if (strcmp(scan_path, path) != 0)
continue;
if (strcmp(current_scan_root, path) == 0)
return false;
return true;
}
return false;
}
bool sm_scan_tree_walk(const char *scan_root, const char *dir_path,
unsigned int depth_from_root,
unsigned int remaining_depth,
const sm_scan_tree_callbacks_t *callbacks, void *ctx) {
if (should_stop_requested() || runtime_sleep_mode_active())
return true;
if (depth_from_root > 0u &&
is_distinct_configured_scan_root(scan_root, dir_path)) {
return true;
}
sm_scan_tree_dir_visit_t dir_visit = SM_SCAN_TREE_DIR_DESCEND;
if (callbacks->on_directory) {
dir_visit = callbacks->on_directory(dir_path, depth_from_root, ctx);
if (dir_visit == SM_SCAN_TREE_DIR_ABORT)
return false;
if (dir_visit == SM_SCAN_TREE_DIR_SKIP_DESCEND)
return true;
}
if (remaining_depth == 0u)
return true;
DIR *d = opendir(dir_path);
if (!d)
return true;
bool scan_root_is_pfsc_mount_base =
is_pfsc_image_mount_base_or_child(scan_root);
bool skip_backports_root =
(depth_from_root == 0u && !is_under_image_mount_base(scan_root));
bool allow_image_file_visits =
callbacks->on_image_file &&
(!path_matches_root_or_child(scan_root, IMAGE_MOUNT_BASE) ||
scan_root_is_pfsc_mount_base);
struct dirent *entry;
while ((entry = readdir(d)) != NULL) {
if (should_stop_requested() || runtime_sleep_mode_active()) {
closedir(d);
return true;
}
if (entry->d_name[0] == '.')
continue;
if (skip_backports_root &&
strcmp(entry->d_name, DEFAULT_BACKPORTS_DIR_NAME) == 0) {
continue;
}
char full_path[MAX_PATH];
snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name);
bool is_dir = false;
bool is_regular = false;
classify_scan_tree_entry(full_path, entry->d_type, &is_dir, &is_regular);
if (allow_image_file_visits && is_regular &&
is_supported_image_file_path(full_path, entry->d_name)) {
if (!callbacks->on_image_file(full_path, entry->d_name, depth_from_root + 1u,
ctx)) {
closedir(d);
return false;
}
}
if (!is_dir)
continue;
if (!sm_scan_tree_walk(scan_root, full_path, depth_from_root + 1u,
remaining_depth - 1u, callbacks, ctx)) {
closedir(d);
return false;
}
}
closedir(d);
return true;
}
+1689
View File
File diff suppressed because it is too large. Load diff
+198
View File
@@ -0,0 +1,198 @@
/* Position-independent bridge copied into SceShellCore executable memory. */
.text
.p2align 4
.global sm_shellcore_bridge_blob_start
.global sm_shellcore_bridge_blob_end
.global sm_shellcore_bridge_launch_hook
.global sm_shellcore_bridge_install_all_hook
.global sm_shellcore_bridge_launch_trampoline
.global sm_shellcore_bridge_install_all_trampoline
.global sm_shellcore_bridge_install_title_dir
.global sm_shellcore_bridge_install_armed
.global sm_shellcore_bridge_install_title_id
.global sm_shellcore_bridge_install_dir
.global sm_shellcore_bridge_socket
.global sm_shellcore_bridge_connect
.global sm_shellcore_bridge_read
.global sm_shellcore_bridge_write
.global sm_shellcore_bridge_close
sm_shellcore_bridge_blob_start:
sm_shellcore_bridge_launch_hook:
pushq %rbx
pushq %r12
movq %rsi, %r12
pushfq
pushq %rax
pushq %rdi
pushq %rsi
pushq %rdx
pushq %rcx
pushq %r8
pushq %r9
pushq %r10
pushq %r11
subq $8, %rsp
movl $1, %edi
/* RSI still contains title_id. */
call sm_shellcore_bridge_exchange
addq $8, %rsp
popq %r11
popq %r10
popq %r9
popq %r8
popq %rcx
popq %rdx
popq %rsi
popq %rdi
popq %rax
popfq
subq $8, %rsp
call *sm_shellcore_bridge_launch_trampoline(%rip)
addq $8, %rsp
movq %rax, %rbx
testl %eax, %eax
jns 1f
movl $2, %edi
movq %r12, %rsi
subq $8, %rsp
call sm_shellcore_bridge_exchange
addq $8, %rsp
1:
movq %rbx, %rax
popq %r12
popq %rbx
ret
.p2align 4
/*
* FW 12+ removed the public AppInstallTitleDir RPC. AppInstallAll still
* reaches this internal dispatcher, so one explicitly armed request is
* redirected to the internal TitleDir implementation. Every ordinary
* AppInstallAll call follows its original trampoline unchanged.
*/
.p2align 4
sm_shellcore_bridge_install_all_hook:
cmpb $0, sm_shellcore_bridge_install_armed(%rip)
je 1f
movb $0, sm_shellcore_bridge_install_armed(%rip)
leaq sm_shellcore_bridge_install_title_id(%rip), %rdi
leaq sm_shellcore_bridge_install_dir(%rip), %rsi
movl $1, %edx
xorl %ecx, %ecx
jmp *sm_shellcore_bridge_install_title_dir(%rip)
1:
jmp *sm_shellcore_bridge_install_all_trampoline(%rip)
/* int exchange(uint16_t operation, const char *title_id) */
.p2align 4
sm_shellcore_bridge_exchange:
pushq %rbp
movq %rsp, %rbp
pushq %rbx
pushq %r12
subq $160, %rsp
/* Build the fixed 40-byte request. */
movq $0, -64(%rbp)
movq $0, -56(%rbp)
movq $0, -48(%rbp)
movq $0, -40(%rbp)
movq $0, -32(%rbp)
movl $0x534d5343, -64(%rbp)
movw $1, -60(%rbp)
movw %di, -58(%rbp)
testq %rsi, %rsi
je 2f
leaq -56(%rbp), %rdi
movl $31, %ecx
1:
movb (%rsi), %al
testb %al, %al
je 2f
movb %al, (%rdi)
incq %rsi
incq %rdi
decl %ecx
jne 1b
2:
/* Use libkernel_sys wrappers. Direct syscalls from SceShellCore are fatal. */
movl $1, %edi
movl $1, %esi
xorl %edx, %edx
call *sm_shellcore_bridge_socket(%rip)
testl %eax, %eax
js 8f
movl %eax, %ebx
/* connect(fd, &sockaddr, sockaddr.sun_len) */
movl %ebx, %edi
leaq sm_shellcore_bridge_sockaddr(%rip), %rsi
movl $31, %edx
call *sm_shellcore_bridge_connect(%rip)
testl %eax, %eax
jne 7f
/* write(fd, &request, sizeof(request)) */
movl %ebx, %edi
leaq -64(%rbp), %rsi
movl $40, %edx
call *sm_shellcore_bridge_write(%rip)
cmpq $40, %rax
jne 7f
/* Only status is needed by launch and launch-failure requests. */
movl %ebx, %edi
leaq -68(%rbp), %rsi
movl $4, %edx
call *sm_shellcore_bridge_read(%rip)
cmpq $4, %rax
jne 7f
movl -68(%rbp), %r12d
jmp 5f
7:
movl $5, %r12d
5:
movl %ebx, %edi
call *sm_shellcore_bridge_close(%rip)
movl %r12d, %eax
jmp 9f
8:
movl $5, %eax
9:
addq $160, %rsp
popq %r12
popq %rbx
popq %rbp
ret
.p2align 3
sm_shellcore_bridge_launch_trampoline:
.quad 0
sm_shellcore_bridge_install_all_trampoline:
.quad 0
sm_shellcore_bridge_install_title_dir:
.quad 0
sm_shellcore_bridge_socket:
.quad 0
sm_shellcore_bridge_connect:
.quad 0
sm_shellcore_bridge_read:
.quad 0
sm_shellcore_bridge_write:
.quad 0
sm_shellcore_bridge_close:
.quad 0
sm_shellcore_bridge_sockaddr:
.byte 31, 1
.asciz "/system_tmp/shadowmount.sock"
.zero 75
.p2align 3
sm_shellcore_bridge_install_armed:
.byte 0
sm_shellcore_bridge_install_title_id:
.zero 32
sm_shellcore_bridge_install_dir:
.zero 1024
sm_shellcore_bridge_blob_end:
.section .note.GNU-stack,"",@progbits
+784
View File
@@ -0,0 +1,784 @@
#include "sm_platform.h"
#include <pthread.h>
#include "sm_game_lifecycle.h"
#include "sm_kstuff.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_runtime.h"
#include "sm_shellcore_flags.h"
#define SHELLCORE_FLAG_WAITMODE_OR 2u
#define SHELLCORE_FLAG_ALL_BITS UINT64_MAX
#define SYSTEM_STATE_MGR_STATUS_SHELLUI_SHUTDOWN_IN_PROGRESS 0x0000000000200000ULL
typedef intptr_t sm_kernel_event_flag_t;
int sceKernelOpenEventFlag(sm_kernel_event_flag_t *ef, const char *name);
int sceKernelPollEventFlag(sm_kernel_event_flag_t ef, uint64_t bit_pattern,
unsigned int wait_mode, uint64_t *result_pattern);
int sceKernelCloseEventFlag(sm_kernel_event_flag_t ef);
typedef struct {
const char *name;
sm_kernel_event_flag_t handle;
uint64_t last_pattern;
int last_rc;
bool required;
bool is_open;
bool has_last_pattern;
bool has_last_rc;
} shellcore_flag_monitor_t;
typedef struct {
uint64_t mask;
const char *name;
} shellcore_flag_bit_desc_t;
static const shellcore_flag_bit_desc_t g_lnc_util_system_status_bits[] = {
{0x0000000000000001ULL, "EXTRA_AUDIO_CPU_BUDGET_AVAILABLE"},
{0x0000000000000002ULL, "SHELLUI_FG_GAME_BG_CPU_MODE"},
};
static shellcore_flag_monitor_t g_shellcore_flags[] = {
{.name = "SceShellCoreUtilAppFocus", .handle = -1, .required = true},
{.name = "SceSystemStateMgrInfo", .handle = -1, .required = true},
{.name = "SceLncUtilSystemStatus", .handle = -1, .required = false},
{.name = "SceSystemStateMgrStatus", .handle = -1},
/* {.name = "SceSysCoreSuspend", .handle = -1},
{.name = "SceShellCoreUtilPowerControl", .handle = -1},
{.name = "SceShellCoreUtilRunLevel", .handle = -1},
{.name = "SceShellCoreUtilUIStatus", .handle = -1},
{.name = "SceAutoMountUsbMass", .handle = -1},
{.name = "SceVshctlInternalUserFlag", .handle = -1},
{.name = "SceBootStatusFlags", .handle = -1},
{.name = "SceSysCoreReboot", .handle = -1},
*/
};
static pthread_t g_shellcore_flag_thread;
static pthread_mutex_t g_shellcore_flag_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t g_shellcore_flag_cond = PTHREAD_COND_INITIALIZER;
static bool g_shellcore_flag_thread_started = false;
static bool g_shellcore_flag_start_ready = false;
static bool g_shellcore_flag_start_success = false;
static volatile sig_atomic_t g_shellcore_flag_stop_requested = 0;
static const shellcore_flag_bit_desc_t g_system_state_mgr_status_bits[] = {
{0x0000000000000001ULL, "CEC_ONE_TOUCH_PLAY_COMMAND"},
{0x0000000000000002ULL, "MEDIA_PLAYBACK_MODE"},
{0x0000000000000004ULL, "BD_DRIVE_READY"},
{0x0000000000000008ULL, "TICK_MUSIC_PLAYBACK"},
{0x0000000000000010ULL, "TICK_VIDEO_PLAYBACK"},
{0x0000000000000020ULL, "TICK_0x20"},
{0x0000000000000040ULL, "TICK_PARTY_CHAT"},
{0x0000000000000100ULL, "TURN_OFF_REQUEST"},
{0x0000000000000200ULL, "REBOOT_REQUEST"},
{0x0000000000000400ULL, "ENTER_STANDBY_REQUEST"},
{0x0000000000010000ULL, "START_SHUTDOWN_TIMER"},
{0x0000000000020000ULL, "START_REBOOT_TIMER"},
{0x0000000000040000ULL, "START_STANDBY_TIMER"},
{0x0000000000080000ULL, "EXTEND_SHUTDOWN_TIMER"},
{0x0000000000100000ULL, "CANCEL_SHUTDOWN_TIMER"},
{SYSTEM_STATE_MGR_STATUS_SHELLUI_SHUTDOWN_IN_PROGRESS,
"SHELLUI_SHUTDOWN_IN_PROGRESS"},
{0x0000000000400000ULL, "EXTEND_SHUTDOWN_TIMER_POST_AUTOUPDATE"},
{0x0000000001000000ULL, "BIGAPP_LAUNCH_READY"},
{0x0000000004000000ULL, "VSH_AUTO_UPDATE_VERIFY_DONE"},
{0x0000000008000000ULL, "START_REBOOT_TIMER_FAST"},
};
#if 0
static const shellcore_flag_bit_desc_t g_power_control_bits[] = {
{0x0000000000020000ULL, "SCREEN_SAVER_ON"},
{0x0000000000040000ULL, "POWER_TRANSITION"},
{0x0000000000080000ULL, "POWER_LOCK"},
{0x0000000000100000ULL, "SOCIAL_SCREEN_STATUS"},
{0x0000000000200000ULL, "REQUEST_CAMERA_CALIBRATION"},
{0x0000000002000000ULL, "USER_INPUT_PULSE"},
{0x0000000004000000ULL, "IMPOSE_OVERLAY"},
{0x0000000008000000ULL, "AUTO_POWER_DOWN_TIMER_RESET"},
{0x0000000010000000ULL, "REMOTE_PLAY_STATUS"},
{0x0000000020000000ULL, "SHARE_PLAY_STATUS"},
{0x0000000040000000ULL, "GAME_LIVE_STREAMING"},
{0x0000000080000000ULL, "BGM_CPU_BUDGET"},
{0x0000000100000000ULL, "PRENOTIFY_REMOTE_PLAY"},
{0x0000000200000000ULL, "PRENOTIFY_SHARE_PLAY"},
{0x0000000400000000ULL, "PRENOTIFY_GAME_LIVE_STREAMING"},
{0x0000000800000000ULL, "REMOTE_PLAY_CPU_BUDGET"},
{0x0000001000000000ULL, "SHARE_PLAY_CPU_BUDGET"},
{0x0000100000000000ULL, "EXT_STORAGE_APP_MOVE_IN_PROGRESS"},
{0x0000200000000000ULL, "STATE_0x2000000000000"},
};
static const shellcore_flag_bit_desc_t g_shellcore_ui_status_bits[] = {
{0x0000000000000001ULL, "LOW_BIT_0x1"},
{0x0000000000000010ULL, "LOW_BIT_0x10"},
{0x0000000000018000ULL, "BASIC_PRODUCT_SHAPE_FIELD"},
{0x0000000000020000ULL, "EXT_HDD_MOUNT_UNBLOCK"},
{0x0000000400000000ULL, "HIGH32_BIT2"},
};
static const shellcore_flag_bit_desc_t g_vshctl_internal_user_bits[] = {
{0x0000000000000001ULL, "INTERNAL_CRONOS_USER"},
};
static const shellcore_flag_bit_desc_t g_boot_status_bits[] = {
{0x0000000000000002ULL, "BIT_0x2"},
{0x0000000000000040ULL, "BIT_0x40"},
{0x0000000000000080ULL, "BIT_0x80"},
{0x0000000000000200ULL, "BIT_0x200"},
{0x0000000000004000ULL, "BIT_0x4000"},
{0x0000000001000000ULL, "BIGAPP_LAUNCH_READY"},
};
static const shellcore_flag_bit_desc_t g_syscore_reboot_bits[] = {
{0x0000000000000040ULL, "BIT_0x40"},
{0x0000000000004000ULL, "BIT_0x4000"},
{0x0000000100000000ULL, "REBOOT_FLAG"},
};
static const shellcore_flag_bit_desc_t g_syscore_suspend_bits[] = {
{0x0000000000000001ULL, "SUSPEND_ACK"},
{0x0000000000000002ULL, "SUSPEND_REQ_BIT1"},
{0x0000000000000004ULL, "SUSPEND_REQ_BIT2"},
{0x0000000000000008ULL, "SUSPEND_REQ_BIT3"},
};
#endif
enum {
SYSTEM_STATE_MGR_STATE_INVALID = 0u,
SYSTEM_STATE_MGR_STATE_INITIALIZING = 10u,
SYSTEM_STATE_MGR_STATE_SHUTDOWN_ON_GOING = 100u,
SYSTEM_STATE_MGR_STATE_POWER_SAVING = 200u,
SYSTEM_STATE_MGR_STATE_SUSPEND_ON_GOING = 300u,
SYSTEM_STATE_MGR_STATE_MAIN_ON_STANDBY = 500u,
SYSTEM_STATE_MGR_STATE_WORKING = 1000u,
};
enum {
SYSTEM_STATE_MGR_REBOOT_CAUSE_NONE = 0u,
SYSTEM_STATE_MGR_REBOOT_CAUSE_CFF = 1u,
};
static const shellcore_flag_bit_desc_t *get_shellcore_flag_bits(
const shellcore_flag_monitor_t *flag, size_t *count_out) {
if (count_out)
*count_out = 0;
if (!flag || !flag->name)
return NULL;
if (strcmp(flag->name, "SceLncUtilSystemStatus") == 0) {
if (count_out) {
*count_out = sizeof(g_lnc_util_system_status_bits) /
sizeof(g_lnc_util_system_status_bits[0]);
}
return g_lnc_util_system_status_bits;
}
if (strcmp(flag->name, "SceSystemStateMgrStatus") == 0) {
if (count_out) {
*count_out = sizeof(g_system_state_mgr_status_bits) /
sizeof(g_system_state_mgr_status_bits[0]);
}
return g_system_state_mgr_status_bits;
}
#if 0
if (strcmp(flag->name, "SceShellCoreUtilPowerControl") == 0) {
if (count_out)
*count_out = sizeof(g_power_control_bits) / sizeof(g_power_control_bits[0]);
return g_power_control_bits;
}
if (strcmp(flag->name, "SceShellCoreUtilUIStatus") == 0) {
if (count_out) {
*count_out = sizeof(g_shellcore_ui_status_bits) /
sizeof(g_shellcore_ui_status_bits[0]);
}
return g_shellcore_ui_status_bits;
}
if (strcmp(flag->name, "SceVshctlInternalUserFlag") == 0) {
if (count_out) {
*count_out = sizeof(g_vshctl_internal_user_bits) /
sizeof(g_vshctl_internal_user_bits[0]);
}
return g_vshctl_internal_user_bits;
}
if (strcmp(flag->name, "SceBootStatusFlags") == 0) {
if (count_out)
*count_out = sizeof(g_boot_status_bits) / sizeof(g_boot_status_bits[0]);
return g_boot_status_bits;
}
if (strcmp(flag->name, "SceSysCoreReboot") == 0) {
if (count_out)
*count_out = sizeof(g_syscore_reboot_bits) / sizeof(g_syscore_reboot_bits[0]);
return g_syscore_reboot_bits;
}
if (strcmp(flag->name, "SceSysCoreSuspend") == 0) {
if (count_out)
*count_out = sizeof(g_syscore_suspend_bits) /
sizeof(g_syscore_suspend_bits[0]);
return g_syscore_suspend_bits;
}
#endif
return NULL;
}
static const char *get_system_state_mgr_state_name(unsigned state) {
switch (state) {
case SYSTEM_STATE_MGR_STATE_INVALID:
return "INVALID";
case SYSTEM_STATE_MGR_STATE_INITIALIZING:
return "INITIALIZING";
case SYSTEM_STATE_MGR_STATE_SHUTDOWN_ON_GOING:
return "SHUTDOWN_ON_GOING";
case SYSTEM_STATE_MGR_STATE_POWER_SAVING:
return "POWER_SAVING";
case SYSTEM_STATE_MGR_STATE_SUSPEND_ON_GOING:
return "SUSPEND_ON_GOING";
case SYSTEM_STATE_MGR_STATE_MAIN_ON_STANDBY:
return "MAIN_ON_STANDBY";
case SYSTEM_STATE_MGR_STATE_WORKING:
return "WORKING";
default:
return "UNKNOWN";
}
}
static const char *get_system_state_mgr_reboot_cause_name(unsigned cause) {
switch (cause) {
case SYSTEM_STATE_MGR_REBOOT_CAUSE_NONE:
return "NONE";
case SYSTEM_STATE_MGR_REBOOT_CAUSE_CFF:
return "CFF";
default:
return "UNKNOWN";
}
}
static void append_shellcore_flag_token(char *dst, size_t dst_size,
const char *token) {
size_t used;
if (!dst || dst_size == 0 || !token || token[0] == '\0')
return;
used = strnlen(dst, dst_size);
if (used >= dst_size - 1)
return;
(void)snprintf(dst + used, dst_size - used, "%s%s", used == 0 ? "" : "|",
token);
}
static bool format_shellcore_flag_special_known_masks(
const shellcore_flag_monitor_t *flag, char *dst, size_t dst_size) {
if (!flag || !flag->name || !dst || dst_size == 0)
return false;
#if 0
if (strcmp(flag->name, "SceShellCoreUtilRunLevel") == 0) {
append_shellcore_flag_token(dst, dst_size, "RUN_LEVEL_NUMERIC");
append_shellcore_flag_token(dst, dst_size, "POLL_MASK=0xFFFFFFF");
append_shellcore_flag_token(dst, dst_size, "INITIAL=10");
append_shellcore_flag_token(dst, dst_size, "READY=100");
return true;
}
if (strcmp(flag->name, "SceAutoMountUsbMass") == 0) {
append_shellcore_flag_token(dst, dst_size, "USB_MOUNT_BITS=0xFFFFFFF");
return true;
}
#endif
if (strcmp(flag->name, "SceSystemStateMgrInfo") == 0) {
append_shellcore_flag_token(dst, dst_size,
"CURRENT_STATE=bits[0..15]");
append_shellcore_flag_token(dst, dst_size,
"TRIGGER_CODE=bits[32..47]");
append_shellcore_flag_token(dst, dst_size,
"REBOOT_CAUSE=bits[48..63]");
append_shellcore_flag_token(dst, dst_size, "STATE_INVALID=0");
append_shellcore_flag_token(dst, dst_size, "STATE_INITIALIZING=10");
append_shellcore_flag_token(dst, dst_size,
"STATE_SHUTDOWN_ON_GOING=100");
append_shellcore_flag_token(dst, dst_size, "STATE_POWER_SAVING=200");
append_shellcore_flag_token(dst, dst_size,
"STATE_SUSPEND_ON_GOING=300");
append_shellcore_flag_token(dst, dst_size,
"STATE_MAIN_ON_STANDBY=500");
append_shellcore_flag_token(dst, dst_size, "STATE_WORKING=1000");
append_shellcore_flag_token(dst, dst_size, "REBOOT_CAUSE_NONE=0");
append_shellcore_flag_token(dst, dst_size, "REBOOT_CAUSE_CFF=1");
append_shellcore_flag_token(dst, dst_size, "INITIAL=0x10000000A");
return true;
}
return false;
}
static void format_shellcore_flag_known_masks(const shellcore_flag_monitor_t *flag,
char *dst, size_t dst_size) {
size_t bit_count = 0;
const shellcore_flag_bit_desc_t *bits = get_shellcore_flag_bits(flag, &bit_count);
if (!dst || dst_size == 0)
return;
dst[0] = '\0';
(void)format_shellcore_flag_special_known_masks(flag, dst, dst_size);
for (size_t i = 0; i < bit_count; ++i) {
char token[96];
(void)snprintf(token, sizeof(token), "%s=0x%llX", bits[i].name,
(unsigned long long)bits[i].mask);
append_shellcore_flag_token(dst, dst_size, token);
}
if (strcmp(flag->name, "SceShellCoreUtilAppFocus") == 0) {
append_shellcore_flag_token(dst, dst_size, "APP_ID");
}
}
static bool format_shellcore_flag_special_value(const shellcore_flag_monitor_t *flag,
uint64_t pattern, char *dst,
size_t dst_size) {
if (!flag || !flag->name || !dst || dst_size == 0)
return false;
#if 0
if (strcmp(flag->name, "SceShellCoreUtilRunLevel") == 0) {
char token[96];
(void)snprintf(token, sizeof(token), "RUN_LEVEL=%u",
(unsigned)(pattern & 0x0FFFFFFFULL));
append_shellcore_flag_token(dst, dst_size, token);
if ((pattern & 0x0FFFFFFFULL) == 10u) {
append_shellcore_flag_token(dst, dst_size, "INITIAL");
} else if ((pattern & 0x0FFFFFFFULL) == 100u) {
append_shellcore_flag_token(dst, dst_size, "READY");
}
if ((pattern & ~0x0FFFFFFFULL) != 0) {
(void)snprintf(token, sizeof(token), "UNKNOWN=0x%llX",
(unsigned long long)(pattern & ~0x0FFFFFFFULL));
append_shellcore_flag_token(dst, dst_size, token);
}
return true;
}
if (strcmp(flag->name, "SceAutoMountUsbMass") == 0) {
uint64_t usb_bits = pattern & 0x0FFFFFFFULL;
for (unsigned i = 0; i < 28; ++i) {
if ((usb_bits & (1ULL << i)) == 0)
continue;
char token[32];
(void)snprintf(token, sizeof(token), "USB%u", i);
append_shellcore_flag_token(dst, dst_size, token);
}
if ((pattern & ~0x0FFFFFFFULL) != 0) {
char token[64];
(void)snprintf(token, sizeof(token), "UNKNOWN=0x%llX",
(unsigned long long)(pattern & ~0x0FFFFFFFULL));
append_shellcore_flag_token(dst, dst_size, token);
}
if (dst[0] == '\0')
(void)snprintf(dst, dst_size, "0");
return true;
}
#endif
if (strcmp(flag->name, "SceSystemStateMgrInfo") == 0) {
unsigned current_state = (unsigned)(pattern & 0xFFFFu);
unsigned mid16 = (unsigned)((pattern >> 16) & 0xFFFFu);
unsigned trigger_code = (unsigned)((pattern >> 32) & 0xFFFFu);
unsigned reboot_cause = (unsigned)((pattern >> 48) & 0xFFFFu);
char token[128];
(void)snprintf(token, sizeof(token), "STATE=%s(0x%04X)",
get_system_state_mgr_state_name(current_state), current_state);
append_shellcore_flag_token(dst, dst_size, token);
(void)snprintf(token, sizeof(token), "TRIGGER=0x%04X", trigger_code);
append_shellcore_flag_token(dst, dst_size, token);
(void)snprintf(token, sizeof(token), "REBOOT_CAUSE=%s(0x%04X)",
get_system_state_mgr_reboot_cause_name(reboot_cause),
reboot_cause);
append_shellcore_flag_token(dst, dst_size, token);
if (mid16 != 0) {
(void)snprintf(token, sizeof(token), "MID16=0x%04X", mid16);
append_shellcore_flag_token(dst, dst_size, token);
}
return true;
}
if (strcmp(flag->name, "SceShellCoreUtilAppFocus") == 0) {
unsigned low32 = (unsigned)(pattern & 0xFFFFFFFFu);
unsigned high32 = (unsigned)(pattern >> 32);
char token[96];
if (low32 != 0) {
(void)snprintf(token, sizeof(token), "APP_ID=0x%08X", low32);
append_shellcore_flag_token(dst, dst_size, token);
}
if (high32 != 0) {
(void)snprintf(token, sizeof(token), "HIGH32=0x%08X", high32);
append_shellcore_flag_token(dst, dst_size, token);
}
if (dst[0] == '\0')
(void)snprintf(dst, dst_size, "0");
return true;
}
#if 0
if (strcmp(flag->name, "SceShellCoreUtilUIStatus") == 0) {
unsigned shape_field = (unsigned)((pattern >> 15) & 0x3u);
if (shape_field != 0) {
char token[96];
(void)snprintf(token, sizeof(token), "BASIC_PRODUCT_SHAPE=%u",
shape_field - 1u);
append_shellcore_flag_token(dst, dst_size, token);
}
return false;
}
#endif
return false;
}
static void format_shellcore_flag_set_bits(const shellcore_flag_monitor_t *flag,
uint64_t pattern, char *dst,
size_t dst_size) {
size_t bit_count = 0;
uint64_t known_mask = 0;
const shellcore_flag_bit_desc_t *bits = get_shellcore_flag_bits(flag, &bit_count);
if (!dst || dst_size == 0)
return;
dst[0] = '\0';
if (format_shellcore_flag_special_value(flag, pattern, dst, dst_size) &&
dst[0] != '\0') {
return;
}
for (size_t i = 0; i < bit_count; ++i) {
known_mask |= bits[i].mask;
if ((pattern & bits[i].mask) == 0)
continue;
char token[96];
(void)snprintf(token, sizeof(token), "%s", bits[i].name);
append_shellcore_flag_token(dst, dst_size, token);
}
if (strcmp(flag->name, "SceShellCoreUtilAppFocus") == 0 &&
(pattern >> 32) == 0 && pattern != 0) {
char token[64];
(void)snprintf(token, sizeof(token), "APP_ID=0x%08X", (unsigned)pattern);
append_shellcore_flag_token(dst, dst_size, token);
}
if ((pattern & ~known_mask) != 0) {
char token[64];
(void)snprintf(token, sizeof(token), "UNKNOWN=0x%llX",
(unsigned long long)(pattern & ~known_mask));
append_shellcore_flag_token(dst, dst_size, token);
}
if (dst[0] == '\0')
(void)snprintf(dst, dst_size, "0");
}
static void set_shellcore_flag_start_result(bool success) {
pthread_mutex_lock(&g_shellcore_flag_mutex);
g_shellcore_flag_start_success = success;
g_shellcore_flag_start_ready = true;
pthread_cond_broadcast(&g_shellcore_flag_cond);
pthread_mutex_unlock(&g_shellcore_flag_mutex);
}
static void enter_sleep_mode_and_cleanup(const char *reason) {
(void)request_runtime_sleep_mode(true, reason);
}
static void reset_shellcore_flag_state(shellcore_flag_monitor_t *flag) {
if (!flag)
return;
flag->handle = -1;
flag->is_open = false;
flag->has_last_pattern = false;
flag->has_last_rc = false;
flag->last_pattern = 0;
flag->last_rc = 0;
}
static void log_shellcore_flag_pattern(const shellcore_flag_monitor_t *flag,
uint64_t pattern) {
char decoded[512];
if (!flag)
return;
format_shellcore_flag_set_bits(flag, pattern, decoded, sizeof(decoded));
if (!flag->has_last_pattern) {
log_debug(" [SHELLFLAG] %s initial=0x%016llX low32=0x%08X high32=0x%08X "
"decoded=%s",
flag->name, (unsigned long long)pattern, (unsigned)(pattern),
(unsigned)(pattern >> 32), decoded);
return;
}
log_debug(" [SHELLFLAG] %s value=0x%016llX delta=0x%016llX low32=0x%08X "
"high32=0x%08X decoded=%s",
flag->name, (unsigned long long)pattern,
(unsigned long long)(flag->last_pattern ^ pattern),
(unsigned)(pattern), (unsigned)(pattern >> 32), decoded);
}
static void close_shellcore_flag(shellcore_flag_monitor_t *flag) {
if (!flag || !flag->is_open)
return;
int rc = sceKernelCloseEventFlag(flag->handle);
if (rc < 0) {
log_debug(" [SHELLFLAG] close failed: %s handle=0x%016llX rc=0x%08X",
flag->name, (unsigned long long)flag->handle, (unsigned)rc);
}
reset_shellcore_flag_state(flag);
}
static void close_shellcore_flags(void) {
for (size_t i = 0; i < sizeof(g_shellcore_flags) / sizeof(g_shellcore_flags[0]);
++i) {
close_shellcore_flag(&g_shellcore_flags[i]);
}
}
static bool open_shellcore_flags(size_t *opened_count_out) {
size_t opened_count = 0;
size_t required_count = 0;
size_t required_opened = 0;
if (opened_count_out)
*opened_count_out = 0;
for (size_t i = 0; i < sizeof(g_shellcore_flags) / sizeof(g_shellcore_flags[0]);
++i) {
shellcore_flag_monitor_t *flag = &g_shellcore_flags[i];
sm_kernel_event_flag_t handle = -1;
if (flag->required)
required_count++;
int rc = sceKernelOpenEventFlag(&handle, flag->name);
if (rc < 0) {
log_debug(" [SHELLFLAG] open failed: %s rc=0x%08X", flag->name,
(unsigned)rc);
reset_shellcore_flag_state(flag);
continue;
}
flag->handle = handle;
flag->is_open = true;
if (flag->required)
required_opened++;
log_debug(" [SHELLFLAG] opened %s handle=0x%016llX", flag->name,
(unsigned long long)flag->handle);
{
char known_masks[1024];
format_shellcore_flag_known_masks(flag, known_masks, sizeof(known_masks));
if (known_masks[0] != '\0') {
log_debug(" [SHELLFLAG] known %s masks: %s", flag->name, known_masks);
}
}
opened_count++;
}
if (opened_count_out)
*opened_count_out = opened_count;
return required_opened == required_count;
}
static void poll_shellcore_flag(shellcore_flag_monitor_t *flag) {
uint64_t result_pattern = 0;
int rc;
bool changed = false;
bool entered_shutdown_on_going = false;
bool entered_main_on_standby = false;
bool entered_suspend_on_going = false;
bool entered_resume_working = false;
bool entered_shellui_shutdown_in_progress = false;
bool is_system_state_mgr_info = false;
unsigned current_state = 0;
unsigned previous_state = 0;
if (!flag || !flag->is_open)
return;
rc = sceKernelPollEventFlag(flag->handle, SHELLCORE_FLAG_ALL_BITS,
SHELLCORE_FLAG_WAITMODE_OR, &result_pattern);
if (rc < 0) {
if (!flag->has_last_rc || flag->last_rc != rc) {
log_debug(" [SHELLFLAG] %s poll rc=0x%08X", flag->name,
(unsigned)rc);
}
flag->last_rc = rc;
flag->has_last_rc = true;
return;
}
if (!flag->has_last_rc || flag->last_rc != 0 || !flag->has_last_pattern ||
flag->last_pattern != result_pattern) {
log_shellcore_flag_pattern(flag, result_pattern);
changed = flag->has_last_pattern && flag->last_pattern != result_pattern;
}
is_system_state_mgr_info = strcmp(flag->name, "SceSystemStateMgrInfo") == 0;
if (is_system_state_mgr_info) {
current_state = (unsigned)(result_pattern & 0xFFFFu);
previous_state =
flag->has_last_pattern ? (unsigned)(flag->last_pattern & 0xFFFFu) : 0;
entered_shutdown_on_going =
current_state == SYSTEM_STATE_MGR_STATE_SHUTDOWN_ON_GOING &&
(!flag->has_last_pattern ||
previous_state != SYSTEM_STATE_MGR_STATE_SHUTDOWN_ON_GOING);
entered_main_on_standby =
current_state == SYSTEM_STATE_MGR_STATE_MAIN_ON_STANDBY &&
(!flag->has_last_pattern ||
previous_state != SYSTEM_STATE_MGR_STATE_MAIN_ON_STANDBY);
entered_suspend_on_going =
current_state == SYSTEM_STATE_MGR_STATE_SUSPEND_ON_GOING &&
(!flag->has_last_pattern ||
previous_state != SYSTEM_STATE_MGR_STATE_SUSPEND_ON_GOING);
entered_resume_working =
flag->has_last_pattern &&
current_state == SYSTEM_STATE_MGR_STATE_WORKING &&
previous_state != SYSTEM_STATE_MGR_STATE_WORKING;
} else if (strcmp(flag->name, "SceSystemStateMgrStatus") == 0) {
entered_shellui_shutdown_in_progress =
(result_pattern &
SYSTEM_STATE_MGR_STATUS_SHELLUI_SHUTDOWN_IN_PROGRESS) != 0 &&
(!flag->has_last_pattern ||
(flag->last_pattern &
SYSTEM_STATE_MGR_STATUS_SHELLUI_SHUTDOWN_IN_PROGRESS) == 0);
}
flag->last_pattern = result_pattern;
flag->has_last_pattern = true;
flag->last_rc = 0;
flag->has_last_rc = true;
if (changed && strcmp(flag->name, "SceShellCoreUtilAppFocus") == 0) {
sm_kstuff_note_app_focus((uint32_t)result_pattern);
wake_game_lifecycle_watcher();
}
if (entered_shutdown_on_going) {
request_shutdown_stop("SceSystemStateMgrInfo=SHUTDOWN_ON_GOING");
}
if (entered_main_on_standby || entered_suspend_on_going) {
const char *sleep_reason =
entered_main_on_standby ? "SceSystemStateMgrInfo=MAIN_ON_STANDBY"
: "SceSystemStateMgrInfo=SUSPEND_ON_GOING";
enter_sleep_mode_and_cleanup(sleep_reason);
}
if (entered_shellui_shutdown_in_progress) {
enter_sleep_mode_and_cleanup(
"SceSystemStateMgrStatus=SHELLUI_SHUTDOWN_IN_PROGRESS");
}
if (entered_resume_working) {
request_scan_now("SceSystemStateMgrInfo=WORKING");
request_runtime_sleep_mode(false, "SceSystemStateMgrInfo=WORKING");
}
}
static void *shellcore_flag_thread_main(void *arg) {
(void)arg;
size_t opened_count = 0;
if (!open_shellcore_flags(&opened_count)) {
if (opened_count != 0)
close_shellcore_flags();
set_shellcore_flag_start_result(false);
return NULL;
}
for (size_t i = 0; i < sizeof(g_shellcore_flags) / sizeof(g_shellcore_flags[0]);
++i) {
poll_shellcore_flag(&g_shellcore_flags[i]);
}
log_debug(" [SHELLFLAG] monitor started (%zu/%zu flags opened)", opened_count,
sizeof(g_shellcore_flags) / sizeof(g_shellcore_flags[0]));
set_shellcore_flag_start_result(true);
while (!g_shellcore_flag_stop_requested && !should_stop_requested()) {
for (size_t i = 0; i < sizeof(g_shellcore_flags) / sizeof(g_shellcore_flags[0]);
++i) {
poll_shellcore_flag(&g_shellcore_flags[i]);
}
sceKernelUsleep(SHELLCORE_FLAG_POLL_INTERVAL_US);
}
close_shellcore_flags();
return NULL;
}
bool sm_shellcore_flags_start(void) {
int rc;
if (g_shellcore_flag_thread_started)
return g_shellcore_flag_start_success;
g_shellcore_flag_stop_requested = 0;
g_shellcore_flag_start_ready = false;
g_shellcore_flag_start_success = false;
close_shellcore_flags();
rc = pthread_create(&g_shellcore_flag_thread, NULL, shellcore_flag_thread_main,
NULL);
if (rc != 0) {
log_debug(" [SHELLFLAG] pthread_create failed: rc=%d", rc);
return false;
}
pthread_mutex_lock(&g_shellcore_flag_mutex);
while (!g_shellcore_flag_start_ready)
pthread_cond_wait(&g_shellcore_flag_cond, &g_shellcore_flag_mutex);
pthread_mutex_unlock(&g_shellcore_flag_mutex);
if (!g_shellcore_flag_start_success) {
pthread_join(g_shellcore_flag_thread, NULL);
close_shellcore_flags();
return false;
}
g_shellcore_flag_thread_started = true;
return true;
}
void sm_shellcore_flags_stop(void) {
if (!g_shellcore_flag_thread_started)
return;
g_shellcore_flag_stop_requested = 1;
pthread_join(g_shellcore_flag_thread, NULL);
g_shellcore_flag_thread_started = false;
g_shellcore_flag_start_ready = false;
g_shellcore_flag_start_success = false;
close_shellcore_flags();
}
+471
View File
@@ -0,0 +1,471 @@
#include "sm_platform.h"
#include <signal.h>
#include <sys/mman.h>
#include <pthread.h>
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_runtime.h"
#include "sm_shellcore_hooks.h"
#include "sm_shellcore_remote.h"
#define SHELLCORE_BASE_HOOK_COUNT 1u
#define SHELLCORE_MAX_HOOK_COUNT 2u
#define MAX_HOOK_PROLOGUE_SIZE 32u
#define ABSOLUTE_JUMP_SIZE 12u
/*
* Private payload-SDK ABI from src/crt/kernel.h. The public
* <ps5/kernel.h> intentionally omits dynlib_obj_t/kernel_dynlib_obj(), but
* kernel_dynlib_obj() copies the complete 0x180-byte object. Keep the local
* definition layout-checked because ShellCore bridge placement needs the
* module bounds and its executable eh_frame area.
*/
typedef struct {
uintptr_t next;
uintptr_t path;
uintptr_t unknown0[2];
uint32_t refcount;
uintptr_t handle;
uintptr_t mapbase;
uintptr_t mapsize;
uintptr_t textsize;
uintptr_t database;
uintptr_t datasize;
uintptr_t unknown1;
uintptr_t unknown1size;
uintptr_t entry;
uintptr_t unknown2;
uintptr_t vaddrbase;
uint32_t tlsindex;
uintptr_t tlsinit;
uintptr_t tlsinitsize;
uintptr_t tlssize;
uintptr_t tlsoffset;
uintptr_t tlsalign;
uintptr_t pltgot;
uintptr_t unknown3[6];
uintptr_t init;
uintptr_t fini;
uintptr_t eh_frame_hdr;
uintptr_t eh_frame_hdr_size;
uintptr_t eh_frame;
uintptr_t eh_frame_size;
int32_t status;
int32_t flags;
uintptr_t unknown4[5];
uintptr_t dynsec;
uintptr_t unknown5[6];
} shellcore_dynlib_obj_t;
_Static_assert(offsetof(shellcore_dynlib_obj_t, mapbase) == 0x30,
"unexpected dynlib_obj mapbase offset");
_Static_assert(offsetof(shellcore_dynlib_obj_t, eh_frame) == 0x108,
"unexpected dynlib_obj eh_frame offset");
_Static_assert(sizeof(shellcore_dynlib_obj_t) == 0x180,
"unexpected dynlib_obj size");
int kernel_dynlib_obj(pid_t pid, uint32_t handle, shellcore_dynlib_obj_t *obj);
extern const uint8_t sm_shellcore_bridge_blob_start[];
extern const uint8_t sm_shellcore_bridge_blob_end[];
extern const uint8_t sm_shellcore_bridge_launch_hook[];
extern const uint8_t sm_shellcore_bridge_install_all_hook[];
extern const uint8_t sm_shellcore_bridge_launch_trampoline[];
extern const uint8_t sm_shellcore_bridge_install_all_trampoline[];
extern const uint8_t sm_shellcore_bridge_install_title_dir[];
extern const uint8_t sm_shellcore_bridge_install_armed[];
extern const uint8_t sm_shellcore_bridge_install_title_id[];
extern const uint8_t sm_shellcore_bridge_install_dir[];
extern const uint8_t sm_shellcore_bridge_socket[];
extern const uint8_t sm_shellcore_bridge_connect[];
extern const uint8_t sm_shellcore_bridge_read[];
extern const uint8_t sm_shellcore_bridge_write[];
extern const uint8_t sm_shellcore_bridge_close[];
typedef struct {
const uint8_t *slot;
const char *name;
} shellcore_import_t;
static const shellcore_import_t k_bridge_imports[] = {
{sm_shellcore_bridge_socket, "socket"},
{sm_shellcore_bridge_connect, "connect"},
{sm_shellcore_bridge_read, "read"},
{sm_shellcore_bridge_write, "write"},
{sm_shellcore_bridge_close, "close"},
};
typedef struct {
bool installed;
sm_shellcore_remote_t remote;
uintptr_t bridge_address;
size_t hook_count;
sm_shellcore_target_t target_ids[SHELLCORE_MAX_HOOK_COUNT];
uint8_t original[SHELLCORE_MAX_HOOK_COUNT][MAX_HOOK_PROLOGUE_SIZE];
uint8_t original_size[SHELLCORE_MAX_HOOK_COUNT];
} shellcore_hooks_state_t;
static shellcore_hooks_state_t g_hooks;
static pthread_mutex_t g_install_mutex = PTHREAD_MUTEX_INITIALIZER;
static uintptr_t remote_bridge_symbol(const uint8_t *symbol) {
return g_hooks.bridge_address +
(uintptr_t)(symbol - sm_shellcore_bridge_blob_start);
}
static void build_absolute_jump(uint8_t jump[ABSOLUTE_JUMP_SIZE],
uintptr_t destination) {
jump[0] = 0x48;
jump[1] = 0xb8;
memcpy(jump + 2, &destination, sizeof(destination));
jump[10] = 0xff;
jump[11] = 0xe0;
}
static bool patch_remote_jump(pid_t pid, uintptr_t source,
uintptr_t destination, size_t patch_size) {
if (patch_size < ABSOLUTE_JUMP_SIZE || patch_size > MAX_HOOK_PROLOGUE_SIZE) {
return false;
}
uint8_t patch[MAX_HOOK_PROLOGUE_SIZE];
memset(patch, 0x90, patch_size);
build_absolute_jump(patch, destination);
if (!sm_remote_process_write(pid, source, patch, patch_size))
return false;
uint8_t verify[MAX_HOOK_PROLOGUE_SIZE];
return sm_remote_process_read(pid, source, verify, patch_size) &&
memcmp(verify, patch, patch_size) == 0;
}
static bool verify_remote_bytes(pid_t pid, uintptr_t address,
const void *expected, size_t size) {
uint8_t buffer[256];
const uint8_t *bytes = (const uint8_t *)expected;
while (size != 0) {
size_t chunk = size < sizeof(buffer) ? size : sizeof(buffer);
if (!sm_remote_process_read(pid, address, buffer, chunk) ||
memcmp(buffer, bytes, chunk) != 0) {
return false;
}
address += chunk;
bytes += chunk;
size -= chunk;
}
return true;
}
static bool append_trampoline(uint8_t *bridge, size_t bridge_capacity,
size_t *cursor, const uint8_t *original,
size_t original_size, uintptr_t return_address,
uintptr_t remote_base,
uintptr_t *remote_trampoline_out) {
size_t aligned = (*cursor + 15u) & ~(size_t)15u;
size_t required = aligned + original_size + ABSOLUTE_JUMP_SIZE;
if (required > bridge_capacity)
return false;
memcpy(bridge + aligned, original, original_size);
build_absolute_jump(bridge + aligned + original_size, return_address);
*remote_trampoline_out = remote_base + aligned;
*cursor = required;
return true;
}
static void set_bridge_pointer(uint8_t *bridge, const uint8_t *local_symbol,
uintptr_t value) {
size_t offset = (size_t)(local_symbol - sm_shellcore_bridge_blob_start);
memcpy(bridge + offset, &value, sizeof(value));
}
static bool resolve_bridge_imports(pid_t pid, uint8_t *bridge) {
uint32_t handle = UINT32_MAX;
if (kernel_dynlib_handle(pid, "libkernel_sys.sprx", &handle) != 0)
return false;
for (size_t i = 0; i < sizeof(k_bridge_imports) / sizeof(k_bridge_imports[0]);
++i) {
uintptr_t address =
(uintptr_t)kernel_dynlib_dlsym(pid, handle, k_bridge_imports[i].name);
if (address == 0) {
log_debug(" [SHELLCORE] failed to resolve libkernel %s",
k_bridge_imports[i].name);
return false;
}
set_bridge_pointer(bridge, k_bridge_imports[i].slot, address);
}
return true;
}
static pid_t find_shellcore_pid(void) {
pid_t pid = find_pid_by_name("SceShellCore", true);
if (pid <= 0)
pid = find_pid_by_name("SceShellCore.elf", true);
return pid;
}
bool sm_shellcore_install_bridge_enabled(void) {
uint16_t firmware =
(uint16_t)((kernel_get_fw_version() >> 16) & 0xffffu);
return firmware >= 0x1200u;
}
static bool install_hooks_for_pid(pid_t pid) {
const size_t blob_size =
(size_t)(sm_shellcore_bridge_blob_end - sm_shellcore_bridge_blob_start);
const size_t bridge_capacity =
blob_size + SHELLCORE_MAX_HOOK_COUNT *
(MAX_HOOK_PROLOGUE_SIZE + ABSOLUTE_JUMP_SIZE + 16u);
uint8_t *bridge = calloc(1, bridge_capacity);
if (!bridge)
return false;
memcpy(bridge, sm_shellcore_bridge_blob_start, blob_size);
if (kill(pid, SIGSTOP) != 0) {
free(bridge);
return false;
}
bool ok = false;
sm_shellcore_remote_t remote;
shellcore_dynlib_obj_t object;
if (!sm_shellcore_remote_resolve(pid, &remote) ||
kernel_dynlib_obj(pid, 0, &object) != 0) {
log_debug(" [SHELLCORE] stopped-process layout resolution failed");
goto done;
}
if (object.mapbase != remote.image_base ||
object.eh_frame < object.mapbase ||
object.eh_frame_size < bridge_capacity ||
object.eh_frame > UINTPTR_MAX - bridge_capacity ||
object.eh_frame + bridge_capacity > object.mapbase + object.mapsize) {
log_debug(" [SHELLCORE] invalid layout: base=0x%lx map=0x%lx+0x%lx "
"eh=0x%lx+0x%lx bridge=0x%zx",
(unsigned long)remote.image_base, (unsigned long)object.mapbase,
(unsigned long)object.mapsize, (unsigned long)object.eh_frame,
(unsigned long)object.eh_frame_size, bridge_capacity);
goto done;
}
log_debug(" [SHELLCORE] layout validated: map=0x%lx+0x%lx eh=0x%lx+0x%lx",
(unsigned long)object.mapbase, (unsigned long)object.mapsize,
(unsigned long)object.eh_frame,
(unsigned long)object.eh_frame_size);
const bool install_hook_enabled = sm_shellcore_install_bridge_enabled();
const size_t hook_count = install_hook_enabled
? SHELLCORE_MAX_HOOK_COUNT
: SHELLCORE_BASE_HOOK_COUNT;
uintptr_t targets[SHELLCORE_MAX_HOOK_COUNT] = {
remote.targets[SM_SHELLCORE_TARGET_LAUNCH_APP],
remote.targets[SM_SHELLCORE_TARGET_INSTALL_ALL],
};
sm_shellcore_target_t target_ids[SHELLCORE_MAX_HOOK_COUNT] = {
SM_SHELLCORE_TARGET_LAUNCH_APP,
SM_SHELLCORE_TARGET_INSTALL_ALL,
};
for (size_t i = 0; i < hook_count; ++i) {
uint8_t patch_size = remote.offsets->targets[target_ids[i]].patch_size;
if (patch_size < ABSOLUTE_JUMP_SIZE ||
patch_size > MAX_HOOK_PROLOGUE_SIZE ||
!sm_remote_process_read(pid, targets[i], g_hooks.original[i],
patch_size)) {
goto done;
}
static const uint8_t function_prologue[] = {0x55, 0x48, 0x89, 0xe5};
if (memcmp(g_hooks.original[i], function_prologue,
sizeof(function_prologue)) != 0) {
log_debug(" [SHELLCORE] unexpected prologue: %s at 0x%lx",
sm_shellcore_target_name(target_ids[i]),
(unsigned long)targets[i]);
goto done;
}
g_hooks.original_size[i] = patch_size;
}
size_t cursor = blob_size;
uintptr_t launch_trampoline = 0;
uintptr_t install_all_trampoline = 0;
if (!append_trampoline(bridge, bridge_capacity, &cursor, g_hooks.original[0],
g_hooks.original_size[0],
targets[0] + g_hooks.original_size[0], object.eh_frame,
&launch_trampoline)) {
goto done;
}
set_bridge_pointer(bridge, sm_shellcore_bridge_launch_trampoline,
launch_trampoline);
if (install_hook_enabled) {
if (!append_trampoline(bridge, bridge_capacity, &cursor,
g_hooks.original[1], g_hooks.original_size[1],
targets[1] + g_hooks.original_size[1],
object.eh_frame, &install_all_trampoline)) {
goto done;
}
set_bridge_pointer(bridge, sm_shellcore_bridge_install_all_trampoline,
install_all_trampoline);
set_bridge_pointer(
bridge, sm_shellcore_bridge_install_title_dir,
remote.targets[SM_SHELLCORE_TARGET_INSTALL_TITLE_DIR]);
}
if (!resolve_bridge_imports(pid, bridge))
goto done;
if (kernel_mprotect(pid, (intptr_t)object.mapbase, object.mapsize,
PROT_READ | PROT_WRITE | PROT_EXEC) != 0 ||
!sm_remote_process_write(pid, object.eh_frame, bridge, cursor) ||
!verify_remote_bytes(pid, object.eh_frame, bridge, cursor)) {
log_debug(" [SHELLCORE] bridge write verification failed");
goto done;
}
uintptr_t launch_hook =
object.eh_frame + (uintptr_t)(sm_shellcore_bridge_launch_hook -
sm_shellcore_bridge_blob_start);
uintptr_t install_all_hook =
object.eh_frame +
(uintptr_t)(sm_shellcore_bridge_install_all_hook -
sm_shellcore_bridge_blob_start);
if (!patch_remote_jump(pid, targets[0], launch_hook,
g_hooks.original_size[0]) ||
(install_hook_enabled &&
!patch_remote_jump(pid, targets[1], install_all_hook,
g_hooks.original_size[1]))) {
goto rollback;
}
g_hooks.remote = remote;
g_hooks.bridge_address = object.eh_frame;
g_hooks.hook_count = hook_count;
memcpy(g_hooks.target_ids, target_ids,
hook_count * sizeof(g_hooks.target_ids[0]));
g_hooks.installed = true;
ok = true;
log_debug(" [SHELLCORE] hooks installed: fw=%s pid=%ld lifecycle=1 "
"install=%d",
remote.offsets->name, (long)pid, install_hook_enabled ? 1 : 0);
goto done;
rollback:
for (size_t i = 0; i < hook_count; ++i) {
(void)sm_remote_process_write(pid, targets[i], g_hooks.original[i],
g_hooks.original_size[i]);
}
done:
free(bridge);
(void)kill(pid, SIGCONT);
return ok;
}
bool sm_shellcore_hooks_start(void) {
pthread_mutex_lock(&g_install_mutex);
if (g_hooks.installed)
goto installed;
memset(&g_hooks, 0, sizeof(g_hooks));
pid_t pid = find_shellcore_pid();
if (pid <= 0) {
log_debug(" [SHELLCORE] process not found");
pthread_mutex_unlock(&g_install_mutex);
return false;
}
bool ok = install_hooks_for_pid(pid);
pthread_mutex_unlock(&g_install_mutex);
return ok;
installed:
pthread_mutex_unlock(&g_install_mutex);
return true;
}
void sm_shellcore_hooks_stop(void) {
pthread_mutex_lock(&g_install_mutex);
if (!g_hooks.installed) {
pthread_mutex_unlock(&g_install_mutex);
return;
}
pid_t pid = g_hooks.remote.pid;
if (kill(pid, SIGSTOP) == 0) {
for (size_t i = 0; i < g_hooks.hook_count; ++i) {
sm_shellcore_target_t target = g_hooks.target_ids[i];
(void)sm_remote_process_write(pid, g_hooks.remote.targets[target],
g_hooks.original[i],
g_hooks.original_size[i]);
}
(void)kill(pid, SIGCONT);
}
memset(&g_hooks, 0, sizeof(g_hooks));
pthread_mutex_unlock(&g_install_mutex);
}
bool sm_shellcore_install_title_dir(const char *title_id,
const char *install_dir,
int *result_out) {
if (!title_id || !install_dir || !result_out)
return false;
size_t title_len = strnlen(title_id, MAX_TITLE_ID);
size_t dir_len = strnlen(install_dir, MAX_PATH);
if (title_len == 0 || title_len >= MAX_TITLE_ID || dir_len == 0 ||
dir_len >= MAX_PATH) {
return false;
}
pthread_mutex_lock(&g_install_mutex);
if (!g_hooks.installed ||
g_hooks.hook_count < SHELLCORE_MAX_HOOK_COUNT) {
pthread_mutex_unlock(&g_install_mutex);
return false;
}
char remote_title[MAX_TITLE_ID] = {0};
char remote_dir[MAX_PATH] = {0};
memcpy(remote_title, title_id, title_len);
memcpy(remote_dir, install_dir, dir_len);
pid_t pid = g_hooks.remote.pid;
uint8_t armed = 1;
bool written =
sm_remote_process_write(pid,
remote_bridge_symbol(
sm_shellcore_bridge_install_title_id),
remote_title, sizeof(remote_title)) &&
sm_remote_process_write(
pid, remote_bridge_symbol(sm_shellcore_bridge_install_dir),
remote_dir, sizeof(remote_dir)) &&
sm_remote_process_write(
pid, remote_bridge_symbol(sm_shellcore_bridge_install_armed),
&armed, sizeof(armed));
if (!written) {
armed = 0;
(void)sm_remote_process_write(
pid, remote_bridge_symbol(sm_shellcore_bridge_install_armed), &armed,
sizeof(armed));
pthread_mutex_unlock(&g_install_mutex);
return false;
}
int result = sceAppInstUtilAppInstallAll(NULL);
uint8_t remaining = 1;
bool read_back = sm_remote_process_read(
pid, remote_bridge_symbol(sm_shellcore_bridge_install_armed), &remaining,
sizeof(remaining));
bool consumed = read_back && remaining == 0;
if (!consumed) {
armed = 0;
(void)sm_remote_process_write(
pid, remote_bridge_symbol(sm_shellcore_bridge_install_armed), &armed,
sizeof(armed));
}
pthread_mutex_unlock(&g_install_mutex);
if (!consumed) {
log_debug(" [SHELLCORE] AppInstallAll trigger was not consumed: %s",
title_id);
return false;
}
log_debug(" [SHELLCORE] AppInstallAll bridge consumed: %s result=0x%08x",
title_id, (uint32_t)result);
*result_out = result;
return true;
}
+33
View File
@@ -0,0 +1,33 @@
#include "sm_shellcore_offsets.h"
#include <stddef.h>
static const sm_shellcore_firmware_offsets_t g_shellcore_offsets[] = {
#include "sm_shellcore_offsets.inc"
};
const sm_shellcore_firmware_offsets_t *
sm_shellcore_offsets_for_firmware(uint32_t raw_firmware) {
uint16_t firmware = (uint16_t)((raw_firmware >> 16) & 0xffffu);
for (size_t i = 0;
i < sizeof(g_shellcore_offsets) / sizeof(g_shellcore_offsets[0]); ++i) {
if (g_shellcore_offsets[i].firmware == firmware)
return &g_shellcore_offsets[i];
}
return NULL;
}
const char *sm_shellcore_target_name(sm_shellcore_target_t target) {
switch (target) {
case SM_SHELLCORE_TARGET_LAUNCH_APP:
return "launchApp";
case SM_SHELLCORE_TARGET_UNMOUNT_WORKSPACE:
return "unmountWorkspace";
case SM_SHELLCORE_TARGET_INSTALL_TITLE_DIR:
return "AppInstallTitleDirMain";
case SM_SHELLCORE_TARGET_INSTALL_ALL:
return "AppInstallAll";
default:
return "unknown";
}
}
+285
View File
@@ -0,0 +1,285 @@
{
.firmware = 0x0100u,
.name = "1.00",
.targets = {{.offset = 0x3465a0u, .patch_size = 12u}, {.offset = 0x3479b0u, .patch_size = 12u}, {.offset = 0x1adc50u, .patch_size = 12u}, {.offset = 0xa05c40u, .patch_size = 14u}},
},
{
.firmware = 0x1000u,
.name = "10.00",
.targets = {{.offset = 0x5ca1a0u, .patch_size = 12u}, {.offset = 0x605140u, .patch_size = 13u}, {.offset = 0x2b6700u, .patch_size = 12u}, {.offset = 0x17d1f00u, .patch_size = 16u}},
},
{
.firmware = 0x1001u,
.name = "10.01",
.targets = {{.offset = 0x5ca1a0u, .patch_size = 12u}, {.offset = 0x605140u, .patch_size = 13u}, {.offset = 0x2b6700u, .patch_size = 12u}, {.offset = 0x17d1f00u, .patch_size = 16u}},
},
{
.firmware = 0x0101u,
.name = "1.01",
.targets = {{.offset = 0x3465a0u, .patch_size = 12u}, {.offset = 0x3479b0u, .patch_size = 12u}, {.offset = 0x1adc50u, .patch_size = 12u}, {.offset = 0xa05c40u, .patch_size = 14u}},
},
{
.firmware = 0x0102u,
.name = "1.02",
.targets = {{.offset = 0x3465a0u, .patch_size = 12u}, {.offset = 0x3479b0u, .patch_size = 12u}, {.offset = 0x1adc50u, .patch_size = 12u}, {.offset = 0xa05c40u, .patch_size = 14u}},
},
{
.firmware = 0x1020u,
.name = "10.20",
.targets = {{.offset = 0x5ca1a0u, .patch_size = 12u}, {.offset = 0x605140u, .patch_size = 13u}, {.offset = 0x2b6700u, .patch_size = 12u}, {.offset = 0x17d61c0u, .patch_size = 16u}},
},
{
.firmware = 0x1040u,
.name = "10.40",
.targets = {{.offset = 0x5ca0f0u, .patch_size = 12u}, {.offset = 0x605090u, .patch_size = 13u}, {.offset = 0x2b6760u, .patch_size = 12u}, {.offset = 0x17d61e0u, .patch_size = 16u}},
},
{
.firmware = 0x1060u,
.name = "10.60",
.targets = {{.offset = 0x5cb980u, .patch_size = 12u}, {.offset = 0x606920u, .patch_size = 13u}, {.offset = 0x2b80c0u, .patch_size = 12u}, {.offset = 0x17d7a60u, .patch_size = 16u}},
},
{
.firmware = 0x1100u,
.name = "11.00",
.targets = {{.offset = 0x62a940u, .patch_size = 12u}, {.offset = 0x6668d0u, .patch_size = 15u}, {.offset = 0x2bd880u, .patch_size = 12u}, {.offset = 0x2c4e60u, .patch_size = 15u}},
},
{
.firmware = 0x0112u,
.name = "1.12",
.targets = {{.offset = 0x3469b0u, .patch_size = 12u}, {.offset = 0x347dc0u, .patch_size = 12u}, {.offset = 0x1adc30u, .patch_size = 12u}, {.offset = 0xa063a0u, .patch_size = 14u}},
},
{
.firmware = 0x1120u,
.name = "11.20",
.targets = {{.offset = 0x62aab0u, .patch_size = 12u}, {.offset = 0x666a40u, .patch_size = 15u}, {.offset = 0x2bd930u, .patch_size = 12u}, {.offset = 0x2c4f10u, .patch_size = 15u}},
},
{
.firmware = 0x0114u,
.name = "1.14",
.targets = {{.offset = 0x3469b0u, .patch_size = 12u}, {.offset = 0x347dc0u, .patch_size = 12u}, {.offset = 0x1adc30u, .patch_size = 12u}, {.offset = 0xa06660u, .patch_size = 14u}},
},
{
.firmware = 0x1140u,
.name = "11.40",
.targets = {{.offset = 0x62ba90u, .patch_size = 12u}, {.offset = 0x667a20u, .patch_size = 15u}, {.offset = 0x2be910u, .patch_size = 12u}, {.offset = 0x2c5ef0u, .patch_size = 15u}},
},
{
.firmware = 0x1160u,
.name = "11.60",
.targets = {{.offset = 0x633500u, .patch_size = 12u}, {.offset = 0x66f490u, .patch_size = 15u}, {.offset = 0x2c2c10u, .patch_size = 12u}, {.offset = 0x2ca1f0u, .patch_size = 15u}},
},
{
.firmware = 0x1200u,
.name = "12.00",
.targets = {{.offset = 0x647410u, .patch_size = 12u}, {.offset = 0x683720u, .patch_size = 15u}, {.offset = 0x2e0050u, .patch_size = 12u}, {.offset = 0x2dd100u, .patch_size = 12u}},
},
{
.firmware = 0x1202u,
.name = "12.02",
.targets = {{.offset = 0x647410u, .patch_size = 12u}, {.offset = 0x683720u, .patch_size = 15u}, {.offset = 0x2e0050u, .patch_size = 12u}, {.offset = 0x2dd100u, .patch_size = 12u}},
},
{
.firmware = 0x1220u,
.name = "12.20",
.targets = {{.offset = 0x647410u, .patch_size = 12u}, {.offset = 0x683720u, .patch_size = 15u}, {.offset = 0x2e0050u, .patch_size = 12u}, {.offset = 0x2dd100u, .patch_size = 12u}},
},
{
.firmware = 0x1240u,
.name = "12.40",
.targets = {{.offset = 0x647410u, .patch_size = 12u}, {.offset = 0x683720u, .patch_size = 15u}, {.offset = 0x2e0050u, .patch_size = 12u}, {.offset = 0x2dd100u, .patch_size = 12u}},
},
{
.firmware = 0x1260u,
.name = "12.60",
.targets = {{.offset = 0x648660u, .patch_size = 12u}, {.offset = 0x684970u, .patch_size = 15u}, {.offset = 0x2e0320u, .patch_size = 12u}, {.offset = 0x2dd3d0u, .patch_size = 12u}},
},
{
.firmware = 0x1270u,
.name = "12.70",
.targets = {{.offset = 0x648660u, .patch_size = 12u}, {.offset = 0x684970u, .patch_size = 15u}, {.offset = 0x2e0320u, .patch_size = 12u}, {.offset = 0x2dd3d0u, .patch_size = 12u}},
},
{
.firmware = 0x0200u,
.name = "2.00",
.targets = {{.offset = 0x3a4660u, .patch_size = 12u}, {.offset = 0x3a5aa0u, .patch_size = 12u}, {.offset = 0x1df8f0u, .patch_size = 12u}, {.offset = 0xaf3740u, .patch_size = 14u}},
},
{
.firmware = 0x0220u,
.name = "2.20",
.targets = {{.offset = 0x3a4930u, .patch_size = 12u}, {.offset = 0x3a5d70u, .patch_size = 12u}, {.offset = 0x1dfb80u, .patch_size = 12u}, {.offset = 0xaf3aa0u, .patch_size = 14u}},
},
{
.firmware = 0x0225u,
.name = "2.25",
.targets = {{.offset = 0x3a4e80u, .patch_size = 12u}, {.offset = 0x3a62c0u, .patch_size = 12u}, {.offset = 0x1dfb80u, .patch_size = 12u}, {.offset = 0xaf3fe0u, .patch_size = 14u}},
},
{
.firmware = 0x0226u,
.name = "2.26",
.targets = {{.offset = 0x3a65f0u, .patch_size = 12u}, {.offset = 0x3a7a30u, .patch_size = 12u}, {.offset = 0x1e0d50u, .patch_size = 12u}, {.offset = 0xaf57a0u, .patch_size = 14u}},
},
{
.firmware = 0x0230u,
.name = "2.30",
.targets = {{.offset = 0x3a6d90u, .patch_size = 12u}, {.offset = 0x3a81d0u, .patch_size = 12u}, {.offset = 0x1e0f00u, .patch_size = 12u}, {.offset = 0xaf6360u, .patch_size = 14u}},
},
{
.firmware = 0x0250u,
.name = "2.50",
.targets = {{.offset = 0x3a8140u, .patch_size = 12u}, {.offset = 0x3a9580u, .patch_size = 12u}, {.offset = 0x1e0ca0u, .patch_size = 12u}, {.offset = 0xaf7700u, .patch_size = 14u}},
},
{
.firmware = 0x0270u,
.name = "2.70",
.targets = {{.offset = 0x3a8140u, .patch_size = 12u}, {.offset = 0x3a9580u, .patch_size = 12u}, {.offset = 0x1e0ca0u, .patch_size = 12u}, {.offset = 0xaf7700u, .patch_size = 14u}},
},
{
.firmware = 0x0300u,
.name = "3.00",
.targets = {{.offset = 0x3f1960u, .patch_size = 12u}, {.offset = 0x3f2ef0u, .patch_size = 12u}, {.offset = 0x2138e0u, .patch_size = 12u}, {.offset = 0x212ab0u, .patch_size = 12u}},
},
{
.firmware = 0x0310u,
.name = "3.10",
.targets = {{.offset = 0x3f19a0u, .patch_size = 12u}, {.offset = 0x3f2f30u, .patch_size = 12u}, {.offset = 0x213920u, .patch_size = 12u}, {.offset = 0x212af0u, .patch_size = 12u}},
},
{
.firmware = 0x0320u,
.name = "3.20",
.targets = {{.offset = 0x3f1a50u, .patch_size = 12u}, {.offset = 0x3f2fe0u, .patch_size = 12u}, {.offset = 0x2139d0u, .patch_size = 12u}, {.offset = 0x212ba0u, .patch_size = 12u}},
},
{
.firmware = 0x0321u,
.name = "3.21",
.targets = {{.offset = 0x3f1a50u, .patch_size = 12u}, {.offset = 0x3f2fe0u, .patch_size = 12u}, {.offset = 0x2139d0u, .patch_size = 12u}, {.offset = 0x212ba0u, .patch_size = 12u}},
},
{
.firmware = 0x0400u,
.name = "4.00",
.targets = {{.offset = 0x432630u, .patch_size = 12u}, {.offset = 0x433d90u, .patch_size = 12u}, {.offset = 0x226c20u, .patch_size = 12u}, {.offset = 0x225e40u, .patch_size = 12u}},
},
{
.firmware = 0x0403u,
.name = "4.03",
.targets = {{.offset = 0x432630u, .patch_size = 12u}, {.offset = 0x433d90u, .patch_size = 12u}, {.offset = 0x226c20u, .patch_size = 12u}, {.offset = 0x225e40u, .patch_size = 12u}},
},
{
.firmware = 0x0450u,
.name = "4.50",
.targets = {{.offset = 0x432d80u, .patch_size = 12u}, {.offset = 0x4344e0u, .patch_size = 12u}, {.offset = 0x227350u, .patch_size = 12u}, {.offset = 0x226570u, .patch_size = 12u}},
},
{
.firmware = 0x0451u,
.name = "4.51",
.targets = {{.offset = 0x432d80u, .patch_size = 12u}, {.offset = 0x4344e0u, .patch_size = 12u}, {.offset = 0x227350u, .patch_size = 12u}, {.offset = 0x226570u, .patch_size = 12u}},
},
{
.firmware = 0x0500u,
.name = "5.00",
.targets = {{.offset = 0x498950u, .patch_size = 12u}, {.offset = 0x49a0a0u, .patch_size = 12u}, {.offset = 0x2556a0u, .patch_size = 12u}, {.offset = 0x254900u, .patch_size = 12u}},
},
{
.firmware = 0x0502u,
.name = "5.02",
.targets = {{.offset = 0x498940u, .patch_size = 12u}, {.offset = 0x49a090u, .patch_size = 12u}, {.offset = 0x2556a0u, .patch_size = 12u}, {.offset = 0x254900u, .patch_size = 12u}},
},
{
.firmware = 0x0510u,
.name = "5.10",
.targets = {{.offset = 0x4998f0u, .patch_size = 12u}, {.offset = 0x49b650u, .patch_size = 12u}, {.offset = 0x256570u, .patch_size = 12u}, {.offset = 0x2557d0u, .patch_size = 12u}},
},
{
.firmware = 0x0550u,
.name = "5.50",
.targets = {{.offset = 0x4998f0u, .patch_size = 12u}, {.offset = 0x49b650u, .patch_size = 12u}, {.offset = 0x256570u, .patch_size = 12u}, {.offset = 0x2557d0u, .patch_size = 12u}},
},
{
.firmware = 0x0600u,
.name = "6.00",
.targets = {{.offset = 0x4c7380u, .patch_size = 12u}, {.offset = 0x4fdf70u, .patch_size = 15u}, {.offset = 0x26b9b0u, .patch_size = 12u}, {.offset = 0x271bb0u, .patch_size = 12u}},
},
{
.firmware = 0x0602u,
.name = "6.02",
.targets = {{.offset = 0x4c7380u, .patch_size = 12u}, {.offset = 0x4fdf70u, .patch_size = 15u}, {.offset = 0x26b9b0u, .patch_size = 12u}, {.offset = 0x271bb0u, .patch_size = 12u}},
},
{
.firmware = 0x0650u,
.name = "6.50",
.targets = {{.offset = 0x4c73f0u, .patch_size = 12u}, {.offset = 0x4fdfe0u, .patch_size = 15u}, {.offset = 0x26ba20u, .patch_size = 12u}, {.offset = 0x271c20u, .patch_size = 12u}},
},
{
.firmware = 0x0700u,
.name = "7.00",
.targets = {{.offset = 0x568920u, .patch_size = 12u}, {.offset = 0x5a85d0u, .patch_size = 15u}, {.offset = 0x2852e0u, .patch_size = 12u}, {.offset = 0x28b690u, .patch_size = 12u}},
},
{
.firmware = 0x0701u,
.name = "7.01",
.targets = {{.offset = 0x568920u, .patch_size = 12u}, {.offset = 0x5a85d0u, .patch_size = 15u}, {.offset = 0x2852e0u, .patch_size = 12u}, {.offset = 0x28b690u, .patch_size = 12u}},
},
{
.firmware = 0x0720u,
.name = "7.20",
.targets = {{.offset = 0x568940u, .patch_size = 12u}, {.offset = 0x5a85f0u, .patch_size = 15u}, {.offset = 0x2852e0u, .patch_size = 12u}, {.offset = 0x28b690u, .patch_size = 12u}},
},
{
.firmware = 0x0740u,
.name = "7.40",
.targets = {{.offset = 0x56d430u, .patch_size = 12u}, {.offset = 0x5ad0e0u, .patch_size = 15u}, {.offset = 0x2896b0u, .patch_size = 12u}, {.offset = 0x28fa60u, .patch_size = 12u}},
},
{
.firmware = 0x0760u,
.name = "7.60",
.targets = {{.offset = 0x56d430u, .patch_size = 12u}, {.offset = 0x5ad0e0u, .patch_size = 15u}, {.offset = 0x2896b0u, .patch_size = 12u}, {.offset = 0x28fa60u, .patch_size = 12u}},
},
{
.firmware = 0x0761u,
.name = "7.61",
.targets = {{.offset = 0x56d430u, .patch_size = 12u}, {.offset = 0x5ad0e0u, .patch_size = 15u}, {.offset = 0x2896b0u, .patch_size = 12u}, {.offset = 0x28fa60u, .patch_size = 12u}},
},
{
.firmware = 0x0800u,
.name = "8.00",
.targets = {{.offset = 0x597eb0u, .patch_size = 12u}, {.offset = 0x5d3060u, .patch_size = 13u}, {.offset = 0x29a730u, .patch_size = 12u}, {.offset = 0x2a12f0u, .patch_size = 12u}},
},
{
.firmware = 0x0820u,
.name = "8.20",
.targets = {{.offset = 0x598f80u, .patch_size = 12u}, {.offset = 0x5d4130u, .patch_size = 13u}, {.offset = 0x29a9a0u, .patch_size = 12u}, {.offset = 0x2a1560u, .patch_size = 12u}},
},
{
.firmware = 0x0840u,
.name = "8.40",
.targets = {{.offset = 0x598f80u, .patch_size = 12u}, {.offset = 0x5d4130u, .patch_size = 13u}, {.offset = 0x29a9a0u, .patch_size = 12u}, {.offset = 0x2a1560u, .patch_size = 12u}},
},
{
.firmware = 0x0860u,
.name = "8.60",
.targets = {{.offset = 0x598d60u, .patch_size = 12u}, {.offset = 0x5d3f10u, .patch_size = 13u}, {.offset = 0x29a750u, .patch_size = 12u}, {.offset = 0x2a1310u, .patch_size = 12u}},
},
{
.firmware = 0x0900u,
.name = "9.00",
.targets = {{.offset = 0x5cc0a0u, .patch_size = 12u}, {.offset = 0x6088e0u, .patch_size = 13u}, {.offset = 0x2b74d0u, .patch_size = 12u}, {.offset = 0x2be0e0u, .patch_size = 12u}},
},
{
.firmware = 0x0905u,
.name = "9.05",
.targets = {{.offset = 0x5cc0a0u, .patch_size = 12u}, {.offset = 0x6088e0u, .patch_size = 13u}, {.offset = 0x2b74d0u, .patch_size = 12u}, {.offset = 0x2be0e0u, .patch_size = 12u}},
},
{
.firmware = 0x0920u,
.name = "9.20",
.targets = {{.offset = 0x5cbdc0u, .patch_size = 12u}, {.offset = 0x608600u, .patch_size = 13u}, {.offset = 0x2b74d0u, .patch_size = 12u}, {.offset = 0x2be0e0u, .patch_size = 12u}},
},
{
.firmware = 0x0940u,
.name = "9.40",
.targets = {{.offset = 0x5cc490u, .patch_size = 12u}, {.offset = 0x608cd0u, .patch_size = 13u}, {.offset = 0x2b7ba0u, .patch_size = 12u}, {.offset = 0x2be7b0u, .patch_size = 12u}},
},
{
.firmware = 0x0960u,
.name = "9.60",
.targets = {{.offset = 0x5cc500u, .patch_size = 12u}, {.offset = 0x608d40u, .patch_size = 13u}, {.offset = 0x2b7c10u, .patch_size = 12u}, {.offset = 0x2be820u, .patch_size = 12u}},
}
+144
View File
@@ -0,0 +1,144 @@
#include "sm_platform.h"
#include <ps5/mdbg.h>
#include "sm_log.h"
#include "sm_shellcore_remote.h"
#define X86_PAGE_FRAME 0x000ffffffffff000ull
#define X86_PAGE_VALID 0x001ull
#define X86_PAGE_LARGE 0x080ull
static uintptr_t resolve_vmspace_pmap(uintptr_t vmspace) {
uint32_t version = kernel_get_fw_version() >> 16;
if (version >= 0x0100u && version <= 0x0102u)
return vmspace + 0x2c0u;
if (version >= 0x0105u && version <= 0x0550u)
return vmspace + 0x2e0u;
if (version >= 0x0600u && version <= 0x1270u)
return vmspace + 0x2e8u;
return 0;
}
static bool resolve_process_paging(pid_t pid, uint64_t *cr3_out,
uint64_t *dmap_out) {
uintptr_t process = (uintptr_t)kernel_get_proc(pid);
if (!process)
return false;
uintptr_t vmspace = (uintptr_t)kernel_getlong(
process + (uintptr_t)KERNEL_OFFSET_PROC_P_VMSPACE);
if (!vmspace)
return false;
uintptr_t pmap = resolve_vmspace_pmap(vmspace);
if (!pmap)
return false;
uint64_t paging[2] = {0};
if (kernel_copyout((intptr_t)(pmap + 32u), paging, sizeof(paging)) != 0)
return false;
if (!paging[0] || !paging[1])
return false;
*cr3_out = paging[1];
*dmap_out = paging[0] - paging[1];
return true;
}
static uint64_t virtual_to_physical(uint64_t address, uint64_t dmap,
uint64_t page_map,
uint64_t *physical_limit_out) {
page_map &= X86_PAGE_FRAME;
for (int shift = 39; shift >= 12; shift -= 9) {
uint64_t index = (address >> shift) & 0x1ffu;
page_map = kernel_getlong((intptr_t)(dmap + page_map + index * 8u));
if ((page_map & X86_PAGE_VALID) == 0)
return UINT64_MAX;
if ((page_map & X86_PAGE_LARGE) != 0 || shift == 12) {
page_map &= (1ull << 52) - (1ull << shift);
page_map |= address & ((1ull << shift) - 1ull);
if (physical_limit_out)
*physical_limit_out = (page_map | ((1ull << shift) - 1ull)) + 1ull;
return page_map;
}
page_map &= X86_PAGE_FRAME;
}
return UINT64_MAX;
}
bool sm_remote_process_read(pid_t pid, uintptr_t address, void *buffer,
size_t size) {
if (!buffer || size == 0)
return false;
return mdbg_copyout(pid, (intptr_t)address, buffer, size) == 0;
}
bool sm_remote_process_write(pid_t pid, uintptr_t address, const void *buffer,
size_t size) {
if (!buffer || size == 0)
return false;
if ((kernel_get_fw_version() >> 16) <= 0x0820u)
return mdbg_copyin(pid, buffer, (intptr_t)address, size) == 0;
void *probe = malloc(size);
if (!probe)
return false;
bool faulted = sm_remote_process_read(pid, address, probe, size);
free(probe);
if (!faulted)
return false;
uint64_t cr3 = 0;
uint64_t dmap = 0;
if (!resolve_process_paging(pid, &cr3, &dmap))
return false;
const uint8_t *source = (const uint8_t *)buffer;
while (size != 0) {
uint64_t physical_limit = 0;
uint64_t physical =
virtual_to_physical(address, dmap, cr3, &physical_limit);
if (physical == UINT64_MAX || physical_limit <= physical)
return false;
size_t chunk = (size_t)(physical_limit - physical);
if (chunk > size)
chunk = size;
if (kernel_copyin(source, (intptr_t)(dmap + physical), chunk) != 0)
return false;
address += chunk;
source += chunk;
size -= chunk;
}
return true;
}
bool sm_shellcore_remote_resolve(pid_t pid, sm_shellcore_remote_t *remote_out) {
if (pid <= 0 || !remote_out)
return false;
memset(remote_out, 0, sizeof(*remote_out));
const sm_shellcore_firmware_offsets_t *firmware =
sm_shellcore_offsets_for_firmware(kernel_get_fw_version());
if (!firmware) {
log_debug(" [SHELLCORE] unsupported firmware: 0x%08x",
kernel_get_fw_version());
return false;
}
uintptr_t image_base = (uintptr_t)kernel_dynlib_mapbase_addr(pid, 0);
if (!image_base) {
log_debug(" [SHELLCORE] failed to resolve image base for pid=%ld",
(long)pid);
return false;
}
for (int target = 0; target < SM_SHELLCORE_TARGET_COUNT; ++target) {
remote_out->targets[target] = image_base + firmware->targets[target].offset;
}
remote_out->pid = pid;
remote_out->image_base = image_base;
remote_out->offsets = firmware;
log_debug(" [SHELLCORE] resolved fw=%s pid=%ld base=0x%lx", firmware->name,
(long)pid, (unsigned long)image_base);
return true;
}
+395
View File
@@ -0,0 +1,395 @@
#include "sm_platform.h"
#include <pthread.h>
#include <sys/socket.h>
#include <sys/un.h>
#include "sm_filesystem.h"
#include "sm_image.h"
#include "sm_log.h"
#include "sm_path_utils.h"
#include "sm_runtime.h"
#include "sm_scan.h"
#include "sm_scanner.h"
#include "sm_shellcore_protocol.h"
#include "sm_shellcore_service.h"
_Static_assert(sizeof(sm_shellcore_request_t) == 40,
"unexpected ShellCore request size");
_Static_assert(sizeof(sm_shellcore_response_t) == 4,
"unexpected ShellCore response size");
typedef struct {
pthread_t thread;
pthread_mutex_t mutex;
bool started;
bool stop_requested;
int listen_fd;
// Owner of the prepared runtime mount, not the lifecycle active-game state.
char prepared_title_id[MAX_TITLE_ID];
} shellcore_service_state_t;
static shellcore_service_state_t g_service = {
.mutex = PTHREAD_MUTEX_INITIALIZER,
.listen_fd = -1,
};
static void publish_prepared_title(const char *title_id) {
bool changed = false;
pthread_mutex_lock(&g_service.mutex);
if (strcmp(g_service.prepared_title_id, title_id) != 0) {
(void)strlcpy(g_service.prepared_title_id, title_id,
sizeof(g_service.prepared_title_id));
changed = true;
}
pthread_mutex_unlock(&g_service.mutex);
if (changed)
sm_scanner_wake();
}
static void clear_prepared_title(const char *title_id) {
bool changed = false;
pthread_mutex_lock(&g_service.mutex);
if (strcmp(g_service.prepared_title_id, title_id) == 0) {
g_service.prepared_title_id[0] = '\0';
changed = true;
}
pthread_mutex_unlock(&g_service.mutex);
if (changed)
sm_scanner_wake();
}
static bool read_full(int fd, void *buffer, size_t size) {
uint8_t *bytes = (uint8_t *)buffer;
while (size != 0) {
ssize_t count = recv(fd, bytes, size, 0);
if (count == 0)
return false;
if (count < 0) {
if (errno == EINTR)
continue;
return false;
}
bytes += (size_t)count;
size -= (size_t)count;
}
return true;
}
static bool write_full(int fd, const void *buffer, size_t size) {
const uint8_t *bytes = (const uint8_t *)buffer;
while (size != 0) {
ssize_t count = send(fd, bytes, size, 0);
if (count == 0)
return false;
if (count < 0) {
if (errno == EINTR)
continue;
return false;
}
bytes += (size_t)count;
size -= (size_t)count;
}
return true;
}
static bool find_required_image_layer(const char *root,
const char *runtime_source,
unsigned int depth,
char image_path[MAX_PATH]) {
DIR *dir = opendir(root);
if (!dir)
return false;
bool found = false;
struct dirent *entry;
while (!found && (entry = readdir(dir)) != NULL) {
if (entry->d_name[0] == '.')
continue;
char path[MAX_PATH];
int written = snprintf(path, sizeof(path), "%s/%s", root, entry->d_name);
if (written < 0 || (size_t)written >= sizeof(path))
continue;
struct stat st;
if (stat(path, &st) != 0)
continue;
if (S_ISREG(st.st_mode) &&
is_supported_image_file_path(path, entry->d_name)) {
char mount_point[MAX_PATH];
get_image_mount_point_for_source(path, mount_point);
if (path_matches_root_or_child(runtime_source, mount_point)) {
(void)strlcpy(image_path, path, MAX_PATH);
found = true;
}
continue;
}
if (depth > 0 && S_ISDIR(st.st_mode))
found = find_required_image_layer(path, runtime_source, depth - 1u,
image_path);
}
closedir(dir);
return found;
}
static bool repair_image_chain_for_runtime_source(
const char *title_id, const char *runtime_source, const char *eboot_path,
char image_chain[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH], size_t *image_count) {
while (*image_count < MAX_IMAGE_CHAIN_DEPTH) {
char mounted_root[MAX_PATH];
get_image_mount_point_for_source(image_chain[*image_count - 1u],
mounted_root);
char image_path[MAX_PATH];
if (!find_required_image_layer(mounted_root, runtime_source,
MAX_SCAN_DEPTH, image_path)) {
return false;
}
const char *name = strrchr(image_path, '/');
name = name ? name + 1 : image_path;
bool unstable = false;
if (!maybe_mount_image_file(image_path, name, &unstable))
return false;
(void)strlcpy(image_chain[*image_count], image_path, MAX_PATH);
(*image_count)++;
if (!write_mount_image_chain(title_id, image_chain, *image_count)) {
log_debug(" [SHELLCORE] failed to persist repaired image chain: %s",
title_id);
} else {
log_debug(" [SHELLCORE] image chain repaired: title=%s layers=%zu",
title_id, *image_count);
}
if (path_exists(eboot_path)) {
return true;
}
}
return false;
}
static bool prepare_image_source(const char *title_id,
const char *runtime_source) {
char eboot_path[MAX_PATH];
int written = snprintf(eboot_path, sizeof(eboot_path), "%s/eboot.bin",
runtime_source);
if (written < 0 || (size_t)written >= sizeof(eboot_path))
return false;
if (path_exists(eboot_path))
return true;
if (!is_under_image_mount_base(runtime_source))
return false;
char image_chain[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH];
size_t image_count = 0;
if (!read_mount_image_chain(title_id, image_chain, &image_count))
return false;
// The USB filesystem can become available shortly after WORKING. Avoid
// repeatedly entering the image attach path until its outer layer exists.
if (!path_exists(image_chain[0]))
return false;
for (size_t i = 0; i < image_count; ++i) {
const char *name = strrchr(image_chain[i], '/');
name = name ? name + 1 : image_chain[i];
bool unstable = false;
if (!maybe_mount_image_file(image_chain[i], name, &unstable)) {
log_debug(" [SHELLCORE] image chain mount failed: title=%s layer=%zu "
"path=%s",
title_id, i, image_chain[i]);
return false;
}
}
if (path_exists(eboot_path))
return true;
if (repair_image_chain_for_runtime_source(title_id, runtime_source,
eboot_path, image_chain,
&image_count))
return true;
log_debug(" [SHELLCORE] image chain ready but eboot missing: %s",
eboot_path);
return false;
}
static bool prepare_title_runtime(const char *title_id,
const char *source_path) {
if (!prepare_image_source(title_id, source_path))
return false;
runtime_mount_state_lock();
bool ready = !runtime_sleep_mode_active() &&
mount_title_nullfs(title_id, source_path);
if (ready)
ready = mount_backport_overlay_for_title(source_path, title_id, NULL, NULL);
runtime_mount_state_unlock();
return ready;
}
bool sm_shellcore_ensure_title_runtime(const char *title_id) {
if (!title_id || title_id[0] == '\0' || runtime_sleep_mode_active())
return false;
char source_path[MAX_PATH];
if (!read_mount_link(title_id, source_path, sizeof(source_path)))
return true;
return prepare_title_runtime(title_id, source_path);
}
static int handle_launch_request(const char *title_id) {
if (!title_id || title_id[0] == '\0' || runtime_sleep_mode_active())
return EINVAL;
char source_path[MAX_PATH];
if (!read_mount_link(title_id, source_path, sizeof(source_path)))
return ENOENT;
publish_prepared_title(title_id);
bool ready = prepare_title_runtime(title_id, source_path);
if (!ready) {
clear_prepared_title(title_id);
return EIO;
}
log_debug(" [SHELLCORE] launch mount ready: %s", title_id);
return 0;
}
static void release_backing_image(const char *title_id) {
char image_chain[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH];
size_t image_count = 0;
if (!read_mount_image_chain(title_id, image_chain, &image_count))
return;
for (size_t layer = image_count; layer > 0; --layer) {
(void)release_runtime_image_mount(image_chain[layer - 1u]);
}
}
bool sm_shellcore_release_title_runtime(const char *title_id) {
if (!title_id || title_id[0] == '\0')
return false;
runtime_mount_state_lock();
bool released = unmount_title_runtime_layers(title_id);
if (released)
release_backing_image(title_id);
runtime_mount_state_unlock();
if (released)
clear_prepared_title(title_id);
return released;
}
bool sm_shellcore_service_has_prepared_mount(void) {
pthread_mutex_lock(&g_service.mutex);
bool prepared = g_service.prepared_title_id[0] != '\0';
pthread_mutex_unlock(&g_service.mutex);
return prepared;
}
static int handle_launch_failed_request(const char *title_id) {
if (!title_id || title_id[0] == '\0')
return 0;
pthread_mutex_lock(&g_service.mutex);
bool prepared = strcmp(g_service.prepared_title_id, title_id) == 0;
pthread_mutex_unlock(&g_service.mutex);
if (!prepared)
return 0;
return sm_shellcore_release_title_runtime(title_id) ? 0 : EBUSY;
}
static void handle_client(int fd) {
sm_shellcore_request_t request;
sm_shellcore_response_t response;
if (!read_full(fd, &request, sizeof(request)))
return;
if (request.magic != SM_SHELLCORE_PROTOCOL_MAGIC ||
request.version != SM_SHELLCORE_PROTOCOL_VERSION ||
strnlen(request.title_id, sizeof(request.title_id)) ==
sizeof(request.title_id)) {
response.status = EPROTO;
} else if (request.operation == SM_SHELLCORE_REQUEST_LAUNCH) {
response.status = handle_launch_request(request.title_id);
} else if (request.operation == SM_SHELLCORE_REQUEST_LAUNCH_FAILED) {
response.status = handle_launch_failed_request(request.title_id);
} else {
response.status = ENOTSUP;
}
(void)write_full(fd, &response, sizeof(response));
}
static void *service_thread_main(void *arg) {
(void)arg;
while (true) {
int client = accept(g_service.listen_fd, NULL, NULL);
if (client < 0) {
if (errno == EINTR)
continue;
pthread_mutex_lock(&g_service.mutex);
bool stopping = g_service.stop_requested;
pthread_mutex_unlock(&g_service.mutex);
if (!stopping)
log_debug(" [SHELLCORE] accept failed: %s", strerror(errno));
break;
}
handle_client(client);
close(client);
}
return NULL;
}
bool sm_shellcore_service_start(void) {
if (g_service.started)
return true;
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0)
return false;
struct sockaddr_un address;
memset(&address, 0, sizeof(address));
address.sun_family = AF_UNIX;
(void)strlcpy(address.sun_path, SM_SHELLCORE_SOCKET_PATH,
sizeof(address.sun_path));
(void)unlink(SM_SHELLCORE_SOCKET_PATH);
if (bind(fd, (struct sockaddr *)&address, sizeof(address)) != 0 ||
listen(fd, 4) != 0) {
int saved_errno = errno;
close(fd);
(void)unlink(SM_SHELLCORE_SOCKET_PATH);
errno = saved_errno;
return false;
}
g_service.listen_fd = fd;
g_service.stop_requested = false;
g_service.prepared_title_id[0] = '\0';
int rc = pthread_create(&g_service.thread, NULL, service_thread_main, NULL);
if (rc != 0) {
close(fd);
g_service.listen_fd = -1;
(void)unlink(SM_SHELLCORE_SOCKET_PATH);
errno = rc;
return false;
}
g_service.started = true;
return true;
}
void sm_shellcore_service_stop(void) {
if (!g_service.started)
return;
pthread_mutex_lock(&g_service.mutex);
g_service.stop_requested = true;
int fd = g_service.listen_fd;
g_service.listen_fd = -1;
pthread_mutex_unlock(&g_service.mutex);
if (fd >= 0)
(void)shutdown(fd, SHUT_RDWR);
if (fd >= 0)
close(fd);
pthread_join(g_service.thread, NULL);
g_service.started = false;
g_service.stop_requested = false;
g_service.prepared_title_id[0] = '\0';
(void)unlink(SM_SHELLCORE_SOCKET_PATH);
}
+49
View File
@@ -0,0 +1,49 @@
#include "sm_platform.h"
#include "sm_stability.h"
#include "sm_config_mount.h"
#include "sm_log.h"
#include "sm_types.h"
static time_t get_path_last_change_time(const struct stat *st) {
if (!st)
return 0;
return (st->st_ctime > st->st_mtime) ? st->st_ctime : st->st_mtime;
}
bool is_path_stable_now(const char *path, double *root_diff_out,
int *stat_errno_out) {
struct stat st;
time_t now = time(NULL);
ensure_runtime_config_ready();
if (stat_errno_out)
*stat_errno_out = 0;
if (stat(path, &st) != 0) {
if (root_diff_out)
*root_diff_out = -1.0;
if (stat_errno_out)
*stat_errno_out = errno;
return false;
}
double root_diff = difftime(now, get_path_last_change_time(&st));
if (root_diff_out)
*root_diff_out = root_diff;
if (root_diff < 0.0)
return true;
return root_diff > (double)runtime_config()->stability_wait_seconds;
}
bool wait_for_stability_fast(const char *path, const char *name) {
double diff = 0.0;
int st_err = 0;
if (is_path_stable_now(path, &diff, &st_err))
return true;
if (st_err != 0)
log_debug(" [WAIT] %s stat failed for %s: %s", name, path,
strerror(st_err));
else
log_debug(" [WAIT] %s modified %.0fs ago. Waiting...", name, diff);
return false;
}
+12
View File
@@ -0,0 +1,12 @@
#include "sm_platform.h"
#include "sm_time.h"
uint64_t monotonic_time_us(void) {
struct timespec ts;
memset(&ts, 0, sizeof(ts));
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0)
return 0;
return (uint64_t)ts.tv_sec * 1000000ull + (uint64_t)ts.tv_nsec / 1000ull;
}
+164
View File
@@ -0,0 +1,164 @@
#include "sm_platform.h"
#include "sm_title_state.h"
#include "sm_hash.h"
#include "sm_limits.h"
#include "sm_log.h"
struct TitleStateEntry {
char title_id[MAX_TITLE_ID];
uint8_t mount_reg_attempts;
uint8_t register_attempts;
bool duplicate_notified_once;
bool valid;
};
static struct TitleStateEntry g_title_state[TITLE_STATE_CAPACITY];
static uint16_t g_title_state_hash[STATE_HASH_SIZE];
static void rebuild_title_state_hash(void) {
memset(g_title_state_hash, 0, sizeof(g_title_state_hash));
for (int k = 0; k < TITLE_STATE_CAPACITY; k++) {
if (!g_title_state[k].valid || g_title_state[k].title_id[0] == '\0')
continue;
uint32_t slot =
sm_fnv1a32(g_title_state[k].title_id) & (STATE_HASH_SIZE - 1u);
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
if (g_title_state_hash[slot] == 0) {
g_title_state_hash[slot] = (uint16_t)(k + 1);
break;
}
slot = (slot + 1u) & (STATE_HASH_SIZE - 1u);
}
}
}
static struct TitleStateEntry *find_title_state(const char *title_id) {
uint32_t slot = sm_fnv1a32(title_id) & (STATE_HASH_SIZE - 1u);
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
uint16_t idx = g_title_state_hash[slot];
if (idx == 0)
return NULL;
struct TitleStateEntry *entry = &g_title_state[idx - 1u];
if (entry->valid && strcmp(entry->title_id, title_id) == 0)
return entry;
slot = (slot + 1u) & (STATE_HASH_SIZE - 1u);
}
return NULL;
}
static struct TitleStateEntry *create_title_state(const char *title_id) {
int slot_k = -1;
for (int k = 0; k < TITLE_STATE_CAPACITY; k++) {
if (!g_title_state[k].valid) {
slot_k = k;
break;
}
}
if (slot_k < 0) {
int evict_k = -1;
for (int k = 0; k < TITLE_STATE_CAPACITY; k++) {
if (!g_title_state[k].valid)
continue;
if (g_title_state[k].mount_reg_attempts == 0 &&
g_title_state[k].register_attempts == 0) {
evict_k = k;
break;
}
}
if (evict_k < 0) {
for (int k = 0; k < TITLE_STATE_CAPACITY; k++) {
if (!g_title_state[k].valid)
continue;
if (g_title_state[k].mount_reg_attempts == 0) {
evict_k = k;
break;
}
}
}
if (evict_k < 0)
evict_k = 0;
memset(&g_title_state[evict_k], 0, sizeof(g_title_state[evict_k]));
rebuild_title_state_hash();
slot_k = evict_k;
}
memset(&g_title_state[slot_k], 0, sizeof(g_title_state[slot_k]));
g_title_state[slot_k].valid = true;
(void)strlcpy(g_title_state[slot_k].title_id, title_id,
sizeof(g_title_state[slot_k].title_id));
uint32_t slot = sm_fnv1a32(title_id) & (STATE_HASH_SIZE - 1u);
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
if (g_title_state_hash[slot] == 0) {
g_title_state_hash[slot] = (uint16_t)(slot_k + 1);
return &g_title_state[slot_k];
}
slot = (slot + 1u) & (STATE_HASH_SIZE - 1u);
}
g_title_state[slot_k].valid = false;
g_title_state[slot_k].title_id[0] = '\0';
rebuild_title_state_hash();
return NULL;
}
static struct TitleStateEntry *get_or_create_title_state(const char *title_id) {
struct TitleStateEntry *entry = find_title_state(title_id);
return entry ? entry : create_title_state(title_id);
}
bool was_register_attempted(const char *title_id) {
struct TitleStateEntry *entry = find_title_state(title_id);
return entry ? (entry->register_attempts >= MAX_REGISTER_ATTEMPTS) : false;
}
uint8_t get_register_attempts(const char *title_id) {
struct TitleStateEntry *entry = find_title_state(title_id);
return entry ? entry->register_attempts : 0;
}
void mark_register_attempted(const char *title_id) {
struct TitleStateEntry *entry = get_or_create_title_state(title_id);
if (entry->register_attempts < UINT8_MAX)
entry->register_attempts++;
}
void clear_register_attempts(const char *title_id) {
struct TitleStateEntry *entry = find_title_state(title_id);
if (entry)
entry->register_attempts = 0;
}
void notify_duplicate_title_once(const char *title_id, const char *path_a,
const char *path_b) {
struct TitleStateEntry *entry = get_or_create_title_state(title_id);
if (entry->duplicate_notified_once)
return;
entry->duplicate_notified_once = true;
notify_system("Duplicate %s ignored:\n%s\nexisting: %s", title_id, path_a,
path_b);
}
void clear_duplicate_title_notification(const char *title_id) {
struct TitleStateEntry *entry = find_title_state(title_id);
if (entry)
entry->duplicate_notified_once = false;
}
uint8_t get_failed_mount_attempts(const char *title_id) {
struct TitleStateEntry *entry = find_title_state(title_id);
return entry ? entry->mount_reg_attempts : 0;
}
void clear_failed_mount_attempts(const char *title_id) {
struct TitleStateEntry *entry = find_title_state(title_id);
if (entry)
entry->mount_reg_attempts = 0;
}
uint8_t bump_failed_mount_attempts(const char *title_id) {
struct TitleStateEntry *entry = get_or_create_title_state(title_id);
if (entry->mount_reg_attempts < UINT8_MAX)
entry->mount_reg_attempts++;
return entry->mount_reg_attempts;
}
+229
View File
@@ -0,0 +1,229 @@
#!/usr/bin/env python3
"""Generate SceShellCore lifecycle offsets from unpacked firmware files.
The generator identifies functions through semantic log strings, finds the
containing function prologue and emits a compact firmware offset table. It is
intentionally independent from IDA databases so the checked-in table can be
reproduced from unpacked firmware files.
"""
from __future__ import annotations
import argparse
import struct
from dataclasses import dataclass
from pathlib import Path
from capstone import CS_ARCH_X86, CS_MODE_64, Cs
PROLOGUE = b"\x55\x48\x89\xe5"
TARGET_NAMES = (
"launch_app",
"unmount_workspace",
"install_title_dir",
"install_all",
)
@dataclass
class LoadSegment:
file_offset: int
virtual_address: int
file_size: int
flags: int
class ShellCore:
def __init__(self, path: Path):
self.path = path
self.data = path.read_bytes()
header = struct.unpack_from("<16sHHIQQQIHHHHHH", self.data, 0)
phoff, phentsize, phnum = header[5], header[9], header[10]
self.loads: list[LoadSegment] = []
self.executable: LoadSegment | None = None
for index in range(phnum):
values = struct.unpack_from(
"<IIQQQQQQ", self.data, phoff + index * phentsize
)
p_type, flags, offset, address, _, file_size, _, _ = values
if p_type != 1:
continue
segment = LoadSegment(offset, address, file_size, flags)
self.loads.append(segment)
if flags & 1:
self.executable = segment
if self.executable is None:
raise ValueError(f"{path}: executable PT_LOAD not found")
def file_to_virtual(self, offset: int) -> int:
for segment in self.loads:
relative = offset - segment.file_offset
if 0 <= relative < segment.file_size:
return segment.virtual_address + relative
raise ValueError(f"file offset 0x{offset:x} is not mapped")
def virtual_to_file(self, address: int) -> int:
for segment in self.loads:
relative = address - segment.virtual_address
if 0 <= relative < segment.file_size:
return segment.file_offset + relative
raise ValueError(f"virtual address 0x{address:x} is not mapped")
def string_addresses(self, needle: bytes, use_string_start: bool) -> list[int]:
result: list[int] = []
offset = 0
while True:
offset = self.data.find(needle, offset)
if offset < 0:
return result
string_offset = offset
if use_string_start:
string_offset = self.data.rfind(
b"\0", max(0, offset - 256), offset
) + 1
result.append(self.file_to_virtual(string_offset))
offset += 1
def rip_xrefs(self, targets: list[int], slop: int = 0) -> list[int]:
segment = self.executable
assert segment is not None
code = self.data[
segment.file_offset : segment.file_offset + segment.file_size
]
result: list[int] = []
for index in range(len(code) - 7):
if code[index] not in (0x48, 0x4C):
continue
if code[index + 1] not in (0x8D, 0x8B):
continue
if code[index + 2] & 0xC7 != 0x05:
continue
displacement = struct.unpack_from("<i", code, index + 3)[0]
instruction_address = segment.virtual_address + index
destination = instruction_address + 7 + displacement
if any(target - slop <= destination <= target + slop for target in targets):
result.append(instruction_address)
return result
def preceding_prologue(self, address: int, window: int = 0x40000) -> int:
end = self.virtual_to_file(address)
start = max(self.executable.file_offset, end - window) # type: ignore[union-attr]
found = self.data.rfind(PROLOGUE, start, end)
if found < 0:
raise ValueError(f"no prologue before 0x{address:x}")
return self.file_to_virtual(found)
def locate_targets(self) -> dict[str, int]:
launch_strings = self.string_addresses(b"launchApp(%s)", True)
launch_xrefs = self.rip_xrefs(launch_strings)
if len(launch_xrefs) != 1:
raise ValueError(f"launchApp xrefs: {launch_xrefs!r}")
launch = self.preceding_prologue(launch_xrefs[0])
unmount_strings = self.string_addresses(
b"m_workspace.unmountWorkspace()", False
)
unmount_groups = {
self.preceding_prologue(xref)
for xref in self.rip_xrefs(unmount_strings, slop=8)
}
unmount_groups.discard(launch)
if len(unmount_groups) != 1:
raise ValueError(f"unmountWorkspace candidates: {unmount_groups!r}")
unmount = next(iter(unmount_groups))
install_strings = self.string_addresses(b"AppInstallTitleDirMain", False)
install_groups: dict[int, int] = {}
for xref in self.rip_xrefs(install_strings, slop=8):
prologue = self.preceding_prologue(xref)
install_groups[prologue] = install_groups.get(prologue, 0) + 1
if not install_groups:
raise ValueError("AppInstallTitleDirMain xrefs not found")
best_count = max(install_groups.values())
best = [address for address, count in install_groups.items() if count == best_count]
if len(best) != 1:
raise ValueError(f"ambiguous AppInstallTitleDirMain: {install_groups!r}")
app_install_all_strings = self.string_addresses(b"AppInstallAll()", True)
app_install_all_xrefs = self.rip_xrefs(app_install_all_strings)
call_targets: dict[int, int] = {}
for xref in app_install_all_xrefs:
xref_file = self.virtual_to_file(xref)
for call_file in range(xref_file - 32, xref_file):
if self.data[call_file] != 0xE8:
continue
call_address = self.file_to_virtual(call_file)
displacement = struct.unpack_from("<i", self.data, call_file + 1)[0]
target = call_address + 5 + displacement
call_targets[target] = call_targets.get(target, 0) + 1
if not call_targets:
raise ValueError("AppInstallAll call target not found")
app_install_all = max(call_targets, key=call_targets.get)
return {
"launch_app": launch,
"unmount_workspace": unmount,
"install_title_dir": best[0],
"install_all": app_install_all,
}
def patch_size(self, address: int, minimum: int = 12) -> int:
offset = self.virtual_to_file(address)
decoder = Cs(CS_ARCH_X86, CS_MODE_64)
size = 0
for instruction in decoder.disasm(self.data[offset : offset + 64], address):
size += instruction.size
if size >= minimum:
return size
raise ValueError(f"cannot determine patch size at 0x{address:x}")
def firmware_key(name: str) -> int:
major, minor = (int(part) for part in name.split(".", 1))
if major > 99 or minor > 99:
raise ValueError(f"firmware version is not BCD-compatible: {name}")
major_bcd = ((major // 10) << 4) | (major % 10)
minor_bcd = ((minor // 10) << 4) | (minor % 10)
return (major_bcd << 8) | minor_bcd
def generate(root: Path) -> str:
firmware_dirs = sorted(
(path for path in root.iterdir() if (path / "system/vsh/SceShellCore.elf").is_file()),
key=lambda path: tuple(int(part) for part in path.name.split(".")),
)
records: list[str] = []
for firmware_dir in firmware_dirs:
shellcore = ShellCore(firmware_dir / "system/vsh/SceShellCore.elf")
targets = shellcore.locate_targets()
records.append(
" {\n"
f" .firmware = 0x{firmware_key(firmware_dir.name):04x}u,\n"
f" .name = \"{firmware_dir.name}\",\n"
" .targets = {"
+ ", ".join(
"{.offset = "
f"0x{targets[name]:x}u, .patch_size = "
f"{shellcore.patch_size(targets[name])}u}}"
for name in TARGET_NAMES
)
+ "},\n }"
)
return "/* Generated by tools/generate_shellcore_offsets.py. */\n" + ",\n".join(records) + "\n"
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("root", type=Path)
parser.add_argument("--output", type=Path)
args = parser.parse_args()
output = generate(args.root)
if args.output:
with args.output.open("w", encoding="utf-8", newline="\n") as file:
file.write(output)
else:
print(output, end="")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""Generate SceShellCore lifecycle offsets from a flat ELF directory.
Input files must be named ``SceShellCoreXXX.elf`` or ``SceShellCoreXXXX.elf``.
The last two digits are the minor firmware version; the preceding digits are
the major version. For example, ``SceShellCore1140.elf`` is firmware 11.40.
"""
from __future__ import annotations
import argparse
import re
from pathlib import Path
from generate_shellcore_offsets import ShellCore, TARGET_NAMES, firmware_key
SHELLCORE_NAME = re.compile(r"^SceShellCore(\d{3,4})\.elf$")
def firmware_files(root: Path) -> list[tuple[int, int, Path]]:
result: list[tuple[int, int, Path]] = []
for path in root.iterdir():
if not path.is_file():
continue
match = SHELLCORE_NAME.fullmatch(path.name)
if match is None:
continue
version = match.group(1)
result.append((int(version[:-2]), int(version[-2:]), path))
return sorted(result, key=lambda item: (item[0], item[1]))
def generate(root: Path) -> str:
records: list[str] = []
for major, minor, path in firmware_files(root):
firmware_name = f"{major}.{minor:02d}"
shellcore = ShellCore(path)
targets = shellcore.locate_targets()
records.append(
" {\n"
f" .firmware = 0x{firmware_key(firmware_name):04x}u,\n"
f" .name = \"{firmware_name}\",\n"
" .targets = {"
+ ", ".join(
"{.offset = "
f"0x{targets[name]:x}u, .patch_size = "
f"{shellcore.patch_size(targets[name])}u}}"
for name in TARGET_NAMES
)
+ "},\n }"
)
return (
"/* Generated by tools/generate_shellcore_offsets_from_files.py. */\n"
+ ",\n".join(records)
+ "\n"
)
def main() -> int:
parser = argparse.ArgumentParser(
description="Generate offsets from SceShellCoreXXXX.elf files"
)
parser.add_argument("root", type=Path, help="directory containing the ELF files")
parser.add_argument("--output", type=Path)
args = parser.parse_args()
output = generate(args.root)
if args.output:
with args.output.open("w", encoding="utf-8", newline="\n") as file:
file.write(output)
else:
print(output, end="")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""Verify that generated ShellCore hook prologues are safe to copy."""
from __future__ import annotations
import argparse
from pathlib import Path
from capstone import CS_ARCH_X86, CS_GRP_CALL, CS_GRP_JUMP, CS_MODE_64, Cs
from capstone.x86 import X86_OP_MEM, X86_REG_RIP
from generate_shellcore_offsets import ShellCore
def verify_firmware(path: Path) -> None:
shellcore = ShellCore(path)
targets = shellcore.locate_targets()
decoder = Cs(CS_ARCH_X86, CS_MODE_64)
decoder.detail = True
names = ["launch_app"]
# AppInstallAll is hooked after the public TitleDir RPC disappeared.
try:
firmware = int(path.parents[2].name.replace(".", ""), 16)
except ValueError:
firmware = 0
if firmware >= 0x1200:
names.append("install_all")
for name in names:
address = targets[name]
patch_size = shellcore.patch_size(address)
offset = shellcore.virtual_to_file(address)
code = shellcore.data[offset : offset + patch_size]
instructions = list(decoder.disasm(code, address))
if sum(instruction.size for instruction in instructions) != patch_size:
raise ValueError(f"{path}: {name}: split instruction")
for instruction in instructions:
if instruction.group(CS_GRP_CALL) or instruction.group(CS_GRP_JUMP):
raise ValueError(
f"{path}: {name}: control flow in copied prologue at "
f"0x{instruction.address:x}"
)
for operand in instruction.operands:
if operand.type == X86_OP_MEM and operand.mem.base == X86_REG_RIP:
raise ValueError(
f"{path}: {name}: RIP-relative operand at "
f"0x{instruction.address:x}"
)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("root", type=Path)
args = parser.parse_args()
firmware_dirs = sorted(
path
for path in args.root.iterdir()
if (path / "system/vsh/SceShellCore.elf").is_file()
)
for firmware_dir in firmware_dirs:
verify_firmware(firmware_dir / "system/vsh/SceShellCore.elf")
print(f"verified {len(firmware_dirs)} firmware trampoline layouts")
return 0
if __name__ == "__main__":
raise SystemExit(main())