Skip to content

Commit 04f35b0

Browse files
committed
Add spinlock_validate, cpu_mask, ipi modules + fix Rocq CI
Three new verified kernel modules porting foundational SMP safety logic: - spinlock_validate: owner encoding/validation (6 properties, 15 tests) Replaces the silent & 3U mask with configurable MAX_CPUS check. - cpu_mask: thread affinity mask arithmetic (4 properties, 33 tests) Enforces PIN_ONLY invariant at runtime (Zephyr only asserts in debug). - ipi: inter-processor interrupt mask computation (5 properties, 22 tests) Pure decision function for which CPUs need scheduling interrupts. Also updates rules_rocq_rust to 2cfb99ca (hermetic nightly download, fixes libLLVM linker error in sandboxed CI) and removes the LIBRARY_PATH workaround from formal-verification.yml.
1 parent 37a8cc4 commit 04f35b0

14 files changed

Lines changed: 1969 additions & 8 deletions

.github/workflows/formal-verification.yml

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,7 @@ jobs:
8787
- uses: cachix/install-nix-action@v30
8888

8989
- name: Install Rust toolchain
90-
uses: dtolnay/rust-toolchain@nightly
91-
92-
- name: Set LLVM library path for rocq_of_rust
93-
run: |
94-
SYSROOT=$(rustc +nightly --print sysroot)
95-
echo "LIBRARY_PATH=${SYSROOT}/lib" >> "$GITHUB_ENV"
96-
echo "LD_LIBRARY_PATH=${SYSROOT}/lib" >> "$GITHUB_ENV"
90+
uses: dtolnay/rust-toolchain@stable
9791

9892
- name: Run Rocq proof verification
9993
run: |

BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ VERUS_SRCS = [
6262
"src/sched.rs",
6363
"src/poll.rs",
6464
"src/zms.rs",
65+
"src/spinlock_validate.rs",
66+
"src/cpu_mask.rs",
67+
"src/ipi.rs",
6568
]
6669

6770
PLAIN_SRCS = ["//plain:srcs"]

MODULE.bazel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ bazel_dep(name = "rules_rocq_rust", version = "0.1.0")
2323
git_override(
2424
module_name = "rules_rocq_rust",
2525
remote = "https://github.com/pulseengine/rules_rocq_rust.git",
26-
commit = "6a8da0bd30b5f80f811acefbf6ac5740a08d4a8c",
26+
commit = "2cfb99ca46eb570c5dc54ed1ee2124b8add212e2",
2727
)
2828

2929
# rocq-of-rust toolchain — translates Rust to Rocq

plain/src/cpu_mask.rs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
//! Verified CPU affinity mask model for Zephyr RTOS.
2+
//!
3+
//! This is a formally verified port of zephyr/kernel/cpu_mask.c.
4+
//! All safety-critical properties are proven with Verus (SMT/Z3).
5+
//!
6+
//! Source mapping:
7+
//! cpu_mask_mod -> cpu_mask_mod (cpu_mask.c:19-45)
8+
//! k_thread_cpu_mask_clear -> (enable=0, disable=0xFFFFFFFF)
9+
//! k_thread_cpu_mask_enable_all -> (enable=0xFFFFFFFF, disable=0)
10+
//! k_thread_cpu_mask_enable -> (enable=BIT(cpu), disable=0)
11+
//! k_thread_cpu_mask_disable -> (enable=0, disable=BIT(cpu))
12+
//! k_thread_cpu_pin -> (enable=BIT(cpu), disable=!BIT(cpu))
13+
//! validate_pin_mask -> power-of-2 check (cpu_mask.c:38-41)
14+
//! cpu_pin_compute -> BIT(cpu) with bounds check
15+
//!
16+
//! Omitted (not safety-relevant):
17+
//! - K_SPINLOCK(&_sched_spinlock) — locking handled in C
18+
//! - z_is_thread_prevented_from_running — caller supplies `is_running`
19+
//! - CONFIG_POLL, CONFIG_OBJ_CORE — debug/tracing
20+
//! - CONFIG_USERSPACE (z_vrfy_*) — syscall marshaling
21+
//!
22+
//! ASIL-D verified properties:
23+
//! CM1: Running threads cannot have mask modified (returns EINVAL)
24+
//! CM2: PIN_ONLY mode requires exactly one bit set: (m & (m-1)) == 0
25+
//! CM3: New mask = (current | enable) & !disable
26+
//! CM4: Result mask is never zero (at least one CPU)
27+
//! CM5: Mask arithmetic is overflow-safe (bitwise ops on u32)
28+
//! CM6: cpu_pin_compute bounds-checks cpu_id < max_cpus <= 32
29+
use crate::error::*;
30+
/// Maximum supported CPUs (matches Zephyr BUILD_ASSERT: max 16).
31+
pub const MAX_CPUS: u32 = 16;
32+
/// Result of a cpu_mask_mod operation.
33+
///
34+
/// On success, holds the new mask. On failure, holds the error code.
35+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36+
pub struct CpuMaskResult {
37+
/// The resulting CPU affinity mask (valid only when `error == OK`).
38+
pub mask: u32,
39+
/// Error code: OK on success, EINVAL on failure.
40+
pub error: i32,
41+
}
42+
/// Check whether a mask is a valid PIN_ONLY mask (exactly one bit set).
43+
///
44+
/// This is the power-of-two check from cpu_mask.c:38-41:
45+
/// `(m == 0) || ((m & (m - 1)) == 0)`
46+
/// We strengthen to require m != 0 (a zero mask is never valid).
47+
///
48+
/// CM2: PIN_ONLY mode requires exactly one bit set.
49+
pub fn validate_pin_mask(mask: u32) -> bool {
50+
mask != 0 && (mask & (mask - 1)) == 0
51+
}
52+
/// Core CPU mask modification function.
53+
///
54+
/// Models cpu_mask_mod (cpu_mask.c:19-45).
55+
///
56+
/// Parameters:
57+
/// - `current_mask`: the thread's current cpu_mask
58+
/// - `enable`: bits to OR into the mask
59+
/// - `disable`: bits to AND-complement out of the mask
60+
/// - `is_running`: true if the thread is currently running (not prevented)
61+
/// - `pin_only`: true if CONFIG_SCHED_CPU_MASK_PIN_ONLY is enabled
62+
///
63+
/// CM1: Running threads cannot have mask modified (returns EINVAL).
64+
/// CM2: PIN_ONLY mode requires exactly one bit set in the result.
65+
/// CM3: New mask = (current | enable) & !disable.
66+
/// CM4: Result mask is never zero.
67+
/// CM5: All arithmetic is overflow-safe (bitwise ops on u32).
68+
pub fn cpu_mask_mod(
69+
current_mask: u32,
70+
enable: u32,
71+
disable: u32,
72+
is_running: bool,
73+
pin_only: bool,
74+
) -> CpuMaskResult {
75+
if is_running {
76+
return CpuMaskResult {
77+
mask: current_mask,
78+
error: EINVAL,
79+
};
80+
}
81+
let new_mask: u32 = (current_mask | enable) & !disable;
82+
if new_mask == 0 {
83+
return CpuMaskResult {
84+
mask: current_mask,
85+
error: EINVAL,
86+
};
87+
}
88+
if pin_only && (new_mask & (new_mask - 1)) != 0 {
89+
return CpuMaskResult {
90+
mask: current_mask,
91+
error: EINVAL,
92+
};
93+
}
94+
CpuMaskResult {
95+
mask: new_mask,
96+
error: OK,
97+
}
98+
}
99+
/// Compute the pin mask for a specific CPU.
100+
///
101+
/// Models the BIT(cpu) computation from k_thread_cpu_pin (cpu_mask.c:69).
102+
/// Returns `Ok(1u32 << cpu_id)` if `cpu_id < max_cpus` and `max_cpus <= 32`,
103+
/// otherwise returns `Err(EINVAL)`.
104+
///
105+
/// CM6: bounds check ensures shift is within u32 range.
106+
pub fn cpu_pin_compute(cpu_id: u32, max_cpus: u32) -> Result<u32, i32> {
107+
if max_cpus > 32 || cpu_id >= max_cpus {
108+
return Err(EINVAL);
109+
}
110+
let mask: u32 = 1u32 << cpu_id;
111+
Ok(mask)
112+
}

plain/src/ipi.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
//! Verified IPI mask creation model for Zephyr RTOS.
2+
//!
3+
//! This is a formally verified port of zephyr/kernel/ipi.c.
4+
//! All safety-critical properties are proven with Verus (SMT/Z3).
5+
//!
6+
//! This module models the **IPI mask creation** logic from Zephyr's SMP
7+
//! IPI subsystem. The mask determines which CPUs need an inter-processor
8+
//! interrupt when a thread becomes ready.
9+
//!
10+
//! Source mapping:
11+
//! ipi_mask_create -> compute_ipi_mask (ipi.c:29-70)
12+
//!
13+
//! Omitted (not safety-relevant):
14+
//! - CONFIG_IPI_OPTIMIZE bypass (trivially returns IPI_ALL_CPUS_MASK)
15+
//! - thread_is_metairq — MetaIRQ preemption override
16+
//! - thread_is_preemptible — cooperative thread guard
17+
//! - signal_pending_ipi — actual IPI delivery
18+
//! - CONFIG_USERSPACE (z_vrfy_*) — syscall marshaling
19+
//! - SYS_PORT_TRACING_* — instrumentation
20+
//!
21+
//! ASIL-D verified properties:
22+
//! IP1: current CPU is never in the result mask
23+
//! IP2: only CPUs within [0, num_cpus) can be in the mask
24+
//! IP3: only CPUs allowed by target_cpu_mask are considered
25+
//! IP4: a CPU is included only if its priority > target (lower importance)
26+
//! IP5: result fits in max_cpus bits (no stray high bits)
27+
/// Maximum supported CPUs (matches CONFIG_MP_MAX_NUM_CPUS).
28+
pub const MAX_CPUS: u32 = 16;
29+
/// Compute the IPI bitmask for a newly ready thread.
30+
///
31+
/// This is the verified model of `ipi_mask_create()` from ipi.c:29-70.
32+
///
33+
/// Parameters:
34+
/// - `current_cpu`: CPU executing the scheduling decision
35+
/// - `target_prio`: priority of the newly ready thread (lower = higher importance)
36+
/// - `target_cpu_mask`: CPU affinity mask for the thread (CONFIG_SCHED_CPU_MASK)
37+
/// - `cpu_prios`: per-CPU current thread priorities
38+
/// - `cpu_active`: per-CPU active flags
39+
/// - `num_cpus`: number of CPUs present (arch_num_cpus())
40+
/// - `max_cpus`: CONFIG_MP_MAX_NUM_CPUS (upper bound for bit width)
41+
///
42+
/// Returns a bitmask where bit `i` is set iff CPU `i` should receive an IPI.
43+
pub fn compute_ipi_mask(
44+
current_cpu: u32,
45+
target_prio: i32,
46+
target_cpu_mask: u32,
47+
cpu_prios: &[i32],
48+
cpu_active: &[bool],
49+
num_cpus: u32,
50+
max_cpus: u32,
51+
) -> u32 {
52+
let mut mask: u32 = 0u32;
53+
let mut idx: u32 = 0u32;
54+
while idx < num_cpus {
55+
if idx != current_cpu {
56+
if cpu_active[idx as usize] {
57+
let bit: u32 = 1u32 << idx;
58+
if (target_cpu_mask & bit) != 0u32 {
59+
if cpu_prios[idx as usize] > target_prio {
60+
mask = mask | bit;
61+
}
62+
}
63+
}
64+
}
65+
idx = idx + 1u32;
66+
}
67+
mask
68+
}
69+
/// Validate a previously computed IPI mask.
70+
///
71+
/// Checks structural properties that must hold for any valid mask:
72+
/// - current CPU bit is not set
73+
/// - no bits at or above max_cpus are set
74+
pub fn validate_ipi_mask(mask: u32, current_cpu: u32, max_cpus: u32) -> bool {
75+
let current_bit: u32 = 1u32 << current_cpu;
76+
let current_excluded = (mask & current_bit) == 0u32;
77+
let bounded = (mask >> max_cpus) == 0u32;
78+
current_excluded && bounded
79+
}

plain/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
//! - [`sem`] — Counting semaphore (port of kernel/sem.c)
1919
//! - [`mutex`] — Reentrant mutex (port of kernel/mutex.c)
2020
//! - [`condvar`] — Condition variable (port of kernel/condvar.c)
21+
//! - [`cpu_mask`] — CPU affinity mask (port of kernel/cpu_mask.c)
2122
#![no_std]
2223
#![allow(unused_imports)]
2324
#![allow(
@@ -39,6 +40,7 @@ pub mod wait_queue;
3940
pub mod sem;
4041
pub mod mutex;
4142
pub mod condvar;
43+
pub mod cpu_mask;
4244
pub mod msgq;
4345
pub mod pipe;
4446
pub mod stack;
@@ -63,10 +65,12 @@ pub mod fault_decode;
6365
pub mod mempool;
6466
pub mod dynamic;
6567
pub mod smp_state;
68+
pub mod ipi;
6669
pub mod stack_config;
6770
pub mod device_init;
6871
pub mod mem_domain;
6972
pub mod spinlock;
73+
pub mod spinlock_validate;
7074
pub mod atomic;
7175
pub mod userspace;
7276
pub mod ring_buf;

plain/src/spinlock_validate.rs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
//! Verified spinlock validation helpers for Zephyr RTOS.
2+
//!
3+
//! This is a formally verified port of zephyr/kernel/spinlock_validate.c.
4+
//! All safety-critical properties are proven with Verus (SMT/Z3).
5+
//!
6+
//! The C code encodes lock ownership by OR-ing the CPU ID into the low
7+
//! bits of a thread pointer (which is guaranteed aligned). The `& 3U`
8+
//! mask hard-codes support for at most 4 CPUs. This module replaces
9+
//! the magic constant with a configurable `MAX_CPUS` and explicit
10+
//! alignment requirement, then proves the encoding is injective.
11+
//!
12+
//! Source mapping:
13+
//! z_spin_lock_valid -> spin_lock_valid (spinlock_validate.c:10-20)
14+
//! z_spin_unlock_valid -> spin_unlock_valid (spinlock_validate.c:23-37)
15+
//! z_spin_lock_set_owner -> spin_lock_compute_owner (spinlock_validate.c:39-43)
16+
//!
17+
//! Omitted (not safety-relevant):
18+
//! - CONFIG_KERNEL_COHERENCE (z_spin_lock_mem_coherent) — cache coherency check
19+
//! - ISR + _THREAD_DUMMY edge case — modeled as a separate flag in the caller
20+
//!
21+
//! ASIL-D verified properties:
22+
//! SV1: owner encoding is injective (distinct (cpu, thread) -> distinct owner)
23+
//! SV2: lock_valid returns false iff the lock is already held by the same CPU
24+
//! SV3: unlock_valid returns true iff the stored owner matches (cpu | thread)
25+
//! SV4: CPU ID is recoverable from the encoded owner via masking
26+
//! SV5: thread pointer is recoverable from the encoded owner via masking
27+
//! SV6: MAX_CPUS bounds the CPU mask (replaces hard-coded `& 3U`)
28+
/// Maximum number of CPUs supported.
29+
///
30+
/// Zephyr hard-codes `& 3U` (2 bits, 4 CPUs). We use the same default
31+
/// but express it as a named constant so it can be widened.
32+
pub const MAX_CPUS: u32 = 4;
33+
/// Bit mask to extract the CPU ID from an encoded owner value.
34+
///
35+
/// CPU_MASK == MAX_CPUS - 1 == 0b11 for 4 CPUs.
36+
/// Requires MAX_CPUS to be a power of two.
37+
pub const CPU_MASK: usize = 3;
38+
/// Minimum alignment of thread pointers (in bytes).
39+
///
40+
/// Thread pointers must be aligned to at least MAX_CPUS so that the
41+
/// low bits are zero and available for the CPU ID tag.
42+
pub const THREAD_ALIGN: usize = 4;
43+
/// Check whether acquiring the spinlock is valid.
44+
///
45+
/// Models z_spin_lock_valid() (spinlock_validate.c:10-20):
46+
///
47+
/// ```c
48+
/// bool z_spin_lock_valid(struct k_spinlock *l) {
49+
/// uintptr_t thread_cpu = l->thread_cpu;
50+
/// if (thread_cpu != 0U) {
51+
/// if ((thread_cpu & 3U) == _current_cpu->id)
52+
/// return false;
53+
/// }
54+
/// return true;
55+
/// }
56+
/// ```
57+
///
58+
/// Returns `false` when the lock is already held **by the same CPU**
59+
/// (i.e. the CPU bits in `thread_cpu` match `current_cpu_id`).
60+
/// A `thread_cpu` of 0 means the lock is free.
61+
///
62+
/// SV2: lock_valid returns false iff lock is held and CPU matches.
63+
pub fn spin_lock_valid(thread_cpu: usize, current_cpu_id: u32) -> bool {
64+
if thread_cpu != 0 {
65+
if (thread_cpu & CPU_MASK) == (current_cpu_id as usize) {
66+
return false;
67+
}
68+
}
69+
true
70+
}
71+
/// Check whether releasing the spinlock is valid.
72+
///
73+
/// Models z_spin_unlock_valid() (spinlock_validate.c:23-37):
74+
///
75+
/// ```c
76+
/// bool z_spin_unlock_valid(struct k_spinlock *l) {
77+
/// uintptr_t tcpu = l->thread_cpu;
78+
/// l->thread_cpu = 0;
79+
/// ...
80+
/// if (tcpu != (_current_cpu->id | (uintptr_t)_current))
81+
/// return false;
82+
/// return true;
83+
/// }
84+
/// ```
85+
///
86+
/// Returns `true` iff the stored `thread_cpu` matches the encoded
87+
/// identity of the current thread on the current CPU.
88+
///
89+
/// Note: the C function also zeroes `l->thread_cpu` and handles an ISR
90+
/// edge case. Both are side-effects handled by the FFI layer; this
91+
/// function is a pure validity predicate.
92+
///
93+
/// SV3: unlock_valid returns true iff owner matches (cpu | thread).
94+
pub fn spin_unlock_valid(
95+
thread_cpu: usize,
96+
current_cpu_id: u32,
97+
current_thread: usize,
98+
) -> bool {
99+
let expected = (current_cpu_id as usize) | current_thread;
100+
thread_cpu == expected
101+
}
102+
/// Compute the owner tag for a spinlock.
103+
///
104+
/// Models z_spin_lock_set_owner() (spinlock_validate.c:39-43):
105+
///
106+
/// ```c
107+
/// void z_spin_lock_set_owner(struct k_spinlock *l) {
108+
/// l->thread_cpu = _current_cpu->id | (uintptr_t)_current;
109+
/// }
110+
/// ```
111+
///
112+
/// Encodes the current CPU ID and thread pointer into a single `usize`.
113+
///
114+
/// SV4/SV5: CPU and thread are recoverable.
115+
/// SV6: CPU ID fits within the mask.
116+
pub fn spin_lock_compute_owner(current_cpu_id: u32, current_thread: usize) -> usize {
117+
(current_cpu_id as usize) | current_thread
118+
}

0 commit comments

Comments
 (0)