Skip to content

Fix storyboard decode hanging on self-referential variable definitions - #38505

Merged
peppy merged 3 commits into
ppy:masterfrom
YuuLuo:fix-storyboard-variable-substitution-hang
Aug 3, 2026
Merged

Fix storyboard decode hanging on self-referential variable definitions#38505
peppy merged 3 commits into
ppy:masterfrom
YuuLuo:fix-storyboard-variable-substitution-hang

Conversation

@YuuLuo

@YuuLuo YuuLuo commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

A storyboard [Variables] entry whose value re-introduces its own key causes LegacyStoryboardDecoder.decodeVariables() to loop forever, hanging the game.

Details

decodeVariables() substitutes every variable into a line, repeating until a full pass makes no change:

while (line.Contains('$'))
{
    string origLine = line;

    foreach (var v in variables)
        line = line.Replace(v.Key, v.Value);

    if (line == origLine)
        break;
}

If a variable's value contains its own key (e.g. $a=x$a), each pass replaces $a with x$a, so the line grows by one character every iteration, always still contains $, and never equals origLine — the loop never terminates. (A doubling form such as $a=$a$a instead grows the line exponentially until it throws OutOfMemoryException.)

The storyboard is decoded lazily when a beatmap is played, so a beatmap containing such a storyboard imports normally and then hangs the client the moment the map is started. The try/catch in WorkingBeatmapCache.GetStoryboard() cannot catch a non-terminating loop and there is no watchdog, so recovery requires force-killing the process.

Minimal reproducer (as a .osb, or the [Variables]/[Events] sections of a .osu):

[Variables]
$a=x$a

[Events]
$a

Fix

Substitute each defined variable a single time instead of repeatedly re-scanning the line. Storyboard variables are plain text substitutions and are not expected to reference other variables, so a single pass resolves every well-formed reference (including the existing variable-with-suffix case) while removing the re-scan that never terminates for a self-referential definition.

Added a regression test that decodes a self-referential definition and asserts the surrounding well-formed sprite still decodes correctly.

A [Variables] entry whose value re-introduces its own key (e.g. $a=x$a) makes decodeVariables() re-scan and grow the line on every pass, so it never stops containing '$' and the loop never terminates. Decoding such a storyboard hangs the game with no way to recover other than force-killing the process.

Substitute each defined variable a single time instead of looping until the line stabilises: storyboard variables are plain text substitutions not expected to reference other variables, so a single pass resolves every well-formed reference. Adds a regression test covering a self-referential definition.
@bdach

bdach commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Did you use "AI" tools when making this PR?

@YuuLuo

YuuLuo commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Did you use "AI" tools when making this PR?

I used translation tool to help me draft this PR.

@bdach bdach added the area:beatmap parsing .osu file format parsing label Aug 3, 2026
@bdach
bdach self-requested a review August 3, 2026 11:31

@bdach bdach left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

matches stable

@peppy
peppy merged commit 535df55 into ppy:master Aug 3, 2026
9 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:beatmap parsing .osu file format parsing size/M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants