Repository navigation
arm64 and Android guests: string literals kept byte for byte (fixes a voice chat crash) - #346
Merged
bnunu merged 2 commits intoOct 16, 2026
Conversation
The arm64 and Android guests are compiled for arm64_32-apple-watchos and
their Darwin assembly converted to ELF (tools/android_asm_convert.py). The
converter took a quoted .ascii/.asciz operand for a quoted symbol name and
dropped a leading "_" from it, as it does from symbols; and its quoted-name
pattern ended at an escaped quote (\"), so the "_" of a word after one was
dropped too. Clang writes byte tables as .ascii strings, so a table holding
95 ("_") lost that byte and everything after it moved down by one.
In Opus that table was CELT's pulse cache (modes.c, cache_bits50): the bits
a band may have are read off it, and shifted by a byte it lets a band have
more pulses than the PVQ tables hold. Decoding a frame from another player
then read past CELT_PVQ_U_ROW in decode_pulses (n 32, k 240) and crashed: a
client on the Steam Frame, in a CTF game on the internet. The guest's own
encoder could crash likewise in encode_pulses. The same build of Opus,
driven with the game's settings and with clean, corrupt, truncated and
random packets and voice_audio_play's loss handling, crashed within a few
thousand frames (and identically at -O0); with the fix it ran 4.8 million
decodes clean, and its encoded bytes and decoded samples are identical to a
native 64-bit build's.
The same loss hit other strings and tables of the guest: musl's iswalpha,
iswpunct and towctrans tables and iconv's charmaps; tomlc17's "_0123..."
digit sets (TOML numbers with "_"); the attract mode's "_de"/"_fr" names,
"_lan_", "_lit" and "_%s"; the progress bar's compressed monk tone and the
DirectSound effects image; and assertion and error texts. All are now
assembled as compiled.
String directives now go through unchanged, and quoted names honour
escapes. tools/test_android_asm_convert.py checks both, and a table of
every byte value through clang, the conversion and the assembler.
…coding Every build sends one 20 ms frame per packet (voice_audio_encode). A packet whose table of contents says otherwise, or that Opus cannot parse, is damaged: voice_audio_play now drops it before the decoder sees it (opus_packet_get_nb_samples), and the next frame makes up for it as lost. Cheap defence in depth: Opus's decoder already refuses such packets, and the crash in decode_pulses came from a valid packet and the guest's garbled pulse cache, fixed in the assembly conversion.
|
This happens to me every time I matchmake. Bumping this PR for visibility cuz I don't want MTV Cribs to crash my game every 5 seconds. |
meowsandstuff
self-requested a review
October 16, 2026 03:12
Collaborator
|
I'm in favour of this. It's a well-scoped fix. The only thing I want to see is that this is actually tested prior to merging, preferably built and play-tested on an Android device to ensure that no further crashes occur. |
bnunu
self-requested a review
October 16, 2026 03:20
bnunu
approved these changes
Oct 16, 2026
bnunu
left a comment
Collaborator
There was a problem hiding this comment.
will merge upon successful testing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The arm64 Linux build (Steam Frame) crashed in an internet CTF game as soon as another player's voice arrived. The cause wasn't Opus or voice chat: the guest's assembly converter was dropping
_bytes from string literals, which shifted data tables in every arm64 and Android guest build.The crash
A client on a Steam Frame crashed in
decode_pulseswith signal 11 at address 0x3c3. The call chain wasnetwork_voice_handle_message→voice_audio_play→opus_decode_float, decoding a teammate's packet.decode_pulseswas called with n=32, k=240.CELT_PVQ_U_ROWhas 15 rows, so the lookup read a garbage pointer.libopus is fuzzed and never reaches those values from any packet. The data it uses to bound pulses was wrong.
Why
The arm64 Linux and Android guests are compiled for
arm64_32-apple-watchos, andtools/android_asm_convert.pyrewrites the Darwin assembly as ELF. Two bugs in the converter combined:.ascii/.ascizwent through the symbol renamer. That took a quoted string for a quoted symbol name and stripped its leading_, as Darwin symbols carry one."[^"]*"ended at an escaped\", so the_of any word after a 0x22 byte was stripped too.Clang emits byte tables as
.ascii. Any table holding 95 (_) lost that byte, and everything after it moved down by one.In Opus that table is CELT's pulse cache (
celt/modes.c,cache_bits50). Shifted by a byte, it lets a band take more pulses than the PVQ tables hold. The guest's encoder can crash the same way inencode_pulses. Before this fix, a Frame player's voice probably reached others as noise.The same loss hit other guest data. No visible failures were confirmed, but all of it is now assembled as compiled:
iswalpha/iswpunct/towctranstables and iconv charmaps;"_0123456789…"digit sets (TOML numbers with_);"_de"/"_fr"(attract mode),"_lan_","_lit","_%s";embed_assets.pyalready avoided.asciibecause "the converter would garble" it, which was this bug.What changes (2 commits)
.ascii/.asciz/.stringoperands pass through unchanged.tools/test_android_asm_convert.pyis added to CI's pytest line. It has unit cases, plus an end-to-end table of all 256 byte values and a_-led string through clang (arm64_32) → the converter → the assembler, compared in.rodata.embed_assets.pycomment are updated.voice_audio_play, usingopus_packet_get_nb_samples).Tested
as. It maps the ILP32 image below 4 GB, as the host does, and drives it with the game's settings: VOIP, 960-sample frames, in-band FEC, 10% loss, complexity 5, 8–64 kbps. It feeds clean, bit-flipped, truncated, extended and random packets, throughvoice_audio_play's sequence, concealment and FEC logic.encode_pulsesafter ~2,500 frames. At-O0it crashed identically, which rules out the optimizer.-ffp-contract=off).tools/test_android_asm_convert.py: 7 passed with the fix. With the old converter, 4 fail.configure.py --vr) and the flat build compile. The VR build's holsters, menu, reload, crouch, death and cinematic checks pass, and it's running on the Steam Frame.Not tested yet