On the Frame the atomic-branch detours never installed ("installation failed"): an mmap hint isn't honoured,
so the stub landed out of B range. Stubs now go into free gaps from /proc/self/maps within +-128 MiB of the
entry, mapped with MAP_FIXED_NOREPLACE (a relocated result is rejected); no gap = no hook. Every failure logs
the target and the failing step.
6.7 KiB
Batman audio geometry input lifetime
Batman: Arkham Shadow 1.4.1 could abort while loading a save or streaming the area
around a real-time cutscene. Unity reported requests for 26,388,238,103 or
147,520,448,640 bytes through MetaXRAudioUnityMemoryAllocator.cpp, followed by
SIGTRAP on an IL2CPP worker. The first captured stack reached
GeometricAudioMeshPreprocessor::readMeshInfo through
ovrAudio_AudioGeometryReadMeshMemory.
The serialized asset on disk was intact. In one failed run its vertex count was
read as 1 instead of 17,251. The managed-code trace showed a lifetime risk:
LoadGeometryAsync obtains a NativeArray<byte>.ReadOnly from a
UnityWebRequest download handler, calls async-void LoadGeometryFromMemory,
then returns. The background task retains the pointer/length pair, but not the
request owning those bytes. Its GC.KeepAlive boxes the read-only view rather
than retaining the request. The trace and device test support premature source
reuse as the cause; no hardware watch established the exact freeing instruction.
Repair and isolation
audio_geometry_owned.h copies the input synchronously at
LoadGeometryFromMemory, before Task.Run can outlive the request. A mutex
protects the list of copies. The original worker still parses and processes the
audio geometry; its completion wrapper removes and frees the corresponding copy.
This also handles a worker finishing before the queuing call returns. NativeArray
length/padding bits and IL2CPP's hidden method-info arguments are preserved.
Null/invalid lengths and allocation failure retain the original call. A task
that never runs retains its copy until process exit; there is no timed expiry
that could free data beneath a late worker.
batman_audio_geometry.h enables the repair only in com.camouflaj.manta, on
AArch64, after the existing audio metadata initializer verifies Meta XR Audio
Wwise build e1619e7fb839badf0ea5ecc22d9f028d0b17c34d. It additionally requires
IL2CPP build 1d48a41d7e9011bef5fae0c23c404c62a4e7a73c and exact 16-byte entry
sequences at both managed hooks. Unknown builds are left untouched. The live
IL2CPP addresses are 0x2e61bb4 (queue) and 0x2e63a28 (worker); text file
offsets in this build are 0x1000 lower. These offsets must not be reused for
another game or build.
Both geometry hooks and the metadata hooks use audio_detour.h. After the
16-byte entry guard, only the first, naturally aligned instruction is replaced:
a release atomic store publishes an AArch64 B to a nearby stub. The stub's
absolute jump and the trampoline (one verified non-PC-relative instruction,
then a direct branch to entry+4) are complete, instruction-cache flushed and RX
before publication. The original-call pointer is published first. Remaining
entry instructions are unchanged, so workers already past the entry can finish
normally. /proc/self/maps supplies free gaps, clipped to the ranges of both
branches. Page-aligned candidates are tried outward from the target (lower
address first on equal distances) with MAP_FIXED_NOREPLACE; EEXIST/EINVAL
advance to the next candidate. Existing mappings are never replaced, and a
kernel that ignores the flag and relocates a mapping has that result unmapped.
If no candidate works, that hook is skipped without changing its entry. No ELF
page-slack fallback is used: zero bytes alone cannot establish that padding is
outside all section content, and the loaded program headers do not prove this.
Every mprotect result is checked. Failures before entry publication free the mapping. If restoring RX after publication fails, the entry word is restored, its instruction cache is flushed, and restoring RX is attempted again. The mapping and original-call pointer must then remain valid until process exit: a worker may already have followed the published branch. Freeing them without quiescing those threads would introduce a use-after-unmap. A persistent kernel mprotect failure can leave the entry page RWX. Each hook failure logs its target and failing operation once; metadata installation is attempted once per verified library. Allocation failure reports the closest eligible free page, last attempted address, actual mmap result, errno and attempt count. Null closest and zero attempts mean no eligible free gap. Protection failures identify stub RX, entry RWX, or entry RX, including the rollback RX errno (zero means it succeeded). This exceptional rollback cannot provide immediate reclamation or guarantee RX when the kernel refuses it. The previous hint-based publication build failed installation on the Steam Frame; this gap-search replacement has host validation only.
This is separate from the existing audio metadata retirement repair. It does not change the decoder, acoustic processing, tracking, foveation, resolution, refresh rate, or other games' recipes. Temporary input dumps, native parser bypasses and redundant late copies used during investigation are not included. Production logs are limited to installation and the first four copy/completion pairs. No game assets are included in the repository.
The frame.adapter package revision increases only for Batman. Existing Batman
installs therefore offer Update on Frame; other games are not marked outdated
by this repair. Updating/rebuilding Batman installs the new adapter through the
normal path; no launcher script or additional option is needed.
Validation
- Native regressions cover deferred and inline completion, source destruction before worker execution, concurrent loads, cleanup, allocation failure, argument preservation, and rejection of unknown build IDs/function entries. Host C tests also cover branch-range endpoints, stub/trampoline encoding, publication order, synthetic maps with in-range/out-of-range/absent gaps and adjacent mappings, outward placement order, EEXIST/EINVAL fallback, rejected relocated mmap results and mprotect failure cleanup/rollback.
- On the headset, the affected save loaded successfully with the repair. Logs showed private copies of 2,599,300, 159,020 and 628,088 bytes made before dispatch; each original parser completed and each copy was released. Another 180,120-byte geometry input also completed normally.
- The game continued for over seven minutes after that load with zero allocation failures, rejected meshes or fatal signals. Acoustic geometry was loaded, not disabled. That device test used the ownership repair with diagnostic guards; none of the guards rejected a mesh. The production repair omits those guards.
- The rebuilt latest adapter passes the repository checks recorded in the PR; the device result above is for the experimental build tested before integration.
This does not establish long-term stability or resolve the separately observed whole-headset power-offs. Those require their own power/reset investigation.