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

AboutRoadmapCareersInternshipsFAQContactPrivacyTerms
Skip to lesson

Course contents

  • How to Use This Course
  • The NanoSoC Tour
  • Reading Path
  • SoC Glossary10
  • SoC Metrics Reference
  • SoC Cheatsheet
  • What SoC Integration Is12
  • Architecture and Budgets21
  • Memory Maps and Address Decode26
  • Interconnect and Bus Fabrics26
    • Topologies and Bridges
    • AXI Integration Details
    • Ordering and Deadlock at the Top
    • QoS and Arbitration
    • NoC Basics
    • Coherent Interconnect View
    • Firewalls and Protection
      • Secure-Access Filters
      • Address-Range Firewalls and Master IDs
      • Failure Modes and Debug
    • Bandwidth and Latency Verification
  • Clock and Reset Architecture24
  • Power Architecture21
  • Interrupts, DMA and Peripherals15
  • Memories, Boot and Security21
  • IP Integration and Handoff21
  • Top-Level Verification and Bring-up21
  • Package, Pads and I/O18
  • Chip Signoff and Tapeout18
  • SoC Interview Prep16
  • Capstone: Integrate NanoSoC14
Loading lesson

←Firewalls and Protection
  1. Topics
  2. SoC Design
  3. Interconnect and Bus Fabrics
  4. Firewalls and Protection
  5. Secure-Access Filters
On this page

    Part 4 of 14, lesson 21 of 26Part 4 of 14/Interconnect and Bus Fabrics/Lesson 21 of 26All levels

    Secure-Access Filters

    AxPROT at the slave door: how NanoSoC enforces secure-only regions, completes a denied burst, and where the partition is still open.

    Why it matters

    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.

    The core idea

    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.

    nano_sec_filter.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 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;
    endmodule

    Denying 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.

    nano_deny_resp.sv
    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
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    // 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
    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
    # 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")

    What the review looks for

    • 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.

    Verify and debug

    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.

    Secure attribute mismatch between master and region: the SBR resets to non-secure and SRAM1 admits secure only, so the job errors on beat zero with the DMA stuck BUSY. Caught by testing both security states per region; fixed by secure software clearing the NS bit, with the reset-safe default documented in the map. The mirror image is the open gap above: whoever can write NS chooses the security state of the SBR.

    If asked where a secure fault is enforced, answer at the slave door, not the master: the filter sits with the region because regions own their attributes. Then explain the double enforcement on NanoSoC (interconnect filter plus APB slot check, since the DMA has no protection sideband), and volunteer the gap: a master whose attribute software can choose is only as secure as the register that sets it.

    Check yourself Secure software reads SRAM1 successfully but the SBR job from SRAM1 errors. Same region, different result: why?

    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.

    Related topics

    • Protocol First Principles: Access Attributes

    • Next: Address-Range Firewalls and Master IDs

    • Verification First Principles: Security Verification

    Previous

    88 / 289

    Next: Address-Range Firewalls and Master IDs

    Up next

    Up nextAddress-Range Firewalls and Master IDs

    Sign in to save progress

    Practice this lessonPrevious lessonReport an error

    See also

    • Failure Modes and Debug