Skip to content

Silent data corruption: incoherent (coherence_protocol=none) cache serves a stale line when a read races a dirty-eviction writeback #2708

Description

@jake-ke

Description

In a non-coherent hierarchy (coherence_protocol=none, the Incoherent / Incoherent_L1 coherence managers) above a noninclusive level, a cache that evicts a dirty (M) line sends the PutM writeback fire-and-forget and immediately deallocates the line without tracking the in-flight writeback (no mshr_->insertWriteback). Nothing serializes a subsequent same-line request behind that writeback. A read that misses can therefore race the writeback down to the next level and read the pre-writeback (stale) value; because incoherent caches never invalidate, the stale value can then be cached and returned to the CPU — silent data corruption.

The coherent managers (MESI) don't hit this: they already use the insertWriteback / handleAckPut / AckPut MSHR-blocking machinery, and inclusive-L2 configs serialize the read at L2. The incoherent managers never wire that machinery up, and the MemController never acknowledges a PutM (it is unconditionally flagged F_NORESPONSE).

Root cause

Paths on devel @ f782290 (identical on v15.0.0):

  • coherencemgr/Incoherent_L1.cchandleEviction case M: sendWriteback(PutM) then immediate deallocate, no insertWriteback; getInitCoherenceEvent advertises recvWBAck=false.
  • coherencemgr/Incoherent.cc — same on eviction; getInitCoherenceEvent advertises sendWBAck=false.
  • memoryController.cchandleEvent case PutM unconditionally sets F_NORESPONSE; init() advertises sendWBAck=false, so memory never acks a writeback that a noninclusive level would need to release a blocked read.

How to reproduce

Build sst-core + sst-elements devel with --enable-debug, then run the repro.py below (standardCPU → L1 none → L2 none, noninclusiveMemController, small caches to force dirty-line eviction under concurrent access). Debug is already enabled on line 0x0. In the trace, look for a GetS/GetSResp fill of line 0x0 being serviced from the level below while a PutM writeback for that same line is in flight — an ordering the MESI managers structurally prevent.

Note: the stale value is timing-dependent and does not surface as a wrong number under stock traffic generators, because they write data-independent payloads (identical bytes on every write) — which is why the defect is latent. It manifests as returned-stale data only under a data-dependent workload; the ordering hazard above and the code path are the direct evidence.

import sst

DBG = "[0]"   # watch line 0x0

cpu = sst.Component("core", "memHierarchy.standardCPU")
cpu.addParams({"clock":"2GHz","memFreq":"2","rngseed":"101","memSize":"32KiB","verbose":0,
    "maxOutstanding":16,"opCount":20000,"reqsPerIssue":4,"write_freq":70,"read_freq":30})
iface = cpu.setSubComponent("memory", "memHierarchy.standardInterface")

l1 = sst.Component("l1cache", "memHierarchy.Cache")
l1.addParams({"access_latency_cycles":"4","cache_frequency":"2 Ghz","replacement_policy":"lru",
    "coherence_protocol":"none","associativity":"2","cache_line_size":"64","cache_size":"2 KB","L1":"1",
    "debug":1,"debug_level":10,"debug_addr":DBG})

l2 = sst.Component("l2cache", "memHierarchy.Cache")
l2.addParams({"access_latency_cycles":"10","mshr_latency_cycles":2,"cache_frequency":"2 Ghz",
    "replacement_policy":"lru","coherence_protocol":"none","associativity":"4","cache_line_size":"64",
    "cache_size":"16 KB","cache_type":"noninclusive",
    "debug":1,"debug_level":10,"debug_addr":DBG})

mc = sst.Component("memory", "memHierarchy.MemController")
mc.addParams({"clock":"1GHz","addr_range_end":512*1024*1024-1,"backing":"malloc","backing_init_zero":"true",
    "debug":1,"debug_level":10,"debug_addr":DBG})
# the fix adds an opt-in "writeback_acks":1 to the MemController params above
be = mc.setSubComponent("backend", "memHierarchy.simpleMem")
be.addParams({"access_time":"100 ns","mem_size":"512MiB"})

sst.Link("a").connect((iface,"lowlink","1000ps"),(l1,"highlink","1000ps"))
sst.Link("b").connect((l1,"lowlink","10000ps"),(l2,"highlink","1000ps"))
sst.Link("c").connect((l2,"lowlink","10000ps"),(mc,"highlink","10000ps"))

Environment

  • OS: Ubuntu 22.04.5 LTS, kernel 6.8.0
  • Compiler: gcc 11.4.0
  • MPI: Open MPI 4.1.6
  • sst-core: 15.0.0 (87e9e3b)
  • sst-elements: v15.0.0 (d713776) and devel (f782290) — the vulnerable code is unchanged across the v16.0.0 release

Proposed fix (PR to follow)

Wire up the existing writeback-ack serialization on the incoherent path — insert an MSHR writeback entry on M eviction (when recvWritebackAck_), advertise/consume WB acks in getInitCoherenceEvent, add handleAckPut, accept Command::AckPut — and gate the MemController ack behind a new default-off writeback_acks parameter so all existing (coherent) configurations remain byte-identical.

Related, separate defect (not fixed by the above)

Under sustained eviction pressure the same incoherent path also segfaults deterministically in MSHR::removeEvictPointer via IncoherentL1::handleNULLCMD (reproducible on a stock build by shrinking the caches/working set in repro.py). It reproduces independently of the writeback fix and is noted because it lives in the same eviction/MSHR bookkeeping.

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions