[llvm-branch-commits] [lldb] release/23.x: [lldb] Fix stale L1 memory cache read after memory write (#208347) (PR #216369)
Douglas Yung via llvm-branch-commits
llvm-branch-commits at lists.llvm.org
Tue Aug 18 01:12:33 PDT 2026
https://github.com/dyung updated https://github.com/llvm/llvm-project/pull/216369
>From bbb622ba935324697403cc4b6785ba25b71c47ac Mon Sep 17 00:00:00 2001
From: Yao Qi <yao_qi at apple.com>
Date: Thu, 30 Jul 2026 10:10:28 +0100
Subject: [PATCH] [lldb] Fix stale L1 memory cache read after memory write
(#208347)
A `memory write` can leave stale bytes in the L1 memory cache, so a later
`memory read` of the address that was just written returns the old value.
The L1 cache (`m_L1_cache`) is a map keyed by each chunk's start address, and
chunks can overlap: a read larger than an L2 cache line
(`target.memory-cache-line-size`, 512 by default) bypasses L2 and is stored
whole in L1, so two large reads can produce two chunks that both cover the same
address.
`Flush()` invalidates the L1 cache on a write. It started at the chunk at or
below the flushed address and walked forward, stopping at the first chunk that
did not intersect. It therefore never inspected a chunk that starts below the
flushed address but is long enough to reach into it, leaving that chunk behind
with the stale byte. A later read fully contained in that chunk is served from
the cache and returns the old value.
Fix `Flush()` to walk the whole L1 cache and erase every chunk that intersects
the flushed range, so a chunk starting below the address is dropped too.
Lookups (`FindL1CacheEntry()`) and inserts (`AddL1CacheData()`) are unaffected:
a lookup that lands on the wrong overlapping chunk simply misses and falls
through to L2 or the inferior, which is not a correctness problem.
Add a unit test covering partial overlaps, containment, disjoint and adjacent
chunks, and a flush that must drop a lower-starting chunk as well as several
overlapping chunks.
(cherry picked from commit 6ddd80af0bfbc1927f8bb2935120f6f4cedadc9a)
---
lldb/source/Target/Memory.cpp | 16 ++--
lldb/unittests/Target/MemoryTest.cpp | 117 +++++++++++++++++++++++++++
2 files changed, 125 insertions(+), 8 deletions(-)
diff --git a/lldb/source/Target/Memory.cpp b/lldb/source/Target/Memory.cpp
index f70da27ec058b..6e372fdf3fbee 100644
--- a/lldb/source/Target/Memory.cpp
+++ b/lldb/source/Target/Memory.cpp
@@ -57,18 +57,18 @@ void MemoryCache::Flush(addr_t addr, size_t size) {
std::lock_guard<std::recursive_mutex> guard(m_mutex);
- // Erase any blocks from the L1 cache that intersect with the flush range
+ // L1 chunks can overlap, and a chunk starting below addr can still reach
+ // into the flushed range, so scan the whole L1 cache and erase every chunk
+ // that intersects it.
if (!m_L1_cache.empty()) {
AddrRange flush_range(addr, size);
- BlockMap::iterator pos = m_L1_cache.upper_bound(addr);
- if (pos != m_L1_cache.begin()) {
- --pos;
- }
+ BlockMap::iterator pos = m_L1_cache.begin();
while (pos != m_L1_cache.end()) {
AddrRange chunk_range(pos->first, pos->second->GetByteSize());
- if (!chunk_range.DoesIntersect(flush_range))
- break;
- pos = m_L1_cache.erase(pos);
+ if (chunk_range.DoesIntersect(flush_range))
+ pos = m_L1_cache.erase(pos);
+ else
+ ++pos;
}
}
diff --git a/lldb/unittests/Target/MemoryTest.cpp b/lldb/unittests/Target/MemoryTest.cpp
index 9d04376b4fd5b..f89e9215de713 100644
--- a/lldb/unittests/Target/MemoryTest.cpp
+++ b/lldb/unittests/Target/MemoryTest.cpp
@@ -120,6 +120,15 @@ class DummyProcess : public Process {
void SetMaxReadSize(size_t size) { m_bytes_left = size; }
void SetFiller(int filler) { m_filler = filler; }
};
+
+// A MemoryCache subclass that exposes the otherwise-protected L1 cache so a
+// test can assert on the exact set of chunks it holds.
+class TestMemoryCache : public MemoryCache {
+public:
+ using MemoryCache::MemoryCache;
+
+ const BlockMap &GetL1Cache() const { return m_L1_cache; }
+};
} // namespace
TargetSP CreateTarget(DebuggerSP &debugger_sp, ArchSpec &arch) {
@@ -283,6 +292,114 @@ TEST_F(MemoryTest, TesetMemoryCacheRead) {
// old cache
}
+TEST_F(MemoryTest, TestL1Cache) {
+ ArchSpec arch("arm64-apple-macosx");
+
+ Platform::SetHostPlatform(PlatformRemoteMacOSX::CreateInstance(true, &arch));
+
+ DebuggerSP debugger_sp = Debugger::CreateInstance();
+ ASSERT_TRUE(debugger_sp);
+
+ TargetSP target_sp = CreateTarget(debugger_sp, arch);
+ ASSERT_TRUE(target_sp);
+
+ ProcessSP process_sp = CreateProcess(target_sp);
+ ASSERT_TRUE(process_sp);
+
+ DummyProcess *process = static_cast<DummyProcess *>(process_sp.get());
+ TestMemoryCache mem_cache(*process);
+
+ auto add = [&](lldb::addr_t addr, size_t size, uint8_t fill) {
+ mem_cache.AddL1CacheData(addr,
+ std::make_shared<DataBufferHeap>(size, fill));
+ };
+
+ // Asserts the L1 cache holds exactly `expected` chunks, matched by start
+ // address, byte size, and a single repeated fill byte, in address order.
+ struct Chunk {
+ lldb::addr_t addr;
+ size_t size;
+ uint8_t fill;
+ };
+ auto expect_l1 = [&](std::vector<Chunk> expected) {
+ const auto &l1 = mem_cache.GetL1Cache();
+ ASSERT_EQ(l1.size(), expected.size());
+ size_t i = 0;
+ for (const auto &[addr, data_sp] : l1) {
+ const Chunk &c = expected[i++];
+ EXPECT_EQ(addr, c.addr);
+ ASSERT_EQ(data_sp->GetByteSize(), c.size);
+ const uint8_t *bytes = data_sp->GetBytes();
+ for (size_t j = 0; j < c.size; ++j)
+ EXPECT_EQ(bytes[j], c.fill)
+ << "chunk 0x" << std::hex << addr << " byte " << std::dec << j;
+ }
+ };
+
+ // Partial overlap: the new chunk overhangs the existing one on the right.
+ mem_cache.Clear();
+ add(0x1000, 0x100, 0xAA);
+ add(0x1080, 0x100, 0xBB);
+ expect_l1({{0x1000, 0x100, 0xAA}, {0x1080, 0x100, 0xBB}});
+
+ // Partial overlap: the new chunk overhangs the existing one on the left.
+ mem_cache.Clear();
+ add(0x2080, 0x100, 0xAA);
+ add(0x2000, 0x100, 0xBB);
+ expect_l1({{0x2000, 0x100, 0xBB}, {0x2080, 0x100, 0xAA}});
+
+ // New chunk fully contains an existing one: both are kept.
+ mem_cache.Clear();
+ add(0x3040, 0x40, 0xAA);
+ add(0x3000, 0x100, 0xBB);
+ expect_l1({{0x3000, 0x100, 0xBB}, {0x3040, 0x40, 0xAA}});
+
+ // New chunk is fully contained by an existing one: both are kept.
+ mem_cache.Clear();
+ add(0x4000, 0x200, 0xAA);
+ add(0x4080, 0x80, 0xBB);
+ expect_l1({{0x4000, 0x200, 0xAA}, {0x4080, 0x80, 0xBB}});
+
+ // New chunk partially overlaps two existing chunks; all three are kept.
+ mem_cache.Clear();
+ add(0x5000, 0x80, 0xAA);
+ add(0x5100, 0x80, 0xCC);
+ add(0x5040, 0x100, 0xBB);
+ expect_l1(
+ {{0x5000, 0x80, 0xAA}, {0x5040, 0x100, 0xBB}, {0x5100, 0x80, 0xCC}});
+
+ // Disjoint chunks stay separate.
+ mem_cache.Clear();
+ add(0x6000, 0x80, 0xAA);
+ add(0x6100, 0x80, 0xBB);
+ expect_l1({{0x6000, 0x80, 0xAA}, {0x6100, 0x80, 0xBB}});
+
+ // Adjacent (touching but not overlapping) chunks stay separate.
+ mem_cache.Clear();
+ add(0x7000, 0x80, 0xAA);
+ add(0x7080, 0x80, 0xBB);
+ expect_l1({{0x7000, 0x80, 0xAA}, {0x7080, 0x80, 0xBB}});
+
+ // Flush must erase every chunk intersecting the flush range, including a
+ // chunk that starts below the flushed address. Here 0x8140 lies only in the
+ // lower-starting, longer chunk; it must be dropped while the chunk that does
+ // not intersect survives untouched.
+ mem_cache.Clear();
+ add(0x8000, 0x180, 0xAA);
+ add(0x8080, 0x40, 0xBB);
+ mem_cache.Flush(0x8140, 0x4);
+ expect_l1({{0x8080, 0x40, 0xBB}});
+
+ // A flush intersecting several partially overlapping chunks drops all of
+ // them, while a chunk it does not intersect is left in place.
+ mem_cache.Clear();
+ add(0x9000, 0x80, 0xAA);
+ add(0x9040, 0x100, 0xBB);
+ add(0x9100, 0x80, 0xCC);
+ mem_cache.Flush(0x9060, 0x1);
+ expect_l1({{0x9100, 0x80, 0xCC}});
+}
+
TEST_F(MemoryTest, TestReadInteger) {
ArchSpec arch("x86_64-apple-macosx-");
More information about the llvm-branch-commits
mailing list