Hardware Logic Contains Race Conditions

Description

Hardware Logic Contains Race Conditions occurs when a race condition in the hardware logic results in undermining security guarantees of the system. Race conditions in logic circuits occur when logic gates receive inputs from signals that originate from the same source but traverse different paths, causing inputs to change at slightly different times. This creates timing errors or glitches that cause outputs to shift to unwanted states before stabilizing. When these errors occur in access control logic or security-critical finite state machines, attackers may exploit them to bypass existing protections.

Risk

Hardware race conditions have severe security implications. Security protections bypassed during glitches. Privilege escalation possible. Execution logic alterable. Access control decisions corrupted. State machines enter unexpected states. Metastability issues in security logic. Attackers can time operations to exploit races. Physical attacks can induce race conditions.

Solution

Adopt design practices that recognize and eliminate race conditions, such as Karnaugh maps, during architecture and design phase. Implement logic redundancy along security-critical paths. Default to a secure state where access is denied to untrusted agents to avoid metastability issues. Add consensus logic to eliminate glitches. Register inputs to security-critical logic to prevent mid-operation changes.

Common Consequences

ImpactDetails
Access ControlScope: Access Control

Protection mechanisms bypassed during race condition windows.
AuthorizationScope: Authorization

Privilege escalation or identity assumption through timing attacks.
IntegrityScope: Integrity

Execution logic altered during glitches.

Example Code

Vulnerable Code

// Vulnerable: Multiplexor with glitch (race condition)

module vulnerable_glitch_mux (
    input  wire in0,
    input  wire in1,
    input  wire sel,
    output wire z
);

    wire not_sel, and_out1, and_out2;

    // VULNERABLE: Creates glitch in signal z
    // When sel changes, there's a brief window where both
    // and_out1 and and_out2 could be high or low incorrectly

    assign not_sel = ~sel;
    assign and_out1 = not_sel & in0;
    assign and_out2 = sel & in1;
    assign z = and_out1 | and_out2;  // VULNERABLE: Glitch occurs here

    // Race condition:
    // When sel transitions 0->1:
    // 1. sel becomes 1
    // 2. not_sel still 1 (propagation delay)
    // 3. Brief moment: and_out1=in0, and_out2=in1 (both could be 1!)
    // 4. z glitches to unexpected value
    // 5. Eventually settles to correct value

    // If z controls access decisions, brief glitch could allow access

endmodule

// Vulnerable: Security decision with race condition
module vulnerable_access_control (
    input  wire        clk,
    input  wire        rst_n,
    input  wire        user_request,
    input  wire        admin_request,
    input  wire        is_admin,
    output wire        access_granted
);

    wire user_access, admin_access;

    // VULNERABLE: Race between is_admin and request signals
    assign user_access = user_request & ~is_admin;
    assign admin_access = admin_request & is_admin;
    assign access_granted = user_access | admin_access;

    // Attack: Toggle is_admin while request is active
    // Glitch could grant admin access to non-admin user

endmodule

// Vulnerable: DMA with race condition on PMP signals
module vulnerable_dma_pmp (
    input  wire        clk_i,
    input  wire        rst_ni,
    input  wire [7:0][15:0] pmpcfg_i,     // PMP configuration
    input  wire [15:0][53:0] pmpaddr_i,   // PMP addresses
    input  wire [2:0]  dma_ctrl_reg,
    input  wire [31:0] dma_src_addr,
    input  wire [31:0] dma_dst_addr,
    output reg         dma_transfer_valid
);

    localparam CTRL_IDLE = 3'b000;
    localparam CTRL_SETUP = 3'b001;
    localparam CTRL_TRANSFER = 3'b010;
    localparam CTRL_DONE = 3'b011;

    wire pmp_allow;

    // VULNERABLE: Direct use of asynchronous PMP signals
    // pmpaddr_i and pmpcfg_i can change during DMA operation

    pmp_checker u_pmp (
        .cfg(pmpcfg_i),          // VULNERABLE: Direct connection
        .addr(pmpaddr_i),        // VULNERABLE: Can change mid-transfer
        .check_addr(dma_src_addr),
        .allow(pmp_allow)
    );

    always @(posedge clk_i or negedge rst_ni) begin
        if (!rst_ni) begin
            dma_transfer_valid <= 1'b0;
        end else begin
            case (dma_ctrl_reg)
                CTRL_SETUP: begin
                    // VULNERABLE: PMP checked at setup
                    // But signals can change during transfer
                    if (pmp_allow) begin
                        dma_transfer_valid <= 1'b1;
                    end
                end

                CTRL_TRANSFER: begin
                    // VULNERABLE: No re-check during transfer
                    // Attacker can swap PMP config after initial check
                    // to access privileged memory
                end

                CTRL_DONE: begin
                    dma_transfer_valid <= 1'b0;
                end
            endcase
        end
    end

    // Attack:
    // 1. Set PMP to allow access to unprivileged region
    // 2. Start DMA transfer (passes check)
    // 3. During transfer, modify PMP to point to privileged region
    // 4. DMA continues with initial "allow" status to privileged memory!

endmodule

// Vulnerable: State machine with race condition
module vulnerable_security_fsm (
    input  wire        clk,
    input  wire        rst_n,
    input  wire        auth_signal,
    input  wire        access_request,
    output reg         secure_access
);

    // VULNERABLE: Combinational security decision
    // Subject to race conditions on inputs

    localparam STATE_LOCKED = 2'b00;
    localparam STATE_AUTH   = 2'b01;
    localparam STATE_OPEN   = 2'b10;

    reg [1:0] state;

    // VULNERABLE: Direct combinational logic for access
    always @(*) begin
        // Race condition when auth_signal and access_request change
        secure_access = (state == STATE_OPEN) ||
                        (auth_signal && access_request);  // GLITCH!
    end

endmodule

Fixed Code

// Fixed: Glitch-free multiplexor with consensus logic

module secure_glitch_free_mux (
    input  wire in0,
    input  wire in1,
    input  wire sel,
    output wire z
);

    wire not_sel, and_out1, and_out2;

    assign not_sel = ~sel;
    assign and_out1 = not_sel & in0;
    assign and_out2 = sel & in1;

    // FIXED: Add consensus term to eliminate glitch
    // When in0 == in1, output is correct regardless of sel glitch
    assign z = and_out1 | and_out2 | (in0 & in1);

    // Alternative: Register the output
    // reg z_reg;
    // always @(posedge clk) z_reg <= and_out1 | and_out2;

endmodule

// Fixed: Security decision with registered inputs
module secure_access_control (
    input  wire        clk,
    input  wire        rst_n,
    input  wire        user_request,
    input  wire        admin_request,
    input  wire        is_admin,
    output reg         access_granted
);

    // FIXED: Register all inputs to synchronize
    reg user_request_r, admin_request_r, is_admin_r;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            user_request_r <= 1'b0;
            admin_request_r <= 1'b0;
            is_admin_r <= 1'b0;
        end else begin
            // FIXED: Sample inputs synchronously
            user_request_r <= user_request;
            admin_request_r <= admin_request;
            is_admin_r <= is_admin;
        end
    end

    // FIXED: Make security decision with registered values
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            access_granted <= 1'b0;
        end else begin
            // FIXED: Default deny, explicit grant
            access_granted <= 1'b0;

            if (is_admin_r && admin_request_r) begin
                access_granted <= 1'b1;
            end else if (!is_admin_r && user_request_r) begin
                access_granted <= 1'b1;
            end
        end
    end

endmodule

// Fixed: DMA with registered PMP signals
module secure_dma_pmp (
    input  wire        clk_i,
    input  wire        rst_ni,
    input  wire [7:0][15:0] pmpcfg_i,
    input  wire [15:0][53:0] pmpaddr_i,
    input  wire [2:0]  dma_ctrl_reg,
    input  wire [31:0] dma_src_addr,
    input  wire [31:0] dma_dst_addr,
    output reg         dma_transfer_valid
);

    localparam CTRL_IDLE = 3'b000;
    localparam CTRL_SETUP = 3'b001;
    localparam CTRL_TRANSFER = 3'b010;
    localparam CTRL_DONE = 3'b011;

    // FIXED: Register PMP signals at start of operation
    reg [7:0][15:0] pmpcfg_reg;
    reg [15:0][53:0] pmpaddr_reg;

    wire pmp_allow;

    // FIXED: Capture PMP configuration at idle/done states only
    always @(posedge clk_i or negedge rst_ni) begin
        if (!rst_ni) begin
            pmpcfg_reg <= '0;
            pmpaddr_reg <= '0;
        end else begin
            // FIXED: Only update when not in active transfer
            if (dma_ctrl_reg == CTRL_IDLE || dma_ctrl_reg == CTRL_DONE) begin
                pmpcfg_reg <= pmpcfg_i;
                pmpaddr_reg <= pmpaddr_i;
            end
            // FIXED: PMP values locked during transfer
        end
    end

    // FIXED: Use registered values for security check
    pmp_checker u_pmp (
        .cfg(pmpcfg_reg),        // FIXED: Registered value
        .addr(pmpaddr_reg),      // FIXED: Cannot change mid-transfer
        .check_addr(dma_src_addr),
        .allow(pmp_allow)
    );

    always @(posedge clk_i or negedge rst_ni) begin
        if (!rst_ni) begin
            dma_transfer_valid <= 1'b0;
        end else begin
            case (dma_ctrl_reg)
                CTRL_SETUP: begin
                    // FIXED: Check uses stable registered values
                    if (pmp_allow) begin
                        dma_transfer_valid <= 1'b1;
                    end else begin
                        dma_transfer_valid <= 1'b0;
                    end
                end

                CTRL_TRANSFER: begin
                    // FIXED: Transfer uses same values as check
                    // Attacker cannot change PMP during transfer
                end

                CTRL_DONE, CTRL_IDLE: begin
                    dma_transfer_valid <= 1'b0;
                end

                default: begin
                    dma_transfer_valid <= 1'b0;
                end
            endcase
        end
    end

endmodule

// Fixed: Security FSM with proper synchronization
module secure_security_fsm (
    input  wire        clk,
    input  wire        rst_n,
    input  wire        auth_signal,
    input  wire        access_request,
    output reg         secure_access
);

    localparam STATE_LOCKED = 2'b00;
    localparam STATE_AUTH   = 2'b01;
    localparam STATE_OPEN   = 2'b10;

    reg [1:0] state;

    // FIXED: Register inputs
    reg auth_signal_r, access_request_r;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            auth_signal_r <= 1'b0;
            access_request_r <= 1'b0;
        end else begin
            auth_signal_r <= auth_signal;
            access_request_r <= access_request;
        end
    end

    // FIXED: State machine with registered security decision
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state <= STATE_LOCKED;
            secure_access <= 1'b0;  // FIXED: Default deny
        end else begin
            // FIXED: Default deny each cycle
            secure_access <= 1'b0;

            case (state)
                STATE_LOCKED: begin
                    if (auth_signal_r) begin
                        state <= STATE_AUTH;
                    end
                end

                STATE_AUTH: begin
                    if (auth_signal_r && access_request_r) begin
                        state <= STATE_OPEN;
                    end else if (!auth_signal_r) begin
                        state <= STATE_LOCKED;
                    end
                end

                STATE_OPEN: begin
                    // FIXED: Grant access only in proper state
                    secure_access <= 1'b1;

                    if (!auth_signal_r) begin
                        state <= STATE_LOCKED;
                        secure_access <= 1'b0;
                    end
                end

                default: begin
                    state <= STATE_LOCKED;
                end
            endcase
        end
    end

endmodule

CVE Examples

  • CVE-2020-15802: Race condition in Bluetooth pairing allowed attackers to inject keys.
  • CVE-2019-11091: Hardware race condition enabled microarchitectural data sampling.

  • CWE-362: Concurrent Execution using Shared Resource with Improper Synchronization (parent)
  • CWE-1199: General Circuit and Logic Design Concerns (category)
  • CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition (related)

References

  1. MITRE Corporation. "CWE-1298: Hardware Logic Contains Race Conditions." https://cwe.mitre.org/data/definitions/1298.html
  2. CAPEC-26: Leveraging Race Conditions
  3. IEEE. "Digital Design Principles"