Access control fails silently in the dangerous direction: a missing check returns good data to the wrong caller, and nothing in the system complains. The only defence is a test matrix that enumerates every master against every region class in every relevant state and asserts the answer, including the answers that must be errors. This lesson builds that matrix for NanoSoC and checks its completeness mechanically, because a matrix with a hole in it is worse than none: it looks like coverage.
The matrix dimensions come straight from the map: masters with a security state (the CPU, and the stream bridge in each of its two states) crossed with region classes (open memory, secure-only memory, open slots, the secure-only slot, and one hole of each responder kind: H1 answering DECERR and H3 answering SLVERR). Each cell predicts data, SLVERR or DECERR. Twenty-four cells cover NanoSoC: secure callers succeed everywhere mapped, non-secure callers succeed everywhere except SRAM1 and the reset controller, and holes answer their responder's code regardless of state. The script below encodes the table, verifies that every master-region pair is present exactly once, verifies the secure-only denials, and verifies the rule that holes never change their answer with the caller's state. It also asserts the known NanoSoC weakness from the previous lesson: because the bridge's NS bit is writable by non-secure software, a non-secure caller can reprogram the bridge into its secure state, so the matrix alone does not make SRAM1 a boundary.
REGIONS = [ # (name, secure_only, kind)
("SRAM0", False, "mem"), ("SRAM1", True, "mem"),
("DMA", False, "slot"), ("RSTPWR", True, "slot"),
("H1", False, "hole_decerr"), ("H3", False, "hole_slverr"),
]
CALLERS = ["cpu_secure", "cpu_nonsecure", "sbr_ns0", "sbr_ns1"]
def expected(caller, region):
_name, secure_only, kind = region
ns = caller.endswith("nonsecure") or caller.endswith("ns1")
if kind == "hole_decerr":
return "DECERR" # holes answer the same in either state
if kind == "hole_slverr":
return "SLVERR"
if secure_only and ns:
return "SLVERR"
return "DATA"
matrix = {(c, r[0]): expected(c, r) for c in CALLERS for r in REGIONS}
assert len(matrix) == len(CALLERS) * len(REGIONS) == 24
for c in CALLERS: # every caller meets every region exactly once
assert sum(1 for (cc, _r) in matrix if cc == c) == len(REGIONS)
denied = [k for k, v in matrix.items() if v == "SLVERR" and not k[1].startswith("H")]
assert len(denied) == 4 # 2 secure-only regions x 2 non-secure caller-states
assert all(matrix[(c, "H3")] == "SLVERR" for c in CALLERS) # H3 in every state
for c in ("cpu_secure", "sbr_ns0"):
assert all(v != "SLVERR" for (cc, r), v in matrix.items() if cc == c and r != "H3")
assert matrix[("cpu_secure", "H1")] == matrix[("cpu_nonsecure", "H1")] == "DECERR"
# Known weakness (previous lesson): the NS bit sits in a non-secure slot,
# so a non-secure caller can move the bridge from sbr_ns1 to sbr_ns0 at will.
assert matrix[("sbr_ns1", "SRAM1")] == "SLVERR" and matrix[("sbr_ns0", "SRAM1")] == "DATA"
print("PASS: 24-cell matrix complete; 4 secure denials; NS weakness asserted")One count in there deserves a pause: four denials, because two caller-states are non-secure (the CPU in non-secure software and the bridge with NS set) and each is denied at exactly two regions. If a future region goes secure-only, the secure-denial count must grow by exactly the number of non-secure caller-states; a test plan where it does not is a test plan with a hole. The four H3 cells are SLVERR too but are not denials: H3 refuses everyone, in every state, because nothing is there behind the bridge. The matrix is a living artifact: it regenerates from the map table, and the verification topic reruns it on every map change.
The matrix is generated from the map table, and its cell count multiplies out: callers times region classes, no gaps.
Denial cells assert no data as well as the error code; an SLVERR with valid-looking data still fails.
Both master states of configurable masters (the bridge with NS set and clear) appear as separate rows.
Run the matrix as directed accesses in simulation, then again on the bench through the debug access where possible. A cell that returns data where SLVERR is predicted means the filter is bypassed for that master; a cell that returns DECERR where SLVERR is predicted means the fault is routed to the default slave, the decode inconsistency from the first topics. Either way the failing cell names the level at fault, which is what makes the matrix a debug tool and not just a test list.
One new region class adds four cells, one per caller-state. Denials grow by the two non-secure caller-states (non-secure CPU and bridge with NS set), so the denial count goes from four to six.