A system has secure memory for keys, ordinary memory for everyone, and peripherals some agents may program and others may not. Each target could check every request itself, but most targets (a memory, a simple peripheral) know nothing about security states or requester IDs. The check has to sit in the fabric, in front of them, and look at every request on the way through.
How does a firewall in the fabric decide which requests reach a target?
The obvious answer is to drop disallowed requests. Dropping breaks the rule that every request gets a response (lesson 5.2): the requester waits for ever, and a buggy or hostile requester can now hang itself or, worse, a shared bridge. The firewall must refuse and answer: return an error response, so the requester completes and software can see what happened.
Region | Addresses | Rule |
|---|---|---|
0 | 0x00 to 0x3F | Secure requests only |
1 | 0x40 to 0x7F | Anyone |
(none) | Everything else | Unmapped: refuse |
Match: find the region containing the address (and, for multi-beat requests, check that the whole burst stays inside it).
Check: compare the request's attributes (security state, privilege, requester ID, read or write) with the region's rule.
Allow or refuse: allowed requests pass unchanged; refused ones never reach the target and are answered with an error. Reads return no real data (often zeros).
Record: log the refused address and requester, and raise an interrupt to secure software.
Four requests: secure to region 0, non-secure to region 0, non-secure to region 1, secure to an unmapped address.
// An address-range firewall in front of a target. Each request carries an address
// and a non-secure bit (ns, as AXI's AxPROT[1]). Region 0 (0x00-0x3F) is secure
// only; region 1 (0x40-0x7F) is open to everyone; any other address is unmapped.
// Allowed requests pass to the target; refused ones never reach it and get an error
// response (deny), so the requester does not hang. Four requests, cycles 1 to 4:
// secure to region 0 (pass), non-secure to region 0 (deny), non-secure to region 1
// (pass), secure to an unmapped address (deny).
module tb;
logic clk = 0;
always #5 clk = ~clk;
initial begin $dumpfile("wave.vcd"); $dumpvars(0, tb); end
int cyc = 0;
always @(posedge clk) cyc <= cyc + 1;
logic req = 0, ns = 0;
logic [7:0] addr = 0;
always @(posedge clk) begin
req <= 0; addr <= 0; ns <= 0;
case (cyc)
0: begin req <= 1; addr <= 8'h10; ns <= 0; end
1: begin req <= 1; addr <= 8'h10; ns <= 1; end
2: begin req <= 1; addr <= 8'h50; ns <= 1; end
3: begin req <= 1; addr <= 8'h90; ns <= 0; end
default: ;
endcase
end
// The firewall: decide per request, in one cycle
logic allowed;
always_comb begin
if (addr < 8'h40) allowed = !ns; // region 0: secure only
else if (addr < 8'h80) allowed = 1'b1; // region 1: anyone
else allowed = 1'b0; // unmapped
end
logic pass = 0, deny = 0;
always @(posedge clk) begin
pass <= req && allowed; // forwarded to the target
deny <= req && !allowed; // answered with an error here
end
int passed = 0, denied = 0, leaked = 0;
logic [7:0] addr_q = 0; logic ns_q = 0;
always @(posedge clk) begin
addr_q <= addr; ns_q <= ns;
if (pass) begin passed++; if ((addr_q < 8'h40 && ns_q) || addr_q >= 8'h80) leaked++; end
if (deny) denied++;
end
initial begin
#120;
if (passed == 2 && denied == 2 && leaked == 0)
$display("PASS: two requests passed, the non-secure access to secure memory and the unmapped access were refused with errors");
else $display("FAIL: passed %0d denied %0d leaked %0d", passed, denied, leaked);
$finish;
end
endmoduleHow to read it, slot by slot (allowed is the firewall's decision for whatever is on addr and ns; it matters only while req is high):
Slot 0: secure request (ns = 0) to 0x10: allowed; slot 1: pass.
Slot 1: non-secure request (ns = 1) to the same address: refused; slot 2: deny, an error response.
Slot 2: non-secure request to 0x50 in the open region: allowed; slot 3: pass.
Slot 3: secure request to 0x90, unmapped: refused; slot 4: deny.
Placement | Protects | Note |
|---|---|---|
In front of a target (memory, peripheral) | That target, from every requester | Common for memory controllers and secure peripherals |
At a requester's port into the fabric | Everything, from that requester | Good for untrusted agents such as DMA engines and debug |
In the interconnect's address decoder | Whole address ranges | Cheap, coarse |
The firewall's own configuration (its region table) must itself be protected: writable only by secure, privileged software, or locked after boot. A firewall anyone can reprogram is no firewall.
System | Firewalls |
|---|---|
Arm TrustZone systems | A TrustZone address space controller such as the TZC-400 in front of memory permits or refuses each access by region, security state and master ID (fact A27); refused accesses get error responses. |
AXI | Refused requests are answered with SLVERR or DECERR (fact A6 and lesson 10.7). |
PCI Express | Access control services in switches can block or redirect peer-to-peer requests; an IOMMU checks DMA (lesson 14.3). |
A firewall (illustrative) checked only a request's start address. A non-secure burst starting at the last allowed address of an open region ran 64 bytes into the secure region next to it, and the firewall let it through. Checking that every byte of the burst falls inside one permitted region fixed it; boundary-crossing bursts are a standard firewall test.
0x90 lies outside both regions: it is unmapped, so it is refused whatever its attributes.
Every request needs a response; a dropped one leaves its requester waiting for ever.
A firewall matches each request's address to a region and checks its attributes against the region's rule.
Refused requests never reach the target and are answered with an error, then logged.
Check whole bursts, protect the firewall's own configuration, and test every region edge.