The NanoSoC secure-attribute bug is a one-bit story with a stuck DMA: the SBR master issues non-secure accesses by reset default, SRAM1 is secure-only, and the first beat of a job sourced from SRAM1 returns SLVERR, wedging the stream with the DMA BUSY and COUNT at zero. Nothing is miswired and no protocol is violated; the security attribute of the master simply disagrees with the region. The filter did its job and the integration did not do its.
AxPROT[1] travels with every transaction: 0 means secure, 1 means non-secure. At each protected slave a filter compares that bit against the region attribute from the map: a non-secure request to a secure-only region is denied with SLVERR, everything else passes. NanoSoC enforces this twice: the interconnect filter guards ROM, the SRAMs and the APB window, and the APB decoder repeats the check per slot, because the DMA's APB port has no protection sideband of its own and the slot attribute must stand in for it. The decision is combinational, and the module below is all of it.
// NanoSoC teaching values: secure-access filter, one per protected region.
// AxPROT[1]: 0 = secure, 1 = non-secure. A denied request goes to the SLVERR responder.
module nano_sec_filter (
input logic req, // arvalid or awvalid
input logic ax_ns, // AxPROT[1] of the request
input logic region_secure, // region attribute from the map: 1 = secure only
output logic allow,
output logic deny
);
assign deny = req && ax_ns && region_secure;
assign allow = req && !deny;
endmoduleDenying is not the same as dropping. AXI requires every accepted request to be completed, so a denied transaction must still finish its protocol: a denied write must consume every W beat until WLAST and then return exactly one B response with SLVERR, and a denied read must return ARLEN + 1 R beats with SLVERR and RLAST on the last. A filter that simply raised an error flag and dropped the request would leave the master waiting for ever, which is the hang the filter exists to avoid. The responder below does this; the filter keeps AWREADY and ARREADY low while it is busy, so one denied transaction of each kind is in progress at a time. The testbench sends a denied 16-beat write and a denied 16-beat read and fails if the beat counts, the single B, the SLVERR code, the ID or the single RLAST are wrong.
// NanoSoC teaching values: completing a DENIED transaction so the requester never hangs.
// A denied write must still consume every W beat (up to WLAST) and return one B with SLVERR.
// A denied read must still return ARLEN+1 R beats with SLVERR and RLAST on the last.
// The filter holds AWREADY and ARREADY low while busy_w or busy_r is set (one denied
// transaction of each kind at a time).
module nano_deny_resp (
input logic aclk,
input logic aresetn,
input logic aw_denied, // an AW handshake the filter refused
input logic [3:0] aw_id,
input logic w_valid,
input logic w_last,
output logic w_ready,
output logic b_valid,
input logic b_ready,
output logic [3:0] b_id,
output logic [1:0] b_resp,
input logic ar_denied, // an AR handshake the filter refused
input logic [3:0] ar_id,
input logic [3:0] ar_len,
output logic r_valid,
input logic r_ready,
output logic r_last,
output logic [3:0] r_id,
output logic [1:0] r_resp,
output logic busy_w,
output logic busy_r
);
typedef enum logic [1:0] {W_IDLE, W_DATA, W_RESP} wst_t;
typedef enum logic {R_IDLE, R_DATA} rst_t;
wst_t wst;
rst_t rst;
logic [3:0] wid, rid, left;
always_ff @(posedge aclk or negedge aresetn) begin
if (!aresetn) begin
wst <= W_IDLE;
rst <= R_IDLE;
wid <= 4'h0;
rid <= 4'h0;
left <= 4'h0;
end else begin
case (wst)
W_IDLE: if (aw_denied) begin wid <= aw_id; wst <= W_DATA; end
W_DATA: if (w_valid && w_last) wst <= W_RESP; // accept beats until WLAST
W_RESP: if (b_ready) wst <= W_IDLE;
default: wst <= W_IDLE;
endcase
case (rst)
R_IDLE: if (ar_denied) begin rid <= ar_id; left <= ar_len; rst <= R_DATA; end
R_DATA: if (r_ready) begin
if (left == 4'h0) rst <= R_IDLE;
else left <= left - 4'h1;
end
default: rst <= R_IDLE;
endcase
end
end
assign w_ready = (wst == W_DATA);
assign b_valid = (wst == W_RESP);
assign b_id = wid;
assign b_resp = 2'b10; // SLVERR
assign r_valid = (rst == R_DATA);
assign r_last = (rst == R_DATA) && (left == 4'h0);
assign r_id = rid;
assign r_resp = 2'b10;
assign busy_w = (wst != W_IDLE);
assign busy_r = (rst != R_IDLE);
endmodule
// ---- self-checking testbench: denied 16-beat write and denied 16-beat read ----
module tb;
logic aclk = 0, aresetn = 0;
always #5 aclk = ~aclk;
logic aw_denied = 0, w_valid = 0, w_last = 0, b_ready = 1, ar_denied = 0, r_ready = 1;
logic [3:0] aw_id = 0, ar_id = 0, ar_len = 0;
wire w_ready, b_valid, r_valid, r_last, busy_w, busy_r;
wire [3:0] b_id, r_id;
wire [1:0] b_resp, r_resp;
nano_deny_resp dut (.*);
int errors = 0, w_beats = 0, b_count = 0, r_beats = 0, last_count = 0;
always @(posedge aclk) if (aresetn) begin
if (w_valid && w_ready) w_beats++;
if (b_valid && b_ready) begin
b_count++;
if (b_resp !== 2'b10 || b_id !== 4'd5) begin errors++; $display("FAIL B resp=%b id=%0d", b_resp, b_id); end
end
if (r_valid && r_ready) begin
r_beats++;
if (r_resp !== 2'b10 || r_id !== 4'd9) begin errors++; $display("FAIL R resp=%b id=%0d", r_resp, r_id); end
if (r_last) last_count++;
end
end
initial begin
repeat (3) @(negedge aclk); aresetn = 1;
// denied write, 16 beats
@(negedge aclk); aw_denied = 1; aw_id = 4'd5;
@(negedge aclk); aw_denied = 0;
for (int i = 0; i < 16; i++) begin
w_valid = 1; w_last = (i == 15);
while (!w_ready) @(negedge aclk); // wait until the sink is ready
@(negedge aclk); // the beat is taken at the posedge in between
end
w_valid = 0; w_last = 0;
repeat (4) @(negedge aclk);
if (w_beats != 16 || b_count != 1) begin errors++; $display("FAIL write: %0d W beats, %0d B", w_beats, b_count); end
// denied read, ARLEN = 15 (16 beats)
@(negedge aclk); ar_denied = 1; ar_id = 4'd9; ar_len = 4'd15;
@(negedge aclk); ar_denied = 0;
repeat (24) @(negedge aclk);
if (r_beats != 16 || last_count != 1) begin errors++; $display("FAIL read: %0d R beats, %0d RLAST", r_beats, last_count); end
if (busy_w || busy_r) begin errors++; $display("FAIL: still busy"); end
if (errors == 0) $display("PASS: denied write consumed 16 W beats and returned one SLVERR B, denied read returned 16 SLVERR beats with one RLAST");
$finish;
end
initial #50000 begin $display("FAIL timeout"); $finish; end
endmodule# NanoSoC teaching values: the secure-access matrix the filters enforce.
SECURE_ONLY = {"SRAM1", "RSTPWR"}
# (master, master_is_ns, region) -> expected outcome
CASES = [
("CPU", True, "SRAM0", "allow"),
("CPU", True, "SRAM1", "SLVERR"),
("CPU", False, "SRAM1", "allow"),
("SBR", True, "SRAM1", "SLVERR"), # the NanoSoC secure-attribute bug: NS reset value vs secure SRAM1
("SBR", False, "SRAM1", "allow"), # after secure software clears CTRL.NS
("CPU", True, "RSTPWR", "SLVERR"),
("CPU", False, "RSTPWR", "allow"),
("CPU", True, "UART", "allow"),
]
def decide(master_ns, region):
if master_ns and region in SECURE_ONLY:
return "SLVERR"
return "allow"
for (master, ns, region, expect) in CASES:
assert decide(ns, region) == expect, (master, region)
# The known gap: the SBR register slot is non-secure, so non-secure software may write CTRL.NS.
def sbr_ns_after_write(write_ns_bit, writer_is_secure, fixed):
# returns the NS attribute the SBR will use after software writes CTRL.NS = write_ns_bit
if fixed and not writer_is_secure:
return 1 # the fix: the NS field is writable from the secure side only
return write_ns_bit
def reaches_sram1(fixed):
# non-secure software tries to clear NS, then starts an SBR copy from SRAM1
ns = sbr_ns_after_write(0, writer_is_secure=False, fixed=fixed)
return decide(bool(ns), "SRAM1") == "allow"
assert reaches_sram1(fixed=False) # today: non-secure software reads SRAM1 through the SBR
assert not reaches_sram1(fixed=True) # with the field made secure-only the partition closes
print("PASS: 8-entry secure matrix, SBR needs NS cleared for SRAM1, the open NS field is caught")Every secure-only region names its filter location: interconnect, APB slot, or both.
Every master states its reset-default security attribute; SBR non-secure by reset is written down, not discovered.
A denied burst completes: all W beats consumed, one B or ARLEN + 1 R beats with RLAST, all with SLVERR, so the requester cannot hang.
The known gap is on the list: the SBR slot is non-secure and CTRL.NS resets to 1, so non-secure software can write NS = 0 and read or write SRAM1 through the SBR. The partition is not closed until CTRL.NS is writable from the secure side only, or the SBR slot becomes secure-only.
Test every protected region from both security states: secure must pass, non-secure must return SLVERR within the documented cycles. Test the denied burst shape, not just the code: a denied 16-beat write must accept 16 W beats and return one B, and a denied 16-beat read must return 16 R beats with one RLAST. Force the SBR default: with CTRL.NS still set, a job sourced from SRAM1 must end in SBR STATUS.ERR on the first beat, and clearing NS from secure software must clear the failure. Then try the same write to CTRL.NS from non-secure software: until the gap is closed it succeeds, and the test should say so. If a secure access fails instead, the filter attribute is wrong, not the master.
The CPU issued a secure access while the SBR still carries its reset-default non-secure attribute. Regions decide per transaction, and the two masters presented different states.