Skip to content

Fix Android startup crash: Linux libbass.so packaged in place of Android arm64 binary - #224

Merged
winnerspiros merged 14 commits into
masterfrom
copilot/fix-crash-on-start
Apr 21, 2026
Merged

Fix Android startup crash: Linux libbass.so packaged in place of Android arm64 binary#224
winnerspiros merged 14 commits into
masterfrom
copilot/fix-crash-on-start

Conversation

Copilot AI commented Apr 21, 2026

Copy link
Copy Markdown

Crash root cause

System.DllNotFoundException: bass at AudioManager startup (v143 / 2026.421.143).

The APK shipped lib/arm64-v8a/libbass.so at 261160 bytes — the Linux arm64 binary from ppy.osu.Framework.NativeLibs (runtimes/linux-arm64/native/libbass.so), not the Android arm64 binary (322992 bytes) from ppy.osu.Framework.Android's AAR. Same wrong-binary substitution for libbass_fx.so (85536 vs 104344) and libbassmix.so (53360 vs 59072). The Linux ELF references GLIBC_* versioned symbols; bionic can't resolve them.

FixRuntimePackAssetTypes in osu.Android.props indiscriminately marked every .so in RuntimePackAsset / ResolvedFileToPublish as AssetType=native, so the .NET Android SDK packed runtimes/linux-arm64/native/libbass.so into lib/arm64-v8a/, racing with — and replacing — the AAR copy. Other natives were spared because their Linux variants carry .so.58 suffixes and didn't collide by name. The release-workflow verify step matched on filename only, so the swap slipped past CI.

Checklist

Packaging fix (the actual launch crash)

  • osu.Android.propsFixRuntimePackAssetTypes only marks Android-RID .so files as native; explicitly removes desktop/iOS runtime .so files (runtimes/{linux,osx,ios,maccatalyst,win,browser,freebsd}-*/) from ResolvedFileToPublish. Filter is name-agnostic.
  • release.yml — Verify step extracts each bass .so and rejects the build if it contains GLIBC_* versioned symbols.

Upstream merge

Copilot AI review feedback

  • RankedPlayCard.SongPreviewContainer — re-wired Enabled / CardHoveredStart()/Stop() lost in upstream's Rewrite ranked play card song preview playback logic to hopefully work around framework breakage ppy/osu#37453 rewrite. Bind happens inside the LoadComponentAsync continuation so previews never race the track's async load.
  • WebSocketChannel.max_message_size boundary>=>, so a payload of exactly 4096 bytes is accepted.
  • WebSocketChannel UTF-8 validation — switched to a static UTF8Encoding(throwOnInvalidBytes: true) so malformed payloads actually trigger the InvalidPayloadData close path; catch DecoderFallbackException.
  • WebSocketServer.Dispose — cancels runningTokenSource, disposes the listener, then waits up to 2 s for handleRequestTask to exit before disposing the cancellation/reset-event handles.
  • OsuWebSocketProvider.Dispose — local-swap server, dispose the CancellationTokenSource via using, always dispose the server in finally.
  • Skipped: WebSocketTest.cs hardcoded port 54321 — tests-only, low value, would diverge from upstream.

CI sanity check + CodeQL / GitHub Advanced Security

  • Compile failure on bc9b8aea: CS0246: 'OsuWebSocketProvider' could not be found in osu.Desktop/OsuGameDesktop.cs(155,25) — fixed by adding using osu.Desktop.IPC; (alphabetically ordered amongst the osu.Desktop.* usings to satisfy InspectCode).
  • CodeQL / GitHub Advanced Security (code-scanning alert The letter 'i' doesn't get capitalized properly in the Notifications panel ppy/osu#3110): WebSocketServer.syncRoot typed as object — changed to System.Threading.Lock (= new Lock()). Repo-wide convention verified across 9 sites (OnlineLookupCache, ScreenshotManager, APIRequest, OAuth, TaskChain, BeatmapDifficultyCache, WorkingBeatmap, SkinProvidingContainer, SubmittingPlayer). System.Threading is already imported.
  • Code Quality job passed on run #24732037106 after the CS0246 fix: Dotnet code style ✅, CodeFileSanity ✅, InspectCode ✅. Build-only (iOS) ✅. Compile ✅ on every test matrix. Only pending: tests themselves (Linux SingleThread legitimately takes ~52 min; job timeout-minutes: 120 accommodates).
  • No other code-style / nullable / using-order issues in the touched files.
  • Local dotnet build not possible in this sandbox (GitHub Packages auth is sandboxed away) — relying on CI for full validation.

Full-code sanity pass over PR diff

  • PlayerHandOfCards.cs — brace fix properly scopes the card local to the Space case; no duplicate decls elsewhere.
  • WebSocketChannel.csstrict_utf8 follows private static readonly <Type> <snake_case> convention (verified across ~50 sites).
  • WebSocketServer.Dispose() — touches only fields always initialized (runningTokenSource, handleRequestTask, contextResetEvent, listener); 2 s bounded wait prevents shutdown hang.
  • OsuWebSocketProvider.Dispose() — null-swap + using var cts + finally server.Dispose(); idempotent if Dispose() is called twice.
  • OsuGameDesktopusing osu.Desktop.IPC; in place; EnableWebSocketServer gate unchanged.
  • Scanned the 13-file diff for easy non-harmful perf wins: intentionally declined. Remaining LINQ lives in cold input/network/shutdown paths (≤ 9-element HandOfCards, localhost-only IPC, StopAsync). Real wins (HitObject.DefaultsApplied leak, MemoryCachingComponent bounding) are too invasive for this PR — deferred.

README

  • Added new entries under Stability improvements for the architecture-correct native libraries packaging, the IPC/WebSocket hardening, and the song-preview playback wiring (no crash framing).

Out of scope / follow-ups

  • HitObject.DefaultsApplied event leak (HitObjectLifetimeEntry) — real, but a proper fix needs an IDisposable flow on the entry plus rewiring every entry-owning Playfield/HitObjectContainer; queued for a separate PR.
  • MemoryCachingComponent unbounded cache → LRU. Lives in osu-framework submodule, not in this PR's scope.

smoogipoo and others added 9 commits April 21, 2026 11:07
Fixes ppy#37434

---------

Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
)

- Supersedes / closes ppy#18129. Reasons I
didn't use that PR are hopefully obvious upon comparing diffs but I can
elaborate if they are not.
- Single metric included for demonstration purposes.
- Do not want to talk about further schema design at this time.
- Specify `OSU_WEBSOCKET_SERVER=1` envvar to enable.
- Can test consumption with [this five minute html
job](https://github.com/user-attachments/files/26839923/index.html)
(works even as a standalone file opened in browser, no CORS bs!)
- There's a lot of inline comments, go read them. There are many WTFs
because the .NET frozen websocket API is weird and stanky and reeks of
the year 2007. The inline comments attempt to explain.
Due to the push of the relevant screens being delayed it's possible that
the room goes away between the scheduling of the push and the actual
execution of the push.

This maybe closes ppy#37374 but my hopes
are not high.

Includes some extra cleanups I noticed along the way.
RFC.

The reason I'm bringing this up is
ppy#37383.

In this case, the retrieval of the beatmap failed on a timeout:

```
2026-04-19 02:43:10 [verbose]: Request to https://osu.ppy.sh/api/v2/beatmaps/?ids[]=5090069 failed with System.Net.WebException: Request to https://osu.ppy.sh/api/v2/beatmaps/?ids[]=5090069 timed out after 10 seconds idle (read 0 bytes, retried 0 times)..
2026-04-19 02:43:10 [verbose]: Failing request osu.Game.Online.API.Requests.GetBeatmapsRequest (System.Net.WebException: Request to https://osu.ppy.sh/api/v2/beatmaps/?ids[]=5090069 timed out after 10 seconds idle (read 0 bytes, retried 0 times).)
```

```
2026-04-19 02:43:10 [error]: Failed to load beatmap 5090069 for playlistItem 0.
2026-04-19 02:43:10 [verbose]: ⚠️ Failed to load beatmap 5090069 for playlistItem 0.
```

This fails at


https://github.com/ppy/osu/blob/0e9664bfdfa69b4b26ff9cf84615c4b83a195a0e/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Card/RankedPlayCard.cs#L167-L171

Just this failing wouldn't cause the game to soft-lock; when I suppress
the lookup locally, the game continues as normal. *However*, suppressing
the lookup is not the same as the lookup *failing*, because a failed
lookup will write a null to the cache, which means that when a beatmap
download is initiated later in


https://github.com/ppy/osu/blob/1b488949e13568c23faa0d88b18c8036f4a7dbc8/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs#L308-L320

it'll just do nothing and the user will be stuck, even though they could
very well attempt to download the beatmap here if the lookup were to
succeed on the second go.

To a degree changing the whole cache for this could be viewed as the
tail wagging the dog, but I think in general caching nulls here seems
pretty anti-user. From within the client I would generally not expect
very many complete misses when looking up beatmaps.

Note that this wouldn't soft-lock the match anymore since
ppy/osu-server-spectator#471. It's just another
step in making degradation less impactful.
…y#37455)

Suboptimal? Sure. But the primary goal is not to crash. Crashing is a
failure of the game programmer.

Better can be done later.

Remedies/fixes ppy#37421.
…k around framework breakage (ppy#37453)

Similar idea to
ppy@622216d
(which was included in ppy#37218).

Probably closes ppy#37420.

I wouldn't be myself if I didn't remark that the WTF/sec on ranked play
code continues to be quite high as I fix these issues. Like, look at the
old code: why does `Enabled` becoming false stop the preview track, but
`CardHovered` becoming false *doesn't*? Even though `shouldBePlaying`
was explicitly defined as derived from *both flags*? Maybe me changing
this to be actually consistent incurs a behaviour change, but like... I
can't tell if it is a bug or not.

Not to mention this nugget:


https://github.com/ppy/osu/blob/0e9664bfdfa69b4b26ff9cf84615c4b83a195a0e/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Hand/HandOfCards.HandCard.cs#L107

https://github.com/ppy/osu/blob/0e9664bfdfa69b4b26ff9cf84615c4b83a195a0e/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Card/RankedPlayCard.cs#L47-L50

What is this naming even?
…ds` (ppy#37423)

Attempt at adressing the points made in
ppy#37419 (comment)

- `CardContainer` is now being sorted immediately instead of only doing
it once per frame. Given that its only ever gonna have a handful of
children there wasn't really a need to optimize that that in the first
place.
- `HandOfCards.Cards` now exposes `cardLookup.Values` as an
`IEnumerable` instead of exposing the card container's children
directly.
- `HandOfCard` now exposes `GetCardsInDisplayOrder` which returns a copy
of all cards in display order. Since it's making a copy I made sure this
isn't called on any hot code paths.
- `HandOfCard.Clear` previously didn't clear the `cardLookup`
dictionary. Didn't cause any issues since we're not re-adding cards to
the hand anywhere but not good regardless.
Switched to looping over all cards and calling `RemoveCard` to make sure
changes to the removal logic can't get overlooked there again.

---------

Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
… server skeleton

Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
…eam master

Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/1e7cb6f9-16ce-458e-921d-22823a0b1260

Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
@winnerspiros
winnerspiros marked this pull request as ready for review April 21, 2026 14:57
Copilot AI review requested due to automatic review settings April 21, 2026 14:57
@gitar-bot

gitar-bot Bot commented Apr 21, 2026

Copy link
Copy Markdown

Gitar is working

Gitar

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes an Android launch crash caused by incorrectly packaging desktop (glibc-linked) libbass*.so binaries into the APK, and adds CI verification to prevent regressions. The PR also includes upstream changes around ranked-play/matchmaking robustness and introduces an optional desktop WebSocket server for external integrations.

Changes:

  • Restrict Android publish-time .so classification/removal to prevent non-Android runtime natives from being packed into lib/<abi>/, and add release workflow verification for glibc symbol leakage.
  • Harden ranked-play/matchmaking screens against unresolved users and improve hand-of-cards ordering/keyboard navigation; adjust queue screen scheduling to avoid delayed push after leaving a room.
  • Add a localhost-only WebSocket server/channel plus desktop wiring (env-var gated) and IPC tests.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
osu.Android.props Filters out non-Android runtime .so files from publish set; only marks Android runtime assets as native.
.github/workflows/release.yml Enhances APK verification to detect glibc-linked libbass*.so via GLIBC_ symbol checks.
osu.Game/Online/API/Requests/Responses/APIUser.cs Adds APIUser.UnknownUser(int) helper for consistent fallback users.
osu.Game/Screens/Play/Leaderboards/MultiplayerLeaderboardProvider.cs Uses APIUser.UnknownUser() fallback when lookup misses.
osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs Uses APIUser.UnknownUser() fallback for roll events.
osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayScreen.cs Adds safe fallbacks for local/opponent user resolution.
osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Intro/IntroScreen.cs Avoids First() exceptions when user lookup fails; provides fallbacks.
osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Hand/HandOfCards.cs Refactors card enumeration/sorting and layout invalidation; introduces display-order helper.
osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Hand/PlayerHandOfCards.cs Updates keyboard selection to respect display order and ignore modifier combos.
osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Card/RankedPlayCard.SongPreview.cs Refactors song preview container; removes hover/enabled-driven playback wiring (needs attention).
osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs Fire-and-forget matchmaking lobby calls; cancels delayed screen push when state changes.
osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs Removes outdated remarks.
osu.Game/Database/BeatmapLookupCache.cs Disables caching of null lookups.
osu.Game/IPC/WebSocketServer.cs Adds simple localhost-only WebSocket server built on HttpListener.
osu.Game/IPC/WebSocketChannel.cs Adds WebSocket read/write loop with message size enforcement.
osu.Desktop/Program.cs Adds env-var flag to enable WebSocket server.
osu.Desktop/OsuGameDesktop.cs Wires optional OsuWebSocketProvider into desktop game.
osu.Desktop/IPC/OsuWebSocketProvider.cs Broadcasts hitcount updates over WebSocket.
osu.Desktop/IPC/Messages/OsuWebSocketMessage.cs Base message type with type discriminator.
osu.Desktop/IPC/Messages/HitCountMessage.cs Message payload for incremental hit count updates.
osu.Game.Tests/Visual/RankedPlay/TestSceneRankedPlayScreen.cs Adjusts setup and adds unresolved-user coverage.
osu.Game.Tests/Visual/RankedPlay/TestScenePlayerCardHand.cs Updates tests to use display-order list accessor.
osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingQueueScreen.cs Adds regression test for delayed room screen push cancellation.
osu.Game.Tests/IPC/WebSocketTest.cs Adds integration-style tests for WebSocket server/channel.
osu.Game.Tests/IPC/WebSocketClient.cs Test client helper for WebSocket IPC tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 80 to 101
@@ -126,9 +97,6 @@ public void LoadPreview(APIBeatmap beatmap)
{
TrackRunning = { BindTarget = trackRunning }
});

if (shouldBePlaying)
startPreviewIfAvailable();
});
}

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LoadPreview() no longer starts playback when Enabled/CardHovered are true, and the removed BindValueChanged handlers mean CardHovered/Enabled no longer control playback at all. Since RankedPlayCard.PlayAudioPreview still drives songPreviewContainer.CardHovered, this effectively disables (or leaves stale) song previews. Please reintroduce start/stop logic (binding to Enabled and CardHovered), or alternatively remove these bindables and make the caller explicitly control PreviewTrack.Start()/Stop().

Copilot uses AI. Check for mistakes.
Comment thread osu.Game/IPC/WebSocketChannel.cs Outdated
Comment on lines +103 to +108
if (currentBufferPosition >= max_message_size)
{
await webSocket.CloseOutputAsync(WebSocketCloseStatus.MessageTooBig, $@"Exceeded maximum message size of {max_message_size} bytes.", token).ConfigureAwait(false);
ClosedPrematurely?.Invoke();
return;
}

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The max message size check rejects messages of exactly max_message_size bytes (currentBufferPosition >= max_message_size). The docstring says messages must not exceed the limit, so a message with length exactly 4096 bytes should be accepted (and the buffer is sized for it). Adjust the condition to only fail when the message would exceed the limit (taking EndOfMessage into account).

Copilot uses AI. Check for mistakes.
Comment on lines +114 to +123
try
{
message = Encoding.UTF8.GetString(receiveBuffer, 0, currentBufferPosition);
}
catch (ArgumentException)
{
await webSocket.CloseOutputAsync(WebSocketCloseStatus.InvalidPayloadData, @"UTF-8 encoded strings expected.", token).ConfigureAwait(false);
ClosedPrematurely?.Invoke();
return;
}

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Encoding.UTF8.GetString() will not throw for invalid UTF-8 sequences (it uses replacement fallback by default), so the catch (ArgumentException) path won't detect malformed payloads as intended. If you want to enforce UTF-8 validity and close with InvalidPayloadData, decode using a UTF8Encoding(..., throwOnInvalidBytes: true) instance and catch DecoderFallbackException (or similar).

Copilot uses AI. Check for mistakes.
Comment on lines +261 to +278
public void Dispose()
{
if (isDisposed)
return;

isDisposed = true;

// no clue why this isn't accessible without casting.
// sidebar: `Stop()` unregisters addresses on Windows, but `Abort()` doesn't!
// this `Dispose()` implementation calls the former.
(listener as IDisposable)?.Dispose();

foreach (var channel in channels.Values)
channel.Dispose();

runningTokenSource.Dispose();
contextResetEvent.Dispose();
}

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dispose() disposes runningTokenSource/contextResetEvent and the HttpListener without first ensuring the request loop has exited. If Dispose() is called without a prior StopAsync(), handleRequests() can still be running and may access runningTokenSource.Token.WaitHandle / contextResetEvent, leading to ObjectDisposedException or other undefined behaviour. Consider making Dispose() reliably stop the server (cancel runningTokenSource, stop/close the listener, and wait for handleRequestTask), or at least call into StopAsync() with a bounded timeout before disposing resources.

Copilot uses AI. Check for mistakes.
Comment thread osu.Desktop/IPC/OsuWebSocketProvider.cs Outdated
Comment on lines +66 to +72
if (server?.IsRunning == true)
{
var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(10));
server.StopAsync(cts.Token).WaitSafely();
server = null;
}

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dispose() creates a CancellationTokenSource but never disposes it, and also never calls Dispose() on the WebSocketServer after stopping it. This can leak unmanaged handles (listener, wait handles) across screen transitions/shutdown. Use a using/try-finally for the CTS and ensure the server is disposed after stopping (even if stop fails/times out).

Suggested change
if (server?.IsRunning == true)
{
var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(10));
server.StopAsync(cts.Token).WaitSafely();
server = null;
}
var server = this.server;
this.server = null;
if (server == null)
return;
try
{
if (server.IsRunning)
{
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(10));
server.StopAsync(cts.Token).WaitSafely();
}
}
finally
{
server.Dispose();
}

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +23
const int port = 54321;

var server = new WebSocketServer(port);
var client = new WebSocketClient(port);

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All tests in this fixture hardcode the same port (54321). This can make the suite flaky if the port is already in use on the runner, or if multiple test processes/assemblies execute concurrently. Consider selecting a free ephemeral port at runtime (e.g., bind a TcpListener on port 0 to reserve a port, read it, then start the server) and passing that into both server/client.

Copilot uses AI. Check for mistakes.
Copilot AI and others added 2 commits April 21, 2026 15:07
… + README note

Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/46b5e755-967f-4136-b92d-96697f1ecd3b

Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/46b5e755-967f-4136-b92d-96697f1ecd3b

Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
…dary/Dispose

Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/a7e9baba-d7f9-4d66-bac6-a6540c230360

Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
…ider reference

Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/1fd19d7e-9231-44c6-af3c-c266fbf170f3

Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
@bdach

bdach commented Apr 21, 2026

Copy link
Copy Markdown

Your attempts at cosplaying software development are causing continuous annoyance and disruption to our processes. Your robot army's "PRs" keep getting linked to ours, your attempts at tagging releases have required us to yank sentry keys, and from a cursory look through your "development history" you're still having trouble getting anything to run at all which is not surprising in the slightest.

For the interest of our sanity and presumably your wallet, I implore you to stop and delete this repository.

Comment thread osu.Game/IPC/WebSocketServer.cs Fixed
…ebSocketServer.syncRoot

Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/c8ad2ddc-3e69-424b-aa69-46b457a9df9b

Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.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.

7 participants