A filter that knows secure from non-secure but not DMA from CPU cannot express a policy like the SBR may stream SRAM buffers but must never touch the reset controller, even when both masters are secure. On NanoSoC today that policy is not enforced: with CTRL.NS cleared the SBR is a secure master and can write RSTPWR. Address-range firewalls keyed on master identity would add the who dimension. This lesson writes the whole policy table, compares what the hardware enforces with what a master-ID firewall would add, and labels the second as a proposed extension.
The policy is one table: each row names a region, the masters admitted, and the response for everyone else. A complete table has a row for every mapped region and every hole, and a cell for every master: the review rule is that no cell is blank. NanoSoC enforces only the secure column of the map through AxPROT[1]. The proposed firewall would add master identity: the identity is the AXI ID with the master-index bit the interconnect adds (see the ID lesson), and it is trustworthy as an identity only because the interconnect adds those bits, not because the master drives them. Every bridge, converter and CDC stage would have to carry it untouched, because a bridge that drops sideband bits silently widens every region to every master.
Region | Secure only (built) | CPU (proposed) | SBR (proposed) |
|---|---|---|---|
BOOTROM | No | Read | Read |
SRAM0 | No | Read, write | Read, write |
SRAM1 | Yes | Read, write | Read, write |
DMA registers | No | Read, write | Deny |
SBR registers | No | Read, write | Deny |
UART, SPI, GPIO | No | Read, write | Deny |
INTC | No | Read, write | Deny |
RSTPWR | Yes | Read, write | Deny |
Holes H1, H2, H4 | n/a | DECERR | DECERR |
Hole H3 | n/a | SLVERR | SLVERR |
# NanoSoC teaching values: complete policy tables. BUILT is the AxPROT filter, PROPOSED adds master IDs.
REGIONS = ["BOOTROM", "SRAM0", "SRAM1", "DMA", "SBR", "UART", "SPI", "GPIO", "INTC", "RSTPWR",
"H1", "H2", "H3", "H4"]
MASTERS = ["CPU", "SBR"]
SECURE_ONLY = {"SRAM1", "RSTPWR"}
HOLES = {"H1": "DECERR", "H2": "DECERR", "H3": "SLVERR", "H4": "DECERR"}
def built(master, master_ns, region):
# what NanoSoC enforces today: only the secure column, no master identity
if region in HOLES:
return HOLES[region]
if master_ns and region in SECURE_ONLY:
return "SLVERR"
return "allow"
# PROPOSED: a master-ID firewall; every row lists every master, deny is explicit
PROPOSED = {
"BOOTROM": {"CPU": True, "SBR": True},
"SRAM0": {"CPU": True, "SBR": True},
"SRAM1": {"CPU": True, "SBR": True},
"DMA": {"CPU": True, "SBR": False},
"SBR": {"CPU": True, "SBR": False},
"UART": {"CPU": True, "SBR": False},
"SPI": {"CPU": True, "SBR": False},
"GPIO": {"CPU": True, "SBR": False},
"INTC": {"CPU": True, "SBR": False},
"RSTPWR": {"CPU": True, "SBR": False},
}
def proposed(master, master_ns, region):
if region in HOLES:
return HOLES[region]
if not PROPOSED[region][master]:
return "SLVERR"
return built(master, master_ns, region)
# completeness: no blank cell, every mapped region has an explicit answer for every master
for r in REGIONS:
if r in HOLES:
continue
assert r in PROPOSED, "missing policy row: " + r
assert set(PROPOSED[r]) == set(MASTERS), "blank master cell in row " + r
# the policy difference: a secure SBR can write RSTPWR today, not with the firewall
assert built("SBR", False, "RSTPWR") == "allow"
assert proposed("SBR", False, "RSTPWR") == "SLVERR"
assert proposed("CPU", False, "UART") == "allow" and built("CPU", True, "UART") == "allow"
assert proposed("SBR", True, "SRAM1") == "SLVERR" # NS still blocks SRAM1 either way
assert built("CPU", True, "H3") == "SLVERR" and built("SBR", False, "H4") == "DECERR"
# a table with a missing row would be caught by the completeness loop
broken = dict(PROPOSED)
del broken["UART"]
assert any(r not in broken for r in REGIONS if r not in HOLES)
print("PASS: 14-region table complete, a secure SBR reaches RSTPWR today and is fenced by the proposed firewall")The policy table lists every region (holes included) against every master, with deny as the explicit default, never a blank cell, and it says which columns are built and which are proposed.
Sideband width is checked at every bridge: lint proves no ID or protection bit is truncated in transit.
Reserved regions have rows that deny, so growth opens them deliberately instead of discovering them open.
Run security-directed tests that walk the whole table: each admitted pair completes, each denied pair returns SLVERR, from the right responder. Today that means the secure column; with a master-ID firewall it would also walk the master column. Then run sideband connectivity checks that prove the ID and protection bits arriving at each filter equal the bits the master sent; a bridge that drops them passes functional tests and fails only these. Re-run the table whenever a region is added: H3 slots open one row at a time.
Today nothing can: the AxPROT filter has no master identity and the UART slot is non-secure. In the proposed firewall it is one table row, plus the directed test that proves the denial and the sideband check that the SBR identity reaches the filter.