Skip to content
Merged
27 changes: 13 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,16 @@ 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
# These three used to be reachable here only with `--no-default-features`,
# because their default `simd` feature pulled in `portable_simd` and so
# needed nightly. There is no SIMD in the tree any more and no feature
# gating it, so this now runs what ships.
- 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
# `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
4 changes: 1 addition & 3 deletions 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 @@ -161,15 +161,13 @@ tracing-pii = ["wacore/tracing-pii", "wacore-binary/tracing-pii"]
danger-skip-tls-verify = ["whatsapp-rust-tokio-transport?/danger-skip-tls-verify"]
danger-skip-cert-chain-verify = ["wacore/danger-skip-cert-chain-verify"]
default = [
"simd",
"sqlite-storage",
Comment thread
jlucaso1 marked this conversation as resolved.
"tokio-transport",
"tokio-runtime",
"ureq-client",
"tokio-native",
"signal",
]
simd = ["wacore/simd"]
ureq-client = ["dep:whatsapp-rust-ureq-http-client"]
tokio-transport = ["dep:whatsapp-rust-tokio-transport"]
tokio-runtime = ["dep:tokio"]
Expand Down
1 change: 0 additions & 1 deletion tests/bench-integration/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ env_logger = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "sync", "time"] }
whatsapp-rust = { path = "../..", default-features = false, features = [
"danger-skip-tls-verify",
"simd",
"tokio-runtime",
"tokio-native",
"signal",
Expand Down
1 change: 0 additions & 1 deletion tests/e2e/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ wacore-binary = { path = "../../wacore/binary" }
whatsapp-rust = { path = "../..", default-features = false, features = [
"danger-skip-cert-chain-verify",
"danger-skip-tls-verify",
"simd",
"tokio-runtime",
"tokio-native",
"signal",
Expand Down
2 changes: 0 additions & 2 deletions wacore/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ autobenches = false
ignored = ["getrandom"]

[features]
default = ["simd"]
simd = ["wacore-appstate/simd"]
debug-snapshots = []
# Typed interop with the decoded libsignal SessionRecord v1 model.
legacy-session-interop = ["wacore-libsignal/legacy-session-interop"]
Expand Down
4 changes: 0 additions & 4 deletions wacore/appstate/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ description = "Appstate for WhatsApp protocol"
[lib]
crate-type = ["rlib"]

[features]
default = ["simd"]
simd = ["wacore-binary/simd"]

[dependencies]
anyhow = { workspace = true }
buffa = { 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
136 changes: 69 additions & 67 deletions wacore/appstate/src/lthash.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
#[cfg(feature = "simd")]
use core::simd::u16x8;
use hkdf::Hkdf;
use hmac::digest::KeyInit;
use hmac::{Hmac, Mac};
Expand Down Expand Up @@ -57,59 +55,19 @@ impl LTHash {
}
}

/// Deliberately scalar. A hand-vectorised version of this loop lived here
/// until it was measured: over an 812-MAC batch it moved the total by 0.28%,
/// because HKDF above it dominates and LLVM already auto-vectorizes this loop
/// about as well as the intrinsics did.
fn perform_pointwise_with_overflow(base: &mut [u8], input: &[u8], subtract: bool) {
assert_eq!(base.len(), input.len(), "length mismatch");
// Use `% 2` instead of `.is_multiple_of(2)` for stable Rust compatibility.
#[allow(clippy::manual_is_multiple_of)]
{
assert!(base.len() % 2 == 0, "slice lengths must be even");
}

#[allow(unused_mut, unused_assignments)]
let (mut base_remaining, mut input_remaining): (&mut [u8], &[u8]) = (base, input);
assert!(base.len().is_multiple_of(2), "slice lengths must be even");

// WA Web treats the accumulator as little-endian u16 lanes
// (`new DataView(...).getUint16(off, true)` in WA/Crypto/LtHash.js).
// Snapshot/patch MACs are HMACs over the accumulator bytes, so the lane
// endianness is part of the wire spec.
#[cfg(feature = "simd")]
{
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();
}
}

base_remaining = base_rem;
input_remaining = input_rem;
}

for (base_pair, input_pair) in base_remaining
.chunks_exact_mut(2)
.zip(input_remaining.chunks_exact(2))
{
for (base_pair, input_pair) in base.chunks_exact_mut(2).zip(input.chunks_exact(2)) {
let x = u16::from_le_bytes([base_pair[0], base_pair[1]]);
let y = u16::from_le_bytes([input_pair[0], input_pair[1]]);

Expand All @@ -118,9 +76,7 @@ fn perform_pointwise_with_overflow(base: &mut [u8], input: &[u8], subtract: bool
} else {
x.wrapping_add(y)
};
let bytes = result.to_le_bytes();
base_pair[0] = bytes[0];
base_pair[1] = bytes[1];
base_pair.copy_from_slice(&result.to_le_bytes());
}
}

Expand Down Expand Up @@ -170,27 +126,73 @@ 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}");
}
}

/// Reference for the test below. It reaches the same answer by a
/// different route than the implementation: lanes are assembled by hand
/// from byte positions and the arithmetic is done in `u32` and masked, so
/// it shares neither `from_le_bytes` nor `wrapping_*` with the code under
/// test. A reference that mirrors the implementation proves nothing.
fn reference_pointwise(base: &mut [u8], input: &[u8], subtract: bool) {
for i in (0..base.len()).step_by(2) {
let x = base[i] as u32 | ((base[i + 1] as u32) << 8);
let y = input[i] as u32 | ((input[i + 1] as u32) << 8);
let r = if subtract {
x.wrapping_sub(y) & 0xFFFF
} else {
(x + y) & 0xFFFF
};
base[i] = (r & 0xFF) as u8;
base[i + 1] = (r >> 8) as u8;
}
}

/// Sizes straddle the 16-byte boundary a vectorised implementation would
/// chunk on, so the coverage still holds if one ever comes back. Inputs
/// are seeded onto the wrap boundaries in both directions, which is where
/// a lane-width or endianness mistake shows up rather than in round data.
#[test]
fn pointwise_matches_independent_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