StarletBootstrapProbe: wait for StarOS's whole panic path
--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]>
This commit is contained in:
1 parent
a62c65615a
commit
6ede30f99e
4 files changed
+254
-39
No files matched your search
@@ -16,6 +16,9 @@ add_dolphin_test(ESFormatsTest IOS/ES/FormatsTest.cpp)
|
||||
|
||||
add_dolphin_test(StarletARMCoreTest IOS/Starlet/ARMCoreTest.cpp)
|
||||
add_dolphin_test(StarletBootstrapELFTest IOS/Starlet/BootstrapELFTest.cpp)
|
||||
# StarletBootstrapProbe's StarOS panic check (Tools/StarletBootstrapProbe/StarOSPanicCheck.h).
|
||||
add_dolphin_test(StarletProbePanicCheckTest IOS/Starlet/ProbePanicCheckTest.cpp)
|
||||
target_include_directories(StarletProbePanicCheckTest PRIVATE ${CMAKE_SOURCE_DIR}/Tools/StarletBootstrapProbe)
|
||||
add_dolphin_test(StarletDMATest IOS/Starlet/DMATest.cpp)
|
||||
add_dolphin_test(StarletSRAMReadTest IOS/Starlet/SRAMReadTest.cpp)
|
||||
add_dolphin_test(StarletSRAMWriteTest IOS/Starlet/SRAMWriteTest.cpp)
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
// 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
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
|
||||
// What StarletBootstrapProbe requires of a finished StarOS panic path (StarOS ADR-0014 R6). The
|
||||
// path sets __panic_state first, then __panic_code, then publishes the SBR1 boot record when
|
||||
// there is one, so the first nonzero guard can show it half done: the probe keeps running until
|
||||
// every condition holds. Words come through a callback, so the check is tested without a CPU.
|
||||
namespace StarletBootstrapProbe
|
||||
{
|
||||
// StarOS's SBR1 boot record in MEM1 (StarOS crates/boot-contract, report.rs).
|
||||
enum class SBR1Mode
|
||||
{
|
||||
None,
|
||||
// --sbr1-record: the probe creates the record as the loader does.
|
||||
Record,
|
||||
// --sbr1-absent: no record; its area must stay zero.
|
||||
Absent,
|
||||
};
|
||||
constexpr u32 SBR1_BASE = 0x00200000;
|
||||
constexpr u32 SBR1_SIZE = 0x1000;
|
||||
constexpr u32 SBR1_MAGIC = 0x53425231;
|
||||
constexpr u32 SBR1_VERSION = 1;
|
||||
// Word offsets.
|
||||
constexpr u32 SBR1_EXI_OWNER = 0x0c;
|
||||
constexpr u32 SBR1_RELAY_STATUS = 0x10;
|
||||
constexpr u32 SBR1_STARLET_STATE = 0x18;
|
||||
constexpr u32 SBR1_FATAL_REASON = 0x1c;
|
||||
constexpr u32 SBR1_FAULT_CODE = 0x20;
|
||||
constexpr u32 SBR1_RING_LENGTH = 0x2c;
|
||||
constexpr u32 SBR1_RING_DROPPED = 0x30;
|
||||
// Values.
|
||||
constexpr u32 SBR1_EXI_PPC = 1;
|
||||
constexpr u32 SBR1_EXI_STARLET = 2;
|
||||
constexpr u32 SBR1_RELAY_JUMPING = 4;
|
||||
constexpr u32 SBR1_STATE_PANIC = 5;
|
||||
// __panic_state once the path is entered; __panic_code of a Rust panic.
|
||||
constexpr u32 PANIC_ENTERED = 1;
|
||||
constexpr u32 PANIC_CODE_RUST = 1;
|
||||
|
||||
// The first condition of a finished panic path that does not hold, or an empty string:
|
||||
// - __panic_state 1 and __panic_code 1;
|
||||
// - with --sbr1-record: STARLET_STATE 5 (Panic), FATAL_REASON 0 and EXI_OWNER 1 (PPC);
|
||||
// - with --sbr1-absent: every word of the record area still zero.
|
||||
// read_mem1 returns the big-endian MEM1 word at an address.
|
||||
inline std::string UnmetPanicCondition(u32 panic_state, u32 panic_code, SBR1Mode sbr1,
|
||||
const std::function<u32(u32)>& read_mem1)
|
||||
{
|
||||
if (panic_state != PANIC_ENTERED)
|
||||
return fmt::format("__panic_state is {}, not {}", panic_state, PANIC_ENTERED);
|
||||
if (panic_code != PANIC_CODE_RUST)
|
||||
return fmt::format("__panic_code is {}, not {}", panic_code, PANIC_CODE_RUST);
|
||||
if (sbr1 == SBR1Mode::Record)
|
||||
{
|
||||
const struct
|
||||
{
|
||||
const char* name;
|
||||
u32 offset;
|
||||
u32 expected;
|
||||
} fields[] = {{"STARLET_STATE", SBR1_STARLET_STATE, SBR1_STATE_PANIC},
|
||||
{"FATAL_REASON", SBR1_FATAL_REASON, 0},
|
||||
{"EXI_OWNER", SBR1_EXI_OWNER, SBR1_EXI_PPC}};
|
||||
for (const auto& field : fields)
|
||||
{
|
||||
const u32 value = read_mem1(SBR1_BASE + field.offset);
|
||||
if (value != field.expected)
|
||||
return fmt::format("SBR1 {} is {}, not {}", field.name, value, field.expected);
|
||||
}
|
||||
}
|
||||
if (sbr1 == SBR1Mode::Absent)
|
||||
{
|
||||
for (u32 offset = 0; offset < SBR1_SIZE; offset += 4)
|
||||
{
|
||||
const u32 value = read_mem1(SBR1_BASE + offset);
|
||||
if (value != 0)
|
||||
return fmt::format("SBR1 area word +{:#x} is {:#x} without a record", offset, value);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
} // namespace StarletBootstrapProbe
|
||||
@@ -17,9 +17,12 @@
|
||||
#include "Core/IOS/Starlet/BootstrapELF.h"
|
||||
#include "Core/IOS/Starlet/StarletMemory.h"
|
||||
#include "Core/System.h"
|
||||
#include "StarOSPanicCheck.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace StarletBootstrapProbe;
|
||||
|
||||
enum class ExpectedOutcome
|
||||
{
|
||||
Idle,
|
||||
@@ -32,21 +35,8 @@ enum class ExpectedOutcome
|
||||
// Exit status of a run stopped by a Dolphin alert (1: failed expectation, 2: usage).
|
||||
constexpr int ALERT_EXIT_STATUS = 3;
|
||||
|
||||
// StarOS's SBR1 boot record in MEM1 (StarOS crates/boot-contract, report.rs).
|
||||
enum class SBR1Mode
|
||||
{
|
||||
None,
|
||||
// --sbr1-record: create the record as the loader does, report it after the run.
|
||||
Record,
|
||||
// --sbr1-absent: no record; report the area after the run.
|
||||
Absent,
|
||||
};
|
||||
constexpr u32 SBR1_BASE = 0x00200000;
|
||||
constexpr u32 SBR1_MAGIC = 0x53425231;
|
||||
constexpr u32 SBR1_VERSION = 1;
|
||||
constexpr u32 SBR1_SIZE = 0x1000;
|
||||
constexpr u32 SBR1_EXI_STARLET = 2;
|
||||
constexpr u32 SBR1_RELAY_JUMPING = 4;
|
||||
// Slices run once a StarOS panic path looks finished, to see that it stays so.
|
||||
constexpr int PANIC_SETTLE_SLICES = 2;
|
||||
|
||||
// Dolphin's default handler shows an alert in a modal MessageBox on Windows, which blocks the
|
||||
// probe until someone closes it. Report the alert on stderr and fail the run at once instead.
|
||||
@@ -57,7 +47,8 @@ bool FailOnAlert(const char* caption, const char* text, bool, Common::MsgType)
|
||||
std::_Exit(ALERT_EXIT_STATUS);
|
||||
}
|
||||
|
||||
bool Run(const std::string& path, bool jit, bool mmu_faults, ExpectedOutcome expected)
|
||||
bool Run(const std::string& path, bool jit, bool mmu_faults, SBR1Mode sbr1,
|
||||
ExpectedOutcome expected)
|
||||
{
|
||||
std::ifstream file(path, std::ios::binary | std::ios::ate);
|
||||
if (!file || file.tellg() <= 0 || file.tellg() > 16 * 1024 * 1024)
|
||||
@@ -95,6 +86,26 @@ bool Run(const std::string& path, bool jit, bool mmu_faults, ExpectedOutcome exp
|
||||
core.SetMMUFaultsEnabled(mmu_faults);
|
||||
constexpr u32 TRACE_MAGIC = 0x52455631;
|
||||
constexpr u32 TRACE_CAPACITY = 1024;
|
||||
auto& mem1 = system.GetMemory();
|
||||
const auto read_mem1 = [&mem1](u32 address) { return mem1.Read_U32(address); };
|
||||
const auto panic_unmet = [&] {
|
||||
return UnmetPanicCondition(memory.Read32(info.panic_state), memory.Read32(info.panic_code),
|
||||
sbr1, read_mem1);
|
||||
};
|
||||
// StarOS's panic path writes no trace line. Print the trace published so far, for the
|
||||
// scenario's ordered checks, and the panic words.
|
||||
const auto report_panic = [&] {
|
||||
const u32 length = memory.Read32(info.trace) == TRACE_MAGIC ?
|
||||
std::min(memory.Read32(info.trace + 4), TRACE_CAPACITY) :
|
||||
0;
|
||||
for (u32 index = 0; index < length; ++index)
|
||||
std::cout << static_cast<char>(memory.Read8(info.trace + 8 + index));
|
||||
std::cout << "PC=0x" << std::hex << core.GetRegister(15) << std::dec
|
||||
<< " instructions=" << core.GetExecutedInstructions() << '\n';
|
||||
std::cerr << "Starlet panic state " << memory.Read32(info.panic_state) << " code "
|
||||
<< memory.Read32(info.panic_code) << '\n';
|
||||
};
|
||||
int panic_settled = 0;
|
||||
for (int slice = 0; slice < 512; ++slice)
|
||||
{
|
||||
core.SetIRQLine(system.GetWiiIPC().IsStarletIRQAsserted());
|
||||
@@ -109,21 +120,25 @@ bool Run(const std::string& path, bool jit, bool mmu_faults, ExpectedOutcome exp
|
||||
return expected == ExpectedOutcome::UndefinedFault && fault_code == 1 &&
|
||||
memory.Read32(info.fault_lr) != 0 && memory.Read32(info.fault_spsr) != 0;
|
||||
}
|
||||
// StarOS's panic path writes no trace line: it sets its guard word, then its code, and halts.
|
||||
// The trace published so far is printed for the scenario's ordered checks.
|
||||
// StarOS's panic path sets its guard word first, then its code, then publishes the SBR1
|
||||
// record when there is one, and halts. An expected panic passes once all of that holds
|
||||
// (UnmetPanicCondition) and still holds PANIC_SETTLE_SLICES slices later.
|
||||
if (info.panic_state != 0 && memory.Read32(info.panic_state) != 0)
|
||||
{
|
||||
const u32 panic_state = memory.Read32(info.panic_state);
|
||||
const u32 panic_code = memory.Read32(info.panic_code);
|
||||
const u32 length = memory.Read32(info.trace) == TRACE_MAGIC ?
|
||||
std::min(memory.Read32(info.trace + 4), TRACE_CAPACITY) :
|
||||
0;
|
||||
for (u32 index = 0; index < length; ++index)
|
||||
std::cout << static_cast<char>(memory.Read8(info.trace + 8 + index));
|
||||
std::cout << "PC=0x" << std::hex << core.GetRegister(15) << std::dec
|
||||
<< " instructions=" << core.GetExecutedInstructions() << '\n';
|
||||
std::cerr << "Starlet panic state " << panic_state << " code " << panic_code << '\n';
|
||||
return expected == ExpectedOutcome::Panic && panic_state == 1;
|
||||
if (expected != ExpectedOutcome::Panic)
|
||||
{
|
||||
report_panic();
|
||||
return false;
|
||||
}
|
||||
if (!panic_unmet().empty())
|
||||
{
|
||||
panic_settled = 0;
|
||||
continue;
|
||||
}
|
||||
if (++panic_settled <= PANIC_SETTLE_SLICES)
|
||||
continue;
|
||||
report_panic();
|
||||
return true;
|
||||
}
|
||||
if (memory.Read32(info.trace) != TRACE_MAGIC)
|
||||
continue;
|
||||
@@ -174,6 +189,14 @@ bool Run(const std::string& path, bool jit, bool mmu_faults, ExpectedOutcome exp
|
||||
expected == ExpectedOutcome::Idle && trace.find("REV:BOOT:IDLE\n") != std::string::npos;
|
||||
return startup_ok && outcome_ok;
|
||||
}
|
||||
if (info.panic_state != 0 && memory.Read32(info.panic_state) != 0)
|
||||
{
|
||||
report_panic();
|
||||
const std::string unmet = panic_unmet();
|
||||
std::cerr << "StarOS panic path unfinished after 2097152 ARM cycles: "
|
||||
<< (unmet.empty() ? "it ended inside the settle slices" : unmet) << '\n';
|
||||
return false;
|
||||
}
|
||||
std::cerr << "No terminal bootstrap marker after 2097152 ARM cycles, PC=0x" << std::hex
|
||||
<< core.GetRegister(15) << std::dec << '\n';
|
||||
return false;
|
||||
@@ -246,21 +269,22 @@ int main(int argc, char** argv)
|
||||
// channel 1 given to the Starlet, the magic written last.
|
||||
memory.Write_U32(SBR1_VERSION, SBR1_BASE + 0x04);
|
||||
memory.Write_U32(SBR1_SIZE, SBR1_BASE + 0x08);
|
||||
memory.Write_U32(SBR1_EXI_STARLET, SBR1_BASE + 0x0c);
|
||||
memory.Write_U32(SBR1_RELAY_JUMPING, SBR1_BASE + 0x10);
|
||||
memory.Write_U32(SBR1_EXI_STARLET, SBR1_BASE + SBR1_EXI_OWNER);
|
||||
memory.Write_U32(SBR1_RELAY_JUMPING, SBR1_BASE + SBR1_RELAY_STATUS);
|
||||
memory.Write_U32(SBR1_MAGIC, SBR1_BASE);
|
||||
}
|
||||
const bool passed = Run(argv[1], jit, mmu_faults, expected);
|
||||
const bool passed = Run(argv[1], jit, mmu_faults, sbr1, expected);
|
||||
if (sbr1 != SBR1Mode::None)
|
||||
{
|
||||
// The record's summary after the run; with --sbr1-absent every word must still be zero.
|
||||
// The record's summary after the run; with --sbr1-absent every word must still be zero (an
|
||||
// expected panic fails otherwise).
|
||||
std::cerr << std::hex << "SBR1 magic 0x" << memory.Read_U32(SBR1_BASE) << std::dec
|
||||
<< " state " << memory.Read_U32(SBR1_BASE + 0x18) << " reason "
|
||||
<< memory.Read_U32(SBR1_BASE + 0x1c) << " owner "
|
||||
<< memory.Read_U32(SBR1_BASE + 0x0c) << " fault "
|
||||
<< memory.Read_U32(SBR1_BASE + 0x20) << " ring "
|
||||
<< memory.Read_U32(SBR1_BASE + 0x2c) << " dropped "
|
||||
<< memory.Read_U32(SBR1_BASE + 0x30);
|
||||
<< " state " << memory.Read_U32(SBR1_BASE + SBR1_STARLET_STATE) << " reason "
|
||||
<< memory.Read_U32(SBR1_BASE + SBR1_FATAL_REASON) << " owner "
|
||||
<< memory.Read_U32(SBR1_BASE + SBR1_EXI_OWNER) << " fault "
|
||||
<< memory.Read_U32(SBR1_BASE + SBR1_FAULT_CODE) << " ring "
|
||||
<< memory.Read_U32(SBR1_BASE + SBR1_RING_LENGTH) << " dropped "
|
||||
<< memory.Read_U32(SBR1_BASE + SBR1_RING_DROPPED);
|
||||
u32 written = 0;
|
||||
for (u32 offset = 0; offset < SBR1_SIZE; offset += 4)
|
||||
written += memory.Read_U32(SBR1_BASE + offset) != 0;
|
||||
|
||||
Reference in new issue
Block a user