Repository: lowRISC/opentitan
Pull request #26589: [ac_range_check/rtl] Fix permission checking and deny index
Author: login: martin-velay
Labels: name: Component:RTL
name: IP:ac_range_check
Created: 2025-03-11T13:34:55Z | Merged: 2025-03-28T15:39:41Z

This fixes #26588 as well as the value stored in the `log_status.deny_range_index` CSR field.

Linked issue #26588: [ac_range_check,rtl] Erroneous permissions
This bug has been already mentioned here https://github.com/lowRISC/opentitan/issues/26572#issuecomment-2710291368 while I was requesting a spec clarification.

The issue is due to `deny_mask`, it is a common signal for read, write and execute, but I think it cannot be. I'll push a PR proposal to fix this soon.

Diff:
diff --git a/hw/ip_templates/ac_range_check/rtl/ac_range_check.sv.tpl b/hw/ip_templates/ac_range_check/rtl/ac_range_check.sv.tpl
index 5b0f9044e6bc3..bc465878d1c33 100644
--- a/hw/ip_templates/ac_range_check/rtl/ac_range_check.sv.tpl
+++ b/hw/ip_templates/ac_range_check/rtl/ac_range_check.sv.tpl
@@ -107,7 +107,10 @@ module ${module_instance_name}
   // Range Check Logic
   //////////////////////////////////////////////////////////////////////////////
 
-  logic [NumRanges-1:0] addr_hit, deny_mask, read_mask, write_mask, execute_mask, log_enable_mask;
+  logic [NumRanges-1:0] addr_hit;
+  logic [NumRanges-1:0] r_deny_mask, w_deny_mask, x_deny_mask, deny_mask;
+  logic [NumRanges-1:0] r_grant_mask, w_grant_mask, x_grant_mask, grant_mask;
+  logic [NumRanges-1:0] log_enable_mask;
   logic [NumRanges-1:0] racl_read_hit, racl_write_hit;
 
   // Retrieve RACL role from user bits and one-hot encode that for the comparison bitmap
@@ -123,6 +126,22 @@ module ${module_instance_name}
     .out_o( racl_role_vec )
   );
 
+  // Figure out whether the access is an instruction fetch ("execute") or not. Note that the
+  // following two signals are *not* complementary strictly speaking: if `instr_type` isn't a valid
+  // MuBi value, neither of them will be true, hence the access will not be granted at all.
+  logic no_exec_access, exec_access;
+  assign no_exec_access = prim_mubi_pkg::mubi4_test_false_strict(
+                            prim_mubi_pkg::mubi4_t'(ctn_tl_h2d_i.a_user.instr_type));
+  assign exec_access    = prim_mubi_pkg::mubi4_test_true_strict(
+                            prim_mubi_pkg::mubi4_t'(ctn_tl_h2d_i.a_user.instr_type));
+
+  // Figure out whether the access is a read, write, or execute.
+  logic read_access, write_access, execute_access;
+  assign read_access    = (ctn_tl_h2d_i.a_opcode == Get) & no_exec_access;
+  assign write_access   = (ctn_tl_h2d_i.a_opcode == PutFullData) |
+                          (ctn_tl_h2d_i.a_opcode == PutPartialData);
+  assign execute_access = (ctn_tl_h2d_i.a_opcode == Get) & exec_access;
+
   for (genvar i = 0; i < NumRanges; i++) begin : gen_range_checks
     // Extend base, limit, and mask to 32 bits
     logic [31:0] base_ext, limit_ext;
@@ -154,62 +173,62 @@ module ${module_instance_name}
                                    prim_mubi_pkg::mubi4_t'(reg2hw.range_perm[i].execute_access.q)) &
                                    racl_read_hit[i];
 
-    // Access is denied if no read_, write_, or execute access is set in the permission mask
-    // The permission masks need to be reversed to allow for the right priority order.
-    // Range 0 has the highest priority and is the MSB in the mask.
-    assign deny_mask[NumRanges - 1 - i] =
-      addr_hit[i] & ~(perm_read_access | perm_write_access | perm_execute_access);
+    // A range grants a request if the request address hits and the type of the access (R/W/X) is
+    // permitted by that range. In the grant masks and the deny masks (see below), the range with
+    // index 0 is at the MSB, giving it the highest priority in the greater-than comparison to
+    // decide between grant and denial.
+    assign r_grant_mask[NumRanges-1-i] = addr_hit[i] & read_access & perm_read_access;
+    assign w_grant_mask[NumRanges-1-i] = addr_hit[i] & write_access & perm_write_access;
+    assign x_grant_mask[NumRanges-1-i] = addr_hit[i] & execute_access & perm_execute_access;
+
+    // A range denies a request if the request address hits and the type of the access (R/W/X) is
+    // *not* permitted by that range.
+    assign r_deny_mask[NumRanges-1-i] = addr_hit[i] & read_access & ~perm_read_access;
+    assign w_deny_mask[NumRanges-1-i] = addr_hit[i] & write_access & ~perm_write_access;
+    assign x_deny_mask[NumRanges-1-i] = addr_hit[i] & execute_access & ~perm_execute_access;
 
     // TODO(#25456) Use log_enable_mask to mask logging
     assign log_enable_mask[NumRanges - 1 - i] = prim_mubi_pkg::mubi4_test_true_strict(
       prim_mubi_pkg::mubi4_t'(reg2hw.range_perm[i].log_denied_access.q));
-
-    // Determine the read, write, and execute mask. Store a hit in their index
-    assign read_mask   [NumRanges - 1 - i] = addr_hit[i] & perm_read_access;
-    assign write_mask  [NumRanges - 1 - i] = addr_hit[i] & perm_write_access;
-    assign execute_mask[NumRanges - 1 - i] = addr_hit[i] & perm_execute_access;
   end
 
-  // Fiddle out bits to determine if it's an execute request or not
-  logic no_exec_access, exec_access;
-  assign no_exec_access = prim_mubi_pkg::mubi4_test_false_strict(
-                            prim_mubi_pkg::mubi4_t'(ctn_tl_h2d_i.a_user.instr_type));
-  assign exec_access    = prim_mubi_pkg::mubi4_test_true_strict(
-                            prim_mubi_pkg::mubi4_t'(ctn_tl_h2d_i.a_user.instr_type));
-
-  // Fiddle out what access we are performing
-  logic read_access, write_access, execute_access;
-  assign read_access = (ctn_tl_h2d_i.a_opcode == Get) & no_exec_access;
-  assign write_access = ((ctn_tl_h2d_i.a_opcode == PutFullData) |
-                         (ctn_tl_h2d_i.a_opcode == PutPartialData));
-  assign execute_access = (ctn_tl_h2d_i.a_opcode == Get) & exec_access;
-
-  // Priority comparison. If the deny mask is larger than the read, write, or execute mask, there
-  // was an address match with a higher priority for the range to be denied
-  logic read_allowed, write_allowed, execute_allowed;
-  assign read_allowed    = read_access    & (read_mask    > deny_mask);
-  assign write_allowed   = write_access   & (write_mask   > deny_mask);
-  assign execute_allowed = execute_access & (execute_mask > deny_mask);
+  // The overall grant and deny mask is simply the OR combination of the access-type-specific masks.
+  assign grant_mask = r_grant_mask | w_grant_mask | x_grant_mask;
+  assign deny_mask  = r_deny_mask  | w_deny_mask  | x_deny_mask;
 
   // Based on the deny mask, we compute the leading bit in the mask. The index of the leading
-  // bit determines the index of the range that denied the request.
+  // bit determines the index of the range that denied the request. As `prim_leading_one_ppc` starts
+  // its search for the "leading" bit at the LSB, `deny_mask` needs to be reversed to compute the
+  // index. The result is then directly the index of the SW-configured range, due to how
+  // `grant_mask` and `deny_mask` get built (see above).
+  logic [NumRanges-1:0] deny_mask_reversed;
+  assign deny_mask_reversed = {<<{deny_mask}};
 
   localparam int unsigned NumRangesWidth = prim_util_pkg::vbits(NumRanges);
   logic [NumRangesWidth-1:0] deny_index;
   prim_leading_one_ppc #(
     .N ( NumRanges )
   ) u_leading_one (
-    .in_i          ( deny_mask  ),
-    .leading_one_o (            ),
-    .ppc_out_o     (            ),
-    .idx_o         ( deny_index )
+    .in_i          ( deny_mask_reversed ),
+    .leading_one_o (                    ),
+    .ppc_out_o     (                    ),
+    .idx_o         ( deny_index         )
   );
 
-  // The access fails if nothing is allowed and no overwrite is present
+  // A request gets granted if and only if
+  // (1) the request is valid and
+  // (2.1) at least one range matches and among the matching ranges the one with the highest
+  //       priority grants the request or
+  // (2.2) range checks are bypassed.
+  logic range_check_grant;
+  assign range_check_grant = ctn_tl_h2d_i.a_valid & (
+                               (|addr_hit & (grant_mask > deny_mask)) |
+                               prim_mubi_pkg::mubi8_test_true_strict(range_check_overwrite_i)
+                             );
+
+  // A request gets denied if and only if it is valid and doesn't get granted.
   logic range_check_fail;
-  assign range_check_fail =
-    ctn_tl_h2d_i.a_valid & ~(|{read_allowed, write_allowed, execute_allowed,
-                               prim_mubi_pkg::mubi8_test_true_strict(range_check_overwrite_i)});
+  assign range_check_fail = ctn_tl_h2d_i.a_valid & ~range_check_grant;
 
   //////////////////////////////////////////////////////////////////////////////
   // TLUL Loopback for failing accesses
diff --git a/hw/top_darjeeling/ip_autogen/ac_range_check/rtl/ac_range_check.sv b/hw/top_darjeeling/ip_autogen/ac_range_check/rtl/ac_range_check.sv
index feef102e80b54..46d7ce0e20e90 100644
--- a/hw/top_darjeeling/ip_autogen/ac_range_check/rtl/ac_range_check.sv
+++ b/hw/top_darjeeling/ip_autogen/ac_range_check/rtl/ac_range_check.sv
@@ -107,7 +107,10 @@ module ac_range_check
   // Range Check Logic
   //////////////////////////////////////////////////////////////////////////////
 
-  logic [NumRanges-1:0] addr_hit, deny_mask, read_mask, write_mask, execute_mask, log_enable_mask;
+  logic [NumRanges-1:0] addr_hit;
+  logic [NumRanges-1:0] r_deny_mask, w_deny_mask, x_deny_mask, deny_mask;
+  logic [NumRanges-1:0] r_grant_mask, w_grant_mask, x_grant_mask, grant_mask;
+  logic [NumRanges-1:0] log_enable_mask;
   logic [NumRanges-1:0] racl_read_hit, racl_write_hit;
 
   // Retrieve RACL role from user bits and one-hot encode that for the comparison bitmap
@@ -123,6 +126,22 @@ module ac_range_check
     .out_o( racl_role_vec )
   );
 
+  // Figure out whether the access is an instruction fetch ("execute") or not. Note that the
+  // following two signals are *not* complementary strictly speaking: if `instr_type` isn't a valid
+  // MuBi value, neither of them will be true, hence the access will not be granted at all.
+  logic no_exec_access, exec_access;
+  assign no_exec_access = prim_mubi_pkg::mubi4_test_false_strict(
+                            prim_mubi_pkg::mubi4_t'(ctn_tl_h2d_i.a_user.instr_type));
+  assign exec_access    = prim_mubi_pkg::mubi4_test_true_strict(
+                            prim_mubi_pkg::mubi4_t'(ctn_tl_h2d_i.a_user.instr_type));
+
+  // Figure out whether the access is a read, write, or execute.
+  logic read_access, write_access, execute_access;
+  assign read_access    = (ctn_tl_h2d_i.a_opcode == Get) & no_exec_access;
+  assign write_access   = (ctn_tl_h2d_i.a_opcode == PutFullData) |
+                          (ctn_tl_h2d_i.a_opcode == PutPartialData);
+  assign execute_access = (ctn_tl_h2d_i.a_opcode == Get) & exec_access;
+
   for (genvar i = 0; i < NumRanges; i++) begin : gen_range_checks
     // Extend base, limit, and mask to 32 bits
     logic [31:0] base_ext, limit_ext;
@@ -154,62 +173,62 @@ module ac_range_check
                                    prim_mubi_pkg::mubi4_t'(reg2hw.range_perm[i].execute_access.q)) &
                                    racl_read_hit[i];
 
-    // Access is denied if no read_, write_, or execute access is set in the permission mask
-    // The permission masks need to be reversed to allow for the right priority order.
-    // Range 0 has the highest priority and is the MSB in the mask.
-    assign deny_mask[NumRanges - 1 - i] =
-      addr_hit[i] & ~(perm_read_access | perm_write_access | perm_execute_access);
+    // A range grants a request if the request address hits and the type of the access (R/W/X) is
+    // permitted by that range. In the grant masks and the deny masks (see below), the range with
+    // index 0 is at the MSB, giving it the highest priority in the greater-than comparison to
+    // decide between grant and denial.
+    assign r_grant_mask[NumRanges-1-i] = addr_hit[i] & read_access & perm_read_access;
+    assign w_grant_mask[NumRanges-1-i] = addr_hit[i] & write_access & perm_write_access;
+    assign x_grant_mask[NumRanges-1-i] = addr_hit[i] & execute_access & perm_execute_access;
+
+    // A range denies a request if the request address hits and the type of the access (R/W/X) is
+    // *not* permitted by that range.
+    assign r_deny_mask[NumRanges-1-i] = addr_hit[i] & read_access & ~perm_read_access;
+    assign w_deny_mask[NumRanges-1-i] = addr_hit[i] & write_access & ~perm_write_access;
+    assign x_deny_mask[NumRanges-1-i] = addr_hit[i] & execute_access & ~perm_execute_access;
 
     // TODO(#25456) Use log_enable_mask to mask logging
     assign log_enable_mask[NumRanges - 1 - i] = prim_mubi_pkg::mubi4_test_true_strict(
       prim_mubi_pkg::mubi4_t'(reg2hw.range_perm[i].log_denied_access.q));
-
-    // Determine the read, write, and execute mask. Store a hit in their index
-    assign read_mask   [NumRanges - 1 - i] = addr_hit[i] & perm_read_access;
-    assign write_mask  [NumRanges - 1 - i] = addr_hit[i] & perm_write_access;
-    assign execute_mask[NumRanges - 1 - i] = addr_hit[i] & perm_execute_access;
   end
 
-  // Fiddle out bits to determine if it's an execute request or not
-  logic no_exec_access, exec_access;
-  assign no_exec_access = prim_mubi_pkg::mubi4_test_false_strict(
-                            prim_mubi_pkg::mubi4_t'(ctn_tl_h2d_i.a_user.instr_type));
-  assign exec_access    = prim_mubi_pkg::mubi4_test_true_strict(
-                            prim_mubi_pkg::mubi4_t'(ctn_tl_h2d_i.a_user.instr_type));
-
-  // Fiddle out what access we are performing
-  logic read_access, write_access, execute_access;
-  assign read_access = (ctn_tl_h2d_i.a_opcode == Get) & no_exec_access;
-  assign write_access = ((ctn_tl_h2d_i.a_opcode == PutFullData) |
-                         (ctn_tl_h2d_i.a_opcode == PutPartialData));
-  assign execute_access = (ctn_tl_h2d_i.a_opcode == Get) & exec_access;
-
-  // Priority comparison. If the deny mask is larger than the read, write, or execute mask, there
-  // was an address match with a higher priority for the range to be denied
-  logic read_allowed, write_allowed, execute_allowed;
-  assign read_allowed    = read_access    & (read_mask    > deny_mask);
-  assign write_allowed   = write_access   & (write_mask   > deny_mask);
-  assign execute_allowed = execute_access & (execute_mask > deny_mask);
+  // The overall grant and deny mask is simply the OR combination of the access-type-specific masks.
+  assign grant_mask = r_grant_mask | w_grant_mask | x_grant_mask;
+  assign deny_mask  = r_deny_mask  | w_deny_mask  | x_deny_mask;
 
   // Based on the deny mask, we compute the leading bit in the mask. The index of the leading
-  // bit determines the index of the range that denied the request.
+  // bit determines the index of the range that denied the request. As `prim_leading_one_ppc` starts
+  // its search for the "leading" bit at the LSB, `deny_mask` needs to be reversed to compute the
+  // index. The result is then directly the index of the SW-configured range, due to how
+  // `grant_mask` and `deny_mask` get built (see above).
+  logic [NumRanges-1:0] deny_mask_reversed;
+  assign deny_mask_reversed = {<<{deny_mask}};
 
   localparam int unsigned NumRangesWidth = prim_util_pkg::vbits(NumRanges);
   logic [NumRangesWidth-1:0] deny_index;
   prim_leading_one_ppc #(
     .N ( NumRanges )
   ) u_leading_one (
-    .in_i          ( deny_mask  ),
-    .leading_one_o (            ),
-    .ppc_out_o     (            ),
-    .idx_o         ( deny_index )
+    .in_i          ( deny_mask_reversed ),
+    .leading_one_o (                    ),
+    .ppc_out_o     (                    ),
+    .idx_o         ( deny_index         )
   );
 
-  // The access fails if nothing is allowed and no overwrite is present
+  // A request gets granted if and only if
+  // (1) the request is valid and
+  // (2.1) at least one range matches and among the matching ranges the one with the highest
+  //       priority grants the request or
+  // (2.2) range checks are bypassed.
+  logic range_check_grant;
+  assign range_check_grant = ctn_tl_h2d_i.a_valid & (
+                               (|addr_hit & (grant_mask > deny_mask)) |
+                               prim_mubi_pkg::mubi8_test_true_strict(range_check_overwrite_i)
+                             );
+
+  // A request gets denied if and only if it is valid and doesn't get granted.
   logic range_check_fail;
-  assign range_check_fail =
-    ctn_tl_h2d_i.a_valid & ~(|{read_allowed, write_allowed, execute_allowed,
-                               prim_mubi_pkg::mubi8_test_true_strict(range_check_overwrite_i)});
+  assign range_check_fail = ctn_tl_h2d_i.a_valid & ~range_check_grant;
 
   //////////////////////////////////////////////////////////////////////////////
   // TLUL Loopback for failing accesses


Reviews:
user: login: andreaskurth
body: Thx for flagging this issue and proposing a fix, @martin-velay.

@Razer6: This is what I meant in my DM. Essentially, `ac_range_checks` currently _grants_ an access that is granted by a range even though a range with lower index _denies_ it.

For example, assume a read request and two ranges that match the address of the request:
- range 0 _denies_ reads but allows other accesses, e.g., writes
- range 1 _allows_ reads

Then we would expect that range 0 prevails and `ac_range_checks` denies the request. But the current implementation would then compute (note that in the following code, range 0 corresponds to bit 1 and range 1 to bit 0 -- which is correct due to the `>` comparison used later to make the overall decision):
```sv
deny_mask = 2'b00; // bit [1] = 0 because perm_write_access[1] = 1
read_mask = 2'b01; // bit [1] = 0 because perm_read_access[1] = 0
read_allowed = 1'b1; // because read_access & (read_mask > deny_mask);
```
and hence _grant_ the request.

I think the logic we instead want could be simply implemented as follows:
```sv
logic [NumRanges-1:0] r_deny_mask, w_deny_mask, x_deny_mask, deny_mask;
logic [NumRanges-1:0] r_grant_mask, w_grant_mask, x_grant_mask, grant_mask;

for (genvar i = 0; i < NumRanges-1:0; i++) begin
  assign r_grant_mask[NumRanges-1-i] = addr_hit[i] & read_access & perm_read_access[i];
  assign w_grant_mask[NumRanges-1-i] = addr_hit[i] & write_access & perm_write_access[i];
  assign x_grant_mask[NumRanges-1-i] = addr_hit[i] & execute_access & perm_execute_access[i];
  
  assign r_deny_mask[NumRanges-1-i] = addr_hit[i] & read_access & ~perm_read_access[i];
  assign w_deny_mask[NumRanges-1-i] = addr_hit[i] & write_access & ~perm_write_access[i];
  assign x_deny_mask[NumRanges-1-i] = addr_hit[i] & execute_access & ~perm_execute_access[i];
end

assign grant_mask = r_grant_mask | w_grant_mask | x_grant_mask;
assign deny_mask = r_deny_mask | w_deny_mask | x_deny_mask;

assign range_check_fail = ctn_tl_h2d_i.a_valid & (deny_mask > grant_mask) & ~prim_mubi_pkg::mubi8_test_true_strict(range_check_overwrite_i);

// deny_index will need to be computed as `NumRanges - (idx_o of PPC)` -- that issue is orthogonal to the one with the permissions, though
```
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T17:06:31Z
user: login: martin-velay
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T16:31:57Z
user: login: andreaskurth
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T17:30:36Z
user: login: martin-velay
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T17:39:49Z
user: login: andreaskurth
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T13:20:43Z
user: login: Razer6
body: Thanks for fixing this @martin-velay and @andreaskurth. This looks good to me! Go ahead!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T13:23:57Z
user: login: martin-velay
body: Thanks @andreaskurth for the changes. All look good to me!
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T14:38:52Z

Review comments:
diff_hunk: @@ -187,15 +190,34 @@ module ${module_instance_name}
   // Priority comparison. If the deny mask is larger than the read, write, or execute mask, there
   // was an address match with a higher priority for the range to be denied
   logic read_allowed, write_allowed, execute_allowed;
-  assign read_allowed    = read_access    & (read_mask    > deny_mask);
-  assign write_allowed   = write_access   & (write_mask   > deny_mask);
-  assign execute_allowed = execute_access & (execute_mask > deny_mask);
+  assign read_allowed    = read_access    & (read_grant_mask    > read_perm_n_mask);
+  assign write_allowed   = write_access   & (write_grant_mask   > write_perm_n_mask);
+  assign execute_allowed = execute_access & (execute_grant_mask > execute_perm_n_mask);
 
   // Based on the deny mask, we compute the leading bit in the mask. The index of the leading
   // bit determines the index of the range that denied the request.
 
   localparam int unsigned NumRangesWidth = prim_util_pkg::vbits(NumRanges);
+  logic [NumRanges-1:0] read_deny_mask, write_deny_mask, execute_deny_mask, deny_mask;
   logic [NumRangesWidth-1:0] deny_index;
+
+  always_comb begin: assign_deny_mask
+    read_deny_mask    = '0;
+    write_deny_mask   = '0;
+    execute_deny_mask = '0;
+    if (read_access & ~read_allowed) begin
+      read_deny_mask = read_perm_n_mask;
+    end
+    if (write_access & ~write_allowed) begin
+      write_deny_mask = write_perm_n_mask;
+    end
+    if (execute_access & ~execute_allowed) begin
+      execute_deny_mask = execute_perm_n_mask;
+    end
+    // Need to be reversed as prim_leading_one_ppc returns the leading one from LSB
+    deny_mask = {<<{read_deny_mask | write_deny_mask | execute_deny_mask}};
path: hw/ip_templates/ac_range_check/rtl/ac_range_check.sv.tpl
user: login: martin-velay
body: @andreaskurth I took the opportunity to correct that. I think this should also go into your RTL fix either. What do you think?
created_at: 2025-03-27T16:31:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -187,15 +190,34 @@ module ${module_instance_name}
   // Priority comparison. If the deny mask is larger than the read, write, or execute mask, there
   // was an address match with a higher priority for the range to be denied
   logic read_allowed, write_allowed, execute_allowed;
-  assign read_allowed    = read_access    & (read_mask    > deny_mask);
-  assign write_allowed   = write_access   & (write_mask   > deny_mask);
-  assign execute_allowed = execute_access & (execute_mask > deny_mask);
+  assign read_allowed    = read_access    & (read_grant_mask    > read_perm_n_mask);
+  assign write_allowed   = write_access   & (write_grant_mask   > write_perm_n_mask);
+  assign execute_allowed = execute_access & (execute_grant_mask > execute_perm_n_mask);
 
   // Based on the deny mask, we compute the leading bit in the mask. The index of the leading
   // bit determines the index of the range that denied the request.
 
   localparam int unsigned NumRangesWidth = prim_util_pkg::vbits(NumRanges);
+  logic [NumRanges-1:0] read_deny_mask, write_deny_mask, execute_deny_mask, deny_mask;
   logic [NumRangesWidth-1:0] deny_index;
+
+  always_comb begin: assign_deny_mask
+    read_deny_mask    = '0;
+    write_deny_mask   = '0;
+    execute_deny_mask = '0;
+    if (read_access & ~read_allowed) begin
+      read_deny_mask = read_perm_n_mask;
+    end
+    if (write_access & ~write_allowed) begin
+      write_deny_mask = write_perm_n_mask;
+    end
+    if (execute_access & ~execute_allowed) begin
+      execute_deny_mask = execute_perm_n_mask;
+    end
+    // Need to be reversed as prim_leading_one_ppc returns the leading one from LSB
+    deny_mask = {<<{read_deny_mask | write_deny_mask | execute_deny_mask}};
path: hw/ip_templates/ac_range_check/rtl/ac_range_check.sv.tpl
user: login: andreaskurth
body: Ah yes, thanks for the reminder. I just added that, could you please give it a try?
created_at: 2025-03-27T17:30:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -187,15 +190,34 @@ module ${module_instance_name}
   // Priority comparison. If the deny mask is larger than the read, write, or execute mask, there
   // was an address match with a higher priority for the range to be denied
   logic read_allowed, write_allowed, execute_allowed;
-  assign read_allowed    = read_access    & (read_mask    > deny_mask);
-  assign write_allowed   = write_access   & (write_mask   > deny_mask);
-  assign execute_allowed = execute_access & (execute_mask > deny_mask);
+  assign read_allowed    = read_access    & (read_grant_mask    > read_perm_n_mask);
+  assign write_allowed   = write_access   & (write_grant_mask   > write_perm_n_mask);
+  assign execute_allowed = execute_access & (execute_grant_mask > execute_perm_n_mask);
 
   // Based on the deny mask, we compute the leading bit in the mask. The index of the leading
   // bit determines the index of the range that denied the request.
 
   localparam int unsigned NumRangesWidth = prim_util_pkg::vbits(NumRanges);
+  logic [NumRanges-1:0] read_deny_mask, write_deny_mask, execute_deny_mask, deny_mask;
   logic [NumRangesWidth-1:0] deny_index;
+
+  always_comb begin: assign_deny_mask
+    read_deny_mask    = '0;
+    write_deny_mask   = '0;
+    execute_deny_mask = '0;
+    if (read_access & ~read_allowed) begin
+      read_deny_mask = read_perm_n_mask;
+    end
+    if (write_access & ~write_allowed) begin
+      write_deny_mask = write_perm_n_mask;
+    end
+    if (execute_access & ~execute_allowed) begin
+      execute_deny_mask = execute_perm_n_mask;
+    end
+    // Need to be reversed as prim_leading_one_ppc returns the leading one from LSB
+    deny_mask = {<<{read_deny_mask | write_deny_mask | execute_deny_mask}};
path: hw/ip_templates/ac_range_check/rtl/ac_range_check.sv.tpl
user: login: martin-velay
body: It looks good now, thanks for the change. I think this [commit](https://github.com/andreaskurth/opentitan/commit/05e4737f9c84d2847d28a6ade94dbdd76502d46f) is now OK.
created_at: 2025-03-27T17:39:49Z
author_association: CONTRIBUTOR

PR comments:
user: login: martin-velay
created_at: 2025-03-11T13:39:26Z
body: This is a proposition. It fixes the mentioned issue but I am not sure whether there is no better/optimized option to solve this issue.
author_association: CONTRIBUTOR
user: login: martin-velay
created_at: 2025-03-17T08:54:37Z
body: @Razer6 and @alees24, would you have time to review this PR or should I tag somebody else?
author_association: CONTRIBUTOR
user: login: martin-velay
created_at: 2025-03-20T16:00:01Z
body: FYI @andreaskurth 
I don't understand why, but this bug is not visible anymore with the current DV state on the master. I am investigating.
author_association: CONTRIBUTOR
user: login: Razer6
created_at: 2025-03-24T20:06:28Z
body: Agreed, this is an RTL bug. Would you make a PR for this fix? Would there need to be separate mask comparison for each permission?

```systemverilog
  assign read_allowed    = read_access    & (r_grant_mask > r_deny_mask);
  assign write_allowed   = write_access   & (w_grant_mask > w_deny_mask);
  assign execute_allowed = execute_access & (x_grant_mask > x_deny_mask);
```
author_association: CONTRIBUTOR
user: login: andreaskurth
created_at: 2025-03-25T09:34:55Z
body: > Agreed, this is an RTL bug. Would you make a PR for this fix?

:+1: WIP impl [here](https://github.com/andreaskurth/opentitan/commits/ac_range_check-fix-perm/); @martin-velay will test it.

> Would there need to be separate mask comparison for each permission?
> 
> ```systemverilog
>   assign read_allowed    = read_access    & (r_grant_mask > r_deny_mask);
>   assign write_allowed   = write_access   & (w_grant_mask > w_deny_mask);
>   assign execute_allowed = execute_access & (x_grant_mask > x_deny_mask);
> ```

I don't _think_ so, because the RWX-specific `deny` and `grant` masks individually already factor in the access type, so the combined `deny` and `grant` mask contain the information which range denies or grants a request. All that is left is the priority resolution, which happens with one `fail = deny_mask > grant_mask` comparison.
author_association: CONTRIBUTOR
user: login: Razer6
created_at: 2025-03-27T11:55:43Z
body: > I don't _think_ so, because the RWX-specific `deny` and `grant` masks individually already factor in the access type, so the combined `deny` and `grant` mask contain the information which range denies or grants a request. All that is left is the priority resolution, which happens with one `fail = deny_mask > grant_mask` comparison.

That makes sense. @martin-velay can you confirm this change works as expected?
author_association: CONTRIBUTOR
user: login: martin-velay
created_at: 2025-03-27T13:36:17Z
body: Sure, I'll work on it this afternoon, sorry for the delay
author_association: CONTRIBUTOR
user: login: martin-velay
created_at: 2025-03-27T16:44:54Z
body: @Razer6 and @andreaskurth

The fix proposed [here](https://github.com/andreaskurth/opentitan/commit/1937e296e76dd4191cedfdcab4f45f569dd0bd72) works against my current TB version.

But I also wanted to make sure that you don't want to add another point I had fixed in my proposal, see this comment: https://github.com/lowRISC/opentitan/pull/26589/files#r2017071222
author_association: CONTRIBUTOR
user: login: andreaskurth
created_at: 2025-03-28T13:20:33Z
body: Thanks @martin-velay for testing this. I've just pushed the cleaned-up version of the commit to this PR. If it looks good to you and @Razer6, I'd go ahead and merge the fix.
author_association: CONTRIBUTOR
user: login: andreaskurth
created_at: 2025-03-28T14:21:55Z
body: I've just given the case where `a_user.instr_type` is not a valid MuBi4 value a thought: In that case, the previous implementation would have *granted* an access if it matched the address of at least one range (even if that range should have denied the request). This is now fixed.
author_association: CONTRIBUTOR
user: login: andreaskurth
created_at: 2025-03-28T14:34:39Z
body: I should mention that the internal logic of `ac_range_check` currently isn't FI-hardened very well -- there are multiple places where a single bit flip (in combinational logic, not in flops) could grant an access that should have been denied.

@Razer6: I think this isn't an immediate concern, though, right? In that case, I'd open an issue to track this for the future.
author_association: CONTRIBUTOR
user: login: Razer6
created_at: 2025-03-28T14:57:37Z
body: > @Razer6: I think this isn't an immediate concern, though, right? In that case, I'd open an issue to track this for the future.

No immediate concerns. Issue is good.
author_association: CONTRIBUTOR
user: login: andreaskurth
created_at: 2025-03-28T15:39:14Z
body: > > @Razer6: I think this isn't an immediate concern, though, right? In that case, I'd open an issue to track this for the future.
> 
> No immediate concerns. Issue is good.

:+1: --> https://github.com/lowRISC/opentitan/issues/26772
author_association: CONTRIBUTOR