Patches for SteamVR on Linux
A collection of binary patches fixing bugs in SteamVR, SteamOS for the Steam Frame, and Half-Life: Alyx.
With all of these, I can no longer get Alyx to crash, and over the span of a 2h 6m session I got only two stream resets.
See the repository for historical patchers. See the project README for instructions on how to install these patches.
SteamVR
Latest patcher: https://the-tk.com/cgit/steamvr-patches/plain/steamvr/patch-steamvr
Most of the annoyances in SteamVR are in the streaming protocol implementation which has a tendency to make stream resets worse by setting things up for additional stream resets.
There's also a bug which affects games where the compositor will re-use resources before the GPU is done with them.
Frame SteamOS
Latest patcher: https://the-tk.com/files/frame-steamos-0.5.6.tar.zst
The headset implementation of VRLink is mostly okay, it does still have some issues which from experience can make resets worse than they need to be.
Half-Life: Alyx
Latest patcher: https://the-tk.com/cgit/steamvr-patches/plain/hlvr/patch-hlvr
There are three issues plaguing Alyx, but one of them is in SteamVR itself. So I recommend patching both. With both patched, I've not experienced any crashes since.
Instance Allocator Crash (Barnacle Splatter Crash)
GitHub Issue: Half-Life: Alyx crashes when a barnacle blood spray fills the view #983
This is easiest to trigger when looking at a barnacle as it explodes from a red canister. This produces a lot of particles. There's a shared per-frame pool of instances of things that can be instance rendered (shared, it seems, among all particles?). If more than 8192 instances are created, this fills an allocator. The game ignores the resulting NULL and tries to write through it triggering the crash.
Patch
Build ID: 25487405
File: game/bin/linuxsteamrt64/libscenesystem.so
SHA-256: ec29842a06e0f0e729724dd92d4ec06fa921875e8703533f288255c8cac888ac
The crash happens at 0x258b2c, which is where the pointer is dereferenced. The call to the allocator happens at 0x25c9eb. The first part of the patch hooks immediately after the call to allocate more instances.
25c9eb | call 0x164ae0 | ...
25c9f0 | mov DWORD PTR [rbp-0x48c4],eax | jmp 0x25821e
| | nop
25c9f6 | mov rax,QWORD PTR [rbp-0x48f8] | ...
The allocator function writes the pointer to [rbp-0x48a8]. The patch first restores the mov clobbered by adding the jmp into the hook, and then compares the returned pointer against NULL before jumping to the function's epilogue in that case, or otherwise jumping back to the next instruction above.
25821c | jmp 0x2581c0 | ...
25821e | int3 x6 (padding) | mov DWORD PTR [rbp-0x48c4],eax
258224 | int3 x8 (padding) | cmp QWORD PTR [rbp-0x48a8],0
25822c | int3 x6 (padding) | je 0x25cb7f
258232 | int3 x5 (padding) | jmp 0x25c9f6
258237 | int3 x9 (padding) | ...
258240 | push rbp | ...
Even with the above error handling patch, the allocator itself has a bug where it unconditionally increments the number of allocations it handed out regardless of if it returns NULL. The code is already doing a comparison (against the total number of allocator pages) before writing the new value. The next patch hooks by replacing the assignment and following conditional jump.
164bfe | cmp edi,0x1f | ...
164c01 | mov DWORD PTR [rbx+0x5f0c],esi | jbe 0x164cb4
164c07 | jbe 0x164c60 | nop
164c09 | mov QWORD PTR [r13],0 | ...
Subsequently, the assignment can be restored (as the jump was conditional) and the patch can jump to the original conditional jump target from the block above.
164cb2 | jmp 0x164c6a | ...
164cb4 | int3 x6 (padding) | mov DWORD PTR [rbx+0x5f0c],esi
164cba | int3 x5 (padding) | jmp 0x164c60
164cbf | int3 x1 (padding) | ...
164cc0 | push rbp | ...
A consequence of this approach is that some particle effects end up skipped, but that's certainly better than just crashing the entire game and this is only a per-frame cap.
Snappy UB Leading to State Corruption
GitHub Issues:
- Half-Life: Alyx crashing steamvr whenever loading into a new area in quarantine zone. #405 (debatable if this is the right issue, the reports look like a mixture of this bug and the SteamVR GPU resource bug, but others got closed in favour of it)
- [BUG] Half-Life: Alyx crashes (SIGSEGV) after assert in networksystem #1023
- [BUG] Half-Life: Alyx (native) fatal assert serializedentity.h:797 GetBitRange( 6661 -> 6661 ) during a2_pistol train ride, reproducible from same autosave #986
This can happen at any time, but seems to hit especially often during level transitions.
Specifically the one instance I ran into was with the decompression of CHLVRHoloHackingPlug state which happened while loading a save on a2_pistol.
Patch
Build ID: 25487405
File: game/bin/linuxsteamrt64/libnetworksystem.so
SHA-256: 5a40f16e5c8deb83cbefd762386572faf3a5acf1eeadf97bb89bafb67a89c488
The game, despite being single player, still uses a client server architecture. To speed up transfer of data between the client and server, it uses Google's snappy compression library.
The version of snappy that HLVR vendors predates some changes made to upstream snappy in 2020. These changes remove use of reinterpret_cast to perform copies.
With the right compiler flags (I tried and succeeded), the compiler can be made to demonstrate the unusual compiled output. The use of uint64_t * with reinterpret_cast let the compiler assume alignment of the pointer. And the compiler optimises a specific copy loop in a way which only makes sense if 8 byte alignment was assumed.
Specifically, one of the ways snappy does compression is copy repetition. A specific number of bytes are copied repeatedly until the specified number of bytes are generated. The optimisation is to use 8 byte wide copies when the copy-loop distance is 8 bytes or more.
The specific affected code path is only taken if the distance is at least 8 bytes, and the buggy code is emitted because the compiler assumes that the distance must be a multiple of 8. Some kind of auto-vectorization adds a 16 byte wide branch, and takes it for the >=8 byte case whenever the distance is not 8 bytes (it can't be less than 0, so it must be more than 8, and since alignment requirements let the compiler assume that it must be a multiple of 8, every multiple of 8 above 8 is valid for the 16 byte loop).
There are some additional conditions, but anything that compresses into a repeat of 9 to 15 bytes could end up mangled.
For example, let's say the decompressed data so far is:
00 01 02 03 04 05 06 07 08 09 0a 0b
Suppose the next element in the compressed stream is a copy with an offset of 12 and a length of 36. The correct fast path would be to copy 8 bytes at a time to the end of decompressed stream, moving both pointers each time, 4 times, and then copy some remaining bytes.
This would look something like...
1
0 8 6
00 01 02 03 04 05 06 07 08 09 0a 0b .. .. .. .. .. .. .. ..
\ source / \ destination /
1 2
0 8 6 4
00 01 02 03 04 05 06 07 08 09 0a 0b 00 01 02 03 04 05 06 07 .. .. .. .. .. .. .. ..
\ source / \ destination /
1 2 3
8 6 4 2
08 09 0a 0b 00 01 02 03 04 05 06 07 08 09 0a 0b 00 01 02 03 .. .. .. .. .. .. .. ..
\ source / \ destination /
...
If instead 16 bytes are copied:
1 2
0 8 6 4
00 01 02 03 04 05 06 07 08 09 0a 0b .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..
\ source \ / destination /
1 2
0 8 6 4
00 01 02 03 04 05 06 07 08 09 0a 0b 00 01 02 03 04 05 06 07 08 09 0a 0b .. .. .. ..
\ source /
1 2 3 4
6 4 2 0
04 05 06 07 08 09 0a 0b .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..
\ source \ / destination /
...
This will copy data past the end of what has been decompressed so far, and that will grow with every copy corrupting the destination.
For the patch, since in this branch the code is already on the 8 byte fast-path, we can assume the offset is at least 8. The compiled binary compares against an exact offset of 8 and uses the 16 byte path if it's not 8. The fix substitutes two bytes, modifying the branch to use the 8 byte path for distances <= 15 rather than == 8.
1fbeaf | sub r9,rdi | ...
1fbeb2 | lea rdi,[r9+0x8] | lea rdi,[r9+0xf]
1fbeb6 | cmp rsi,rdi | ...
1fbeb9 | je 0x1fbf62 | jbe 0x1fbf62
1fbebf | shr edx,0x3 | ...
- Source
- https://the-tk.com/cgit/steamvr-patches/