Skip to content

test(fn-dsa): execute flr_emu.rs — the float backend shipped to wasm32/armv7 that nothing ever ran - #74

Merged
Nexlab-One merged 3 commits into
mainfrom
test/flr-emu-coverage
Jul 29, 2026
Merged

test(fn-dsa): execute flr_emu.rs — the float backend shipped to wasm32/armv7 that nothing ever ran#74
Nexlab-One merged 3 commits into
mainfrom
test/flr-emu-coverage

Conversation

@Nexlab-One

Copy link
Copy Markdown
Contributor

flr_emu.rs is the software binary64 backend FN-DSA signing ships to wasm32, armv7 and 32-bit x86. Until this PR, nothing anywhere executed a single line of it.

fn-dsa-sign/src/flr.rs picks the float backend by target_arch, independently of AVX2: flr_native.rs for x86_64/aarch64/arm64ec/riscv64, flr_emu.rs for everything else. So forcing --features no_avx2 on x86_64 does not reach it — that flag selects the portable vector path, while the float backend is chosen by architecture. It was compiled by the wasm/armv7 cargo check gates and executed by nothing, on any machine, since import.

Falcon/FN-DSA signing is sensitive to floating-point behaviour, so a defect here would plausibly yield wrong or non-verifying signatures rather than a clean failure.

What this adds

fn-dsa-sign/src/flr_emu_diff.rs — a bit-exact differential of the emulated backend against the native one, comparing through encode() (raw binary64 bit patterns, so +0.0 and -0.0 are distinguished) across the whole pub(crate) surface: constants, conversions, rounding to integer, rounding ties, multiplication ties, expm_p63, and pseudo-random ops with a fixed seed.

Plus a fn-dsa-emulated-float CI job so it actually runs, wired into final-validation.

Result: flr_emu.rs agrees with flr_native.rs bit-for-bit on everything tested, except a small set of signed-zero cases which are explicitly pinned rather than papered over. That is a good outcome — the backend was correct, and is now guarded.

Two defects found in review and fixed here

The new test was red under the crate's own features. fn-dsa-sign declares div_emu and sqrt_emu, which are not vector flags — they swap flr_native's set_div and sqrt for integer emulations that reproduce flr_emu's signed-zero behaviour. The first draft hardcoded the opposite for the native side: 18 passed; 1 failed under either flag. Now 19 passed; 0 failed under all four combinations (none / div_emu / sqrt_emu / both).

A false claim in a production comment — the same defect class this PR exists to fix. The comment said "no CI row sets either feature today". False: ci.yml:76 runs cargo clippy --all-targets --all-features unscoped at the workspace root, which enables both. The accurate statement, now in the file, is that CI compile-checks these features via that clippy pass but never executes tests under them.

Verified

All four feature combinations 19 passed; 0 failed. Mutation-checked: defects injected into flr_emu.rs are caught. Neither flr_emu.rs nor flr_native.rs is modified — production dispatch is untouched, x86_64 still selects flr_native in every non-test build.

Not checked

  • Real wasm32/armv7/32-bit-x86 hardware. This proves flr_emu matches flr_native on x86_64; it does not prove behaviour on the targets that actually ship it. A qemu/wasm execution row remains the right follow-up.
  • div_emu/sqrt_emu are still only compile-checked by CI, never run. This PR does not add a row that executes them.

…m/armv7

WHY
---
`fn-dsa-sign/src/flr.rs` picks the floating-point backend by `target_arch`
alone: flr_native.rs on x86_64/aarch64/arm64ec/riscv64, flr_emu.rs (software
binary64) on everything else -- wasm32, armv7, 32-bit x86. Nothing in this
repo executed flr_emu.rs. It is not a feature: `--features no_avx2` switches
the portable *vector* path and leaves the float backend native, so no x86_64
job could reach it however configured. The wasm rows are `cargo check` /
`wasm-pack build` only, and the aarch64/armv7 rows never run at all on PRs.

FN-DSA signing is decided bit-for-bit by these rounding rules, so a defect
here would have shipped as wrong or non-verifying signatures on wasm and
armv7 rather than as a clean failure. The correctness of that backend was
unknown in both directions. It is now measured, on both axes:

  1. flr_emu_diff.rs (new, test-only). flr.rs compiles flr_emu.rs a SECOND
     time under #[cfg(test)] on the native-backend arches, and the tests diff
     the two backends bit-for-bit -- via encode(), so +0.0 and -0.0 are
     distinguished -- over every operation of the Flr surface. This runs in
     the ordinary `cargo test --workspace` rows; no emulator required.
     Production dispatch is untouched: a non-test x86_64 build still compiles
     flr_native.rs and only flr_native.rs.
  2. A new `fn-dsa-emulated-float` CI job runs this crate's existing suite on
     wasm32-wasip1 under wasmtime. 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, and the part a host build cannot reach.
     The job asserts the build really compiled flr_emu.rs before trusting a
     green run, so it cannot decay into a silent no-op the way `self_test`
     did when has_avx2() sent it down the AVX2 branch.

RESULT: no defect. The emulated backend agrees with the native one on every
in-domain input tested.

WHAT THE DIFFERENTIAL FOUND
---------------------------
Four inputs, all involving a negative zero, on which the two backends do NOT
agree. Every one is inherited verbatim from upstream fn-dsa v0.3.0; neither
backend is modified here. They are PINNED, not fixed -- changing either side
would change signature bytes on some target, which is not a test's call:

  abs(-0.0)     emu +0.0, native -0.0  (emu is the IEEE-correct one; abs() is
                                        test-only in both backends)
  sqrt(-0.0)    emu +0.0, native -0.0  (native is IEEE-correct)
  floor(-0.0)   emu -1,   native 0     (deliberate upstream behaviour, see the
                                        comment in flr_emu.rs)
  (+/-0.0) / x  emu forces the quotient sign to +, native follows IEEE

Only floor() reaches production code (sampler.rs `let s = mu.floor()`); every
division in this crate is `Flr::ONE / x`, so a zero dividend cannot occur, and
abs() is called only from tests. Whether `mu` can be exactly -0.0 is NOT
established here and is left as an open question on the record.

Also pinned: expm_p63 with ccs == 1 exactly, where the implementations differ
because `Flr::ONE.mul2p63().trunc()` applies trunc() to 2^63, one past the top
of its own documented input range. A precondition violation, not a defect.

A NOTE ON THE MULTIPLICATION-TIE VECTORS
----------------------------------------
The first version of this test did not catch a one-bit change to the rounding
table inside `Flr::make()` (0xC8 -> 0x88, i.e. dropping the round-half-to-EVEN
"round up" case). That bit is reachable only through set_mul and set_div, and
a division can never produce an exact tie: with a = a0*2^u and b = b0*2^v, a0
and b0 odd, the quotient (a0/b0)*2^(u-v) has an odd denominator in lowest
terms, so it either terminates within 53 bits (exact, no rounding) or never
terminates (sticky = 1, not a tie). Multiplication is the only route, and
random operands essentially never land on an exact tie. The 16 hardcoded
operand pairs in `flr_emu_matches_native_multiplication_ties` were searched
for explicitly and cross-checked against an arbitrary-precision model; without
them that mutation left all 18 other tests in this crate green.

DETERMINISM: every randomized input is SHAKE256-derived from a hardcoded
seed. Nothing reads the clock or system entropy.

The two `#[allow]`s on the test-only `mod emu_ref` are deliberate and are
documented in flr.rs. They exist because flr_emu.rs raises two clippy
findings when compiled on x86_64 -- findings the repo's own
`cargo clippy --workspace --all-targets --all-features -- -D warnings` gate
has never surfaced, precisely because it never compiles that file. Silencing
them at the module declaration keeps flr_emu.rs byte-faithful to upstream.
…u/sqrt_emu

WHY
---
The differential added in 8e37de4 was RED under two of this crate's own
features. `cargo test -p lib-q-fn-dsa-sign --lib --features sqrt_emu`
panicked at flr_emu_diff.rs:529 (`bits_nat(Nat::NZERO.sqrt()) == NZ`), and
`--features div_emu` panicked at :562; `--all-features` failed at :529.
Both are regressions introduced by that commit -- flr_emu_diff.rs does not
exist on origin/main (b68ed13), so the test could not have been red before.

ROOT CAUSE
----------
The comment 8e37de4 put in flr.rs claimed the float backend "is *not*
influenced by any Cargo feature". That is true of module SELECTION -- which
of flr_native.rs / flr_emu.rs is compiled is `target_arch`-only -- and false
of the selected backend's BEHAVIOUR. `div_emu` swaps flr_native's
`self.0 /= other.0` for the integer `Flr::div_emu()`, and `sqrt_emu` swaps
its SSE2/NEON/RISC-V sqrt opcode for `Flr::sqrt_emu()`; both exist for
targets whose FPU divide/sqrt is not constant-time. Those two routines are
line-for-line ports of flr_emu.rs's own `set_div`/`sqrt`: `sqrt_emu` ends in
the same `make_z(0, ..)` that cannot emit a sign bit, and `div_emu` carries
the same `s &= dm` zero-dividend clamp. So under those features the native
side ADOPTS flr_emu's signed-zero behaviour and two of the four pinned
divergences vanish -- while the test hard-coded the opposite for the native
side. Believing that comment is what produced the bug, so the comment is
fixed too, in the same commit.

WHAT CHANGED
------------
flr.rs: the backend-selection comment now separates SELECTION (target_arch
only, unchanged) from BEHAVIOUR (div_emu/sqrt_emu, native side only), names
exactly what each feature swaps and what that does to signed zeros, and
warns that no CI row sets either feature so a native-side claim that only
holds without them will not be caught.

flr_emu_diff.rs: no assertion is deleted or cfg'd out. The two
feature-sensitive expectations become cfg-selected constants --
NAT_SQRT_NZERO (NZ without sqrt_emu, PZ with) and NAT_ZERO_DIV
([PZ,NZ,NZ,PZ] without div_emu, all-PZ with) -- so every pin still runs
under every feature combination, and under the emu features the test now
additionally asserts that the divergence really is GONE rather than merely
not looking. Cfg-ing the assertions away instead would have left
`--features div_emu,sqrt_emu` -- the configuration a constant-time-conscious
integrator actually ships -- with the signed-zero contract unchecked. The
assert messages now print the observed bits, the pinned bits and the feature
state, so a future failure names its own cause.

VERIFIED
--------
`cargo test -p lib-q-fn-dsa-sign --lib -- --test-threads=1` under no
features / div_emu / sqrt_emu / div_emu,sqrt_emu: `ok. 19 passed` each.
`cargo test -p lib-q-fn-dsa-sign -p lib-q-fn-dsa-comm --all-features --lib`:
`ok. 15 passed` + `ok. 6 passed` (was 14 passed; 1 failed).
`cargo clippy -p lib-q-fn-dsa-sign -p lib-q-fn-dsa-comm --all-targets
--all-features -- -D warnings` and `cargo fmt -p lib-q-fn-dsa-sign --
--check`: clean.

The differential still bites, checked by mutation and reverted after each:
  * flr_emu `make()` 0xC8 -> 0x88 (drop the round-half-to-EVEN round-up
    case): multiplication_ties FAILED under all four feature combinations,
    `emu=0x46802F73BAFEB827 native=0x46802F73BAFEB828`.
  * flr_emu `sqrt` `make_z(0, ..)` -> `make_z(self.0 >> 63, ..)`: pinned
    test FAILED, "emu sqrt(-0.0) != +0.0", with and without sqrt_emu.
  * flr_native `sqrt_emu` made to keep the sign bit: pinned test FAILED
    under --features sqrt_emu, "native sqrt(-0.0) = 0x8000000000000000,
    pinned 0x0000000000000000 (sqrt_emu = true)" -- i.e. the new
    feature-branch expectation is a live check, not a rubber stamp.
  * flr_native `div_emu` `s & dm` -> `s & u64::MAX` (drop the clamp):
    pinned test FAILED under --features div_emu, "native zero-dividend
    div #1 = 0x8000000000000000, pinned 0x0000000000000000 (div_emu = true)".

BLAST RADIUS of the original defect, stated honestly: no CI job sets either
feature (`grep -rn "div_emu\|sqrt_emu" .github/ scripts/` returns nothing)
and no crate forwards them (only fn-dsa-sign/Cargo.toml declares them), so
this was never CI-red. It was a latent trap for anyone building the crate
under its own constant-time divide/sqrt features.

NOT FIXED HERE: `cargo clippy -p lib-q-fn-dsa-sign --all-targets
--all-features` (that crate alone) still fails to compile at lib.rs:819,
`no SHAKE256x4 in shake` -- fn-dsa-comm's `shake256x4` feature is not
enabled by selecting only the dependent crate. Pre-existing, in a file this
commit does not touch, and owned by the fix/shake256x4-feature branch.
…iff.rs comments

The comments claimed no CI row sets div_emu/sqrt_emu, so a claim that only
holds without them "will not be caught by CI." That's false: ci.yml's
workspace-root `cargo clippy --all-targets --all-features` step is not
`-p`-scoped, so it does compile-check both features. The accurate nuance is
that no CI row actually *runs tests* under either feature -- clippy
type-checks but doesn't execute -- so a runtime/behavioral claim could
still slip through untested.
@github-actions

Copy link
Copy Markdown

🔍 Pull Request Summary

Generated: Wed Jul 29 12:54:27 UTC 2026

📋 Validation Results

  • Core Validation: success
  • Security Validation: success
  • Test Coverage: success
  • WASM Compatibility: success
  • Documentation: success

✅ Overall Status: PASSED

🔒 Security Checklist

  • No classical cryptographic algorithms
  • Only SHA-3 family hash functions
  • Constant-time operations
  • Proper memory zeroization
  • Input validation
  • Error handling

📝 Review Notes

Please review the security implications of this change carefully.
All cryptographic changes require security team review.


Automated validation passed! This PR is ready for review.

@github-actions

Copy link
Copy Markdown

🔒 Security Validation Report

Generated: Wed Jul 29 12:54:33 UTC 2026

📊 Summary

  • NIST compliance: success
  • Cryptographic validation: success
  • Constant-time operations: success
  • Memory safety: success
  • Dependency security: success
  • WASM security: success

✅ Overall Security Status: PASSED

All critical security validations passed successfully.

🔍 Details

This report covers:

  • NIST post-quantum algorithm compliance
  • Constant-time operation verification
  • Memory safety and zeroization checks
  • Dependency vulnerability scanning
  • WASM build artifact validation

📋 Next Steps

✅ Security validation passed. Code is ready for deployment.


Security validation passed! This code meets all security requirements.

@Nexlab-One
Nexlab-One merged commit 7fb0fcf into main Jul 29, 2026
159 checks passed
@Nexlab-One
Nexlab-One deleted the test/flr-emu-coverage branch July 29, 2026 13:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant