Tests: stop the DXSTG write-back check skipping on 16K-page hosts

MapAt's candidate addresses are 4K-aligned and none is 16K-aligned, so on
a 16K-page kernel -- Asahi, Apple Silicon, some Android, and one of our own
CI jobs -- the kernel rejects every one of them and the mapping fails. The
write-back check treated that as a precondition and skipped outright, which
took its guest-side assertions with it: the ones that actually pin where a
DXSTG-steered eviction lands, none of which need anything from the host.

The mapping is only the negative control, there to show the write-back did
not ALSO reach the host page carrying the same number. Make it optional.
The guest-side half now runs everywhere and only the control drops out.

DxstgDirtyStaysInsideGuestMemory still skips, and should: it is entirely
about the host page. That leaves one skip here on a 16K-page host instead
of two, and none at all on a 4K one.
This commit is contained in:
Brian Degenhardt
2026-08-10 15:58:14 -07:00
parent 85a88d75da
commit bb9df22fc9
@@ -145,10 +145,14 @@ u32 Obs(int id, const char* name)
// process. Elsewhere the two callers skip. // process. Elsewhere the two callers skip.
// //
// The candidates are 4K-aligned but none is 16K-aligned, so on a 16K-page // The candidates are 4K-aligned but none is 16K-aligned, so on a 16K-page
// kernel — Asahi, Apple Silicon, some Android — every one is rejected outright // kernel — Asahi, Apple Silicon, some Android — every one is rejected outright.
// and the two callers always skip. Re-picking them 16K-aligned would need the // Re-picking them 16K-aligned would need the tag/index constraints re-derived
// tag/index constraints re-derived against the console capture, so it is left // against the console capture, so it is left to whoever holds that data.
// to whoever holds that data. //
// A null return is therefore routine, not exceptional, and callers should treat
// the mapping as an optional negative control rather than a precondition. Only
// DxstgDirtyStaysInsideGuestMemory is wholly about the host page and has to
// skip; the write-back check keeps its guest-side half running everywhere.
void* MapAt(u32* chosen) void* MapAt(u32* chosen)
{ {
#if defined(MAP_FIXED_NOREPLACE) #if defined(MAP_FIXED_NOREPLACE)
@@ -439,14 +443,22 @@ TEST(EeCache2Console, DxstgWriteBackTargetsTheTaggedGuestPage)
constexpr u32 kTargetPage = 0x00129000; constexpr u32 kTargetPage = 0x00129000;
constexpr u32 kTarget = kTargetPage + kSetIndex * 64; constexpr u32 kTarget = kTargetPage + kSetIndex * 64;
// The host mapping is only the negative control: proof that the write-back
// did not ALSO reach the host page that happens to carry the same number.
// Everything else here is guest-side and needs nothing from the host, so it
// runs unconditionally. On a 16K-page kernel -- Asahi, Apple Silicon, some
// Android -- MapAt cannot honour a 4K-aligned request and returns null;
// only the control is skipped then, not the whole test.
u32 page = 0; u32 page = 0;
void* p = MapAt(&page); void* p = MapAt(&page);
if (!p) const u32* host = nullptr;
GTEST_SKIP() << "could not map a page at any candidate host address"; if (p)
ASSERT_EQ(page, kTargetPage) << "the control page moved; re-point kTargetPage"; {
std::memset(p, 0xEE, 0x1000); ASSERT_EQ(page, kTargetPage) << "the control page moved; re-point kTargetPage";
const u32* host = reinterpret_cast<const u32*>( std::memset(p, 0xEE, 0x1000);
static_cast<uptr>(page) + kSetIndex * 64); host = reinterpret_cast<const u32*>(
static_cast<uptr>(page) + kSetIndex * 64);
}
{ {
EeRecTestHarness h; EeRecTestHarness h;
@@ -463,7 +475,8 @@ TEST(EeCache2Console, DxstgWriteBackTargetsTheTaggedGuestPage)
// 64 bytes of guest cache line, at the guest physical page the tag names. // 64 bytes of guest cache line, at the guest physical page the tag names.
EXPECT_EQ(memRead32(kTarget), 0x5A5A0009u); EXPECT_EQ(memRead32(kTarget), 0x5A5A0009u);
EXPECT_EQ(memRead32(kTarget + 4), 0xDEADBEEFu); EXPECT_EQ(memRead32(kTarget + 4), 0xDEADBEEFu);
EXPECT_EQ(host[0], 0xEEEEEEEEu) << "the write-back still reaches a host address"; if (host)
EXPECT_EQ(host[0], 0xEEEEEEEEu) << "the write-back still reaches a host address";
} }
// Never filled, and filled-then-invalidated. Both used to be declined // Never filled, and filled-then-invalidated. Both used to be declined
@@ -471,7 +484,8 @@ TEST(EeCache2Console, DxstgWriteBackTargetsTheTaggedGuestPage)
for (const bool invalidate_first : {false, true}) for (const bool invalidate_first : {false, true})
{ {
SCOPED_TRACE(invalidate_first ? "filled then invalidated" : "never filled"); SCOPED_TRACE(invalidate_first ? "filled then invalidated" : "never filled");
std::memset(p, 0xEE, 0x1000); if (p)
std::memset(p, 0xEE, 0x1000);
EeRecTestHarness h; EeRecTestHarness h;
resetCache(); resetCache();
memWrite32(kTarget, 0xA5A5A5A5u); memWrite32(kTarget, 0xA5A5A5A5u);
@@ -489,10 +503,12 @@ TEST(EeCache2Console, DxstgWriteBackTargetsTheTaggedGuestPage)
RunCacheOp(0x12, kProbeLine); RunCacheOp(0x12, kProbeLine);
RunCacheOp(0x14, kProbeLine); RunCacheOp(0x14, kProbeLine);
EXPECT_EQ(memRead32(kTarget), 0u) << "the cleared line did not write back"; EXPECT_EQ(memRead32(kTarget), 0u) << "the cleared line did not write back";
EXPECT_EQ(host[0], 0xEEEEEEEEu) << "the write-back still reaches a host address"; if (host)
EXPECT_EQ(host[0], 0xEEEEEEEEu) << "the write-back still reaches a host address";
} }
munmap(p, 0x1000); if (p)
munmap(p, 0x1000);
} }
// A DXSTG naming a page that does not resolve to plain guest memory leaves the // A DXSTG naming a page that does not resolve to plain guest memory leaves the