Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,32 @@ jobs:
- name: Architecture check
run: cross run --features cli --bin arch-check --target ${{ matrix.target }}
- name: Test
run: cross test --features cli --target ${{ matrix.target }}
run: cross test --features cli --target ${{ matrix.target }}

miri-test-x86_64:
name: Miri Test x86_64
needs: test-x86_64
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions-rust-lang/setup-rust-toolchain@9d7e65c320fdb52dcd45ffaa68deb6c02c8754d9
with:
toolchain: nightly
components: miri
- name: Detect CPU cores
id: cores
run: |
CORES=$(nproc)
echo "count=$CORES" >> $GITHUB_OUTPUT
echo "use_nextest=$([[ $CORES -ge 2 ]] && echo 'true' || echo 'false')" >> $GITHUB_OUTPUT
- name: Install nextest
if: steps.cores.outputs.use_nextest == 'true'
run: cargo install cargo-nextest --locked
- name: Setup Miri
run: cargo miri setup
- name: Run Miri tests (parallel)
if: steps.cores.outputs.use_nextest == 'true'
run: cargo miri nextest run --all-features -j${{ steps.cores.outputs.count }}
- name: Run Miri tests (serial)
if: steps.cores.outputs.use_nextest == 'false'
run: cargo miri test --all-features
38 changes: 11 additions & 27 deletions src/arch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,7 @@ pub(crate) unsafe fn update(state: u64, bytes: &[u8], params: CrcParams) -> u64
32 => algorithm::update::<_, Width32>(state as u32, bytes, params, ops) as u64,
_ => panic!("Unsupported CRC width: {}", params.width),
},
ArchOpsInstance::SoftwareFallback => {
#[cfg(target_arch = "x86")]
crate::arch::x86_software_update(state, bytes, params);

// This should never happen, but just in case
panic!("x86 features missing (SSE4.1 && PCLMULQDQ)");
}
ArchOpsInstance::SoftwareFallback => crate::arch::software::update(state, bytes, params),
}
}

Expand All @@ -139,27 +133,8 @@ pub(crate) unsafe fn update(state: u64, bytes: &[u8], params: CrcParams) -> u64
32 => algorithm::update::<_, Width32>(state as u32, bytes, params, ops) as u64,
_ => panic!("Unsupported CRC width: {}", params.width),
},
ArchOpsInstance::SoftwareFallback => x86_software_update(state, bytes, params),
}
}

#[inline(always)]
#[allow(unused)]
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
fn x86_software_update(state: u64, bytes: &[u8], params: CrcParams) -> u64 {
if !is_x86_feature_detected!("sse4.1") || !is_x86_feature_detected!("pclmulqdq") {
#[cfg(all(
target_arch = "x86",
any(not(target_feature = "sse4.1"), not(target_feature = "pclmulqdq"))
))]
{
// Use software implementation when no SIMD support is available
crate::arch::software::update(state, bytes, params);
}
ArchOpsInstance::SoftwareFallback => crate::arch::software::update(state, bytes, params),
}

// This should never happen, but just in case
panic!("x86 features missing (SSE4.1 && PCLMULQDQ)");
}

#[inline]
Expand Down Expand Up @@ -347,7 +322,10 @@ mod tests {
}
}

/// Skipping for Miri runs due to time constraints, underlying code already covered by other
/// tests.
#[test]
#[cfg_attr(miri, ignore)]
fn test_small_lengths_all() {
// Test each CRC-64 variant
for config in TEST_ALL_CONFIGS {
Expand All @@ -358,7 +336,10 @@ mod tests {
}
}

/// Skipping for Miri runs due to time constraints, underlying code already covered by other
/// tests.
#[test]
#[cfg_attr(miri, ignore)]
fn test_medium_lengths() {
// Test each CRC-64 variant
for config in TEST_ALL_CONFIGS {
Expand All @@ -369,7 +350,10 @@ mod tests {
}
}

/// Skipping for Miri runs due to time constraints, underlying code already covered by other
/// tests.
#[test]
#[cfg_attr(miri, ignore)]
fn test_large_lengths() {
// Test each CRC-64 variant
for config in TEST_ALL_CONFIGS {
Expand Down
105 changes: 61 additions & 44 deletions src/arch/software.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,13 @@
// Copyright 2025 Don MacAskill. Licensed under MIT or Apache-2.0.

//! This module contains a software fallback for unsupported architectures.
//!
//! Software fallback is conditionally compiled based on target architecture:
//! - Always included for non-SIMD architectures (not x86/x86_64/aarch64)
//! - Included for x86 when SSE4.1/PCLMULQDQ may not be available
//! - Included for aarch64 for runtime fallback when AES is not detected
//! - Excluded for x86_64 since SSE4.1/PCLMULQDQ are always available (but included for testing)

#![cfg(any(
// Non-aarch64/x86/x86_64 architectures always need software fallback
not(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64")),
// x86 may not have SSE4.1/PCLMULQDQ support
all(target_arch = "x86", any(not(target_feature = "sse4.1"), not(target_feature = "pclmulqdq"))),
// aarch64 needs software fallback for runtime detection when AES is not available...
// NEON doesn't guarantee AES, so for rare outlier CPUs this might not work 100%...
all(target_arch = "aarch64", not(target_feature = "aes")),
// Include for testing on all architectures
test
))]

use crate::consts::CRC_64_NVME;
use crate::CrcAlgorithm;
use crate::CrcParams;
use crc::{Algorithm, Table};
use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};

#[allow(unused)]
const RUST_CRC32_AIXM: crc::Crc<u32, Table<16>> =
Expand Down Expand Up @@ -96,6 +80,14 @@ const RUST_CRC64_WE: crc::Crc<u64, Table<16>> = crc::Crc::<u64, Table<16>>::new(
#[allow(unused)]
const RUST_CRC64_XZ: crc::Crc<u64, Table<16>> = crc::Crc::<u64, Table<16>>::new(&crc::CRC_64_XZ);

static CUSTOM_CRC32_CACHE: OnceLock<Mutex<HashMap<Crc32Key, &'static Algorithm<u32>>>> =
OnceLock::new();
static CUSTOM_CRC64_CACHE: OnceLock<Mutex<HashMap<Crc64Key, &'static Algorithm<u64>>>> =
OnceLock::new();

type Crc32Key = (u32, u32, bool, bool, u32, u32);
type Crc64Key = (u64, u64, bool, bool, u64, u64);

#[allow(unused)]
// Dispatch function that handles the generic case
pub(crate) fn update(state: u64, data: &[u8], params: CrcParams) -> u64 {
Expand All @@ -115,19 +107,32 @@ pub(crate) fn update(state: u64, data: &[u8], params: CrcParams) -> u64 {
CrcAlgorithm::Crc32Mpeg2 => RUST_CRC32_MPEG_2,
CrcAlgorithm::Crc32Xfer => RUST_CRC32_XFER,
CrcAlgorithm::Crc32Custom => {
let algorithm: Algorithm<u32> = Algorithm {
width: params.width,
poly: params.poly as u32,
init: params.init as u32,
refin: params.refin,
refout: params.refout,
xorout: params.xorout as u32,
check: params.check as u32,
residue: 0x00000000, // unused in this context
};

// ugly, but the crc crate is difficult to work with...
let static_algorithm = Box::leak(Box::new(algorithm));
let cache = CUSTOM_CRC32_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
let mut cache = cache.lock().unwrap();

// Create a key from params that uniquely identifies this algorithm
let key: Crc32Key = (
params.poly as u32,
params.init as u32,
params.refin,
params.refout,
params.xorout as u32,
params.check as u32,
);

let static_algorithm = cache.entry(key).or_insert_with(|| {
let algorithm = Algorithm {
width: params.width,
poly: params.poly as u32,
init: params.init as u32,
refin: params.refin,
refout: params.refout,
xorout: params.xorout as u32,
check: params.check as u32,
residue: 0x00000000,
};
Box::leak(Box::new(algorithm))
});

crc::Crc::<u32, Table<16>>::new(static_algorithm)
}
Expand All @@ -145,19 +150,31 @@ pub(crate) fn update(state: u64, data: &[u8], params: CrcParams) -> u64 {
CrcAlgorithm::Crc64We => RUST_CRC64_WE,
CrcAlgorithm::Crc64Xz => RUST_CRC64_XZ,
CrcAlgorithm::Crc64Custom => {
let algorithm: Algorithm<u64> = Algorithm {
width: params.width,
poly: params.poly,
init: params.init,
refin: params.refin,
refout: params.refout,
xorout: params.xorout,
check: params.check,
residue: 0x0000000000000000, // unused in this context
};

// ugly, but the crc crate is difficult to work with...
let static_algorithm = Box::leak(Box::new(algorithm));
let cache = CUSTOM_CRC64_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
let mut cache = cache.lock().unwrap();

let key: Crc64Key = (
params.poly,
params.init,
params.refin,
params.refout,
params.xorout,
params.check,
);

let static_algorithm = cache.entry(key).or_insert_with(|| {
let algorithm = Algorithm {
width: params.width,
poly: params.poly,
init: params.init,
refin: params.refin,
refout: params.refout,
xorout: params.xorout,
check: params.check,
residue: 0x0000000000000000,
};
Box::leak(Box::new(algorithm))
});

crc::Crc::<u64, Table<16>>::new(static_algorithm)
}
Expand Down
15 changes: 14 additions & 1 deletion src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1029,14 +1029,27 @@ mod tests {
);
}

/// Non-stress version of the stress test to allow Miri to evaluate without timing out.
#[test]
fn test_cache_memory_allocation_non_stress() {
cache_memory_allocation_stress(2);
}

/// Skipping for Miri runs due to time constraints, underlying code already covered by other
/// tests.
#[test]
#[cfg_attr(miri, ignore)]
fn test_cache_memory_allocation_stress() {
cache_memory_allocation_stress(1000);
}

fn cache_memory_allocation_stress(count: i32) {
clear_cache();

// Test cache behavior under memory allocation stress
// Create a large number of unique cache entries to stress memory allocation
let mut created_entries = Vec::new();
let stress_count = 1000;
let stress_count = count;

// Create many unique cache entries
for i in 0..stress_count {
Expand Down
50 changes: 31 additions & 19 deletions src/crc32/fusion/x86/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,32 +199,40 @@ mod tests {

#[test]
fn test_crc32_iscsi_check() {
assert_eq!(
crc32_iscsi(0xffffffff, TEST_CHECK_STRING) ^ 0xffffffff,
0xe3069283
);
if is_x86_feature_detected!("sse4.2") && is_x86_feature_detected!("pclmulqdq") {
assert_eq!(
crc32_iscsi(0xffffffff, TEST_CHECK_STRING) ^ 0xffffffff,
0xe3069283
);
}
}

#[test]
fn test_crc32_iscsi_small_all_lengths() {
for len in 1..=255 {
test_crc32_iscsi_random(len);
if is_x86_feature_detected!("sse4.2") && is_x86_feature_detected!("pclmulqdq") {
for len in 1..=255 {
test_crc32_iscsi_random(len);
}
}
}

#[test]
fn test_crc32_iscsi_medium_lengths() {
// Test each length from 256 to 1024, which should fold and include handling remainders
for len in 256..=1024 {
test_crc32_iscsi_random(len);
if is_x86_feature_detected!("sse4.2") && is_x86_feature_detected!("pclmulqdq") {
// Test each length from 256 to 1024, which should fold and include handling remainders
for len in 256..=1024 {
test_crc32_iscsi_random(len);
}
}
}

#[test]
fn test_crc32_iscsi_large_lengths() {
// Test 1 MiB just before, at, and just after the folding boundaries
for len in 1048575..1048577 {
test_crc32_iscsi_random(len);
if is_x86_feature_detected!("sse4.2") && is_x86_feature_detected!("pclmulqdq") {
// Test 1 MiB just before, at, and just after the folding boundaries
for len in 1048575..1048577 {
test_crc32_iscsi_random(len);
}
}
}

Expand All @@ -235,7 +243,9 @@ mod tests {

let checksum = RUST_CRC32_ISCSI.checksum(&data);

assert_eq!(crc32_iscsi(0xffffffff, &data) ^ 0xffffffff, checksum);
if is_x86_feature_detected!("sse4.2") && is_x86_feature_detected!("pclmulqdq") {
assert_eq!(crc32_iscsi(0xffffffff, &data) ^ 0xffffffff, checksum);
}

unsafe {
#[cfg(target_arch = "x86_64")]
Expand Down Expand Up @@ -277,13 +287,15 @@ mod tests {

let checksum = RUST_CRC32_ISCSI.checksum(&data);

assert_eq!(crc32_iscsi(0xffffffff, &data) ^ 0xffffffff, checksum);
if is_x86_feature_detected!("sse4.2") && is_x86_feature_detected!("pclmulqdq") {
assert_eq!(crc32_iscsi(0xffffffff, &data) ^ 0xffffffff, checksum);

unsafe {
assert_eq!(
crc32_iscsi_sse_v4s3x3(0xffffffff, data.as_ptr(), data.len()) ^ 0xffffffff,
checksum
);
unsafe {
assert_eq!(
crc32_iscsi_sse_v4s3x3(0xffffffff, data.as_ptr(), data.len()) ^ 0xffffffff,
checksum
);
}
}
}
}
Loading
Loading