--expect-panic passed as soon as __panic_state was nonzero at the end of a 4096-cycle slice. StarOS's handler stores the guard first, then the code, then publishes the SBR1 record, so the run could stop with the path half done. The probe now keeps running until the path is finished and still is two slices later, and fails if the run budget ends first: - __panic_state 1 and __panic_code 1; - with --sbr1-record: STARLET_STATE 5 (Panic), FATAL_REASON 0 and EXI_OWNER 1 (handed back to the PPC); - with --sbr1-absent: every word of the record area still zero. The failure names the first unmet condition. The check is StarOSPanicCheck.h, pure, with the SBR1 constants moved there, and StarletProbePanicCheckTest covers each condition; each of 7 mutants of the check fails a test. Runs that do not expect a panic are unchanged. With StarOS's panic test image (eb930ee), interpreter and JIT: the scenarios pass, two slices later than before. Three images derived from it pass the previous probe and now fail: EXI kept by the Starlet (--sbr1-record), __panic_code never stored, the record written although absent (--sbr1-absent). (StarOS MMU-001 review of F1.) Co-Authored-By: Claude Opus 5.5 <[email protected]>
100 lines
3.6 KiB
C++
100 lines
3.6 KiB
C++
// Copyright 2026 Dolphin Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#include <map>
|
|
#include <string>
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include "StarOSPanicCheck.h"
|
|
|
|
namespace StarletBootstrapProbe
|
|
{
|
|
namespace
|
|
{
|
|
// MEM1 words by address; absent words read as zero.
|
|
class FakeMem1
|
|
{
|
|
public:
|
|
std::map<u32, u32> 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
|