Skip to content

Offer a pinned USVFS stack-alignment fix for MO2 2.5.2 on ARM64 - #77

Merged
skjiisa merged 6 commits into
mainfrom
usvfs-arm64-workaround
Oct 3, 2026
Merged

skjiisa merged 6 commits into
mainfrom
usvfs-arm64-workaround

Conversation

@skjiisa

@skjiisa skjiisa commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

Why

Programs started through MO2 2.5.2 die on ARM64 (Steam Frame): USVFS's injection stub calls InitHooks with a misaligned stack, which real x86 tolerates by luck and FEX doesn't (#62). The fix is ModOrganizer2/usvfs#93, which is open and unreviewed, and no MO2 release carries it. Per the project's policy for non-MO2-LINT dependencies, ModSync carries its own workaround rather than waiting.

What

  • Pinned backport. ndabas's v0.5.6.1-woa.1 release: USVFS 0.5.6.1, the version MO2 2.5.2 ships, plus the stack-alignment fix. It stays statically linked, so it doesn't need the prefix VC++ runtime that USVFS 0.5.7+ does (Offer a current VC++ runtime in the game prefix for USVFS 0.5.7+ #66). The archive is pinned by size and SHA-256, and each of the four binaries by SHA-256.
  • Only known files change. Apply replaces the four USVFS files only when they match stock MO2 2.5.2, and keeps verified originals in <instance>/.modsync-usvfs/backup. Restore puts them back and refuses to overwrite a build installed since. An interrupted replacement can be finished or undone. Unknown builds are left alone.
  • Where. The dashboard's MO2 card, the Steam launch hub, modsync mo2 usvfs status|apply|restore, and doctor. Shown on ARM64, or wherever a backup exists. Always an explicit click.
  • In-use check. Before changing anything, ModSync asks the host whether any of the user's processes maps this instance's USVFS files (/proc/*/maps). Wine maps each DLL it loads from its Linux path, so this catches MO2 and every game or tool started from it. Steam's hook script waiting on the hub doesn't count, and neither does another instance.
  • Docs: docs/usvfs-arm64.md (source commit, license, build workflow, validation plan). The README gets a dependency-table row only.

Review fixes (second commit)

The first commit is the branch as written. The second fixes what review found:

  • The original check matched process command lines. Steam's hook script stays alive while the hub is open, and its arguments end in SkyrimSELauncher.exe, so Apply and Restore always failed from the hub. It also missed tools started from MO2. Replaced with the memory-map check above.
  • Restore on an untouched instance failed for lack of a backup, and left an empty .modsync-usvfs. It now reports "already in place". It also rechecks the files after the process probe, as Apply does.
  • Download and unpack failures (IncompleteRead, a bad proxy, 7-Zip errors) became tracebacks in the CLI. They're UsvfsError now.
  • UI failures show with the ⚠ marker. The x86_64 dashboard and hub no longer hash about 10 MB of binaries just to hide the controls. doctor hashes each instance once.

Testing

  • uv run pytest -q: 412 passed, 3 skipped. New tests run the real host probe against real processes: a process mapping the DLL blocks changes; a process whose arguments end in SkyrimSELauncher.exe doesn't; one mapping another instance's DLL doesn't.
  • On an x86_64 desktop, against a real MO2 2.5.2 started through Proton Experimental and its Steam Linux Runtime: MO2's process maps usvfs_x64.dll under its Linux path. The probe blocks while MO2 runs and clears after it exits, at about 0.2 s per check.
  • On the Frame (aarch64): uv run pytest -q: 398 passed, 18 skipped. The skips are unrelated (no host xdelta3/7z, opt-in sync integration); the real-process probe tests ran and passed.
  • Steam Frame (aarch64 Flatpak from this branch, Proton 11.0 ARM64, stock MO2 2.5.2 from MO2-LINT): see "Frame validation" below.

Frame validation (third and later commits)

Done on a Steam Frame with the CI flatpak-bundle-aarch64 installed as the user's Flatpak:

  • The in-use check works under FEX. MO2 under Proton 11.0 (ARM64) maps usvfs_x64.dll from /home/…/MO2-lint-test/usvfs_x64.dll, and so does every program it starts. The probe through flatpak-spawn --host took 0.17 s. Apply/Restore were refused while MO2 ran, while Explorer++ or Skyrim was still alive after MO2 had been killed, and while both ran. They were allowed once everything exited. Lingering nxmhandler.exe reg and wineserver don't block, which is correct.
  • Modded Skyrim through Steam Play → hub → MO2 → Run. The usvfs log shows inithooks in process 588 successful and … 1240 successful for SkyrimSE.exe (two runs), and the same for Explorer++. Explore Virtual Folder listed the mod's files in Data. A new game started in the Realm of Lorkhan alternate start.
  • Prefix: nothing new needed. The backport's imports are the same as stock 0.5.6.1 (SHLWAPI, KERNEL32, USER32, ADVAPI32, SHELL32, OLEAUT32; no VC++ runtime), and Offer a current VC++ runtime in the game prefix for USVFS 0.5.7+ #66's offer correctly stays off (it reads the backport as 0.5.6.1).
  • Hub: Apply (with download), Restore, Apply from cache, all while the hub was open. Continue/Cancel were disabled during each change.
  • Restore / restore-again / offline: Restore matched the backup hashes. Restore-again said "already in place" and left .modsync-usvfs untouched. flatpak run --unshare=network … mo2 usvfs apply reinstalled from the cache. With no cache it failed cleanly and the instance was unchanged.
  • Bug found and fixed: modsync doctor/diagnostics crashed with NameError: Path on ARM64 once an instance was chosen (missing import in report.py). Now covered by a test that runs with or without Steam on the machine.

Closes #62.

Open question

The download comes from ndabas's fork release. If that release disappears, Apply stops working for anyone without a cached copy. Re-hosting the same archive as an asset on a ModSync release, with the same hash, would remove that dependency. GPL-3 allows it, and the source is linked. I haven't done it, because publishing a release is outward-facing.

🤖 Generated with Claude Code

skjiisa and others added 6 commits October 3, 2026 14:59
Programs started through MO2 2.5.2 die on ARM64 because USVFS's injection
stub calls InitHooks with a misaligned stack (ModOrganizer2/usvfs#93). This
downloads ndabas's 0.5.6.1 backport of that fix, pinned by archive and
per-file SHA-256, replaces only the four stock MO2 2.5.2 USVFS files, and
keeps verified originals in <instance>/.modsync-usvfs/backup for Restore.
Available from the dashboard, the Steam launch hub and
`modsync mo2 usvfs status|apply|restore`; `doctor` reports it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review fixes for the ARM64 USVFS workaround.

- The "is MO2 closed" check matched command lines, so Steam's hook script,
  which waits on the launch hub with arguments ending in
  SkyrimSELauncher.exe, made Apply and Restore always fail from the hub. It
  also missed tools started from MO2 (xEdit, LOOT…). It now asks the host
  whether any of the user's processes maps this instance's USVFS files.
  Wine maps every DLL it loads from its Linux path; checked against MO2
  under Proton Experimental on x86_64, where it is detected while running
  and clear after it exits.
- restore() on an untouched instance returns "already in place" instead of
  failing for lack of a backup, without creating .modsync-usvfs. It also
  rechecks the files after the process probe, like apply().
- Download and unpack failures become UsvfsError, so the CLI and UI report
  them instead of a traceback.
- Failures go out on a separate signal and show with the warning marker.
- No hashing on machines where the fix can't matter (x86_64 without a
  backup); doctor hashes each instance once.
- One guarded path for apply/restore in the service; _usvfs initialised in
  the dashboard instead of getattr checks.
- README keeps only the dependency-table row; the prose stays in docs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
report.py used Path for the chosen instance's USVFS line without importing
it, so `modsync doctor` and `diagnostics` raised NameError on the Steam
Frame (and anywhere a USVFS backup exists) once ModSync had an instance.
Found running the aarch64 Flatpak on a Frame.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
It depends on whether the machine has Steam and the game; CI has neither.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Without a Steam root doctor returns before the instance section.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Steam Frame: MO2's virtual file system (USVFS) crashes every program MO2 starts

1 participant