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
101 changes: 100 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,105 @@ jobs:
CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS: '--cfg getrandom_backend="wasm_js" -C panic=abort'
run: bash ./scripts/wasm-size-check.sh

# The ONLY job in this repo that EXECUTES `lib-q-fn-dsa/fn-dsa-sign/src/flr_emu.rs`
# -- the software binary64 backend that ships to wasm32 and armv7.
#
# flr.rs selects the float backend by `target_arch` alone, never by a feature, so no
# x86_64 job can reach it: `--features no_avx2` switches the portable *vector* path in
# poly.rs/sampler.rs and leaves the float backend on flr_native.rs. wasm32 selects
# flr_emu.rs *and* the generic variants of its four arch-gated helpers (lzcnt_nz,
# ursh, ulsh, irsh) -- the same combination armv7 ships.
#
# Complementary, not redundant: `flr_emu_diff.rs` compiles flr_emu.rs a second time
# under #[cfg(test)] on x86_64 and diffs it against the native backend operation by
# operation; that runs in the ordinary test-matrix rows and covers the arithmetic in
# far more detail than this job, but on a host it gets the *native* variants of those
# four helpers. Keep both.
#
# Deliberately NOT gated behind `if: github.event_name != 'pull_request'`. The
# aarch64/armv7 rows are PR-skipped, and being invisible on PRs is exactly how a
# portable-path FN-DSA defect survived two months of green CI.
fn-dsa-emulated-float:
name: FN-DSA emulated float backend (wasm32 execution)
needs: core-validation
runs-on: ubuntu-latest
timeout-minutes: 20
env:
CARGO_TARGET_WASM32_WASIP1_RUNNER: wasmtime
steps:
- uses: actions/checkout@v7

# Read the channel out of rust-toolchain.toml rather than repeating it, so this
# job cannot silently drift off the pinned toolchain when that file is bumped.
- name: Resolve pinned toolchain
id: toolchain
run: |
set -euo pipefail
CHANNEL="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml)"
test -n "$CHANNEL"
echo "channel=$CHANNEL" >> "$GITHUB_OUTPUT"
echo "Using pinned toolchain: $CHANNEL"

- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ steps.toolchain.outputs.channel }}
targets: wasm32-wasip1

- name: Cache cargo
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-fn-dsa-wasip1-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-fn-dsa-wasip1-

# Pinned to an exact release rather than a third-party setup action, so this job
# cannot go red for reasons unrelated to FN-DSA.
- name: Install wasmtime (pinned)
env:
WASMTIME_VERSION: v37.0.0
run: |
set -euo pipefail
curl -sSLf -o /tmp/wasmtime.tar.xz \
"https://github.com/bytecodealliance/wasmtime/releases/download/${WASMTIME_VERSION}/wasmtime-${WASMTIME_VERSION}-x86_64-linux.tar.xz"
mkdir -p /tmp/wasmtime
tar -xJf /tmp/wasmtime.tar.xz -C /tmp/wasmtime --strip-components=1
echo "/tmp/wasmtime" >> "$GITHUB_PATH"

- name: Show wasmtime version
run: wasmtime --version

# A green run here is only meaningful if the wasm32 build actually compiled the
# emulated backend. If wasm32 were ever added to flr.rs's native-arch list this
# job would otherwise keep passing while testing nothing at all -- the same
# silent-no-op failure mode that hid the AVX2 dispatch trap in `self_test`.
- name: Assert the wasm32 build selects flr_emu.rs
run: |
set -euo pipefail
cargo test --release --target wasm32-wasip1 -p lib-q-fn-dsa-sign --no-run
DEPFILES=(target/wasm32-wasip1/release/deps/fn_dsa_sign-*.d)
if ! grep -qh 'flr_emu\.rs' "${DEPFILES[@]}"; then
echo "::error::wasm32-wasip1 build did not compile flr_emu.rs; this job is no longer covering the emulated float backend"
exit 1
fi
if grep -qh 'flr_native\.rs' "${DEPFILES[@]}"; then
echo "::error::wasm32-wasip1 build compiled flr_native.rs; backend dispatch changed"
exit 1
fi
echo "OK: wasm32-wasip1 build compiled flr_emu.rs and not flr_native.rs"

# `flr::tests::test_spec` is upstream's golden-digest sweep over every Flr
# operation; `tests::sign_512` is a byte-exact FN-DSA-512 signature KAT. Both are
# existing tests -- they have simply never run on a target that picks flr_emu.
- name: Run fn-dsa-sign suite on wasm32-wasip1
run: |
set -euo pipefail
cargo test --release --target wasm32-wasip1 -p lib-q-fn-dsa-sign
cargo test --release --target wasm32-wasip1 -p lib-q-fn-dsa-sign --features small_context

# Execute wasm-bindgen smoke tests added for research/zkp crates.
wasm-bindgen-smoke:
name: WASM bindgen smoke tests
Expand Down Expand Up @@ -1601,7 +1700,7 @@ jobs:
# Final validation - runs after all other jobs
final-validation:
name: Final Validation
needs: [publish-readiness, test-matrix, wasm-validation, wasm-workspace-gate, wasm-bindgen-smoke, romulus-no-std-wasm, zkp-recursive, cross-platform, bench-shards-validate, performance-benches, documentation, algorithm-tests, fn-dsa-no-avx2, simd-debug-tests, ml-dsa-compliance, ml-kem-tests, constant-time, integration-tests]
needs: [publish-readiness, test-matrix, wasm-validation, wasm-workspace-gate, wasm-bindgen-smoke, fn-dsa-emulated-float, romulus-no-std-wasm, zkp-recursive, cross-platform, bench-shards-validate, performance-benches, documentation, algorithm-tests, fn-dsa-no-avx2, simd-debug-tests, ml-dsa-compliance, ml-kem-tests, constant-time, integration-tests]
runs-on: ubuntu-latest
if: always()
steps:
Expand Down
93 changes: 93 additions & 0 deletions lib-q-fn-dsa/fn-dsa-sign/src/flr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,59 @@ use zeroize::DefaultIsZeroes;
// thus should take care not to leak information through
// side-channels, in particular timing.

// BACKEND SELECTION -- and its test-coverage consequences.
//
// Two different questions, which must not be conflated: WHICH backend module
// is compiled, and how the compiled one BEHAVES.
//
// SELECTION is by `target_arch` ALONE. No Cargo feature moves it; in
// particular `no_avx2` selects the portable *vector* path in
// poly.rs/sampler.rs and has no effect here.
//
// x86_64 / aarch64 / arm64ec / riscv64 -> flr_native.rs (hardware f64)
// everything else (wasm32, arm, x86, ...) -> flr_emu.rs (software f64)
//
// BEHAVIOUR of the selected backend IS feature-dependent, for two of the
// operations, and only on the native side:
//
// * `div_emu` replaces flr_native's `self.0 /= other.0` with the integer
// routine `Flr::div_emu()`;
// * `sqrt_emu` replaces flr_native's SSE2/NEON/RISC-V sqrt opcode with the
// integer routine `Flr::sqrt_emu()`.
//
// (Both exist for targets whose FPU divide/square-root is not constant-time;
// they trade the opcode for a data-independent bit-by-bit loop.) Those two
// routines are line-for-line ports of flr_emu.rs's own `set_div` and `sqrt`,
// so under those features flr_native inherits flr_emu's signed-zero
// behaviour -- which is NOT what the hardware opcodes do: `sqrt_emu`
// assembles its result through `make_z(0, ..)` and so cannot return -0.0,
// and `div_emu` clamps the quotient's sign bit to 0 for a zero dividend.
// Anything that asserts what "the native backend" returns must therefore
// say under which features it holds; see the pinned-divergence test in
// `flr_emu_diff.rs`, whose expectations for the native side move with these
// two flags. Note also that CI's workspace-root `--all-features` clippy
// pass (not `-p`-scoped, so it does enable these) compile-checks both
// flags, but no CI row actually runs tests under either one -- a runtime
// claim that only holds without them could still go untested.
//
// Consequence of SELECTION: on a normal x86_64 CI runner, `flr_emu.rs` is
// not compiled at all, so no amount of feature-toggling on that host
// executes it (`div_emu`/`sqrt_emu` change flr_native, they do not pull
// flr_emu in as the production backend). It is
// covered by two things, and if you delete either one the emulated backend
// goes dark again:
//
// 1. `flr_emu_diff.rs` (below, test-only): on the native-backend arches it
// compiles `flr_emu.rs` a SECOND time under `#[cfg(test)]` and diffs it
// against the native backend, bit for bit. This runs in the ordinary
// `cargo test --workspace` CI rows -- no emulator needed. Caveat: a
// host build of `flr_emu.rs` picks the native variants of its own four
// arch-gated helpers (`lzcnt_nz`, `ursh`, `ulsh`, `irsh`), so it covers
// the float algorithm but not those four generic fallbacks.
// 2. The `fn-dsa-emulated-float` CI job, which runs this crate's test suite
// on wasm32-wasip1 under wasmtime. wasm32 selects `flr_emu.rs` *and* the
// generic variants of those four helpers, i.e. exactly what wasm32 and
// armv7 ship.
#[cfg(any(
target_arch = "x86_64",
target_arch = "aarch64",
Expand All @@ -93,6 +146,46 @@ mod backend;

pub(crate) use backend::Flr;

// Test-only second compilation of the emulated backend, on the hosts where
// the *production* backend is the native one. This is what makes the
// differential test in `flr_emu_diff.rs` possible; production dispatch above
// is untouched.
//
// The two `allow`s are deliberate and must NOT be "fixed" inside
// `flr_emu.rs`. That file is a byte-faithful port of upstream fn-dsa v0.3.0;
// editing it to satisfy a host lint would create a needless delta against
// upstream in code that decides signature bytes. They are also, in
// themselves, a symptom of the gap this module closes: because `flr_emu.rs`
// is never compiled on the x86_64 host that runs `cargo clippy --workspace
// --all-targets --all-features -- -D warnings`, these two findings
// (`clippy::needless_range_loop` at flr_emu.rs:346, `clippy::let_and_return`
// at flr_emu.rs:764) have never once been surfaced by the repo's own lint
// gate. Compiling the file here is what makes them visible at all.
#[cfg(all(
test,
any(
target_arch = "x86_64",
target_arch = "aarch64",
target_arch = "arm64ec",
target_arch = "riscv64"
)
))]
#[path = "flr_emu.rs"]
#[allow(clippy::needless_range_loop, clippy::let_and_return)]
mod emu_ref;

#[cfg(all(
test,
any(
target_arch = "x86_64",
target_arch = "aarch64",
target_arch = "arm64ec",
target_arch = "riscv64"
)
))]
#[path = "flr_emu_diff.rs"]
mod emu_diff;

impl Default for Flr {
fn default() -> Self {
Flr::ZERO
Expand Down
Loading