From f22f2bd135380d0ce61f3e5cc03214b9a612f943 Mon Sep 17 00:00:00 2001 From: Vittorio Distefano Date: Thu, 5 Nov 2026 13:56:52 +0100 Subject: [PATCH 1/2] fix(scoring): replace count_state's unsafe usize casts with downcasts dense_raw and key_from_raw reinterpreted a generic K as usize and back through raw pointers, sound only because callers reach them in the K == usize arm, which a debug_assert checked in debug builds only. They now use Any downcasts, which monomorphize to the identity in that arm and panic otherwise, so the scoring crate has no unsafe left and forbids it. --- crates/solverforge-scoring/src/lib.rs | 1 + .../src/stream/relational/count_state.rs | 37 ++++++++++--------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/crates/solverforge-scoring/src/lib.rs b/crates/solverforge-scoring/src/lib.rs index 42149e00..0c3e6c05 100644 --- a/crates/solverforge-scoring/src/lib.rs +++ b/crates/solverforge-scoring/src/lib.rs @@ -18,6 +18,7 @@ #![allow(clippy::type_complexity)] #![warn(unreachable_pub)] #![warn(missing_docs)] +#![forbid(unsafe_code)] // Core modules pub mod api; diff --git a/crates/solverforge-scoring/src/stream/relational/count_state.rs b/crates/solverforge-scoring/src/stream/relational/count_state.rs index dc6dea71..244ed52b 100644 --- a/crates/solverforge-scoring/src/stream/relational/count_state.rs +++ b/crates/solverforge-scoring/src/stream/relational/count_state.rs @@ -11,6 +11,8 @@ //! once at construction; the event path calls one enum method per key and //! nothing else branches. +use std::any::Any; + use super::{FastMap, RowHandle}; /// One key's count plus the epoch of its last touch, stamped against the @@ -291,26 +293,27 @@ impl DenseKeys { } /// View a generic key as its `usize` representation. Only called inside the -/// `K == usize` arm of `KeyState::new`, where the conversion is the identity. +/// `K == usize` arm of `KeyState::new`, where the downcast is the identity and +/// monomorphizes to a plain read. +/// +/// # Panics +/// +/// When `K` is not `usize`. fn dense_raw(key: &K) -> usize { - debug_assert_eq!( - std::any::TypeId::of::(), - std::any::TypeId::of::(), - "dense counts are usize-keyed" - ); - // SAFETY: K is usize in this arm; the reference reinterprets the same - // non-null, aligned, initialized 8 bytes. - unsafe { *(key as *const K as *const usize) } + *(key as &dyn Any) + .downcast_ref::() + .expect("dense counts are usize-keyed") } /// Inverse of `dense_raw`, same `K == usize` arm. +/// +/// # Panics +/// +/// When `K` is not `usize`. fn key_from_raw(raw: usize) -> K { - debug_assert_eq!( - std::any::TypeId::of::(), - std::any::TypeId::of::(), - "dense counts are usize-keyed" - ); - // SAFETY: K is usize in this arm; usize is repr-transparent `K` here and - // the pointer is valid, aligned, initialized. - unsafe { std::ptr::read(&raw as *const usize as *const K) } + let mut slot = Some(raw); + (&mut slot as &mut dyn Any) + .downcast_mut::>() + .and_then(Option::take) + .expect("dense counts are usize-keyed") } From afb879a7b7127e4caf9a9a8edb49abdeac74bd5c Mon Sep 17 00:00:00 2001 From: Vittorio Distefano Date: Thu, 5 Nov 2026 13:56:52 +0100 Subject: [PATCH 2/2] ci: run the existence tests under Miri The existence tests are the ones that drive the dense, usize-keyed count state. A new Miri job runs them on a pinned nightly (nightly-2026-01-20), so a new nightly cannot fail CI on its own. Closes #218. --- .github/workflows/ci.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5f4d11a..40380545 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,6 +113,25 @@ jobs: - name: Check dependencies run: cargo machete && cargo deny check advisories + miri: + if: ${{ github.server_url == 'https://github.com' }} + name: Miri + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + # The existence tests drive the dense, usize-keyed count state. A pinned + # nightly keeps a new nightly from failing CI on its own; bump it on + # purpose. + - name: Install Miri + run: rustup toolchain install nightly-2026-01-20 --profile minimal --component miri --no-self-update + + - name: Run the existence tests under Miri + run: | + cargo +nightly-2026-01-20 miri setup + cargo +nightly-2026-01-20 miri test -p solverforge-scoring --lib -- constraint::tests::exists + package: if: ${{ github.server_url == 'https://github.com' }} name: Package