Hardware Logic with Insecure De-Synchronization between Control and Data Channels
Description
Hardware Logic with Insecure De-Synchronization between Control and Data Channels occurs when hardware error handling and security check logic allow data to propagate forward before security validations complete. High-performance on-chip bus protocols use separate control and data channels for efficiency. Logic bugs can permit data forwarding before security checks finish. If attackers can observe this data in hardware state, confidentiality breaches result. The Meltdown vulnerability exemplifies this—privileged data was speculatively loaded and returned regardless of privilege level, with traces remaining in microarchitectural state despite exception handling.
Risk
Control/data de-synchronization has severe security implications. Privileged data may be leaked speculatively. Security checks may be bypassed through timing. Cache side-channels may reveal protected data. Microarchitectural state may retain secrets. Transient execution attacks become possible. Memory isolation may be violated. Kernel memory may be readable from user space. Cryptographic keys may be exposed.
Solution
Thoroughly verify the data routing logic to ensure that any error handling or security checks effectively block illegal dataflows. Buffer all data until security authorization completes. Implement speculation barriers. Clear microarchitectural state on security faults. Use constant-time implementations. Ensure control and data paths remain synchronized. Validate security checks complete before data forwarding.
Common Consequences
| Impact | Details |
|---|---|
| Confidentiality | Scope: Confidentiality Read Memory - Protected data may be read through transient execution and side-channel analysis. |
| Confidentiality | Scope: Confidentiality Read Application Data - Application secrets may leak through microarchitectural side-channels. |
Example Code
Vulnerable Code
// Vulnerable: Data forwarding before security check completion
module vulnerable_bus_bridge (
input wire clk,
input wire reset_n,
// Request from untrusted CPU
input wire [31:0] req_addr,
input wire [31:0] req_data,
input wire req_read,
input wire req_write,
input wire req_valid,
// Firewall interface
output reg [31:0] fw_addr,
output reg fw_check_request,
input wire fw_access_allowed,
input wire fw_check_complete,
// Memory interface
output reg [31:0] mem_addr,
output reg mem_read,
output reg mem_write,
input wire [31:0] mem_read_data,
// Response to CPU
output reg [31:0] resp_data,
output reg resp_valid
);
reg [31:0] pending_addr;
reg pending_read;
always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
mem_read <= 1'b0;
mem_write <= 1'b0;
resp_valid <= 1'b0;
end
else begin
// Start firewall check
if (req_valid) begin
fw_addr <= req_addr;
fw_check_request <= 1'b1;
pending_addr <= req_addr;
pending_read <= req_read;
end
// VULNERABLE: Forward read to memory BEFORE firewall completes
// This is done for performance, but creates side-channel
if (req_valid && req_read) begin
mem_addr <= req_addr;
mem_read <= 1'b1; // VULNERABLE: Speculative read
end
// When firewall completes, check result
if (fw_check_complete) begin
if (fw_access_allowed) begin
// Access allowed - return data
resp_data <= mem_read_data;
resp_valid <= 1'b1;
end
else begin
// VULNERABLE: Data already read, may be in cache
// Even though we don't return it, attacker can
// observe it through cache timing
resp_data <= 32'h0;
resp_valid <= 1'b1;
end
mem_read <= 1'b0;
end
end
end
// Attack (Meltdown-style):
// 1. Request read to protected address
// 2. Memory read happens speculatively
// 3. Protected data loads into cache
// 4. Firewall denies access, data not returned
// 5. But cache state reveals the data through timing
endmodule
// Vulnerable: Processor with speculative execution leak
module vulnerable_speculative_load (
input wire clk,
input wire reset_n,
input wire [31:0] load_addr,
input wire load_request,
input wire [1:0] load_privilege,
input wire [1:0] current_privilege,
output reg [31:0] load_data,
output reg load_complete,
output reg load_fault
);
reg [31:0] speculative_data;
reg speculative_load_done;
reg privilege_check_done;
reg privilege_ok;
always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
speculative_load_done <= 1'b0;
privilege_check_done <= 1'b0;
load_complete <= 1'b0;
load_fault <= 1'b0;
end
else if (load_request) begin
// VULNERABLE: Load data speculatively
speculative_data <= memory_read(load_addr);
speculative_load_done <= 1'b1;
// Privilege check happens in parallel but takes longer
privilege_ok <= (current_privilege >= load_privilege);
privilege_check_done <= 1'b1;
end
else if (speculative_load_done && privilege_check_done) begin
if (privilege_ok) begin
load_data <= speculative_data;
load_complete <= 1'b1;
end
else begin
// VULNERABLE: Data was already loaded and may have
// affected cache state before we get here
load_fault <= 1'b1;
load_data <= 32'h0;
end
speculative_load_done <= 1'b0;
privilege_check_done <= 1'b0;
end
end
endmodule
// Vulnerable: CPU code susceptible to Meltdown
#include <stdint.h>
// Attacker's array for cache side-channel
uint8_t probe_array[256 * 4096];
void vulnerable_meltdown_gadget(uint8_t* kernel_addr) {
// This code is VULNERABLE to Meltdown attack
uint8_t kernel_byte;
// VULNERABLE: Speculative read of kernel memory
// Even though this will fault, the speculative execution
// will read the byte and affect cache state
kernel_byte = *kernel_addr;
// VULNERABLE: Dependent load based on kernel byte
// This loads probe_array[kernel_byte * 4096] into cache
volatile uint8_t dummy = probe_array[kernel_byte * 4096];
// The fault happens here, but cache state already leaked
}
// Attacker can then measure cache timing to determine kernel_byte
void meltdown_attack(uint8_t* kernel_addr) {
// Flush probe array from cache
for (int i = 0; i < 256; i++) {
_mm_clflush(&probe_array[i * 4096]);
}
// Execute vulnerable gadget (will fault)
try_execute(vulnerable_meltdown_gadget, kernel_addr);
// Measure cache timing for each probe_array element
for (int i = 0; i < 256; i++) {
uint64_t start = rdtsc();
volatile uint8_t dummy = probe_array[i * 4096];
uint64_t time = rdtsc() - start;
if (time < CACHE_HIT_THRESHOLD) {
// This index was in cache - it's the kernel byte value!
printf("Kernel byte: %d\n", i);
}
}
}
Fixed Code
// Fixed: Data blocked until security check completes
module secure_bus_bridge (
input wire clk,
input wire reset_n,
// Request from untrusted CPU
input wire [31:0] req_addr,
input wire [31:0] req_data,
input wire req_read,
input wire req_write,
input wire req_valid,
// Firewall interface
output reg [31:0] fw_addr,
output reg fw_check_request,
input wire fw_access_allowed,
input wire fw_check_complete,
// Memory interface
output reg [31:0] mem_addr,
output reg mem_read,
output reg mem_write,
input wire [31:0] mem_read_data,
// Response to CPU
output reg [31:0] resp_data,
output reg resp_valid
);
// State machine
parameter IDLE = 2'd0;
parameter WAIT_FIREWALL = 2'd1;
parameter DO_ACCESS = 2'd2;
parameter RESPOND = 2'd3;
reg [1:0] state;
reg [31:0] pending_addr;
reg pending_read;
reg pending_write;
reg [31:0] pending_data;
reg access_allowed_reg;
always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
state <= IDLE;
mem_read <= 1'b0;
mem_write <= 1'b0;
resp_valid <= 1'b0;
fw_check_request <= 1'b0;
end
else begin
case (state)
IDLE: begin
resp_valid <= 1'b0;
if (req_valid) begin
// Save request
pending_addr <= req_addr;
pending_read <= req_read;
pending_write <= req_write;
pending_data <= req_data;
// Start firewall check
fw_addr <= req_addr;
fw_check_request <= 1'b1;
state <= WAIT_FIREWALL;
// FIXED: Do NOT access memory yet
mem_read <= 1'b0;
mem_write <= 1'b0;
end
end
WAIT_FIREWALL: begin
fw_check_request <= 1'b0;
if (fw_check_complete) begin
access_allowed_reg <= fw_access_allowed;
if (fw_access_allowed) begin
// FIXED: Only access memory AFTER firewall approves
mem_addr <= pending_addr;
mem_read <= pending_read;
mem_write <= pending_write;
state <= DO_ACCESS;
end
else begin
// FIXED: No memory access at all for denied requests
state <= RESPOND;
end
end
end
DO_ACCESS: begin
// Wait for memory operation
mem_read <= 1'b0;
mem_write <= 1'b0;
state <= RESPOND;
end
RESPOND: begin
if (access_allowed_reg) begin
resp_data <= mem_read_data;
end
else begin
resp_data <= 32'h0;
end
resp_valid <= 1'b1;
state <= IDLE;
end
endcase
end
end
endmodule
// Fixed: Processor with serialized privilege check
module secure_load_unit (
input wire clk,
input wire reset_n,
input wire [31:0] load_addr,
input wire load_request,
input wire [1:0] addr_privilege, // Required privilege for address
input wire [1:0] current_privilege, // Current execution privilege
output reg [31:0] load_data,
output reg load_complete,
output reg load_fault
);
parameter IDLE = 2'd0;
parameter CHECK_PRIVILEGE = 2'd1;
parameter DO_LOAD = 2'd2;
parameter COMPLETE = 2'd3;
reg [1:0] state;
reg [31:0] saved_addr;
reg privilege_ok;
always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
state <= IDLE;
load_complete <= 1'b0;
load_fault <= 1'b0;
end
else begin
case (state)
IDLE: begin
load_complete <= 1'b0;
load_fault <= 1'b0;
if (load_request) begin
saved_addr <= load_addr;
state <= CHECK_PRIVILEGE;
end
end
CHECK_PRIVILEGE: begin
// FIXED: Complete privilege check BEFORE any memory access
privilege_ok = (current_privilege >= addr_privilege);
if (privilege_ok) begin
state <= DO_LOAD;
end
else begin
// FIXED: Fault without ever touching memory
load_fault <= 1'b1;
load_data <= 32'h0;
state <= COMPLETE;
end
end
DO_LOAD: begin
// FIXED: Only reach here if privilege check passed
load_data <= memory_read(saved_addr);
load_complete <= 1'b1;
state <= COMPLETE;
end
COMPLETE: begin
state <= IDLE;
end
endcase
end
end
endmodule
// Fixed: Speculative execution with barriers
module secure_speculative_unit (
input wire clk,
input wire reset_n,
input wire [31:0] addr,
input wire load_request,
input wire [1:0] required_privilege,
input wire [1:0] current_privilege,
output reg [31:0] data,
output reg complete,
output reg fault
);
// FIXED: Use speculation barrier
reg speculation_allowed;
reg [31:0] speculative_result;
reg privilege_verified;
always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
speculation_allowed <= 1'b0;
complete <= 1'b0;
fault <= 1'b0;
end
else begin
// FIXED: Privilege check gates speculation
privilege_verified <= (current_privilege >= required_privilege);
// FIXED: Only allow speculative execution if privilege is verified
speculation_allowed <= privilege_verified;
if (load_request && speculation_allowed) begin
speculative_result <= memory_read(addr);
complete <= 1'b1;
data <= speculative_result;
end
else if (load_request && !speculation_allowed) begin
// FIXED: Block speculative access to protected memory
fault <= 1'b1;
data <= 32'h0;
end
end
end
// FIXED: Clear microarchitectural state on fault
always @(posedge clk) begin
if (fault) begin
// Flush any speculative state
flush_speculation_state();
end
end
endmodule
// Fixed: Meltdown mitigations
#include <stdint.h>
// FIXED: Kernel page table isolation (KPTI)
// Kernel pages are not mapped in user page tables
// Prevents speculative access to kernel memory
void setup_kpti(void) {
// Create separate page tables for user and kernel mode
// User page tables don't contain kernel mappings
user_pgd = create_user_page_tables();
kernel_pgd = create_kernel_page_tables();
// On syscall entry, switch to kernel page tables
// On syscall exit, switch to user page tables
}
// FIXED: Serialize speculation on privilege boundary
void secure_syscall_entry(void) {
// FIXED: Speculation barrier before accessing kernel data
__asm__ volatile("lfence" ::: "memory");
// Now safe to access kernel memory
}
// FIXED: Clear microarchitectural state
void secure_syscall_exit(void) {
// Clear L1 data cache
flush_l1d_cache();
// Clear branch prediction state
clear_branch_history();
// Speculation barrier
__asm__ volatile("lfence" ::: "memory");
// Switch to user page tables
switch_to_user_pgd();
}
// FIXED: Bounds checking with speculation barrier
int secure_array_access(int index, int* array, int array_size) {
// Check bounds
if (index >= array_size) {
return -1;
}
// FIXED: Speculation barrier after bounds check
// Prevents speculative access past the check
__asm__ volatile("lfence" ::: "memory");
// Now safe to access
return array[index];
}
CVE Examples
- CVE-2017-5754 (Meltdown): Microprocessors with speculative execution allowed unauthorized information disclosure via cache side-channel analysis
Related CWEs
- CWE-821: Incorrect Synchronization (parent)
- CWE-1037: Processor Optimization Removal or Modification of Security-critical Code (peer)
- CAPEC-233: Privilege Escalation (attack pattern)
- CAPEC-663: Exploitation of Transient Instruction Execution (attack pattern)
References
- MITRE Corporation. "CWE-1264: Hardware Logic with Insecure De-Synchronization between Control and Data Channels." https://cwe.mitre.org/data/definitions/1264.html
- Lipp et al. "Meltdown: Reading Kernel Memory from User Space"
- Intel. "Speculative Execution Side Channel Mitigations"