// Copyright 2026 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include #include #include "StarOSPanicCheck.h" namespace StarletBootstrapProbe { namespace { // MEM1 words by address; absent words read as zero. class FakeMem1 { public: std::map words; u32 operator()(u32 address) const { const auto it = words.find(address); return it == words.end() ? 0 : it->second; } }; // The record as the loader leaves it, before the image runs. FakeMem1 CreatedRecord() { FakeMem1 mem1; mem1.words[SBR1_BASE] = SBR1_MAGIC; mem1.words[SBR1_BASE + 0x04] = SBR1_VERSION; mem1.words[SBR1_BASE + 0x08] = SBR1_SIZE; mem1.words[SBR1_BASE + SBR1_EXI_OWNER] = SBR1_EXI_STARLET; mem1.words[SBR1_BASE + SBR1_RELAY_STATUS] = SBR1_RELAY_JUMPING; return mem1; } // The record once StarOS's panic path has published it. FakeMem1 PanicRecord() { FakeMem1 mem1 = CreatedRecord(); mem1.words[SBR1_BASE + SBR1_STARLET_STATE] = SBR1_STATE_PANIC; mem1.words[SBR1_BASE + SBR1_EXI_OWNER] = SBR1_EXI_PPC; return mem1; } TEST(StarletProbePanicCheck, WaitsForTheCodeAfterTheGuard) { const FakeMem1 none; EXPECT_EQ(UnmetPanicCondition(1, 1, SBR1Mode::None, none), ""); // The guard is stored before the code: a slice can end in between. EXPECT_EQ(UnmetPanicCondition(1, 0, SBR1Mode::None, none), "__panic_code is 0, not 1"); EXPECT_EQ(UnmetPanicCondition(1, 2, SBR1Mode::None, none), "__panic_code is 2, not 1"); EXPECT_EQ(UnmetPanicCondition(0, 1, SBR1Mode::None, none), "__panic_state is 0, not 1"); EXPECT_EQ(UnmetPanicCondition(2, 1, SBR1Mode::None, none), "__panic_state is 2, not 1"); // The words come first, whatever the record says. EXPECT_EQ(UnmetPanicCondition(1, 0, SBR1Mode::Record, PanicRecord()), "__panic_code is 0, not 1"); } TEST(StarletProbePanicCheck, RecordNeedsPanicStateReasonAndEXIBack) { EXPECT_EQ(UnmetPanicCondition(1, 1, SBR1Mode::Record, PanicRecord()), ""); // Panic words set, record not yet published. EXPECT_EQ(UnmetPanicCondition(1, 1, SBR1Mode::Record, CreatedRecord()), "SBR1 STARLET_STATE is 0, not 5"); FakeMem1 running = PanicRecord(); running.words[SBR1_BASE + SBR1_STARLET_STATE] = 2; EXPECT_EQ(UnmetPanicCondition(1, 1, SBR1Mode::Record, running), "SBR1 STARLET_STATE is 2, not 5"); FakeMem1 reason = PanicRecord(); reason.words[SBR1_BASE + SBR1_FATAL_REASON] = 3; EXPECT_EQ(UnmetPanicCondition(1, 1, SBR1Mode::Record, reason), "SBR1 FATAL_REASON is 3, not 0"); // State published, EXI not yet handed back to the PPC. FakeMem1 exi = PanicRecord(); exi.words[SBR1_BASE + SBR1_EXI_OWNER] = SBR1_EXI_STARLET; EXPECT_EQ(UnmetPanicCondition(1, 1, SBR1Mode::Record, exi), "SBR1 EXI_OWNER is 2, not 1"); } TEST(StarletProbePanicCheck, AbsentRecordAreaStaysZero) { EXPECT_EQ(UnmetPanicCondition(1, 1, SBR1Mode::Absent, FakeMem1{}), ""); for (const u32 offset : {0x0u, SBR1_STARLET_STATE, SBR1_EXI_OWNER, SBR1_SIZE - 4}) { FakeMem1 written; written.words[SBR1_BASE + offset] = 5; EXPECT_EQ(UnmetPanicCondition(1, 1, SBR1Mode::Absent, written), fmt::format("SBR1 area word +{:#x} is 0x5 without a record", offset)); } // Words outside the area are not the record's. FakeMem1 outside; outside.words[SBR1_BASE - 4] = 5; outside.words[SBR1_BASE + SBR1_SIZE] = 5; EXPECT_EQ(UnmetPanicCondition(1, 1, SBR1Mode::Absent, outside), ""); // Without an SBR1 option, a written area is not checked. EXPECT_EQ(UnmetPanicCondition(1, 1, SBR1Mode::None, PanicRecord()), ""); } } // namespace } // namespace StarletBootstrapProbe