Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
35 changes: 21 additions & 14 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,8 @@ jobs:
df -h /
sudo rm -rf --one-file-system /usr/local/lib/android
df -h /
# `simd` needs nightly, so the matrix runs on the pinned toolchain.
# Pinned toolchain: .cargo/config.toml carries nightly-only rustflags
# (-Zshare-generics) for this target, and this job does not clear them.
- uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2026-06-16
Expand Down Expand Up @@ -251,7 +252,7 @@ jobs:
-p wacore-libsignal -p wacore-noise -p waproto

test-stable:
name: Test Stable (no-simd)
name: Test Stable (MSRV)
runs-on: ubuntu-latest
# .cargo/config.toml sets nightly-only rustflags (-Zshare-generics) for the
# x86_64-linux target; a set-but-empty RUSTFLAGS takes precedence over
Expand Down Expand Up @@ -279,18 +280,24 @@ jobs:
uses: Swatinem/rust-cache@v2
with:
cache-targets: "true"
- name: Build wacore-binary (stable, no SIMD)
run: cargo build -p wacore-binary --no-default-features --verbose
- name: Test wacore-binary (stable, no SIMD)
run: cargo nextest run --profile ci -p wacore-binary --no-default-features --lib
- name: Build wacore-appstate (stable, no SIMD)
run: cargo build -p wacore-appstate --no-default-features --verbose
- name: Test wacore-appstate (stable, no SIMD)
run: cargo nextest run --profile ci -p wacore-appstate --no-default-features --lib
- name: Build wacore (stable, no SIMD)
run: cargo build -p wacore --no-default-features --verbose
- name: Test wacore (stable, no SIMD)
run: cargo nextest run --profile ci -p wacore --no-default-features --lib
# Default features, which include `simd`. That combination used to be
# nightly-only, so this job could only ever run the scalar half; both
# halves are stable now and the SIMD one is what ships, so it is the one
# worth testing here.
- name: Test wacore-binary (stable)
run: cargo nextest run --profile ci -p wacore-binary --lib
- name: Test wacore-appstate (stable)
run: cargo nextest run --profile ci -p wacore-appstate --lib
- name: Test wacore (stable)
run: cargo nextest run --profile ci -p wacore --lib
# `--no-default-features` turns the LTHash lane math back to scalar. It
# is the configuration wasm and ESP32 build, so it stays covered; a
# compile is enough, since the scalar path is what every test above
# already compares against.
- name: Build the scalar configuration (stable)
run: >
cargo build --verbose --no-default-features
-p wacore-binary -p wacore-appstate -p wacore
# `rust-version` is published metadata for every member, but only the
# three crates above are exercised at that toolchain. This compiles the
# rest of the publishable set so the declared floor is a checked promise
Expand Down
13 changes: 6 additions & 7 deletions .github/workflows/miri.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,14 @@ jobs:
# inflate's uninitialized spare capacity in `zlib_pool`. Both are
# invisible to clippy and to native tests — nothing observes the
# aliasing violation or the uninit read until it miscompiles.
#
# One leg, not two: the crate used to carry portable-SIMD scanners in
# the decoder/encoder alongside scalar fallbacks, so `--no-default-
# features` reached a genuinely different code path. The packed codec
# is table-driven scalar now and there is only one path to check.
- name: wacore-binary
cache-key: binary-simd
cache-key: binary
args: -p wacore-binary --lib
# The portable-SIMD scanners in the decoder/encoder and their scalar
# fallbacks are separate code paths, and `--no-default-features` is the
# only way to reach the latter.
- name: wacore-binary (no simd)
cache-key: binary-scalar
args: -p wacore-binary --no-default-features --lib
# No `unsafe` of its own, but it drives wacore-binary's zero-copy
# decode over real Noise frames and pulls the crypto stack
# (aes/sha2/curve25519), whose unsafe backends this exercises.
Expand Down
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ default-members = [
]

# Minimum supported Rust version. Floor is the stable release CI's
# "Test Stable (no-simd)" job pins; raise both together (that job's
# "Test Stable (MSRV)" job pins; raise both together (that job's
# `toolchain:` and this value) when a dependency or language feature needs it.
[workspace.package]
rust-version = "1.94"
Expand Down Expand Up @@ -95,6 +95,10 @@ diesel_migrations = { version = "2.3.2", default-features = false, features = ["
divan = { package = "codspeed-divan-compat", version = "5.0.1" }
env_logger = { version = "0.11", default-features = false }
event-listener = { version = "5", default-features = false }
# `std` (default) rather than the `libm` path: wacore-appstate is already
# std-only (LazyLock), and libm would add a dependency to serve float ops
# this crate never touches.
fearless_simd = { version = "0.6.0" }
flate2 = { version = "1.1.9", default-features = false, features = ["zlib-rs"] }
futures = { version = "0.3", default-features = false, features = ["alloc", "async-await"] }
getrandom = { version = "0.4", default-features = false }
Expand Down
4 changes: 4 additions & 0 deletions wacore/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ ignored = ["getrandom"]

[features]
default = ["simd"]
# appstate's LTHash is the only SIMD left in the tree; wacore-binary's packed
# codec is table-driven scalar, which measured faster than the vectors it
# replaced. This runs on fearless_simd, so the feature no longer implies
# nightly.
simd = ["wacore-appstate/simd"]
debug-snapshots = []
# Typed interop with the decoded libsignal SessionRecord v1 model.
Expand Down
5 changes: 4 additions & 1 deletion wacore/appstate/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ crate-type = ["rlib"]

[features]
default = ["simd"]
simd = ["wacore-binary/simd"]
# LTHash lane math via fearless_simd: runtime level detection on stable, so
# this feature does not drag the crate onto nightly.
simd = ["dep:fearless_simd"]

[dependencies]
anyhow = { workspace = true }
buffa = { workspace = true }
fearless_simd = { workspace = true, optional = true }
hex = { workspace = true }
hkdf = { workspace = true }
hmac = { workspace = true }
Expand Down
1 change: 0 additions & 1 deletion wacore/appstate/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#![cfg_attr(feature = "simd", feature(portable_simd))]
pub mod decode;
pub mod encode;
pub mod errors;
Expand Down
149 changes: 108 additions & 41 deletions wacore/appstate/src/lthash.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#[cfg(feature = "simd")]
use core::simd::u16x8;
use fearless_simd::{Level, Simd, SimdBase, dispatch, u16x8};
use hkdf::Hkdf;
use hmac::digest::KeyInit;
use hmac::{Hmac, Mac};
Expand All @@ -12,6 +12,12 @@ use std::sync::LazyLock;
static EXTRACT_HMAC: LazyLock<Hmac<Sha256>> =
LazyLock::new(|| Hmac::<Sha256>::new_from_slice(&[0u8; 32]).expect("32-byte HMAC key"));

/// Probing CPU features is comparatively expensive and the answer cannot
/// change while the process runs, so the level is resolved once here:
/// `multiple_op` dispatches once per operand, and a patch carries hundreds.
#[cfg(feature = "simd")]
static SIMD_LEVEL: LazyLock<Level> = LazyLock::new(Level::new);

#[derive(Clone, Debug)]
pub struct LTHash {
pub hkdf_info: &'static [u8],
Expand Down Expand Up @@ -77,30 +83,7 @@ fn perform_pointwise_with_overflow(base: &mut [u8], input: &[u8], subtract: bool
let (base_chunks, base_rem) = base_remaining.as_chunks_mut::<16>();
let (input_chunks, input_rem) = input_remaining.as_chunks::<16>();

for (base_chunk, input_chunk) in base_chunks.iter_mut().zip(input_chunks) {
// `from_le_bytes` per lane states the wire endianness directly, so
// the same code is correct on either host; on little-endian it
// lowers to the plain 16-byte load a transmute would have emitted.
let base_arr: [u16; 8] = core::array::from_fn(|i| {
u16::from_le_bytes([base_chunk[2 * i], base_chunk[2 * i + 1]])
});
let input_arr: [u16; 8] = core::array::from_fn(|i| {
u16::from_le_bytes([input_chunk[2 * i], input_chunk[2 * i + 1]])
});
let base_simd = u16x8::from_array(base_arr);
let input_simd = u16x8::from_array(input_arr);

let result_simd = if subtract {
base_simd - input_simd
} else {
base_simd + input_simd
};

let out = result_simd.to_array();
for (base_pair, lane) in base_chunk.as_chunks_mut::<2>().0.iter_mut().zip(out) {
*base_pair = lane.to_le_bytes();
}
}
dispatch!(*SIMD_LEVEL, simd => pointwise_chunks(simd, base_chunks, input_chunks, subtract));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated

base_remaining = base_rem;
input_remaining = input_rem;
Expand All @@ -124,6 +107,45 @@ fn perform_pointwise_with_overflow(base: &mut [u8], input: &[u8], subtract: bool
}
}

/// The lane math, generic over the SIMD level `dispatch!` picked at runtime.
/// `#[inline(always)]` is load-bearing rather than a hint: it is what makes
/// each instantiation inherit its level's `target_feature` set, so the AVX2
/// copy lowers to AVX2 instead of to the baseline the caller was compiled for.
#[cfg(feature = "simd")]
#[inline(always)]
fn pointwise_chunks<S: Simd>(
simd: S,
base_chunks: &mut [[u8; 16]],
input_chunks: &[[u8; 16]],
subtract: bool,
) {
for (base_chunk, input_chunk) in base_chunks.iter_mut().zip(input_chunks) {
// `from_le_bytes` per lane states the wire endianness directly, so
// the same code is correct on either host; on little-endian it
// lowers to the plain 16-byte load a transmute would have emitted.
let base_arr: [u16; 8] = core::array::from_fn(|i| {
u16::from_le_bytes([base_chunk[2 * i], base_chunk[2 * i + 1]])
});
let input_arr: [u16; 8] = core::array::from_fn(|i| {
u16::from_le_bytes([input_chunk[2 * i], input_chunk[2 * i + 1]])
});
let base_simd = u16x8::from_slice(simd, &base_arr);
let input_simd = u16x8::from_slice(simd, &input_arr);

let result_simd = if subtract {
base_simd - input_simd
} else {
base_simd + input_simd
};

let mut out = [0u16; 8];
result_simd.store_slice(&mut out);
for (base_pair, lane) in base_chunk.as_chunks_mut::<2>().0.iter_mut().zip(out) {
*base_pair = lane.to_le_bytes();
}
}
}

fn hkdf_sha256_into(key: &[u8], info: &[u8], out: &mut [u8]) {
let mut extract = EXTRACT_HMAC.clone();
extract.update(key);
Expand Down Expand Up @@ -170,27 +192,72 @@ mod tests {
}

#[test]
fn test_simd_determinism_and_consistency() {
fn add_then_subtract_returns_to_zero_across_sizes() {
let test_sizes = [2, 4, 8, 16, 18, 32, 64, 128, 256];

for &size in &test_sizes {
let mut base_simd = vec![0u8; size];
let mut base_scalar = vec![0u8; size];
let mut base = vec![0u8; size];
let input = vec![1u8; size];

perform_pointwise_with_overflow(&mut base_simd, &input, false);
perform_pointwise_with_overflow(&mut base_scalar, &input, false);
assert_eq!(base_simd, base_scalar, "Add failed for size {}", size);

perform_pointwise_with_overflow(&mut base_simd, &input, true);
perform_pointwise_with_overflow(&mut base_scalar, &input, true);
assert_eq!(base_simd, base_scalar, "Subtract failed for size {}", size);
assert_eq!(
base_simd,
vec![0u8; size],
"Subtract result incorrect for size {}",
size
);
perform_pointwise_with_overflow(&mut base, &input, false);
perform_pointwise_with_overflow(&mut base, &input, true);
assert_eq!(base, vec![0u8; size], "size {size}");
}
}

/// Straight-line reference: no chunking, no dispatch, no feature gate. The
/// point is to be obviously correct rather than fast, so that the test
/// below is an independent check on the SIMD path rather than a
/// comparison of that path against itself.
fn reference_pointwise(base: &mut [u8], input: &[u8], subtract: bool) {
for (b, i) in base.chunks_exact_mut(2).zip(input.chunks_exact(2)) {
let x = u16::from_le_bytes([b[0], b[1]]);
let y = u16::from_le_bytes([i[0], i[1]]);
let r = if subtract {
x.wrapping_sub(y)
} else {
x.wrapping_add(y)
};
b.copy_from_slice(&r.to_le_bytes());
}
}

/// The SIMD path splits into 16-byte chunks and leaves a scalar tail, so
/// the sizes below straddle that boundary: under one chunk, exactly one,
/// chunk-plus-tail, and several chunks. Inputs are seeded to cover the
/// wrap boundaries in both directions, which is where a lane-width or
/// endianness mistake would show up rather than in round-number data.
#[test]
fn simd_path_matches_independent_scalar_reference() {
let sizes = [0usize, 2, 14, 16, 18, 32, 34, 128, 130, 256];
// Deterministic LCG: reproducible failures, no dev-dependency.
let mut seed = 0x2545_F491u32;
let mut next = move || {
seed = seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
(seed >> 24) as u8
};

for size in sizes {
for subtract in [false, true] {
for edge in [0u8, 0xFF, 0x01] {
let base: Vec<u8> = (0..size)
.map(|i| if i % 3 == 0 { edge } else { next() })
.collect();
let input: Vec<u8> = (0..size)
.map(|i| if i % 5 == 0 { edge } else { next() })
.collect();

let mut actual = base.clone();
let mut expected = base.clone();
perform_pointwise_with_overflow(&mut actual, &input, subtract);
reference_pointwise(&mut expected, &input, subtract);

assert_eq!(
actual, expected,
"size {size}, subtract {subtract}, edge {edge:#04x}"
);
}
}
}
}

Expand Down
2 changes: 0 additions & 2 deletions wacore/binary/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ description = "Binary data and constants for WhatsApp protocol"
crate-type = ["rlib"]

[features]
default = ["simd"]
simd = []
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
serde = ["dep:serde", "compact_str/serde", "smallvec/serde"]
Comment on lines 14 to 15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the published simd feature as an alias

Removing simd from this published crate's feature table breaks dependency resolution for downstream manifests that explicitly use wacore-binary = { ..., features = ["simd"] }, even though retaining it as an empty compatibility feature would have no runtime cost now that the SIMD implementation is gone. Keep a no-op simd = [] entry so existing consumers can upgrade without changing their manifests.

Useful? React with 👍 / 👎.

# Render raw phone numbers in `Jid::observe()` instead of the redacted `pn#<hash>`.
# Local debugging only; never enable in production.
Expand Down
Loading
Loading