Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[rtl] Fix FI vulnerability in RF #2117

Merged
merged 2 commits into from
Jan 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions doc/03_reference/security.rst
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,15 @@ When Ibex is configured with the SecureIbex parameter, the write enable signal i
This can be useful to detect fault injection attacks.
No attempt is made to correct detected errors, but an internal major alert is signaled for the system to take action.

Register file read addresses glitch detection
-------------------------------------------

When Ibex is configured with the SecureIbex parameter, the read addresses provided to the register file are converted to one-hot encoded signals, and a one-hot encoded MUX is used to select the register to read from.
By using one-hot encoding checkers, glitches in the one-hot encoded signals are detected.
Bit-flips inside the plain read addresses before the one-hot conversion happens are detected by the dual core lockstep.
This can be useful to detect fault injection attacks.
No attempt is made to correct detected errors, but an internal major alert is signaled for the system to take action.

ICache ECC
----------

Expand Down
27 changes: 27 additions & 0 deletions dv/uvm/core_ibex/common/prim/prim_and2.sv
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright lowRISC contributors.
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0

// Abstract primitives wrapper.
//
// This file is a stop-gap until the DV file list is generated by FuseSoC.
// Its contents are taken from the file which would be generated by FuseSoC.
// https://github.com/lowRISC/ibex/issues/893

module prim_and2 #(
parameter int Width = 1
) (
input [Width-1:0] in0_i,
input [Width-1:0] in1_i,
output logic [Width-1:0] out_o
);

if (1) begin : gen_generic
prim_generic_and2 #(
.Width(Width)
) u_impl_generic (
.*
);
end

endmodule
4 changes: 4 additions & 0 deletions dv/uvm/core_ibex/ibex_dv.f
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
${PRJ_DIR}/dv/uvm/core_ibex/common/prim/prim_clock_mux2.sv
${LOWRISC_IP_DIR}/ip/prim_generic/rtl/prim_generic_flop.sv
${PRJ_DIR}/dv/uvm/core_ibex/common/prim/prim_flop.sv
${LOWRISC_IP_DIR}/ip/prim_generic/rtl/prim_generic_and2.sv
${PRJ_DIR}/dv/uvm/core_ibex/common/prim/prim_and2.sv

// Shared lowRISC code
${LOWRISC_IP_DIR}/ip/prim/rtl/prim_cipher_pkg.sv
Expand All @@ -64,6 +66,8 @@
${LOWRISC_IP_DIR}/ip/prim/rtl/prim_secded_72_64_enc.sv
${LOWRISC_IP_DIR}/ip/prim/rtl/prim_secded_72_64_dec.sv
${LOWRISC_IP_DIR}/ip/prim/rtl/prim_onehot_check.sv
${LOWRISC_IP_DIR}/ip/prim/rtl/prim_onehot_enc.sv
${LOWRISC_IP_DIR}/ip/prim/rtl/prim_onehot_mux.sv
${LOWRISC_IP_DIR}/ip/prim/rtl/prim_mubi_pkg.sv

// ibex CORE RTL files
Expand Down
8 changes: 8 additions & 0 deletions dv/uvm/core_ibex/tb/core_ibex_tb_top.sv
Original file line number Diff line number Diff line change
Expand Up @@ -336,4 +336,12 @@ module core_ibex_tb_top;
assign dut.u_ibex_top.gen_regfile_ff.register_file_i.gen_wren_check.u_prim_onehot_check.
unused_assert_connected = 1;
end

// Disable the assertion for onhot check in case RdataMuxCheck (set by SecureIbex) is enabled.
if (SecureIbex) begin : gen_disable_rdata_mux_check
assign dut.u_ibex_top.gen_regfile_ff.register_file_i.gen_rdata_mux_check.
u_prim_onehot_check_raddr_a.unused_assert_connected = 1;
assign dut.u_ibex_top.gen_regfile_ff.register_file_i.gen_rdata_mux_check.
u_prim_onehot_check_raddr_b.unused_assert_connected = 1;
end
endmodule
2 changes: 2 additions & 0 deletions ibex_top.core
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ filesets:
depend:
- lowrisc:ibex:ibex_pkg
- lowrisc:ibex:ibex_core
- lowrisc:prim:and2
- lowrisc:prim:buf
- lowrisc:prim:clock_mux2
- lowrisc:prim:flop
- lowrisc:prim:ram_1p_scr
- lowrisc:prim:onehot_check
- lowrisc:prim:onehot
files:
- rtl/ibex_register_file_ff.sv # generic FF-based
- rtl/ibex_register_file_fpga.sv # FPGA
Expand Down
111 changes: 106 additions & 5 deletions rtl/ibex_register_file_ff.sv
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module ibex_register_file_ff #(
parameter int unsigned DataWidth = 32,
parameter bit DummyInstructions = 0,
parameter bit WrenCheck = 0,
parameter bit RdataMuxCheck = 0,
parameter logic [DataWidth-1:0] WordZeroVal = '0
) (
// Clock and Reset
Expand All @@ -39,7 +40,7 @@ module ibex_register_file_ff #(
input logic [DataWidth-1:0] wdata_a_i,
input logic we_a_i,

// This indicates whether spurious WE are detected.
// This indicates whether spurious WE or non-one-hot encoded raddr are detected.
output logic err_o
);

Expand All @@ -49,6 +50,8 @@ module ibex_register_file_ff #(
logic [DataWidth-1:0] rf_reg [NUM_WORDS];
logic [NUM_WORDS-1:0] we_a_dec;

logic oh_raddr_a_err, oh_raddr_b_err, oh_we_err;

always_comb begin : we_a_decoder
for (int unsigned i = 0; i < NUM_WORDS; i++) begin
we_a_dec[i] = (waddr_a_i == 5'(i)) ? we_a_i : 1'b0;
Expand Down Expand Up @@ -78,12 +81,12 @@ module ibex_register_file_ff #(
.oh_i(we_a_dec_buf),
.addr_i(waddr_a_i),
.en_i(we_a_i),
.err_o
.err_o(oh_we_err)
);
end else begin : gen_no_wren_check
logic unused_strobe;
assign unused_strobe = we_a_dec[0]; // this is never read from in this case
assign err_o = 1'b0;
assign oh_we_err = 1'b0;
end

// No flops for R0 as it's hard-wired to 0
Expand Down Expand Up @@ -130,8 +133,106 @@ module ibex_register_file_ff #(
assign rf_reg[0] = WordZeroVal;
end

assign rdata_a_o = rf_reg[raddr_a_i];
assign rdata_b_o = rf_reg[raddr_b_i];
if (RdataMuxCheck) begin : gen_rdata_mux_check
// Encode raddr_a/b into one-hot encoded signals.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC, it is sufficient to do the one-hot encoding in here (not covered by the lockstep) because the same gates driving the raddr_a/b_i signals used here also drive the wires ultimately going into the lockstep comparison. I.e., if someone managed to fault raddr_a_i here, this would still be caught by the lockstep countermeasure, right?

If yes, this would be perfect because it means we don't have to touch the lockstep interfacing and checking logic.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, exactly.

In ibex_top.sv the rf_raddr_a/b signals are generated by the Ibex core:

ibex/rtl/ibex_top.sv

Lines 336 to 337 in 38da151

.rf_raddr_a_o (rf_raddr_a),
.rf_raddr_b_o (rf_raddr_b),

forwarded to the RF:

ibex/rtl/ibex_top.sv

Lines 433 to 435 in 38da151

.raddr_a_i(rf_raddr_a),
.rdata_a_o(rf_rdata_a_ecc),
.raddr_b_i(rf_raddr_b),

and buffered for the lockstep:

ibex/rtl/ibex_top.sv

Lines 862 to 863 in 38da151

rf_raddr_a,
rf_raddr_b,

Inside the lockstep, again the rf_raddr_a/b signals are generated by the shadow Ibex core:

ibex/rtl/ibex_lockstep.sv

Lines 374 to 375 in 38da151

.rf_raddr_a_o (shadow_outputs_d.rf_raddr_a),
.rf_raddr_b_o (shadow_outputs_d.rf_raddr_b),

And finally the comparison happens. Here, the buffered rf_raddr_a/b signals from the main core are then compared to the signals generated by the shadow core.

If a bit-flip inside the RF manipulates the rf_raddr_a/b signals, these faulty signals will get buffered for the lockstep:

ibex/rtl/ibex_top.sv

Lines 862 to 863 in 38da151

rf_raddr_a,
rf_raddr_b,

And the fault is detected when comparing the main vs shadow rf_raddr_a/b signals.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for doing the analysis and writing it up here. LGTM!

logic [NUM_WORDS-1:0] raddr_onehot_a, raddr_onehot_b;
logic [NUM_WORDS-1:0] raddr_onehot_a_buf, raddr_onehot_b_buf;
prim_onehot_enc #(
.OneHotWidth(NUM_WORDS)
) u_prim_onehot_enc_raddr_a (
.in_i (raddr_a_i),
.en_i (1'b1),
.out_o (raddr_onehot_a)
);

prim_onehot_enc #(
.OneHotWidth(NUM_WORDS)
) u_prim_onehot_enc_raddr_b (
.in_i (raddr_b_i),
.en_i (1'b1),
.out_o (raddr_onehot_b)
);

// Buffer the one-hot encoded signals so that the checkers
// are not optimized.
prim_buf #(
.Width(NUM_WORDS)
) u_prim_buf_raddr_a (
.in_i (raddr_onehot_a),
.out_o(raddr_onehot_a_buf)
);

prim_buf #(
.Width(NUM_WORDS)
) u_prim_buf_raddr_b (
.in_i (raddr_onehot_b),
.out_o(raddr_onehot_b_buf)
);

// SEC_CM: DATA_REG_SW.GLITCH_DETECT
// Check the one-hot encoded signals for glitches.
vogelpi marked this conversation as resolved.
Show resolved Hide resolved
prim_onehot_check #(
.AddrWidth(ADDR_WIDTH),
.OneHotWidth(NUM_WORDS),
.AddrCheck(1),
// When AddrCheck=1 also EnableCheck needs to be 1.
.EnableCheck(1)
) u_prim_onehot_check_raddr_a (
.clk_i,
.rst_ni,
.oh_i (raddr_onehot_a_buf),
.addr_i (raddr_a_i),
// Set enable=1 as address is always valid.
.en_i (1'b1),
.err_o (oh_raddr_a_err)
);

prim_onehot_check #(
.AddrWidth(ADDR_WIDTH),
.OneHotWidth(NUM_WORDS),
.AddrCheck(1),
// When AddrCheck=1 also EnableCheck needs to be 1.
.EnableCheck(1)
) u_prim_onehot_check_raddr_b (
.clk_i,
.rst_ni,
.oh_i (raddr_onehot_b_buf),
.addr_i (raddr_b_i),
// Set enable=1 as address is always valid.
.en_i (1'b1),
.err_o (oh_raddr_b_err)
);

// MUX register to rdata_a/b_o according to raddr_a/b_onehot.
prim_onehot_mux #(
.Width(DataWidth),
.Inputs(NUM_WORDS)
) u_rdata_a_mux (
.clk_i,
.rst_ni,
.in_i (rf_reg),
.sel_i (raddr_onehot_a),
.out_o (rdata_a_o)
);

prim_onehot_mux #(
.Width(DataWidth),
.Inputs(NUM_WORDS)
) u_rdata_b_mux (
.clk_i,
.rst_ni,
.in_i (rf_reg),
.sel_i (raddr_onehot_b),
.out_o (rdata_b_o)
);
end else begin : gen_no_rdata_mux_check
assign rdata_a_o = rf_reg[raddr_a_i];
assign rdata_b_o = rf_reg[raddr_b_i];
assign oh_raddr_a_err = 1'b0;
assign oh_raddr_b_err = 1'b0;
end

assign err_o = oh_raddr_a_err || oh_raddr_b_err || oh_we_err;

// Signal not used in FF register file
logic unused_test_en;
Expand Down
118 changes: 111 additions & 7 deletions rtl/ibex_register_file_fpga.sv
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ module ibex_register_file_fpga #(
parameter int unsigned DataWidth = 32,
parameter bit DummyInstructions = 0,
parameter bit WrenCheck = 0,
parameter bit RdataMuxCheck = 0,
parameter logic [DataWidth-1:0] WordZeroVal = '0
) (
// Clock and Reset
Expand All @@ -37,7 +38,7 @@ module ibex_register_file_fpga #(
input logic [DataWidth-1:0] wdata_a_i,
input logic we_a_i,

// This indicates whether spurious WE are detected.
// This indicates whether spurious WE or non-one-hot encoded raddr are detected.
output logic err_o
);

Expand All @@ -47,11 +48,114 @@ module ibex_register_file_fpga #(
logic [DataWidth-1:0] mem[NUM_WORDS];
logic we; // write enable if writing to any register other than R0

// async_read a
assign rdata_a_o = (raddr_a_i == '0) ? '0 : mem[raddr_a_i];
logic [DataWidth-1:0] mem_o_a, mem_o_b;

// async_read b
assign rdata_b_o = (raddr_b_i == '0) ? '0 : mem[raddr_b_i];
// WE strobe and one-hot encoded raddr alert.
logic oh_raddr_a_err, oh_raddr_b_err, oh_we_err;
assign err_o = oh_raddr_a_err || oh_raddr_b_err || oh_we_err;

if (RdataMuxCheck) begin : gen_rdata_mux_check
// Encode raddr_a/b into one-hot encoded signals.
logic [NUM_WORDS-1:0] raddr_onehot_a, raddr_onehot_b;
logic [NUM_WORDS-1:0] raddr_onehot_a_buf, raddr_onehot_b_buf;
prim_onehot_enc #(
.OneHotWidth(NUM_WORDS)
) u_prim_onehot_enc_raddr_a (
.in_i (raddr_a_i),
.en_i (1'b1),
.out_o (raddr_onehot_a)
);

prim_onehot_enc #(
.OneHotWidth(NUM_WORDS)
) u_prim_onehot_enc_raddr_b (
.in_i (raddr_b_i),
.en_i (1'b1),
.out_o (raddr_onehot_b)
);

// Buffer the one-hot encoded signals so that the checkers
// are not optimized.
prim_buf #(
.Width(NUM_WORDS)
) u_prim_buf_raddr_a (
.in_i (raddr_onehot_a),
.out_o(raddr_onehot_a_buf)
);

prim_buf #(
.Width(NUM_WORDS)
) u_prim_buf_raddr_b (
.in_i (raddr_onehot_b),
.out_o(raddr_onehot_b_buf)
);

// SEC_CM: DATA_REG_SW.GLITCH_DETECT
// Check the one-hot encoded signals for glitches.
prim_onehot_check #(
.AddrWidth(ADDR_WIDTH),
.OneHotWidth(NUM_WORDS),
.AddrCheck(1),
// When AddrCheck=1 also EnableCheck needs to be 1.
.EnableCheck(1)
) u_prim_onehot_check_raddr_a (
.clk_i,
.rst_ni,
.oh_i (raddr_onehot_a_buf),
.addr_i (raddr_a_i),
// Set enable=1 as address is always valid.
.en_i (1'b1),
.err_o (oh_raddr_a_err)
);

prim_onehot_check #(
.AddrWidth(ADDR_WIDTH),
.OneHotWidth(NUM_WORDS),
.AddrCheck(1),
// When AddrCheck=1 also EnableCheck needs to be 1.
.EnableCheck(1)
) u_prim_onehot_check_raddr_b (
.clk_i,
.rst_ni,
.oh_i (raddr_onehot_b_buf),
.addr_i (raddr_b_i),
// Set enable=1 as address is always valid.
.en_i (1'b1),
.err_o (oh_raddr_b_err)
);

// MUX register to rdata_a/b_o according to raddr_a/b_onehot.
prim_onehot_mux #(
.Width(DataWidth),
.Inputs(NUM_WORDS)
) u_rdata_a_mux (
.clk_i,
.rst_ni,
.in_i (mem),
.sel_i (raddr_onehot_a),
.out_o (mem_o_a)
);

prim_onehot_mux #(
.Width(DataWidth),
.Inputs(NUM_WORDS)
) u_rdata_b_mux (
.clk_i,
.rst_ni,
.in_i (mem),
.sel_i (raddr_onehot_b),
.out_o (mem_o_b)
);

assign rdata_a_o = (raddr_a_i == '0) ? '0 : mem_o_a;
assign rdata_b_o = (raddr_b_i == '0) ? '0 : mem_o_b;
end else begin : gen_no_rdata_mux_check
// async_read a
assign rdata_a_o = (raddr_a_i == '0) ? '0 : mem[raddr_a_i];

// async_read b
assign rdata_b_o = (raddr_b_i == '0) ? '0 : mem[raddr_b_i];
end

// we select
assign we = (waddr_a_i == '0) ? 1'b0 : we_a_i;
Expand All @@ -60,9 +164,9 @@ module ibex_register_file_fpga #(
// This checks for spurious WE strobes on the regfile.
if (WrenCheck) begin : gen_wren_check
// Since the FPGA uses a memory macro, there is only one write-enable strobe to check.
assign err_o = we && !we_a_i;
assign oh_we_err = we && !we_a_i;
end else begin : gen_no_wren_check
assign err_o = 1'b0;
assign oh_we_err = 1'b0;
end

// Note that the SystemVerilog LRM requires variables on the LHS of assignments within
Expand Down
Loading
Loading