Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • How to Use This Course
  • Reading Path
  • Cache Coherency Glossary
  • The Shared Course Model
  • Cache Coherency Revision Sheet
  • Why Coherence Exists8
  • States and Protocols12
  • Transactions12
  • Races and Controller Design8
  • Progress and the Fabric16
  • Ordering and Synchronization12
  • Performance and Scalability12
  • Real Protocols12
  • SoC Integration12
  • Power, Clocking and Reliability12
    • Power State Transitions and Cache State Safety
    • Clocking and CDC on Coherence Channels
    • ECC, Poison and Recovery
      • ECC on Tags, Data and Directory
      • Poison Propagation
      • Losing Directory State
      • Recovery Limits
  • Verification12
  • Debug and Bring-up12
  • Interview and Design Review17
  • Capstone: A Coherent Model Lab5
Loading lesson

‹ECC, Poison and Recovery
  1. Topics
  2. Cache Coherency
  3. Power, Clocking and Reliability
  4. ECC, Poison and Recovery
  5. Poison Propagation

Part 10 · Power, Clocking and Reliability · All levels

Poison Propagation

Carry an uncorrectable data error with the line as a poison bit, raise it only at the consumer, and keep coherence state legal.

On this page

    Must an uncorrectable data error stop the machine?

    Not if only data is lost. The detecting unit often has no idea who will use the line, and may not be using it itself: a cache answering a forward, a memory controller filling a request, a scrubber. Stopping at detection punishes whoever happened to touch the line. The alternative is poison: mark the data as known-bad, keep moving it through the protocol like any other data, and raise an error only when some core or device actually consumes it.

    Rule

    Mechanism

    Poison travels with the data

    Every Data message carries a poison bit beside the payload. Buffers at H keep it. Memory stores it, for example as a reserved ECC pattern.

    Coherence runs normally

    States, directory updates, Inv, acks and Put-Ack are unchanged. The line still has one legal state per cache.

    Consumption raises the error

    A load that returns poisoned data to a core (or a device read of it) reports a synchronous error to that consumer instead of returning a value.

    Nothing else does

    Moving, forwarding, writing back or evicting a poisoned line raises no error. The detection is logged, as a report, not an interruption.

    Only new data clears it

    A partial store merges into the line and keeps the poison. Writing every byte of the poison granule replaces the bad data and clears it.

    In this model the poison granule is the whole 64-byte line, so X is poisoned even if the bad bits are elsewhere in the line. Designs may track poison per smaller chunk.

    Worked case: a dirty line goes bad

    MSI, extending the dirty read of MSI Protocol with its roles: from X = 0, C0 acquired M and stored 1, so C0 M / 1, H exclusive(C0), memory = 0. C1 sends GetS. When the Fwd-GetS arrives, C0 reads the line out of its data array and its SECDED check reports a double flip: uncorrectable. The value 1 is gone. C0 does the MSI downgrade as usual but sends Data with the poison bit set, written Data(poisoned). The messages are those of the linked trace, so only the table is shown, with a poison column.

    Step

    C0 state / value

    C1 state / value

    H directory

    Memory

    Newest data

    Poisoned copies

    0 warm-up

    M / 1

    I / -

    exclusive(C0)

    0

    C0: 1

    none

    1. GetS from C1 at H

    M / 1

    IS^D / -

    exclusive(C0), pend GetS C1

    0

    C0: 1

    none

    2. Fwd-GetS at C0, ECC finds a double flip

    S / poisoned

    IS^D / -

    exclusive(C0), pend GetS C1

    0

    none: 1 is lost

    C0, Data(poisoned) in flight to H

    3. Data(poisoned) at H

    S / poisoned

    IS^D / -

    exclusive(C0), pend GetS C1

    0

    none: 1 is lost

    C0, H buffer

    H writes memory with poison

    S / poisoned

    IS^D / -

    S{C0}, pend GetS C1

    poisoned

    none: 1 is lost

    C0, memory, Data(poisoned) in flight to C1

    4. Data(poisoned) at C1

    S / poisoned

    S / poisoned

    S{C0}, pend GetS C1

    poisoned

    none: 1 is lost

    C0, C1, memory

    (assumed) grant delivered

    S / poisoned

    S / poisoned

    S{C0,C1}

    poisoned

    none: 1 is lost

    C0, C1, memory

    L. C1 loads X

    S / poisoned

    S / poisoned

    S{C0,C1}

    poisoned

    none: 1 is lost

    C0, C1, memory, error reported to C1's load

    Read the states column by column: every row is a legal MSI snapshot. There is never a writer beside a reader, and H's entry matches the holders. SWMR holds. What has failed is the data-value invariant: no copy anywhere holds 1. Poison does not repair that; it makes the failure visible to exactly the reader who would otherwise have used a wrong value, and to nobody else.

    Three things did not happen. C0 raised no error at step 2, because it was only answering a forward. H raised none at step 3 or when writing memory. And had C1 never loaded X, no error would have been raised at all, beyond a logged report. Without the poison bit, H would have written the garbage bits to memory with fresh, valid ECC, and C1's load would have returned a wrong number with no warning: silent data corruption, the worst outcome in RAS terms.

    Flowchart: an uncorrectable error in C0's array becomes a poisoned Data message, kept in H's buffer and in memory, delivered to C1, and reported only if a core loads the line.
    Poison is carried by every hop that holds the data and is checked only at consumption.
    example.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    import random
    GARBAGE = {"X": 6, "Y": 3}  # what C0's double flip leaves in X's line
    def run(seed, variant=None):
        rng = random.Random(seed)
        c = {0: {"st": "M", "v": {"X": 1, "Y": 5}, "p": False, "flip": True}, 1: {"st": "I", "v": None, "p": False, "flip": False}}
        mem = {"v": {"X": 0, "Y": 0}, "p": False}
        good = {"X": 1, "Y": 5}  # what a correct load must return, None once lost
        st = {"stage": None, "errors": 0, "poisoned_loads": 0, "ops": []}
        def data(rec):  # every Data message carries the poison bit
            return {"v": dict(rec["v"]), "p": rec["p"] and variant != "drop_on_data"}
        def read_array(k):  # ECC check when a line is read out
            if c[k]["flip"]:
                c[k].update(v=dict(GARBAGE), p=True, flip=False)
                good.update(X=None, Y=None)  # the only newest copy is gone
                st["errors"] += variant == "raise_at_detection"
        for _ in range(rng.randint(4, 8)):
            ops = [(st["stage"], st.get("req"))] if st["stage"] else []
            for k in (0, 1) if not ops else ():
                ops.append(("load", k) if c[k]["st"] != "I" else ("fwd_gets", k) if c[1 - k]["st"] == "M" else ("read_mem", k))
                ops += [("partial", k), ("full", k)] + ([("puts", k)] if c[k]["st"] == "S" else [])
            op, k = rng.choice(ops)
            o = 1 - k
            st["ops"].append(op)
            if op == "fwd_gets":  # Fwd-GetS at the M holder: downgrade, Data to H
                read_array(o)
                c[o]["st"] = "S"
                st.update(msg=data(c[o]), req=k, stage="h_buffer")
            elif op == "h_buffer":
                st.update(buf=st["msg"], stage="h_write_mem")  # the buffer keeps the bit
            elif op == "h_write_mem":
                mem.update(v=dict(st["buf"]["v"]), p=st["buf"]["p"])
                st["stage"] = "data_to_req"
            elif op == "data_to_req":
                c[k].update(st="S", **data(st["buf"]))
                st["stage"] = None
            elif op == "read_mem":  # GetS served from memory
                c[k].update(st="S", **data(mem))
            elif op == "puts":  # clean eviction, PutS carries no data
                c[k].update(st="I", v=None, p=False)
            elif op == "load":
                read_array(k)
                w = rng.choice("XY")
                if c[k]["p"]:  # consumption: the error goes to this load only
                    assert None in good.values(), "poison left on a fully overwritten line"
                    st.update(errors=st["errors"] + 1, poisoned_loads=st["poisoned_loads"] + 1)
                else:
                    assert c[k]["v"][w] == good[w], "wrong value returned silently"
            else:  # partial or full store: GetM (Data from the M holder or memory), then write
                if c[k]["st"] == "I":
                    read_array(o) if c[o]["st"] == "M" else None
                    line = data(c[o] if c[o]["st"] == "M" else mem)
                else:
                    read_array(k)
                    line = {"v": dict(c[k]["v"]), "p": c[k]["p"]}
                c[o].update(st="I", v=None, p=False)
                n = rng.randrange(10, 99)
                if op == "partial":  # merge into X only, the poison stays
                    line["v"]["X"], good["X"] = n, n
                    line["p"] = line["p"] and variant != "clear_on_partial"
                else:  # every byte of the granule replaced
                    line, good = {"v": {"X": n, "Y": n + 1}, "p": False}, {"X": n, "Y": n + 1}
                c[k].update(st="M", **line)
            held = [r for r in c.values() if r["st"] != "I"]
            held += [mem] if not st["stage"] and all(r["st"] == "S" for r in held) else []  # memory current
            assert all(r["p"] or r["v"] == good for r in held), "lost or stale bits not marked poisoned"
            assert not (any(r["st"] == "M" for r in c.values()) and "I" not in (c[0]["st"], c[1]["st"]))  # SWMR
            assert st["errors"] == st["poisoned_loads"], "error raised without a poisoned load"
        return st
    stats = [run(seed) for seed in range(400)]
    assert sum(s["poisoned_loads"] for s in stats) > 0 and sum(s["ops"].count("full") for s in stats) > 0
    assert {"fwd_gets", "h_buffer", "h_write_mem", "data_to_req", "read_mem", "puts", "load", "partial", "full"} <= {o for s in stats for o in s["ops"]}
    for variant in ("drop_on_data", "raise_at_detection", "clear_on_partial"):
        failures = set()
        for seed in range(400):
            try:
                run(seed, variant)
            except AssertionError as e:
                failures.add(str(e))
        assert failures, variant  # every broken variant is caught
    print("PASS: errors arise only at poisoned loads, nothing wrong is silent, and all three broken variants fail")

    The check models X's line as a record of values and a poison bit, starting from the warm-up above with C0's double flip not yet read out. With a fixed seed it runs 400 random sequences of 4 to 8 legal operations: the Fwd-GetS at C0, H buffering the Data, H writing memory, the Data to C1, a PutS from either cache, a re-read from memory, loads at C0 or C1, a partial store to X and a full overwrite of the line. After every step it checks that each copy either matches the true value or is marked poisoned, that every error belongs to a load of poisoned data, and that SWMR holds. A partial store must keep the poison and a full overwrite must clear it. Three broken designs each fail: a Data path that drops the bit (C1 holds garbage marked good), an error raised at detection (an error with no consumer) and a partial store that clears the poison (the other bytes are still garbage).

    Why metadata cannot be poisoned the same way

    Poison is a mark on a payload: it says 'these bytes are bad'. It needs a correct address, a correct state and a correct directory entry around it, so that the poisoned line still moves through a legal protocol. If the uncorrectable error is in a state bit, a tag or a directory entry, there is no payload to mark. H does not know who may write or who must be invalidated, and a cache with a bad tag does not know which address its line belongs to. Marking the line poisoned would not make the next grant safe. Metadata errors need a different answer: rebuild the metadata, as in the next lesson, or stop.

    Common pitfall

    Raising a fatal error at the point of detection for data errors. The forwarding cache or the memory controller is rarely the consumer, and a line that nobody reads again never needed to stop anything.

    Common pitfall

    Clearing poison on a partial store. The other bytes of the granule are still bad; only a write of the whole granule replaces them.

    Interview

    Say what poison keeps (coherence state legal, SWMR intact), what it gives up (the value, so the data-value invariant fails), and where the error surfaces (the consuming load only). Then say why the same trick does not work for a corrupted directory entry.

    Check yourself After step 4, C1 sends GetM and stores X = 2 into the poisoned line. Is the line still poisoned?

    Yes, in this model. GetM proceeds normally (C1 goes to SM^D, H sends Inv to C0, collects its Inv-Ack and grants M with permission-only Data, since C1 already holds the line), and C1's store of X writes one location inside the 64-byte granule. The other bytes are still bad, so the merged line stays poisoned. Only a write of every byte of the granule clears the poison.

    Related topics

    • Shared course model

    • The MSI dirty read this trace extends

    • The data-value invariant

    • Next: lost metadata

    Previous

    119 / 167

    Next: Losing Directory State

    Up next

    Up nextLosing Directory State

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Recovery Limits