JeremyGUILLAUME/CVA6_cache_locking
GitHub: JeremyGUILLAUME/CVA6_cache_locking
该项目在 CVA6 RISC-V 处理器的数据缓存上实现了基于分区的缓存锁定机制,作为防御缓存侧信道攻击的硬件级对策。
Stars: 1 | Forks: 0
# 背景
这项工作专注于分区技术,这是一种针对基于缓存的侧信道攻击的防御策略。
它包括在缓存中将敏感进程与非可信进程进行隔离,以防止它们的数据与攻击者的数据发生冲突,预计这会使缓存攻击变得极其困难,甚至无法实现。
Wang 等人提出了 PLcache [1],一种基于分区的防御措施。
然而,后续的研究表明,PLcache 仍然容易受到缓存攻击 [2]。
为了解决这些局限性,Gaudin 等人 [2] 引入了一种细粒度的缓存锁定机制。
该机制引入了两条指令,用于在缓存中锁定和解锁敏感数据。
它的实现依赖于修改缓存替换策略。
从 LRU 策略开始,引入了一个额外的状态来将缓存行标记为锁定状态,从而在锁定处于活动状态时防止它们被驱逐。
为了证明这种锁定机制的安全有效性,它在一个简单的 cv32e40p RISC-V 内核上进行了模拟。
本项目的目标是在更复杂的、能够运行 Linux 等操作系统的处理器上展示这种防御措施的可行性,以便在更真实的条件下研究基于缓存的攻击。
为此选择了 OpenHW Group 的 CVA6 处理器,因为它是一个成熟且文档齐全的 RISC-V 实现。
它具有一个 8 路组相联数据缓存,并采用随机替换策略。
# CVA6 锁定机制概述
cv32e40p 和 CVA6 之间的主要区别在于缓存替换策略 (RP)。
这种差异尤为重要,因为在 cv32e40p 上实现的原始锁定机制依赖于 LRU 替换策略,而 CVA6 实现的是随机替换策略。
为了在 CVA6 上实现此机制,我们提出了一种解决方案,即为每个缓存行添加一个锁定位,以指示其处于锁定状态。
第一个挑战是在处理 CVA6 中使用的随机替换策略的同时,确保锁定的缓存行永远不会被选中进行驱逐。
另一个困难是 CVA6 更高的架构复杂性,它具有六级流水线和部分乱序执行的能力。
因此,实现该机制需要研究用 SystemVerilog 实现的 CVA6 的内部结构,以确保锁定请求信号与解码后的指令一起在流水线中正确传播,并且确保正确地锁定(和解锁)缓存行。
## 集成到缓存子系统中
下图展示了为集成锁定机制而对 CVA6 数据缓存所做的修改。
与加载请求类似,锁定(或解锁)请求由控制单元处理,控制单元会检查数据是否存在于缓存中(缓存命中或未命中)。
在发生缓存未命中的情况下,请求被转发给未命中单元,该单元从主内存中读取数据并将其存入缓存,同时将锁定位设置为 1(对于解锁请求则设置为 0)。
在发生缓存命中的情况下,控制单元会检查锁定位的当前值是否与锁定(或解锁)请求匹配。
如果不匹配,则强制让该请求通过未命中单元,以便用请求的值更新锁定位。
为了处理锁定位,在未命中单元中添加了一个锁定处理单元。

## 锁定处理单元
为了处理存在锁定缓存行情况下的随机替换策略 (RP),我们在未命中单元中添加了一个锁定处理单元。
该单元确保选择一个未锁定的路进行替换。
此外,在收到锁定请求的情况下,它会检查是否允许锁定,即在锁定操作之后,给定组中是否至少有一条路保持未锁定状态。
锁定处理单元的原理图如下图所示:

在最初的设计中,LFSR 生成 3 个随机位,用作随机选择进行替换的路的索引。
在我们的设计中,LFSR 被扩展为生成 13 个随机位(图中的 "LFSR_13"):4 个随机路索引 + 1 个随机位。
锁定处理单元首先测试 4 个随机索引中是否有一个指向未锁定的路。
为此,4 个多路复用器在其输入端获取锁定位的值 ("lck_bits"),并在输出端返回各自随机索引对应的锁定位。
LZC-4 选择第一个未锁定的索引。
如果它们全未解锁,则信号 "none" 等于 1,并选择 LZC-8 单元的输出。
这里有两个 LZC 单元,第一个选择等于 0(即未锁定)的最低有效位 (lsb)。
另一个选择等于 0 的最高有效位 (msb)。
来自 LFSR_13 的最后一个随机位用于在 msb 和 lsb 之间进行选择。
为了检查是否允许锁定,我们验证在执行锁定的情况下,lock_bits 中是否至少有一个位将保持为 0。
如果是,则将 "lock_allowed" 位设置为 1。
如原理图所示,这可以通过检查 LZC-8 单元(包括 lsb 和 msb 单元)是否返回相同的索引来简化;如果相同,则意味着只有一个缓存行 (CL) 仍未锁定,因此不允许锁定。
在锁定处理单元的输出端,我们得到了随机选择的未锁定路的索引 ("repl_way"),以及是否允许锁定 ("lock_allowed")。
# 实现
在本节中,我们描述为了包含锁定机制而对 CVA6 所做的修改。
它已在以下 CVA6 版本上实现并经过测试:2025 年 1 月 15 日的 v5.2.0 (cb5c623)
需要修改的 CVA6 部分以红色高亮显示:

解码器经过修改以处理自定义指令:Custom0,用于处理锁定和解锁指令。
它们的处理方式与加载指令类似,但增加了一个由缓存子系统处理的锁定请求 ("lck_req")。
在缓存子系统中,对缓存存储器进行了修改,将锁定位集成到每个缓存行中。
在锁定请求期间,即使发生缓存命中,我们也会检查锁定位是否与请求的锁定状态相匹配。
如果不匹配,则强制通过未命中单元(然后是锁定处理单元)对其进行更新。
锁定位由未命中单元存储在缓存中,与缓存的数据和元数据(tag、valid 等)放在一起。
表 1 总结了 CVA6 上锁定机制的面积开销,并与最初的 cv32e40p 上的开销进行了比较。

## 修改的文件列表
```
core/cva6.sv
core/include/ariane_pkg.sv
core/decoder.sv
core/load_unit.sv
core/cache_subsystem/wt_dcache.sv
core/cache_subsystem/wt_dcache_mem.sv
core/cache_subsystem/wt_dcache_ctrl.sv
core/cache_subsystem/wt_dcache_missunit.sv
```
## 修改详情
### cva6.sv
在 localparam 类型 dcache_req_i_t 中添加 "cache_lck_req":
```
logic [1:0] cache_lck_req; // line 210
```
### ariane_pkg.sv
在列表中添加两种操作类型:
```
CACHE_LCK, // line 327
CACHE_ULCK, // line 328
```
### decoder.sv
实现 Custom0 指令以处理锁定和解锁指令:
```
// --------------------------------
// Custom
// --------------------------------
riscv::OpcodeCustom0: begin // line 1072
instruction_o.fu = LOAD;
imm_select = IIMM;
instruction_o.rs1 = instr.itype.rs1;
instruction_o.rd = instr.itype.rd;
// LOCK or UNLOCK
unique case (instr.itype.funct3)
3'b000: instruction_o.op = ariane_pkg::CACHE_LCK; //Lock
3'b001: instruction_o.op = ariane_pkg::CACHE_ULCK; //Unlock
default: illegal_instr = 1'b1;
endcase
if (CVA6Cfg.RVH) begin
tinst = {17'b0, instr.itype.funct3, instr.itype.rd, instr.itype.opcode};
tinst[1] = is_compressed_i ? 1'b0 : 'b1;
end
end
```
### load_unit.sv
在收到锁定或解锁请求时,处理来自 "dcache_req_i_t" 的 "cache_lck_req" 信号:
```
assign req_port_o.cache_lck_req[0] = lsu_ctrl_i.operation inside {ariane_pkg::CACHE_LCK, ariane_pkg::CACHE_ULCK}; // line 219
assign req_port_o.cache_lck_req[1] = (lsu_ctrl_i.operation == ariane_pkg::CACHE_LCK)? '1 : '0; // line 220
```
### wt_dcache.sv
为锁定机制添加信号:
```
logic [ CVA6Cfg.DCACHE_SET_ASSOC-1:0 ] wr_lck_bits; // line 87
logic [ CVA6Cfg.DCACHE_SET_ASSOC-1:0 ] rd_lck_bits; // line 125
logic [ NumPorts-1:0 ][ CVA6Cfg.DCACHE_SET_ASSOC-1:0]lock_hit_oh; // line 109
logic [ NumPorts-1:0 ][ CVA6Cfg.DCACHE_SET_ASSOC-1:0]lock_bits; // line 110
logic [ NumPorts-1:0 ][ 1:0 ] lock_req; // line 111
```
为缓存存储器添加端口 (i_wt_dcache_mem):
```
// read ports
.rd_lck_bits_o (rd_lck_bits),
// cacheline write port
.wr_lck_bits_i (wr_lck_bits),
```
为缓存控制器添加端口 (i_wt_dcache_ctrl):
```
// lock interface
.lock_hit_oh_o (lock_hit_oh[k]), // Gives the index of the way which has a hit
.lock_bits_o (lock_bits[k]),
.lock_req_o (lock_req[k])
// from cache mem interface
.rd_lck_bits_i (rd_lck_bits)
```
为未命中单元添加端口 (i_wt_dcache_missunit):
```
// lock handling interface
.lock_hit_oh_i (lock_hit_oh),
.lock_bits_i (lock_bits),
.lock_req_i (lock_req),
.lock_allowed_o,
// to cache memory interface
.wr_lck_bits_o (wr_lck_bits),
```
### wt_dcache_mem.sv
在缓存行的 tag、有效位和数据旁边添加锁定位:
```
logic [CVA6Cfg.DCACHE_TAG_WIDTH-1+2:0] vld_tag_rdata[CVA6Cfg.DCACHE_SET_ASSOC-1:0];
assign tag_rdata[i] = vld_tag_rdata[i][CVA6Cfg.DCACHE_TAG_WIDTH-1:0];
assign rd_vld_bits_o[i] = vld_tag_rdata[i][CVA6Cfg.DCACHE_TAG_WIDTH-1+1];
assign rd_lck_bits_o[i] = vld_tag_rdata[i][CVA6Cfg.DCACHE_TAG_WIDTH-1+2];
```
### wt_dcache_ctrl.sv
检查地址和锁定位状态是否均发生命中:
```
...
end else if ((|rd_hit_oh_i) && cache_en_i) begin
// check if we need to update the lock table
if (lck_req_q[0] && ((|(rd_lck_bits_i & rd_hit_oh_i)) == ~lck_req_q[1] )) begin
state_d = MISS_REQ; // going throught the miss unit is forced (even with a hit), the miss unit handles the locking logic
end else begin
...
```
### wt_dcache_missunit.sv
实现如前所述的锁定处理单元。处理要写入缓存存储器的新锁定位值。
```
// LOCK Handling UNIT
// generate random cacheline index
lfsr #(
.LfsrWidth(CVA6Cfg.DCACHE_SET_ASSOC_WIDTH*4 +5),
.OutWidth (CVA6Cfg.DCACHE_SET_ASSOC_WIDTH*4 +1)
) i_lfsr_inv (
.clk_i (clk_i),
.rst_ni(rst_ni),
.en_i (update_lfsr),
.out_o (lfsr_out)
);
// GENRATE RND_OUT
logic [3:0] lzc_4_in;
logic [1:0] lzc_4_out;
logic lzc_4_empty;
for (genvar k = 0; k < 4; k++) begin : assign_lzc_4_in
assign lzc_4_in[k] = ~lock_bits_i[miss_port_idx][lfsr_out[CVA6Cfg.DCACHE_SET_ASSOC_WIDTH*(4-k)-1:CVA6Cfg.DCACHE_SET_ASSOC_WIDTH*(4-k)-CVA6Cfg.DCACHE_SET_ASSOC_WIDTH]];
end
assign lzc_4_out[0] = (~lzc_4_in[1] || lzc_4_in[2]) && ~lzc_4_in[3];
assign lzc_4_out[1] = ~(lzc_4_in[2] || lzc_4_in[3]);
assign lzc_4_empty = ~(lzc_4_in[0]||lzc_4_in[1]||lzc_4_in[2]||lzc_4_in[3]);
logic [CVA6Cfg.DCACHE_SET_ASSOC_WIDTH-1:0] rnd_out;
for (genvar k = 0; k < CVA6Cfg.DCACHE_SET_ASSOC_WIDTH; k++) begin : assign_rnd_out
assign rnd_out[k] = lfsr_out[CVA6Cfg.DCACHE_SET_ASSOC_WIDTH*lzc_4_out+k];
end
// GENRATE LZC_OUT
logic [CVA6Cfg.DCACHE_SET_ASSOC_WIDTH-1:0] lzc_lsb_out;
logic [CVA6Cfg.DCACHE_SET_ASSOC_WIDTH-1:0] lzc_msb_out;
lzc #(
.WIDTH(CVA6Cfg.DCACHE_SET_ASSOC),
.MODE(1'b0)
) lzc_locked_LSB (
.in_i(~lock_bits_i[miss_port_idx]),
.cnt_o(lzc_lsb_out)
);
lzc #(
.WIDTH(CVA6Cfg.DCACHE_SET_ASSOC),
.MODE(1'b1)
) lzc_locked_MSB (
.in_i(~lock_bits_i[miss_port_idx]),
.cnt_o(lzc_msb_out)
);
logic [CVA6Cfg.DCACHE_SET_ASSOC_WIDTH-1:0] lzc_out;
assign lzc_out = (lfsr_out[CVA6Cfg.DCACHE_SET_ASSOC_WIDTH*4]) ? lzc_lsb_out : CVA6Cfg.DCACHE_SET_ASSOC-1-lzc_msb_out;
// SELECT RND_OUT or LZC_OUT
logic [CVA6Cfg.DCACHE_SET_ASSOC_WIDTH-1:0] rnd_ulck_way;
assign rnd_ulck_way = (lzc_4_empty)? lzc_out : rnd_out;
// CHECK IF THERE IS A HIT
logic is_miss;
lzc #(
.WIDTH(CVA6Cfg.DCACHE_SET_ASSOC),
.MODE(1'b0)
) lzc_rd_hit (
.in_i(lock_hit_oh_i[miss_port_idx]),
.cnt_o(hit_way),
.empty_o(is_miss)
);
// SELECT HIT_WAY or RND_ULCK_WAY
assign repl_way = (~is_miss)? hit_way: (all_ways_valid) ? rnd_ulck_way : inv_way;
// CHECK IF LOCK IS ALLOWED AT REPL_WAY
logic [CVA6Cfg.DCACHE_SET_ASSOC-1:0] lcked_bits_after_lck;
assign lcked_bits_after_lck = ~(lock_bits_i[miss_port_idx][CVA6Cfg.DCACHE_SET_ASSOC-1:0] | (1<
标签:CVA6, RISC-V, 处理器, 硬件安全, 缓存侧信道防护, 缓存锁定