diff --git a/Makefile b/Makefile index f5b569a..635d63a 100644 --- a/Makefile +++ b/Makefile @@ -4,11 +4,12 @@ YOSYS ?= yosys VERILATOR ?= verilator BUILD_DIR ?= build -CPU_RTL = hw/rtl/cpu/alu.sv hw/rtl/cpu/regfile.sv hw/rtl/cpu/rvc_decompress.sv hw/rtl/cpu/cpu_core.sv -BUS_RTL = hw/rtl/bus/wb_error_slave.sv hw/rtl/bus/wb_address_decoder.sv hw/rtl/bus/wb_arbiter.sv hw/rtl/bus/wb_interconnect.sv hw/rtl/bus/wb_cpu_adapter.sv +CPU_RTL = hw/rtl/cpu/alu.sv hw/rtl/cpu/regfile.sv hw/rtl/cpu/rvc_decompress.sv hw/rtl/security/seal_pmp.sv hw/rtl/security/pmp_checker.sv hw/rtl/cpu/cpu_core.sv +BUS_RTL = hw/rtl/bus/wb_error_slave.sv hw/rtl/bus/wb_address_decoder.sv hw/rtl/bus/wb_arbiter.sv hw/rtl/security/wb_firewall.sv hw/rtl/bus/wb_interconnect.sv hw/rtl/bus/wb_cpu_adapter.sv PERIPHERAL_RTL = hw/rtl/peripherals/wb_uart.sv hw/rtl/peripherals/wb_gpio.sv hw/rtl/peripherals/wb_spi.sv hw/rtl/peripherals/wb_i2c.sv hw/rtl/peripherals/wb_timer.sv hw/rtl/peripherals/wb_watchdog.sv hw/rtl/peripherals/interrupt_controller.sv hw/rtl/peripherals/wb_debug_perf.sv hw/rtl/peripherals/wb_power_ctrl.sv hw/rtl/peripherals/peripheral_subsystem.sv +SECURITY_RTL = hw/rtl/security/seal_pmp.sv hw/rtl/security/pmp_checker.sv hw/rtl/security/wb_firewall.sv -.PHONY: all sim lint synth test-cocotb clean +.PHONY: all sim lint synth test-cocotb security-test cpu-pmp-test clean all: sim test-cocotb lint synth @@ -32,6 +33,18 @@ sim: | $(BUILD_DIR) $(VVP) $(BUILD_DIR)/peripheral_subsystem_tb $(IVERILOG) -g2012 -Wall -s peripheral_cpu_irq_tb -o $(BUILD_DIR)/peripheral_cpu_irq_tb $(CPU_RTL) hw/rtl/bus/wb_cpu_adapter.sv hw/rtl/memory/wb_sram.sv hw/rtl/memory/boot_rom.sv hw/rtl/memory/memory_subsystem.sv $(PERIPHERAL_RTL) hw/tb/peripheral_cpu_irq_tb.sv $(VVP) $(BUILD_DIR)/peripheral_cpu_irq_tb + $(IVERILOG) -g2012 -Wall -s security_tb -o $(BUILD_DIR)/security_tb $(SECURITY_RTL) hw/tb/security_tb.sv + $(VVP) $(BUILD_DIR)/security_tb + $(IVERILOG) -g2012 -Wall -s cpu_pmp_tb -o $(BUILD_DIR)/cpu_pmp_tb $(CPU_RTL) hw/tb/cpu_pmp_tb.sv + $(VVP) $(BUILD_DIR)/cpu_pmp_tb + +security-test: | $(BUILD_DIR) + $(IVERILOG) -g2012 -Wall -s security_tb -o $(BUILD_DIR)/security_tb $(SECURITY_RTL) hw/tb/security_tb.sv + $(VVP) $(BUILD_DIR)/security_tb + +cpu-pmp-test: | $(BUILD_DIR) + $(IVERILOG) -g2012 -Wall -s cpu_pmp_tb -o $(BUILD_DIR)/cpu_pmp_tb $(CPU_RTL) hw/tb/cpu_pmp_tb.sv + $(VVP) $(BUILD_DIR)/cpu_pmp_tb lint: $(VERILATOR) --lint-only --Wall -Wno-fatal --top-module counter hw/rtl/counter.sv @@ -40,6 +53,9 @@ lint: $(VERILATOR) --lint-only --Wall -Wno-fatal --top-module wb_cpu_adapter $(BUS_RTL) $(VERILATOR) --lint-only --Wall -Wno-fatal --top-module memory_subsystem hw/rtl/memory/wb_sram.sv hw/rtl/memory/boot_rom.sv hw/rtl/memory/memory_subsystem.sv $(VERILATOR) --lint-only --Wall -Wno-fatal --top-module peripheral_subsystem $(PERIPHERAL_RTL) + $(VERILATOR) --lint-only --Wall -Wno-fatal --top-module seal_pmp $(SECURITY_RTL) + $(VERILATOR) --lint-only --Wall -Wno-fatal --top-module pmp_checker $(SECURITY_RTL) + $(VERILATOR) --lint-only --Wall -Wno-fatal --top-module wb_firewall $(SECURITY_RTL) synth: | $(BUILD_DIR) $(YOSYS) -s scripts/synth.ys diff --git a/docs/BUS_SPECIFICATION.md b/docs/BUS_SPECIFICATION.md index 4de0598..f336752 100644 --- a/docs/BUS_SPECIFICATION.md +++ b/docs/BUS_SPECIFICATION.md @@ -22,7 +22,7 @@ This specification defines the implemented Phase 04 subsystem. The RTL implement ## Arbitration and protection -CPU and DMA requests are arbitrated round-robin. Locking/atomic bus cycles are out of scope while the A extension is excluded. PMP and the DMA firewall are not yet connected to this interconnect. +CPU and DMA requests are arbitrated round-robin. Locking/atomic bus cycles are out of scope while the A extension is excluded. The post-arbitration bus firewall independently validates the granted master before target decode, returns `ERR` for denied requests, and logs the master and address. CPU PMP checks instruction fetches and data operations inside the CPU; it does not apply to DMA. ## CDC and timing diff --git a/docs/MEMORY_MAP.md b/docs/MEMORY_MAP.md index cb50176..b4abe2d 100644 --- a/docs/MEMORY_MAP.md +++ b/docs/MEMORY_MAP.md @@ -28,6 +28,8 @@ All addresses are byte addresses. Boot ROM and main SRAM are implemented by Phas - Boot ROM writes return an error. SRAM accesses outside the 32 KiB range return an error. Unmapped addresses return an error and set an observable fault cause. - Execute permission is denied for MMIO by default. ROM and SRAM execute permissions are governed by CPU privilege/PMP policy. - DMA may access SRAM and explicitly permitted memory windows only. DMA access to ROM, key-vault state, debug controls, or protected peripheral registers is denied by default. +- SRAM `0x1000_7000`–`0x1000_7FFF` is the secure 4 KiB subregion and is accessible only to the CPU in machine mode through the bus firewall. DMA is limited to `0x1000_0000`–`0x1000_6FFF`. +- The eight-entry CPU PMP governs instruction fetches and data accesses independently of the bus firewall. PMP does not authorize DMA or other non-CPU masters. - Address decode uses the complete address so high-address aliases cannot reach a valid target. DMA, crypto, AI accelerator, TRNG, and PMP/firewall register behavior remains to be implemented. Physical clock gating, debug halt/step, and external debug transport are not part of the initial Phase 06 RTL. diff --git a/hw/rtl/bus/wb_arbiter.sv b/hw/rtl/bus/wb_arbiter.sv index e6c5374..d9bde61 100644 --- a/hw/rtl/bus/wb_arbiter.sv +++ b/hw/rtl/bus/wb_arbiter.sv @@ -19,13 +19,15 @@ module wb_arbiter #( output logic [31:0] s_adr, s_dat_w, output logic [3:0] s_sel, input logic [31:0] s_dat_r, - input logic s_ack, s_err + input logic s_ack, s_err, + output logic owner_out ); localparam integer TIMEOUT_LIMIT = (TIMEOUT_CYCLES < 1) ? 1 : TIMEOUT_CYCLES; localparam integer COUNT_W = (TIMEOUT_LIMIT < 2) ? 1 : $clog2(TIMEOUT_LIMIT + 1); logic busy, owner, next_owner; logic [COUNT_W-1:0] wait_count; logic timeout; + assign owner_out = owner; assign s_cyc = busy && (owner ? m1_cyc : m0_cyc); assign s_stb = busy && (owner ? m1_stb : m0_stb); assign s_we = busy && (owner ? m1_we : m0_we); diff --git a/hw/rtl/bus/wb_interconnect.sv b/hw/rtl/bus/wb_interconnect.sv index 91e9582..1ae3055 100644 --- a/hw/rtl/bus/wb_interconnect.sv +++ b/hw/rtl/bus/wb_interconnect.sv @@ -14,6 +14,7 @@ module wb_interconnect #( input logic m1_cyc, m1_stb, m1_we, input logic [31:0] m1_adr, m1_dat_w, input logic [3:0] m1_sel, + input logic [1:0] m0_privilege, m1_privilege, output logic [31:0] m1_dat_r, output logic m1_ack, m1_err, output logic [2:0] s_cyc, s_stb, s_we, @@ -21,7 +22,11 @@ module wb_interconnect #( output logic [95:0] s_dat_w, output logic [11:0] s_sel, input logic [95:0] s_dat_r, - input logic [2:0] s_ack, s_err + input logic [2:0] s_ack, s_err, + output logic violation_valid, + output logic [31:0] violation_addr, + output logic [2:0] violation_master, + output logic [31:0] violation_count ); logic d_cyc, d_stb, d_we; logic [31:0] d_adr, d_dat_w, d_dat_r; @@ -32,6 +37,13 @@ module wb_interconnect #( logic error_ack, error_err; logic [31:0] error_rdata; logic target_ack, target_err; + logic arb_owner; + logic raw_cyc, raw_stb, raw_we; + logic [31:0] raw_adr, raw_dat_w, raw_dat_r; + logic [3:0] raw_sel; + logic fw_ack, fw_err, fw_cyc, fw_stb, fw_we; + logic [31:0] fw_adr, fw_dat_w; + logic [3:0] fw_sel; wb_arbiter #(.TIMEOUT_CYCLES(TIMEOUT_CYCLES)) arbiter ( .clk, .rst_n, @@ -39,10 +51,22 @@ module wb_interconnect #( .m0_dat_r, .m0_ack, .m0_err, .m1_cyc, .m1_stb, .m1_we, .m1_adr, .m1_dat_w, .m1_sel, .m1_dat_r, .m1_ack, .m1_err, - .s_cyc(d_cyc), .s_stb(d_stb), .s_we(d_we), .s_adr(d_adr), .s_dat_w(d_dat_w), .s_sel(d_sel), - .s_dat_r(d_dat_r), .s_ack(d_ack), .s_err(d_err) + .s_cyc(raw_cyc), .s_stb(raw_stb), .s_we(raw_we), .s_adr(raw_adr), .s_dat_w(raw_dat_w), .s_sel(raw_sel), + .s_dat_r(raw_dat_r), .s_ack(fw_ack), .s_err(fw_err), .owner_out(arb_owner) ); + wb_firewall firewall ( + .clk, .rst_n, .master_id(arb_owner ? 3'd1 : 3'd0), + .privilege(arb_owner ? m1_privilege : m0_privilege), .cpu_execute(1'b0), + .m_cyc(raw_cyc), .m_stb(raw_stb), .m_we(raw_we), .m_adr(raw_adr), .m_dat_w(raw_dat_w), .m_sel(raw_sel), + .m_dat_r(raw_dat_r), .m_ack(fw_ack), .m_err(fw_err), + .s_cyc(fw_cyc), .s_stb(fw_stb), .s_we(fw_we), .s_adr(fw_adr), .s_dat_w(fw_dat_w), .s_sel(fw_sel), + .s_dat_r(d_dat_r), .s_ack(d_ack), .s_err(d_err), + .violation_valid, .violation_addr, .violation_master, .violation_count + ); + assign d_cyc=fw_cyc; assign d_stb=fw_stb; assign d_we=fw_we; + assign d_adr=fw_adr; assign d_dat_w=fw_dat_w; assign d_sel=fw_sel; + wb_address_decoder decoder (.adr(d_adr), .select(target), .unmapped); wb_error_slave error_target ( .clk, .rst_n, diff --git a/hw/rtl/cpu/cpu_core.sv b/hw/rtl/cpu/cpu_core.sv index 3b9d10f..3dd2537 100644 --- a/hw/rtl/cpu/cpu_core.sv +++ b/hw/rtl/cpu/cpu_core.sv @@ -86,6 +86,51 @@ module cpu_core ( logic signed [63:0] mul_a_signed, mul_b_signed; logic [63:0] mul_a_unsigned, mul_b_unsigned; logic [31:0] muldiv_result; + logic [63:0] pmpcfg_flat; + logic [255:0] pmpaddr_flat; + logic [7:0] pmp_cfg_we; + logic pmp_addr_we, pmp_data_deny, pmp_fetch_deny, pmp_data_allow, pmp_fetch_allow; + logic [2:0] pmp_addr_index; + logic [31:0] pmp_addr_write_data; + logic [4:0] pmp_data_size, pmp_fetch_size; + logic pmp_mem_ready, pmp_mem_error; + logic [7:0] pmp_cfg_unused; + logic [31:0] pmp_addr_unused; + + assign pmp_cfg_we = (state==ST_EXEC && !pmp_fetch_deny && dec_is_csr && csr_write && !dec_exception && + (csr_addr==12'h3a0 || csr_addr==12'h3a2)) ? + ((csr_addr==12'h3a0) ? 8'h0f : 8'hf0) : 8'h00; + assign pmp_addr_we = (state==ST_EXEC && !pmp_fetch_deny && dec_is_csr && csr_write && !dec_exception && + csr_addr>=12'h3b0 && csr_addr<=12'h3b7); + assign pmp_addr_index = csr_addr[2:0]; + assign pmp_addr_write_data = csr_wdata; + seal_pmp pmp_regs ( + .clk, .rst_n, .cfg_we(pmp_cfg_we), .cfg_wdata({csr_wdata,csr_wdata}), + .addr_we(pmp_addr_we), .addr_index(pmp_addr_index), .addr_wdata(pmp_addr_write_data), + .read_index(csr_addr[2:0]), .cfg_rdata(pmp_cfg_unused), .addr_rdata(pmp_addr_unused), + .cfg_flat(pmpcfg_flat), .addr_flat(pmpaddr_flat) + ); + always_comb begin + case (mem_funct3) + 3'b000,3'b100: pmp_data_size=1; + 3'b001,3'b101: pmp_data_size=2; + default: pmp_data_size=4; + endcase + pmp_fetch_size=compressed ? 2 : 4; + end + pmp_checker pmp_data_check ( + .addr(mem_aligned_addr + {30'b0,mem_lane}), .size_bytes(pmp_data_size), + .access_read(!mem_is_store), .access_write(mem_is_store), .access_execute(1'b0), + .privilege(2'b11), .cfg_flat(pmpcfg_flat), .addr_flat(pmpaddr_flat), + .allow(pmp_data_allow), .matched(), .matched_index() + ); + pmp_checker pmp_fetch_check ( + .addr(pc), .size_bytes(pmp_fetch_size), .access_read(1'b0), .access_write(1'b0), .access_execute(1'b1), + .privilege(2'b11), .cfg_flat(pmpcfg_flat), .addr_flat(pmpaddr_flat), + .allow(pmp_fetch_allow), .matched(), .matched_index() + ); + assign pmp_data_deny = !pmp_data_allow; + assign pmp_fetch_deny = !pmp_fetch_allow; assign compressed_word = instr[15:0]; assign compressed = (compressed_word[1:0] != 2'b11); @@ -131,6 +176,10 @@ module cpu_core ( 12'h304: csr_rdata = mie; 12'h344: begin csr_rdata = mip_value; csr_readonly = 1'b1; end 12'h305: csr_rdata = mtvec; + 12'h3a0: csr_rdata = pmpcfg_flat[31:0]; + 12'h3a2: csr_rdata = pmpcfg_flat[63:32]; + 12'h3b0,12'h3b1,12'h3b2,12'h3b3,12'h3b4,12'h3b5,12'h3b6,12'h3b7: + csr_rdata = pmpaddr_flat[csr_addr[2:0]*32 +: 32]; 12'h341: csr_rdata = mepc; 12'h342: csr_rdata = mcause; 12'h343: csr_rdata = mtval; @@ -313,18 +362,21 @@ module cpu_core ( always_comb begin rf_we=0; rf_rd=rd; rf_wdata=dec_write_data; - if (state==ST_EXEC && dec_legal && dec_write_rd && !dec_exception && !dec_is_load && !dec_is_store && !dec_is_mret && !dec_is_wfi) rf_we=1; - else if (state==ST_MEM && dmem_ready && !dmem_error && !mem_is_store) begin rf_we=1; rf_rd=mem_rd; rf_wdata=load_result; end + if (state==ST_EXEC && !pmp_fetch_deny && dec_legal && dec_write_rd && !dec_exception && !dec_is_load && !dec_is_store && !dec_is_mret && !dec_is_wfi) rf_we=1; + else if (state==ST_MEM && dmem_ready && !dmem_error && !pmp_data_deny && !mem_is_store) begin rf_we=1; rf_rd=mem_rd; rf_wdata=load_result; end end always_comb begin - dmem_valid=(state==ST_MEM); dmem_write=mem_is_store; dmem_addr=mem_aligned_addr; + dmem_valid=(state==ST_MEM) && !pmp_data_deny; dmem_write=mem_is_store; dmem_addr=mem_aligned_addr; dmem_wdata=mem_write_data; dmem_wstrb=mem_write_strobe; end // One-cycle architectural retirement pulse for observability/performance // counters. Memory instructions retire only when their bus operation ends. assign retire = rst_n && !trap_take && ((state==ST_EXEC && dec_legal && !dec_exception && !dec_is_load && !dec_is_store) || - (state==ST_MEM && dmem_ready && !dmem_error)); + (state==ST_MEM && dmem_ready && !dmem_error && !pmp_data_deny)); + + assign pmp_mem_ready = dmem_ready || (state==ST_MEM && pmp_data_deny); + assign pmp_mem_error = dmem_error || pmp_data_deny; always_comb begin trap_take = 1'b0; @@ -332,12 +384,14 @@ module cpu_core ( trap_entry_cause = 32'd0; trap_entry_value = 32'd0; trap_entry_epc = pc; - if (state==ST_EXEC && interrupt_pending && mstatus[3]) begin + if (state==ST_EXEC && pmp_fetch_deny) begin + trap_take=1'b1; trap_entry_cause=32'd1; trap_entry_value=pc; + end else if (state==ST_EXEC && interrupt_pending && mstatus[3]) begin trap_take=1'b1; trap_is_interrupt=1'b1; trap_entry_cause=interrupt_cause; end else if (state==ST_EXEC && (!dec_legal || dec_exception)) begin trap_take=1'b1; trap_entry_cause=dec_legal ? dec_exception_cause : 32'd2; trap_entry_value=dec_legal ? dec_exception_value : (compressed ? {16'b0,compressed_word} : active_instr); - end else if (state==ST_MEM && dmem_ready && dmem_error) begin + end else if (state==ST_MEM && pmp_mem_ready && pmp_mem_error) begin trap_take=1'b1; trap_entry_cause=mem_is_store ? 32'd7 : 32'd5; trap_entry_value=mem_aligned_addr+{30'b0,mem_lane}; trap_entry_epc=mem_pc; end else if (state==ST_SLEEP && interrupt_pending && mstatus[3]) begin @@ -397,7 +451,7 @@ module cpu_core ( end end end - ST_MEM: if (dmem_ready) begin + ST_MEM: if (pmp_mem_ready) begin pc<=mem_pc + (mem_pc_step==2 ? 32'd2 : 32'd4); state<=ST_EXEC; minstret<=minstret+1'b1; end @@ -410,4 +464,11 @@ module cpu_core ( endcase end end + +`ifdef FORMAL + default clocking pmp_cb @(posedge clk); endclocking + assert property (state==ST_MEM && pmp_data_deny |-> !dmem_valid); + assert property (state==ST_EXEC && pmp_fetch_deny |-> trap_take && trap_entry_cause==32'd1 && !rf_we); + assert property (state==ST_EXEC && pmp_fetch_deny |-> !(|pmp_cfg_we) && !pmp_addr_we); +`endif endmodule diff --git a/hw/rtl/security/pmp_checker.sv b/hw/rtl/security/pmp_checker.sv new file mode 100644 index 0000000..a3f502f --- /dev/null +++ b/hw/rtl/security/pmp_checker.sv @@ -0,0 +1,69 @@ +// Byte-accurate RV32 PMP checker: OFF, TOR, NA4 and NAPOT; lowest-index +// overlap wins. A request crossing the winning region boundary is denied. +module pmp_checker ( + input logic [31:0] addr, + input logic [4:0] size_bytes, + input logic access_read, access_write, access_execute, + input logic [1:0] privilege, // 3=M, 0=U; reserved values fail closed + input logic [63:0] cfg_flat, + input logic [255:0] addr_flat, + output logic allow, + output logic matched, + output logic [2:0] matched_index +); + logic found, full_match, perm_ok; + logic [32:0] req_end, lo, hi; + logic [31:0] a; + logic [7:0] c; + logic overlap, covers; + integer i; + always_comb begin + req_end = {1'b0,addr} + (size_bytes == 0 ? 33'd1 : {28'b0,size_bytes}); + found=0; full_match=0; perm_ok=0; + matched=0; matched_index=0; allow=0; + lo=0; hi=0; a=0; c=0; overlap=0; covers=0; + for (i=0;i<8;i=i+1) begin + c=cfg_flat[i*8 +: 8]; a=addr_flat[i*32 +: 32]; + lo=0; hi=0; + case (c[4:3]) + 2'b01: begin // TOR, previous address is exclusive lower bound + lo=(i==0) ? 33'b0 : {1'b0,addr_flat[(i-1)*32 +: 32],2'b00}; + hi={1'b0,a,2'b00}; + end + 2'b10: begin lo={1'b0,a,2'b00}; hi=lo+4; end // NA4 + 2'b11: begin // NAPOT: trailing ones encode size, 8 bytes minimum + integer n; + logic [31:0] mask; + n=0; mask=0; + for (integer k=0;k<32;k=k+1) if ((k==n) && a[k]) n=n+1; + if (n>=29) begin lo=0; hi=33'h1_0000_0000; end + else begin + mask=(32'hffff_ffff << n) << 2; + lo={1'b0,(a & mask),2'b00}; + hi=lo + (33'd1 << (n+3)); + end + end + default: begin lo=0; hi=0; end + endcase + overlap=(c[4:3] != 0) && ({1'b0,addr} < hi) && (req_end > lo) && (hi>lo); + covers=({1'b0,addr} >= lo) && (req_end <= hi) && (hi>lo); + if (!found && overlap) begin + found=1; matched=1; matched_index=i[2:0]; full_match=covers; + perm_ok=(!access_read || c[0]) && (!access_write || c[1]) && (!access_execute || c[2]); + // M bypasses unlocked entries, while locked entries apply to M. + if (privilege==2'b11 && !c[7]) perm_ok=1; + end + end + if (privilege==2'b11) allow= !found || (full_match && + (!cfg_flat[matched_index*8+7] || perm_ok)); + else if (privilege==2'b00) allow= found && full_match && perm_ok; + else allow=0; + // With no entry matching, only machine mode is permitted. + end +`ifdef FORMAL + always_comb begin + assert (allow || !matched || !full_match || !perm_ok); + if (privilege==2'b00 && !matched) assert (!allow); + end +`endif +endmodule diff --git a/hw/rtl/security/seal_pmp.sv b/hw/rtl/security/seal_pmp.sv new file mode 100644 index 0000000..36e121c --- /dev/null +++ b/hw/rtl/security/seal_pmp.sv @@ -0,0 +1,43 @@ +// Eight-entry RV32 PMP register bank. pmpcfg is supplied one byte at a time; +// pmpaddr follows the architectural 4 KiB-granularity encoding. +module seal_pmp ( + input logic clk, + input logic rst_n, + input logic [7:0] cfg_we, + input logic [63:0] cfg_wdata, + input logic addr_we, + input logic [2:0] addr_index, + input logic [31:0] addr_wdata, + input logic [2:0] read_index, + output logic [7:0] cfg_rdata, + output logic [31:0] addr_rdata, + output logic [63:0] cfg_flat, + output logic [255:0] addr_flat +); + logic [7:0] cfg [0:7]; + logic [31:0] paddr [0:7]; + integer i; + always_comb begin + cfg_rdata = cfg[read_index]; + addr_rdata = paddr[read_index]; + for (integer j=0; j<8; j=j+1) begin + cfg_flat[j*8 +: 8] = cfg[j]; + addr_flat[j*32 +: 32] = paddr[j]; + end + end + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) begin + for (i=0; i<8; i=i+1) begin cfg[i] <= 0; paddr[i] <= 0; end + end else begin + for (integer j=0; j<8; j=j+1) if (cfg_we[j] && !cfg[j][7]) begin + cfg[j] <= {cfg_wdata[j*8+7], 2'b00, cfg_wdata[j*8+3 +: 2], + cfg_wdata[j*8+2], (cfg_wdata[j*8+1] && cfg_wdata[j*8]), cfg_wdata[j*8]}; + // Reserved address mode 2 is WARL and reads back as OFF. + if (cfg_wdata[j*8 + 3 +: 2] == 2'b10) cfg[j][4:3] <= 2'b00; + end + if (addr_we && !cfg[addr_index][7] && + !(addr_index < 7 && cfg[addr_index+1'b1][7] && cfg[addr_index+1'b1][4:3] == 2'b01)) + paddr[addr_index] <= {2'b00,addr_wdata[29:0]}; + end + end +endmodule diff --git a/hw/rtl/security/wb_firewall.sv b/hw/rtl/security/wb_firewall.sv new file mode 100644 index 0000000..91a617d --- /dev/null +++ b/hw/rtl/security/wb_firewall.sv @@ -0,0 +1,82 @@ +// Independent Wishbone firewall for every bus master. Master IDs: 0 CPU, +// 1 DMA, 2+ other engines. Address policy is intentionally default-deny. +// Region rules derive from docs/MEMORY_MAP.md; key-vault access is CPU-M only. +module wb_firewall #( + parameter logic [31:0] VIOLATION_RESET = 32'b0 +) ( + input logic clk, rst_n, + input logic [2:0] master_id, + input logic [1:0] privilege, + input logic cpu_execute, + input logic m_cyc, m_stb, m_we, + input logic [31:0] m_adr, m_dat_w, + input logic [3:0] m_sel, + output logic [31:0] m_dat_r, + output logic m_ack, m_err, + output logic s_cyc, s_stb, s_we, + output logic [31:0] s_adr, s_dat_w, + output logic [3:0] s_sel, + input logic [31:0] s_dat_r, + input logic s_ack, s_err, + output logic violation_valid, + output logic [31:0] violation_addr, + output logic [2:0] violation_master, + output logic [31:0] violation_count +); + logic allowed, req, mem_region, periph_region, is_key, is_secure_sram, byte_ok; + integer b; + always_comb begin + req=m_cyc && m_stb; + mem_region=0; periph_region=0; is_key=0; is_secure_sram=0; byte_ok=1; + // Evaluate every selected byte so a beat cannot straddle policy regions. + for (b=0;b<4;b=b+1) if (m_sel[b]) begin + if (({m_adr[31:2],2'b00}+b) <= 32'h0000_0fff) begin + if ((master_id==0 && !m_we)) mem_region=1; + else byte_ok=0; + end else if (({m_adr[31:2],2'b00}+b) >= 32'h1000_0000 && ({m_adr[31:2],2'b00}+b) <= 32'h1000_7fff) begin + mem_region=1; + if (({m_adr[31:2],2'b00}+b) >= 32'h1000_7000) is_secure_sram=1; + end else if (({m_adr[31:2],2'b00}+b) >= 32'h4000_0000 && ({m_adr[31:2],2'b00}+b) <= 32'h4000_ffff) begin + periph_region=1; + if (({m_adr[31:2],2'b00}+b) >= 32'h4000_8000 && ({m_adr[31:2],2'b00}+b) <= 32'h4000_8fff) is_key=1; + end else byte_ok=0; + end + allowed=byte_ok && (m_sel!=0) && !(cpu_execute && (m_we || periph_region)); + if (mem_region && !periph_region) begin + if (m_adr>=32'h0000_0000 && m_adr<=32'h0000_0fff) allowed=allowed && master_id==0 && !m_we; + else if (m_adr>=32'h1000_0000 && m_adr<=32'h1000_7fff) + allowed=allowed && (master_id==0 || master_id==1) && (!cpu_execute || master_id==0); + if (is_secure_sram) + allowed=allowed && master_id==0 && (privilege === 2'b11); + end + if (periph_region) begin + // DMA and accelerator masters cannot access peripheral windows. + allowed=allowed && master_id==0 && !cpu_execute; + if (is_key) allowed=allowed && (privilege === 2'b11) && m_we; + end + s_cyc=m_cyc && allowed; s_stb=m_stb && allowed; s_we=m_we; + s_adr=m_adr; s_dat_w=m_dat_w; s_sel=m_sel; + m_dat_r=allowed ? s_dat_r : 32'b0; + m_ack=req && allowed && s_ack && !s_err; + m_err=req && ((!allowed) || s_err); + end + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) begin + violation_valid<=0; violation_addr<=0; violation_master<=0; + violation_count<=VIOLATION_RESET; + end else begin + violation_valid<=req && !allowed; + if (req && !allowed) begin + violation_addr<=m_adr; violation_master<=master_id; + if (!(&violation_count)) violation_count<=violation_count+1'b1; + end + end + end +`ifdef FORMAL + default clocking cb @(posedge clk); endclocking + assert property (req && !allowed |-> !s_cyc && !s_stb && m_err); + assert property (m_ack |-> s_ack && !s_err); + assert property (m_err && !allowed |-> !s_cyc); + assert property (s_cyc |-> allowed); +`endif +endmodule diff --git a/hw/tb/cpu_pmp_tb.sv b/hw/tb/cpu_pmp_tb.sv new file mode 100644 index 0000000..df5e677 --- /dev/null +++ b/hw/tb/cpu_pmp_tb.sv @@ -0,0 +1,44 @@ +`timescale 1ns/1ps +module cpu_pmp_tb; + logic clk=0, rst_n=0; + always #5 clk=~clk; + logic [31:0] pc, instr; + logic dmem_valid, dmem_write; + logic [31:0] dmem_addr, dmem_wdata; + logic [3:0] dmem_wstrb; + logic trap_valid; + logic [31:0] trap_pc, trap_cause, trap_value; + logic retire; + logic saw_deny=0; + cpu_core dut(.clk,.rst_n,.pc,.instr,.irq_external(1'b0),.irq_timer(1'b0), + .dmem_valid,.dmem_write,.dmem_addr,.dmem_wdata,.dmem_wstrb, + .dmem_ready(1'b0),.dmem_rdata(32'b0),.dmem_error(1'b0), + .trap_valid,.trap_pc,.trap_cause,.trap_value,.retire); + always_comb begin + case (pc) + 32'h00: instr=32'h1000_00b7; // lui x1,0x10000 + 32'h04: instr=32'h0020_d093; // srli x1,x1,2 -> pmpaddr encoding + 32'h08: instr=32'h3b00_9073; // csrw pmpaddr0,x1 + 32'h0c: instr=32'h0980_0113; // addi x2,x0,0x98 (locked, NAPOT, no access) + 32'h10: instr=32'h3a01_1073; // csrw pmpcfg0,x2 + 32'h14: instr=32'h1000_0237; // lui x4,0x10000 + 32'h18: instr=32'h0010_0193; // addi x3,x0,1 + 32'h1c: instr=32'h0032_2023; // sw x3,0(x4), must PMP fault + default: instr=32'h0000_0013; + endcase + end + always @(posedge clk) begin + if (rst_n && dmem_valid && dmem_write && dmem_addr==32'h1000_0000) + $fatal(1,"PMP-denied store escaped to the bus"); + if (rst_n && trap_valid && trap_cause==32'd7) begin + if (trap_value!=32'h1000_0000) $fatal(1,"wrong PMP store-fault value %h",trap_value); + saw_deny<=1; + end + end + initial begin + repeat(3) @(negedge clk); rst_n=1; + repeat(40) @(negedge clk); + if (!saw_deny) $fatal(1,"CPU did not trap on PMP-denied store"); + $display("cpu_pmp_tb PASS"); $finish; + end +endmodule diff --git a/hw/tb/security_tb.sv b/hw/tb/security_tb.sv new file mode 100644 index 0000000..5626339 --- /dev/null +++ b/hw/tb/security_tb.sv @@ -0,0 +1,118 @@ +`timescale 1ns/1ps +module security_tb; + logic clk=0, rst_n=0; + always #5 clk=~clk; + + logic [7:0] cfg_we=0; + logic [63:0] cfg_wdata=0; + logic addr_we=0; + logic [2:0] addr_index=0, read_index=0; + logic [31:0] addr_wdata=0; + wire [7:0] cfg_rdata; + wire [31:0] addr_rdata; + wire [63:0] cfg_flat; + wire [255:0] addr_flat; + logic [31:0] paddr; + logic [4:0] psize; + logic pread, pwrite, pexec; + logic [1:0] ppriv; + wire pallow, pmatch; + wire [2:0] pindex; + seal_pmp pmp_regs(.clk,.rst_n,.cfg_we,.cfg_wdata,.addr_we,.addr_index,.addr_wdata, + .read_index,.cfg_rdata,.addr_rdata,.cfg_flat,.addr_flat); + pmp_checker check(.addr(paddr),.size_bytes(psize),.access_read(pread),.access_write(pwrite), + .access_execute(pexec),.privilege(ppriv),.cfg_flat,.addr_flat,.allow(pallow), + .matched(pmatch),.matched_index(pindex)); + + logic [2:0] master_id=0; + logic [1:0] privilege=3; + logic cpu_execute=0, m_cyc=0, m_stb=0, m_we=0; + logic [31:0] m_adr=0, m_dat_w=0; + logic [3:0] m_sel=4'hf; + wire [31:0] m_dat_r, s_adr, s_dat_w; + wire m_ack,m_err,s_cyc,s_stb,s_we; + wire [3:0] s_sel; + wire violation_valid; + wire [31:0] violation_addr, violation_count; + wire [2:0] violation_master; + wb_firewall firewall(.clk,.rst_n,.master_id,.privilege,.cpu_execute,.m_cyc,.m_stb,.m_we, + .m_adr,.m_dat_w,.m_sel,.m_dat_r,.m_ack,.m_err,.s_cyc,.s_stb,.s_we,.s_adr,.s_dat_w,.s_sel, + .s_dat_r(32'h12345678),.s_ack(s_cyc),.s_err(1'b0),.violation_valid,.violation_addr, + .violation_master,.violation_count); + + task automatic write_cfg(input logic [2:0] ix,input logic [7:0] value); + @(negedge clk); cfg_we=8'b1<