[Lldb-commits] [lldb] [lldb] Keep L1 memory cache chunks disjoint (PR #208347)
Yao Qi via lldb-commits
lldb-commits at lists.llvm.org
Mon Jul 27 08:54:30 PDT 2026
https://github.com/qiyao updated https://github.com/llvm/llvm-project/pull/208347
>From 1ab846b9f6f33d09ad508cc5d6c7316a0a7e59c7 Mon Sep 17 00:00:00 2001
From: Yao Qi <yao_qi at apple.com>
Date: Wed, 8 Jul 2026 21:41:10 +0100
Subject: [PATCH] [lldb] Fix stale L1 memory cache read after memory write
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. 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()` started at the chunk at
or below the flushed address and walked forward, so it never revisited a chunk
that starts lower but is long enough to also cover the address, leaving that
chunk in the cache with the stale byte.
Keep the L1 cache simple and allow chunks to overlap; make invalidation drop
every chunk a write touches. The largest chunk length ever inserted is tracked
in `m_L1_max_chunk_byte_size`, which bounds a scan window so `Flush()` and
`FindL1CacheEntry()` can find every chunk reaching into an address even when it
starts below that address. `AddL1CacheData()` prunes fully-contained
duplicates: it skips an insert already covered by an existing chunk and drops
existing chunks the new one covers, so no chunk is ever fully contained in
another. Partial overlaps are kept as separate chunks.
Add a unit test covering partial overlaps, both containment cases, disjoint and
adjacent chunks, and a flush that must drop a lower-starting chunk as well as
several overlapping chunks.
---
lldb/include/lldb/Target/Memory.h | 17 +++-
lldb/source/Target/Memory.cpp | 63 +++++++++----
lldb/unittests/Target/MemoryTest.cpp | 136 +++++++++++++++++++++++++++
3 files changed, 197 insertions(+), 19 deletions(-)
diff --git a/lldb/include/lldb/Target/Memory.h b/lldb/include/lldb/Target/Memory.h
index 2b8655e277a29..1e400251823aa 100644
--- a/lldb/include/lldb/Target/Memory.h
+++ b/lldb/include/lldb/Target/Memory.h
@@ -63,9 +63,15 @@ class MemoryCache {
typedef Range<lldb::addr_t, lldb::addr_t> AddrRange;
// Classes that inherit from MemoryCache can see and modify these
std::recursive_mutex m_mutex;
- BlockMap m_L1_cache; // A first level memory cache whose chunk sizes vary that
- // will be used only if the memory read fits entirely in
- // a chunk
+ // A first level memory cache whose chunk sizes vary, used only if a read fits
+ // entirely in a chunk. Chunks may partially overlap, but no chunk is ever
+ // fully contained in another: AddL1CacheData skips an insert already covered
+ // by an existing chunk and drops existing chunks the new one covers.
+ BlockMap m_L1_cache;
+ // The largest chunk length ever inserted into m_L1_cache, used to bound the
+ // scan window in AddL1CacheData/Flush/FindL1CacheEntry so they find every
+ // chunk reaching into an address even when it starts below that address.
+ size_t m_L1_max_chunk_byte_size = 0;
BlockMap m_L2_cache; // A memory cache of fixed size chinks
// (m_L2_cache_line_byte_size bytes in size each)
InvalidRanges m_invalid_ranges;
@@ -78,6 +84,11 @@ class MemoryCache {
lldb::DataBufferSP GetL2CacheLine(lldb::addr_t addr, Status &error);
+ // Returns the lowest address at which a cached chunk could still reach into
+ // addr, i.e. addr minus the largest chunk length seen (clamped, never
+ // underflowing). Caller must hold m_mutex.
+ lldb::addr_t GetLowestPossibleChunkStart(lldb::addr_t addr) const;
+
// If the entire range [addr, addr+len) is covered by a single L1 entry,
// returns a pointer into that entry's data at the correct offset. Returns
// nullptr on a miss. Caller must hold m_mutex.
diff --git a/lldb/source/Target/Memory.cpp b/lldb/source/Target/Memory.cpp
index f70da27ec058b..06827ec760016 100644
--- a/lldb/source/Target/Memory.cpp
+++ b/lldb/source/Target/Memory.cpp
@@ -16,6 +16,7 @@
#include "llvm/ADT/STLExtras.h"
+#include <algorithm>
#include <cinttypes>
#include <memory>
@@ -34,6 +35,7 @@ MemoryCache::~MemoryCache() = default;
void MemoryCache::Clear(bool clear_invalid_ranges) {
std::lock_guard<std::recursive_mutex> guard(m_mutex);
m_L1_cache.clear();
+ m_L1_max_chunk_byte_size = 0;
m_L2_cache.clear();
if (clear_invalid_ranges)
m_invalid_ranges.Clear();
@@ -45,10 +47,37 @@ void MemoryCache::AddL1CacheData(lldb::addr_t addr, const void *src,
AddL1CacheData(addr, std::make_shared<DataBufferHeap>(src, src_len));
}
+addr_t MemoryCache::GetLowestPossibleChunkStart(addr_t addr) const {
+ if (m_L1_max_chunk_byte_size == 0)
+ return addr;
+ const addr_t max_reach = m_L1_max_chunk_byte_size - 1;
+ return addr >= max_reach ? addr - max_reach : 0;
+}
+
void MemoryCache::AddL1CacheData(lldb::addr_t addr,
const DataBufferSP &data_buffer_sp) {
std::lock_guard<std::recursive_mutex> guard(m_mutex);
+ const size_t data_byte_size = data_buffer_sp->GetByteSize();
+ if (data_byte_size == 0)
+ return;
+
+ AddrRange new_range(addr, data_byte_size);
+ addr_t lowest_possible_start = GetLowestPossibleChunkStart(addr);
+
+ BlockMap::iterator pos = m_L1_cache.lower_bound(lowest_possible_start);
+ while (pos != m_L1_cache.end() && pos->first < new_range.GetRangeEnd()) {
+ AddrRange chunk_range(pos->first, pos->second->GetByteSize());
+ if (chunk_range.Contains(new_range))
+ return;
+ if (new_range.Contains(chunk_range)) {
+ pos = m_L1_cache.erase(pos);
+ continue;
+ }
+ ++pos;
+ }
+
m_L1_cache[addr] = data_buffer_sp;
+ m_L1_max_chunk_byte_size = std::max(m_L1_max_chunk_byte_size, data_byte_size);
}
void MemoryCache::Flush(addr_t addr, size_t size) {
@@ -57,18 +86,19 @@ 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
+ // Erase any blocks from the L1 cache that intersect with the flush range. A
+ // chunk that intersects cannot start earlier than this, so scan a bounded
+ // window rather than the whole cache.
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;
- }
- while (pos != m_L1_cache.end()) {
+ addr_t lowest_possible_start = GetLowestPossibleChunkStart(addr);
+ BlockMap::iterator pos = m_L1_cache.lower_bound(lowest_possible_start);
+ while (pos != m_L1_cache.end() && pos->first < flush_range.GetRangeEnd()) {
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;
}
}
@@ -129,13 +159,14 @@ const uint8_t *MemoryCache::FindL1CacheEntry(lldb::addr_t addr,
if (m_L1_cache.empty())
return nullptr;
AddrRange read_range(addr, len);
- BlockMap::const_iterator pos = m_L1_cache.upper_bound(addr);
- if (pos != m_L1_cache.begin())
- --pos;
- AddrRange chunk_range(pos->first, pos->second->GetByteSize());
- if (!chunk_range.Contains(read_range))
- return nullptr;
- return pos->second->GetBytes() + (addr - chunk_range.GetRangeBase());
+ addr_t lowest_possible_start = GetLowestPossibleChunkStart(addr);
+ BlockMap::const_iterator pos = m_L1_cache.lower_bound(lowest_possible_start);
+ for (; pos != m_L1_cache.end() && pos->first <= addr; ++pos) {
+ AddrRange chunk_range(pos->first, pos->second->GetByteSize());
+ if (chunk_range.Contains(read_range))
+ return pos->second->GetBytes() + (addr - chunk_range.GetRangeBase());
+ }
+ return nullptr;
}
lldb::DataBufferSP MemoryCache::GetL2CacheLine(lldb::addr_t line_base_addr,
diff --git a/lldb/unittests/Target/MemoryTest.cpp b/lldb/unittests/Target/MemoryTest.cpp
index 9d04376b4fd5b..3651e93257e3a 100644
--- a/lldb/unittests/Target/MemoryTest.cpp
+++ b/lldb/unittests/Target/MemoryTest.cpp
@@ -283,6 +283,142 @@ TEST_F(MemoryTest, TesetMemoryCacheRead) {
// old cache
}
+TEST_F(MemoryTest, TestL1CacheOverlapAndFlush) {
+ 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());
+ MemoryCache &mem_cache = process->GetMemoryCache();
+ Status error;
+
+ auto add = [&](lldb::addr_t addr, size_t size, uint8_t fill) {
+ mem_cache.AddL1CacheData(addr,
+ std::make_shared<DataBufferHeap>(size, fill));
+ };
+
+ auto read_sp = std::make_shared<DataBufferHeap>(0x400, 0);
+ auto l1_read = [&](lldb::addr_t addr, size_t size) -> size_t {
+ process->SetMaxReadSize(0);
+ memset(read_sp->GetBytes(), 0, size);
+ return mem_cache.Read(addr, read_sp->GetBytes(), size, error);
+ };
+ auto bytes_in = [&](size_t offset, size_t len, uint8_t val) -> bool {
+ const uint8_t *bytes = read_sp->GetBytes();
+ for (size_t i = offset; i < offset + len; ++i)
+ if (bytes[i] != val)
+ return false;
+ return true;
+ };
+
+ // Partial overlap (extend right): both chunks are kept and each serves reads
+ // fully inside itself; a read spanning both misses.
+ mem_cache.Clear();
+ add(0x1000, 0x100, 0xAA);
+ add(0x1080, 0x100, 0xBB);
+ EXPECT_EQ(l1_read(0x1000, 0x100), 0x100u);
+ EXPECT_TRUE(bytes_in(0x0, 0x100, 0xAA));
+ EXPECT_EQ(l1_read(0x1080, 0x100), 0x100u);
+ EXPECT_TRUE(bytes_in(0x0, 0x100, 0xBB));
+ EXPECT_NE(l1_read(0x1000, 0x180), 0x180u);
+
+ // Partial overlap (extend left): symmetric to the above.
+ mem_cache.Clear();
+ add(0x2080, 0x100, 0xAA);
+ add(0x2000, 0x100, 0xBB);
+ EXPECT_EQ(l1_read(0x2000, 0x100), 0x100u);
+ EXPECT_TRUE(bytes_in(0x0, 0x100, 0xBB));
+ EXPECT_EQ(l1_read(0x2080, 0x100), 0x100u);
+ EXPECT_TRUE(bytes_in(0x0, 0x100, 0xAA));
+ EXPECT_NE(l1_read(0x2000, 0x180), 0x180u);
+
+ // A new range that fully contains an existing chunk drops that chunk,
+ // leaving only the new, larger chunk.
+ mem_cache.Clear();
+ add(0x3040, 0x40, 0xAA);
+ add(0x3000, 0x100, 0xBB);
+ EXPECT_EQ(l1_read(0x3000, 0x100), 0x100u);
+ EXPECT_TRUE(bytes_in(0x0, 0x100, 0xBB));
+
+ // A new range fully contained by an existing chunk is skipped; the enclosing
+ // chunk keeps serving the sub-range.
+ mem_cache.Clear();
+ add(0x4000, 0x200, 0xAA);
+ add(0x4080, 0x80, 0xBB);
+ EXPECT_EQ(l1_read(0x4000, 0x200), 0x200u);
+ EXPECT_TRUE(bytes_in(0x0, 0x200, 0xAA));
+ EXPECT_EQ(l1_read(0x4080, 0x80), 0x80u);
+ EXPECT_TRUE(bytes_in(0x0, 0x80, 0xAA));
+
+ // Partial overlap (bridge): three chunks with pairwise partial overlap and no
+ // containment are all kept.
+ mem_cache.Clear();
+ add(0x5000, 0x80, 0xAA);
+ add(0x5100, 0x80, 0xCC);
+ add(0x5040, 0x100, 0xBB);
+ EXPECT_EQ(l1_read(0x5000, 0x80), 0x80u);
+ EXPECT_TRUE(bytes_in(0x0, 0x80, 0xAA));
+ EXPECT_EQ(l1_read(0x5040, 0x100), 0x100u);
+ EXPECT_TRUE(bytes_in(0x0, 0x100, 0xBB));
+ EXPECT_EQ(l1_read(0x5100, 0x80), 0x80u);
+ EXPECT_TRUE(bytes_in(0x0, 0x80, 0xCC));
+ EXPECT_NE(l1_read(0x5000, 0x180), 0x180u);
+
+ // Disjoint chunks stay separate.
+ mem_cache.Clear();
+ add(0x6000, 0x80, 0xAA);
+ add(0x6100, 0x80, 0xBB);
+ EXPECT_EQ(l1_read(0x6000, 0x80), 0x80u);
+ EXPECT_TRUE(bytes_in(0x0, 0x80, 0xAA));
+ EXPECT_EQ(l1_read(0x6100, 0x80), 0x80u);
+ EXPECT_TRUE(bytes_in(0x0, 0x80, 0xBB));
+ EXPECT_NE(l1_read(0x6000, 0x180), 0x180u);
+
+ // Adjacent (touching but not overlapping) chunks stay separate.
+ mem_cache.Clear();
+ add(0x7000, 0x80, 0xAA);
+ add(0x7080, 0x80, 0xBB);
+ EXPECT_NE(l1_read(0x7000, 0x100), 0x100u);
+
+ // Flush must drop every chunk intersecting the flush range, including a chunk
+ // that starts below the flushed address. Here 0x8140 is covered only by the
+ // lower-starting, longer chunk; after the flush the re-read comes from the
+ // process rather than returning stale bytes.
+ mem_cache.Clear();
+ add(0x8000, 0x180, 0xAA);
+ add(0x8080, 0x40, 0xBB);
+ mem_cache.Flush(0x8140, 0x4);
+ process->SetFiller(0xDD);
+ process->SetMaxReadSize(0x1000);
+ ASSERT_EQ(mem_cache.Read(0x8140, read_sp->GetBytes(), 0x4, error), 0x4u);
+ EXPECT_TRUE(bytes_in(0x0, 0x4, 0xDD));
+
+ // 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);
+ process->SetFiller(0xDD);
+ process->SetMaxReadSize(0x1000);
+ for (lldb::addr_t a : {0x9000u, 0x9040u, 0x9060u, 0x90ffu}) {
+ ASSERT_EQ(mem_cache.Read(a, read_sp->GetBytes(), 0x1, error), 0x1u);
+ EXPECT_TRUE(bytes_in(0x0, 0x1, 0xDD));
+ }
+ EXPECT_EQ(l1_read(0x9100, 0x80), 0x80u);
+ EXPECT_TRUE(bytes_in(0x0, 0x80, 0xCC));
+}
+
TEST_F(MemoryTest, TestReadInteger) {
ArchSpec arch("x86_64-apple-macosx-");
More information about the lldb-commits
mailing list