Skip to content

Dedicated server SIGSEGV on research completion (any notification): off-by-one in FlavorTextRandomizer random index #286

Description

@Cadacious

Summary

Distinct crash from #285, same file, different bug and different in-game trigger: the random flavor-text pick in ANotifications::FlavorTextRandomizer uses an inclusive upper bound, so every webhook notification has a 1-in-(N+1) chance of an out-of-bounds array access that SIGSEGVs the dedicated server. We hit it on a research completion mid-session (not a player join), via AFGResearchManager::Multicast_ResearchCompleted → FRM's Research notification.

Filing separately from #285 per the different repro path; a comment there (2026-06-07) contains an earlier write-up of the same root cause.

Environment

Same as #285: FRM dev @ a0b6056d CI artifacts, SML 3.12.0, Satisfactory 1.2.2.2 CL#491125, Linux dedicated server (Ubuntu 22.04 container), mod at FactoryGame/Mods/GameFeatures/FicsitRemoteMonitoring/.

Note: this build predates last night's fixes. We applied the #285 inline-JSON workaround (so the flavor JSON parses successfully with 3-entry arrays), which is what exposed this second bug — with 3-entry arrays the crash odds are 25% per notification.

Repro

  1. Run a dedicated server with notifications reachable (on a0b6056d: any state; on current dev HEAD: requires DiscIT.URL set, since the new Dedicated server SIGSEGV on every player join (dev): FlavorTextRandomizer parses config path as JSON content #285 early-out otherwise skips the path).
  2. Ensure the flavor JSON parses (e.g. the Dedicated server SIGSEGV on every player join (dev): FlavorTextRandomizer parses config path as JSON content #285 inline-JSON workaround).
  3. Play normally — each notification event (research completion, player join/leave, doggo, power, train) rolls the dice. With N flavor entries applicable to the event, P(crash) = 1/(N+1) per event.

Observed trigger in our case: research timer completion ~16 minutes into a session.

Stack trace

Assertion failed: (Index >= 0) & (Index < ArrayNum) [File:Runtime/Core/Public\Containers/Array.h] [Line: 1067]
Array index out of bounds: 3 into an array of size 3
Unhandled Exception: SIGSEGV

ANotifications::FlavorTextRandomizer(EFlavorType)            [Array.h:1067]
ANotifications::SendWebhook(TMap<FString,FString>, EFlavorType) [Notifications.cpp:61]
ANotifications::execSendWebhook(...)
... TMulticastScriptDelegate::ProcessMulticastDelegate
... FResearchCompletedDelegate_DelegateWrapper
... AFGResearchManager::Multicast_ResearchCompleted_Implementation
... AFGResearchManager::OnResearchTimerComplete                [FGResearchManager.cpp:918]

Root cause

Source/FicsitRemoteMonitoring/Private/Libraries/Notifications.cpp, end of FlavorTextRandomizer (still present on dev HEAD d38e00e4 as of filing):

const int32 len = FlavorArray.Num();
const int32 rng = UKismetMathLibrary::RandomIntegerInRange(0, len); // inclusive on BOTH ends
return FlavorArray[rng];                                            // OOB when rng == len

UKismetMathLibrary::RandomIntegerInRange(Min, Max) includes Max in its range.

Suggested fix

const int32 len = FlavorArray.Num();
if (len == 0)
{
    return TEXT(""); // or a fixed fallback string — also covers a flavor JSON with no applicable keys
}
return FlavorArray[UKismetMathLibrary::RandomIntegerInRange(0, len - 1)];

The len == 0 guard matters independently: with the new IsValid guard from #285 in place, a valid flavor JSON that lacks the relevant keys produces an empty FlavorArray, and RandomIntegerInRange(0, 0)FlavorArray[0] would be a guaranteed OOB on a size-0 array.

Workarounds


Diagnosis assisted by Claude Code; crash from a live dedicated server.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions