Skip to content

crypto: vendor mlkem-native and build ML-KEM-1024 with runtime backend selection - #225

Open
djh58 wants to merge 20 commits into
djh58/pq184-2-foundationsfrom
djh58/pq184-3-library
Open

djh58 wants to merge 20 commits into
djh58/pq184-2-foundationsfrom
djh58/pq184-3-library

Conversation

@djh58

@djh58 djh58 commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

Summary

Part 3 of #184. Closes #190. Stacked on #224, the foundations.

This vendors mlkem-native v2.0.0 unmodified and builds ML-KEM-1024 into bitcoin_node behind a small C++ wrapper. Native code runs where it is proved, supported and keeps the binary's hardening; portable C runs everywhere else. Nothing in qbitd calls the library yet; the handshake is part 4.

  • D1. Vendoring. src/mlkem-native/ holds upstream's whole mlkem/ directory and LICENSE at d1b2fe782888bdb761a50336012923180be7f502, byte-identical.
    • contrib/devtools/mlkem-native.pin records the upstream tree and blob ids, and update-mlkem-native.sh reproduces the tree.
    • The offline lint test/lint/mlkem-native-check.sh fails on any drift.
    • doc/subtrees/mlkem-native.md is the runbook.
  • D2. CPU check. cpu_features::HasMlkemX86Native() is detected once and cached. It requires exactly the ISA extensions the vendored x86_64 assembly uses: AVX2 with OS YMM state, SSSE3, SSE4.1, POPCNT and BMI2. An instruction audit of the assembled object confirmed that list. A VM that exposes AVX2 but masks BMI2 or POPCNT now runs portable C instead of hitting SIGILL. Only the XGETBV primitive moved out of sha256.cpp.
  • SHA256 SSSE3 fix. SHA256AutoDetect now also requires SSSE3 for the SSE4, 4-way SSE4.1 and SHA-NI paths, which all execute pshufb. A CPU or VM that reports SSE4.1 without SSSE3 used to die with SIGILL in the startup self-test (reproduced under qemu Haswell,-ssse3 and EPYC,-ssse3). This is an upstream Bitcoin Core bug; the change is minimal so it can go upstream.
  • D3. Wrapper and backends.
    • src/crypto/mlkem.{h,cpp} has fixed-size types; secret outputs are cleared on every failure, and the invalid-public-key result is kept apart from internal failure.
    • qbit-owned config and backend headers select native code only on x86_64 (SysV ABI) and on AArch64 Mach-O (macOS).
    • AArch64 ELF builds (Linux) are portable-only. Upstream's AArch64 assembly has no BTI landing pads or GNU property note, so linking it would strip BTI, PAC and GCS marking from the whole binary. This applies the plan's "hardening wins" rule; revisit when upstream ships landing pads.
    • The cached capability hook and a portable override reach every native entry point. On AArch64 this needs a qbit-owned mirror of upstream's single-lane Keccak dispatch glue, because upstream's x1_scalar.h has no capability check. It calls the same upstream assembly.
    • The capability hooks read a per-thread copy of the portable override, taken when each operation starts, so a change on another thread can't mix backends.
    • Assembly flags keep any CMAKE_ASM_FLAGS, ASMFLAGS or toolchain values and add the C flags in front. On MinGW, the assembler flag that rewrites aligned vector moves no longer reaches the vendored assembly.
    • WITH_MLKEM_NATIVE=AUTO|ON|OFF and a truthful configure-summary line. MSan and MSVC builds are portable-only.
    • The library is a private dependency of bitcoin_node only. APPEND_CPPFLAGS/APPEND_CFLAGS reach its C and ASM compiles, as for secp256k1.
  • D5. Fuzz and bench. The mlkem and mlkem_backend_diff targets are registered in the required qbit corpus with seeds, honor MLKEM_FORCE_PORTABLE, and the forced-portable replay runs in CI. bench_qbit has one benchmark per backend.
  • C2. More CI jobs. s390x-unit (big-endian; tests run through ctest's emulator setting) and macos-arm64-unit (with WITH_MLKEM_NATIVE=ON) join ci-pq.yml. All PQ unit jobs now require mlkem_tests, bip324_tests and net_tests to have run and passed.
  • C3. Merge gate. PRs that touch the library, the transport, the CPU helper, their CMake (root, src/, tests, fuzz, bench), CI wiring, the classifier itself or vectors must pass aarch64-unit. A contract test fails if any CMake, workflow or ci/ file that mentions ML-KEM isn't covered. full-validation-gate accepts skipped only when the classifier says the job isn't required.

Testing

  • Built locally: test_qbit, bench_qbit and fuzz, with every commit built and tested on its own.
  • Ran focused unit or functional tests for the changed area:
    • mlkem_tests (NIST ACVP ML-KEM-1024 keyGen and encapDecap, C2SP CCTV modulus vectors, implicit rejection, wrapper lifetimes, backend selection), native and forced-portable with identical outputs; cpu_features_tests, crypto_tests, bip324_tests, net_tests;
    • an MSan unit run with CI's flags and instrumented libc++, clean after the build-flag fix;
    • masked-CPU runs under qemu (Haswell,-bmi2, -popcnt, -sse4.1, -ssse3) run portable with no SIGILL;
    • ci/checks/test_mlkem_build_policy.py: the AUTO/ON/OFF matrix across architectures, compilers and sanitizers, with cross builds for aarch64 and MinGW. It counts wrapped assembly calls and fails if any runs when portable is forced;
    • fuzz seed replay, forced-portable replay and a short mutation run (clang-21 libFuzzer with ASan and UBSan); about 175,000 more executions by a reviewer, with no crash.
  • Ran lint or formatting checks relevant to this change:
    • actionlint on every changed workflow;
    • the ci/checks contract tests, each new rule caught by a mutation;
    • the mlkem-native tree-id check, which prints GOOD offline;
    • the Rust and Python lints. The one failure is the existing feature_segwit.py mypy error, which comes from a newer local mypy.
  • Not run. Reason:

Two independent adversarial reviews, one on crypto and build and one on CI, accepted this with fixes, all folded in here.

  • Crypto and build: the vendored tree matched upstream, ACVP and CCTV vectors checked out, and a --wrap harness proved the portable override reaches every assembly entry point. It found the missing BMI2/POPCNT requirement (now fixed) and the MSan flag issue (now fixed).
  • CI: found the job-level runner.temp use that GitHub rejects (now fixed).

Target Branch

Risk / Review Notes

  • Consensus, script, crypto, wallet, P2P, release, CI, or security-sensitive behavior changed.

Notes:

  • Crypto library and build changes; no consensus change. The vendored tree must stay byte-identical, and the lint enforces it.
  • Every review and verifier finding is fixed in this PR. The build-policy test now runs every compiler case in CI (clang-21, aarch64 and MinGW cross, -m32), so none can be skipped.

Docs / Process Impact

  • I updated public docs because this PR changes user-visible behavior, integration guidance, release/process guidance, or expected validation: doc/subtrees/mlkem-native.md, doc/development/fuzzing.md and test/lint/README.md.

Closes #190 · Part of #184

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

@djh58
djh58 added this pull request to stack #226 October 5, 2026 18:41
@djh58
djh58 removed this pull request from stack #226 October 5, 2026 18:41
@djh58
djh58 added this pull request to stack #227 October 5, 2026 18:41
@djh58

djh58 commented Oct 5, 2026

Copy link
Copy Markdown
Member Author

@codex review

@djh58

djh58 commented Oct 5, 2026

Copy link
Copy Markdown
Member Author

bugbot run

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-06T17:13:37.650677Z f806f2e Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a99f786bcd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cmake/mlkem-native.cmake Outdated
Comment on lines +136 to +138
set(CMAKE_ASM_FLAGS "${CMAKE_C_FLAGS}")
foreach(_mlkem_native_config IN ITEMS DEBUG RELEASE RELWITHDEBINFO MINSIZEREL)
set(CMAKE_ASM_FLAGS_${_mlkem_native_config} "${CMAKE_C_FLAGS_${_mlkem_native_config}}")

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 caller-supplied assembler flags

When native assembly is enabled, these assignments overwrite the CMAKE_ASM_FLAGS and configuration-specific values initialized by CMake, including values explicitly supplied by a toolchain, the ASMFLAGS environment variable, or -DCMAKE_ASM_FLAGS=.... For example, configuring with -DCMAKE_ASM_FLAGS=-DUSER_ASM_FLAG leaves that value in the cache but omits it from the mlkem_native_asm.S compile command. This can discard required target, sysroot, or hardening flags and break or weaken affected builds; preserve the existing ASM flags while adding the desired C flags instead.

Useful? React with 👍 / 👎.

Comment thread ci/checks/classify_merge_profile.py Outdated
Comment on lines +60 to +62
PQ_AARCH64_FILES = frozenset(
{
".github/workflows/ci-pq.yml",

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 Include all ML-KEM wiring in the PQ gate

The allowlist omits several files that now control ML-KEM integration, including the root and src/ CMake files, src/util/CMakeLists.txt, .github/workflows/ci.yml, and the classifier itself. A later PR that changes only one of those files—for example, removing the library link from src/CMakeLists.txt or breaking the reusable-workflow call—will produce pq_aarch64_required=false, so pq-aarch64 is skipped and the full-validation gate explicitly accepts that skip. Add all ML-KEM build and gate wiring paths to this classification so the stated AArch64 requirement cannot be bypassed by an isolated wiring change.

Useful? React with 👍 / 👎.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit a99f786. Configure here.

@djh58 djh58 added the pq-transport Hybrid post-quantum v2 transport (#184) label Oct 5, 2026
@djh58
djh58 force-pushed the djh58/pq184-3-library branch from a99f786 to 2d56c6e Compare October 5, 2026 20:18
@djh58

djh58 commented Oct 5, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2d56c6e628

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/ci-pq.yml Outdated
name: macOS arm64 unit tests
needs: resolve-source
if: ${{ needs.resolve-source.outputs.run_macos_arm64_unit == 'true' }}
runs-on: macos-14

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 Move the PQ job off the retiring macOS 14 image

The newly added macos-arm64-unit job targets macos-14, but GitHub has scheduled macOS 14 runner brownouts beginning October 5, 2026 and full retirement on November 2, 2026. Because an empty/default ci-pq.yml dispatch selects this job, those PQ runs will intermittently fail now and will stop running entirely after retirement, independently of the code under test. Use a supported Apple Silicon label such as macos-15 or macos-26 and update the workflow contract assertion accordingly. See GitHub's macOS 14 retirement announcement.

Useful? React with 👍 / 👎.

@djh58

djh58 commented Oct 6, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f3a6f3548f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cmake/mlkem-native.cmake
if(MSVC)
set(_mlkem_native_arch "MSVC")
else()
mlkem_native_detect_arch(_mlkem_native_arch)

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 Include appended flags in architecture detection

When a build uses the supported APPEND_CFLAGS/APPEND_CPPFLAGS variables for target-changing options—for example -m32, paired with the analogous C++ flags—this probe runs without them and detects the host as x86_64, enabling QBIT_MLKEM_NATIVE. The actual ML-KEM C and assembly compilations receive the appended flags later, see an i386 target, and fail in mlkem_config.h because the selected native backend no longer matches. Run the architecture probes with the same effective appended flags as the target, or otherwise base the policy on the final compilation target.

Useful? React with 👍 / 👎.

@djh58

djh58 commented Oct 6, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: 27f0a2bd31

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@djh58
djh58 removed this pull request from stack #227 October 6, 2026 15:17
@djh58
djh58 added this pull request to stack #232 October 6, 2026 15:17
@djh58

djh58 commented Oct 6, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: f806f2e775

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

djh58 and others added 4 commits October 6, 2026 14:08
Vendor mlkem-native v2.0.0 (d1b2fe782888bdb761a50336012923180be7f502):
the whole upstream mlkem/ directory and LICENSE, byte for byte, in
src/mlkem-native. Nothing compiles it yet.

contrib/devtools/mlkem-native.pin records the commit, tag, the tree id
of mlkem/ and the LICENSE blob id. contrib/devtools/update-mlkem-native.sh
reproduces the tree from upstream and rewrites the pin from the fetched
object ids. test/lint/mlkem-native-check.sh, run by the subtree lint,
proves offline that HEAD still holds exactly the pinned tree. Lints that
scan src/ ignore the vendored tree, and doc/subtrees/mlkem-native.md is
the runbook.

Part of #184 (part 3, #190, D1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move the XGETBV instruction out of sha256.cpp into compat/cpuid.h as
XGetBV(), next to GetCPUID(). SHA256AutoDetect keeps its own selection
logic unchanged; it calls the shared primitive only.

Add compat/cpu_features.{h,cpp} with a standalone answer to "can this
CPU and OS run AVX2 code": the supported leaves are queried first, XCR0
is read only once leaf 1 reports OSXSAVE and AVX, and AVX2 is usable only
with XMM and YMM state enabled and the leaf 7 AVX2 bit set. Unlike the
SHA256 logic it does not depend on SSE4 or SHA-NI.

HasMlkemX86Native() adds the other extensions that mlkem-native's x86_64
assembly executes, found by auditing the assembled object: SSSE3,
SSE4.1, POPCNT and BMI2. A virtual machine can show AVX2 while hiding
those, and checking AVX2 alone would then end in SIGILL. Both checks
detect once and cache the result, because the ML-KEM capability hook
asks about a hundred times per operation. Non-x86 targets and compilers
without <cpuid.h> report false.

Part of #184 (part 3, #190, D2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SHA256AutoDetect selects the SSE4 code whenever CPUID leaf 1 reports
SSE4.1, and only under that check also the 4-way SSE4.1 code and the
SHA-NI code. All three execute SSSE3 instructions. An audit of the built
objects finds pshufb and palignr in the SSE4 and SHA-NI code and pshufb
in the 4-way code, besides SSE4.1's pinsrd, pextrd and pblendw; no SSE4.2
instruction occurs. A virtual machine can report SSE4.1 while masking
SSSE3, and the self-test then dies with SIGILL at startup.

Require SSSE3 as well. Under qemu-x86_64, -cpu Haswell,-ssse3 and the
SHA-NI model EPYC,-ssse3 die with SIGILL before this change and select
the standard implementation after it. Unmasked, Haswell still selects
sse4, sse41 and avx2, and EPYC and Denverton x86_shani.

This is an upstream Bitcoin Core bug, so the change is kept minimal
enough to go upstream.

Part of #184 (part 3, #190).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Build the vendored mlkem-native as ML-KEM-1024 into bitcoin_node only,
behind a small C++ wrapper. Nothing in qbitd calls it yet.

- src/crypto/mlkem_config.h configures the library (MLK_CONFIG_FILE):
  parameter set 1024, the qbit_mlkem1024 prefix, deterministic APIs only,
  zeroization through memory_cleanse, and a capability hook.
- Native backends are chosen by qbit-owned backend headers instead of
  upstream's compile-time defaults, which enable x86_64 only for AVX2
  compilers. No source is compiled with an ISA flag: every native entry
  point asks the hook, which reads only cached CPU detection and one
  atomic portable override. On x86_64 that detection requires every
  extension the assembly executes (AVX2, SSSE3, SSE4.1, POPCNT, BMI2),
  not only the AVX2 that upstream's capability name says.
- Upstream's AArch64 single-lane Keccak entry point skips the hook, so
  the override could not reach it. qbit mirrors that header's dispatch
  glue with the check added; every other entry point checks itself.
- mlkem::KeyGen, CheckPublicKey, Encaps and Decaps take fixed-size spans,
  map upstream's invalid-key results separately from internal failures,
  and clear their outputs on every failure. GetBackendNames() reports the
  implementation that runs. ForcePortableForTesting and
  InjectResultForTesting are test-only guards.
- cmake/mlkem-native.cmake adds WITH_MLKEM_NATIVE=AUTO|ON|OFF and the
  configure summary line. The library gets core_interface's hardening
  flags but not qbit's C++ warnings, then APPEND_CPPFLAGS and
  APPEND_CFLAGS, so the MSan jobs' -U_FORTIFY_SOURCE applies to it as to
  the other C libraries. MSVC builds without assembly, MSan
  builds portable C, and AArch64 ELF targets build portable C because
  the upstream assembly has no BTI landing pads or GNU property note and
  would strip BTI/PAC marking from the binary (doc/subtrees has the
  evidence and the revisit trigger). ON fails where native is impossible.

mlkem_tests covers NIST ACVP keygen, encapsulation, decapsulation and key
checks, all 1040 C2SP CCTV modulus vectors and the 3328/3329 boundary,
implicit rejection against an independent SHAKE256, injected failures,
moves and wiping, and backend selection, each on the native backend and
with portable C forced. ci/checks/test_mlkem_build_policy.py checks the
WITH_MLKEM_NATIVE matrix, builds the library with each compiler found
(core-checks requires the AArch64 cross compiler), and counts assembly
calls to show that the portable override reaches every x86_64 routine.

Part of #184 (part 3, #190, D3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
djh58 and others added 16 commits October 6, 2026 14:08
Add two fuzz targets over the mlkem:: wrapper:

- mlkem: honest key generation, encapsulation and decapsulation agree;
  an arbitrary encapsulation key is accepted exactly when an independent
  modulus check passes, and a rejected one leaves no output; any
  ciphertext of the right length decapsulates to a secret that differs
  from the honest one (implicit rejection is never proof of validity);
  an arbitrary decapsulation key is rejected exactly when an independent
  SHA3-256 hash check fails, and the secret is then cleared.
- mlkem_backend_diff: the detected backend and forced portable C give
  equal statuses and outputs for the same inputs, and the override is
  restored afterwards.

Both honor MLKEM_FORCE_PORTABLE=1. They join the required qbit corpus
with nine generated seeds each (CCTV modulus key and 3328/3329/4095
coefficients, NIST ACVP decapsulation vectors, a ciphertext bit flip).
CI replays them a second time with MLKEM_FORCE_PORTABLE=1 and includes
them in the mutation phase; the PQ CI contract test checks that wiring.
UBSan's -fsanitize=integer checks are suppressed for the vendored tree,
as for the other external libraries.

Part of #184 (part 3, #190, D5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Benchmark ML-KEM-1024 key generation, encapsulation and decapsulation
twice each: on the detected backend and with portable C forced. Like the
per-implementation SHA256 benchmarks, each is named after the backend
that ran, so on an AVX2 host bench_qbit -filter='MLKEM.*' lists both,
and the portable guard restores auto-detection afterwards.

Part of #184 (part 3, #190, D5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add two jobs to the PQ workflow, selectable by name like aarch64-unit:

- s390x-unit cross-compiles only test_bitcoin for big-endian s390x on
  the x64 runner, with Ubuntu's g++-s390x-linux-gnu and the depends tree,
  and runs the suites through ctest with qemu-s390x as the crosscompiling
  emulator, so only the tests run emulated. It stays out of the merge
  gate. Its timeout is 180 minutes until runs are measured.
- macos-arm64-unit runs natively on a GitHub-hosted macos-14 runner
  without Docker, asserts that uname -m is arm64, and shares the unit
  job's source checks and evidence report through YAML anchors. It is
  the job that runs native AArch64 ML-KEM, and configures with
  WITH_MLKEM_NATIVE=ON so that losing native code fails it.

All three jobs now expect mlkem_tests besides bip324_tests and net_tests
(CTEST_REGEX ^(mlkem_tests|bip324_tests|net_tests)$), and report the
suites through ctest_evidence.py. The aarch64 job's description says
that ML-KEM runs portable C on AArch64 Linux. The PQ CI contract test
covers selecting each job, the s390x cross and emulator settings, the
macOS architecture check and report, and that every expression uses a
context GitHub makes available there (no runner in job env, no matrix
in a job without one), which GitHub enforces when loading the file.

Part of #184 (part 3, #190, C2 and the CI part of D3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The merge-profile classifier gains a pq_aarch64_required output, true when
a change touches the ML-KEM library (src/mlkem-native, src/crypto/mlkem*,
cmake/mlkem-native.cmake), the v2 transport (src/bip324*, src/net.h,
src/net.cpp), the CPU feature checks, the PQ workflow or its env files,
or the PQ test suites and vectors. A path it cannot read also requires
the job.

Full Validation calls ci-pq.yml with jobs: aarch64-unit on the change
itself (a pull request's merge commit) when the output is true. The gate
accepts a skipped job only when the classifier says it is not required;
a required job that was skipped, failed or cancelled, or a missing or
unreadable classifier output, fails it. The changed-file lists that feed
the classifier, in Full Validation and in the Required Merge Gate, use
--no-renames, so moving a file out of a gated path still names it.

ci/checks/test_pq_merge_gate_contract.py runs the real classifier, job
condition, ci-pq selection step and gate script: a pull request touching
src/bip324.cpp requires and runs the job, while docs-only and other
source pull requests skip it and pass, and a rename out of a gated path
still requires it. The Required Merge Gate runs it with the other PQ CI
contracts.

Part of #184 (part 3, #190, C3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GitHub is retiring the macos-14 runner image: brownouts start on
2026-10-05 and the image is removed on 2026-11-02. Move the
macos-arm64-unit job to macos-15, which is also Apple Silicon.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
APPEND_CPPFLAGS and APPEND_CFLAGS reach mlkem_native's C and assembly
compiles, but the architecture probes ran without them. A build that
appends a target-changing flag such as -m32 was detected as x86_64,
enabled the native backend, and then failed in mlkem_config.h. Run the
probes with the appended flags, and probe afresh on every configure,
since those flags may change between configures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No pull request ever built or ran ML-KEM's native code on macOS: the
macos-arm64-unit job could only be dispatched, and nothing ran the x86_64
native code on an Intel Mac at all. Apple Silicon and Intel macOS are
release targets with native code enabled.

ci-pq.yml gains a macos-x86_64-unit job on GitHub's Intel image
(macos-15-intel), sharing every step with macos-arm64-unit. Both build
with WITH_MLKEM_NATIVE=ON, check the runner's architecture, and after the
suites pass require the native backend by name from
mlkem_tests/backend_selection, so a runner whose CPU lacks an extension
cannot pass on portable C.

Full Validation's PQ job, renamed pq-unit, now calls ci-pq.yml with
aarch64-unit, macos-arm64-unit and macos-x86_64-unit, so all three are
required exactly when the classifier says the change needs them. The
classifier output is renamed pq_unit_required to match. On-demand runs
accept the new job name; an empty jobs input runs it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The build-policy test counted assembly calls only for the x86_64 backend,
so nothing in CI showed that the portable override reaches every AArch64
assembly routine. With upstream's x1 Keccak glue in place of qbit's
mirror, 221 assembly calls ran with portable C forced, and every test
passed: outputs agree, and the backend name comes from the hook.

The counting harness now also runs on AArch64: a static binary built with
aarch64-linux-gnu-g++, from a copy of mlkem_config.h without the AArch64
ELF refusal, and run under qemu-aarch64. A negative control builds
upstream's x1 glue and requires the leaked calls to show.

Each entry point also runs alone on a new thread while portable C is
forced, and must make no assembly call: one that skipped BeginOperation()
would run on the new thread's stale copy of the override. The earlier
round trip could not show this, since KeyGen set the copy for the whole
thread.

The build smoke job installs g++-aarch64-linux-gnu and qemu-user and
lists them as required, so the AArch64 case cannot skip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two wrapper mutations passed every test. An entry point without
BeginOperation() went unnoticed because ForEachBackend calls
GetBackendNames() on the same thread first, which takes the override for
it. A shared (not thread_local) copy of the override went unnoticed
because only one thread ran operations.

every_entry_point_takes_the_override runs each entry point alone on a new
thread with portable C forced, then asks the capability hooks what that
thread's copy says. override_copy_is_per_thread lets one thread take the
override set while another holds it unset, and checks that the first
thread's copy did not change. override_changed_by_another_thread now runs
operations on two threads at once, so a shared copy also gives wrong
outputs on x86_64 and a data race under ThreadSanitizer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI replays the mlkem and mlkem_backend_diff targets a second time with
MLKEM_FORCE_PORTABLE=1, but nothing checked that the binary honored it:
any other value, such as "true", silently selected native code, and the
second replay would have repeated the first.

Initialize now refuses any value but unset, empty, 0 or 1, prints the
active backend once, and aborts when MLKEM_FORCE_PORTABLE=1 but the
active backend is not portable/portable. A host without native code also
shows in the log, where mlkem_backend_diff compares portable C with
itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SHA256AutoDetect requires SSSE3 besides SSE4.1 before it selects its SSE4,
SSE4.1 or SHA-NI code, all of which execute SSSE3 instructions. Nothing
tested that: dropping the SSSE3 term passed every test, because real
SSE4.1 hardware always has SSSE3.

An overload of SHA256AutoDetect takes the CPUID and XGETBV functions, as
cpu_features::DetectX86Features does, and the existing one passes the real
ones, so the queries and the selection are unchanged. A cpu_features_tests
case runs it on synthetic CPUs derived from this one: unmasked it selects
what this CPU gets, and with SSSE3 or SSE4.1 masked the standard code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The wrapper's header said native code runs on "x86_64 with AVX2,
AArch64", and the WITH_MLKEM_NATIVE help text spoke of "the x86_64 and
AArch64 ML-KEM assembly". AArch64 runs native code only on macOS (AArch64
ELF builds stay portable to keep their BTI and PAC marking), and x86_64
also needs SSSE3, SSE4.1, POPCNT and BMI2, as
cpu_features::HasMlkemX86Native checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
update-mlkem-native.sh fetched the upstream release with --depth=1 into
the qbit repository itself. That writes .git/shallow, so a full clone
then reported itself as shallow to git and every tool that asks.

Fetch into a temporary bare repository instead, and read the commit, the
object ids and the archive from there. Staging and the byte-for-byte
check still run in the qbit repository. Re-vendoring v2.0.0 by tag and by
commit leaves no diff, and the clone stays complete.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#190 asks for ELF property evidence that linking the x86_64 assembly keeps
the CET marking. Record what a local Ubuntu build shows: the assembly and
C objects and the test_qbit and fuzz binaries that link the native
backend all carry IBT and SHSTK, the binaries need only baseline x86-64,
and the assembly loses its note only when built with
-fcf-protection=none. The Guix release evidence is still outstanding and
is required before the post-quantum feature branch merges into 1.x.x.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
every_entry_point_takes_the_override captured a structured binding in
the thread's lambda, which compilers accept only from C++20 (P1091R3,
P1381R1). Bind the label and the entry point to ordinary variables, so
the test does not depend on that support.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first macOS arm64 run failed to compile txmempool.h: with
NO_DEPENDS=1 the job used Homebrew's Boost, now 1.92, and Boost 1.91
stopped accepting a struct derived from multi_index::indexed_by as an
index list. Bitcoin Core fixed the source in bitcoin/bitcoin#35175; that
backport is handled on its own.

Build Boost and libevent with depends, natively for the runner, so the
jobs compile against the versions qbit releases with. Homebrew now
installs GNU Make 4 for depends instead of the two libraries. A step
sets HOST and reads depends' own build id (packages, recipes and
compilers), which keys an actions/cache of depends/built. It is saved
once the toolchain file exists, even if the suites fail. After the
build, a step requires that depends built that id and that CMake took
the toolchain, libevent and Boost at boost.mk's version from it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pq-transport Hybrid post-quantum v2 transport (#184)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant