diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,16 @@
 # CHANGELOG for crypton
 
+## 2.1.11
+
+**crypton runs again on x86-64 processors without AVX2.**  From 2.1.8 the
+whole library was compiled with `-mavx2 -mbmi2`, so a program built on a
+machine with AVX2 was killed on one without.  This is the fix 2.2.1 made.
+
+* fix: stop raising the x86-64 baseline for the whole library
+  [#351](https://github.com/kazu-yamamoto/crypton/pull/351)
+* fix: ask the processor before using the ML-KEM and ML-DSA backends
+  [#351](https://github.com/kazu-yamamoto/crypton/pull/351)
+
 ## 2.1.10
 
 * build: ask the processor for AES-NI on every x86 system, not four of them
diff --git a/cbits/mldsa/crypton_mldsa.h b/cbits/mldsa/crypton_mldsa.h
--- a/cbits/mldsa/crypton_mldsa.h
+++ b/cbits/mldsa/crypton_mldsa.h
@@ -4,7 +4,8 @@
  * choice crypton makes is made here instead of by editing it.
  *
  * Included first by both crypton_mldsa.c and crypton_mldsa_asm.S, so it must
- * hold nothing but preprocessor directives.
+ * hold nothing the assembler cannot read: directives, and C behind
+ * __ASSEMBLER__.
  */
 #ifndef CRYPTON_MLDSA_H
 #define CRYPTON_MLDSA_H
@@ -36,6 +37,43 @@
 #ifdef CRYPTON_MLDSA_NATIVE_BACKEND
 #define MLD_CONFIG_USE_NATIVE_BACKEND_ARITH
 #define MLD_CONFIG_USE_NATIVE_BACKEND_FIPS202
+
+/*
+ * Which of the backend's instructions this processor actually has.
+ *
+ * Without this both packages decide at compile time: the backend is built
+ * because the architecture has the instructions in principle, and is then
+ * called with no check.  Upstream says so plainly -- if the machine that
+ * runs the binary may differ from the one that built it, this function
+ * MUST be provided -- and crypton is a library whose builds are copied
+ * between machines and shipped by distributions.  Without it a binary
+ * built on a host with AVX2 dies with SIGILL on one without (#316).
+ *
+ * The dispatch sites are upstream's; each falls back to the portable C
+ * when this answers no.  The answers come from crypton's own detection,
+ * the same that AES-NI, PCLMUL and the s2n-bignum variants use.
+ */
+#define MLD_CONFIG_CUSTOM_CAPABILITY_FUNC
+#ifndef __ASSEMBLER__
+#include "src/sys.h"
+#include "crypton_cpu.h"
+
+static MLD_INLINE int mld_sys_check_capability(mld_sys_cap cap)
+{
+#if defined(__x86_64__)
+    if (cap == MLD_SYS_CAP_X86_64_AVX2)
+        return (crypton_x86_simd_features() & CRYPTON_X86_AVX2) != 0;
+#elif defined(__aarch64__)
+    /* NEON is not optional on AArch64; the Keccak instructions are. */
+    if (cap == MLD_SYS_CAP_AARCH64_NEON)
+        return 1;
+    if (cap == MLD_SYS_CAP_AARCH64_SHA3)
+        return (crypton_arm_features() & CRYPTON_ARM_SHA3) != 0;
+#endif
+    (void)cap;
+    return 0;
+}
+#endif /* !__ASSEMBLER__ */
 #endif
 
 #endif /* CRYPTON_MLDSA_H */
diff --git a/cbits/mlkem/crypton_mlkem.h b/cbits/mlkem/crypton_mlkem.h
--- a/cbits/mlkem/crypton_mlkem.h
+++ b/cbits/mlkem/crypton_mlkem.h
@@ -4,7 +4,8 @@
  * choice crypton makes is made here instead of by editing it.
  *
  * Included first by both crypton_mlkem.c and crypton_mlkem_asm.S, so it must
- * hold nothing but preprocessor directives.
+ * hold nothing the assembler cannot read: directives, and C behind
+ * __ASSEMBLER__.
  */
 #ifndef CRYPTON_MLKEM_H
 #define CRYPTON_MLKEM_H
@@ -36,6 +37,43 @@
 #ifdef CRYPTON_MLKEM_NATIVE_BACKEND
 #define MLK_CONFIG_USE_NATIVE_BACKEND_ARITH
 #define MLK_CONFIG_USE_NATIVE_BACKEND_FIPS202
+
+/*
+ * Which of the backend's instructions this processor actually has.
+ *
+ * Without this both packages decide at compile time: the backend is built
+ * because the architecture has the instructions in principle, and is then
+ * called with no check.  Upstream says so plainly -- if the machine that
+ * runs the binary may differ from the one that built it, this function
+ * MUST be provided -- and crypton is a library whose builds are copied
+ * between machines and shipped by distributions.  Without it a binary
+ * built on a host with AVX2 dies with SIGILL on one without (#316).
+ *
+ * The dispatch sites are upstream's; each falls back to the portable C
+ * when this answers no.  The answers come from crypton's own detection,
+ * the same that AES-NI, PCLMUL and the s2n-bignum variants use.
+ */
+#define MLK_CONFIG_CUSTOM_CAPABILITY_FUNC
+#ifndef __ASSEMBLER__
+#include "src/sys.h"
+#include "crypton_cpu.h"
+
+static MLK_INLINE int mlk_sys_check_capability(mlk_sys_cap cap)
+{
+#if defined(__x86_64__)
+    if (cap == MLK_SYS_CAP_X86_64_AVX2)
+        return (crypton_x86_simd_features() & CRYPTON_X86_AVX2) != 0;
+#elif defined(__aarch64__)
+    /* NEON is not optional on AArch64; the Keccak instructions are. */
+    if (cap == MLK_SYS_CAP_AARCH64_NEON)
+        return 1;
+    if (cap == MLK_SYS_CAP_AARCH64_SHA3)
+        return (crypton_arm_features() & CRYPTON_ARM_SHA3) != 0;
+#endif
+    (void)cap;
+    return 0;
+}
+#endif /* !__ASSEMBLER__ */
 #endif
 
 #endif /* CRYPTON_MLKEM_H */
diff --git a/cbits/tests/ct/run.sh b/cbits/tests/ct/run.sh
--- a/cbits/tests/ct/run.sh
+++ b/cbits/tests/ct/run.sh
@@ -63,7 +63,11 @@
 # And the hand-written backend, where the architecture has one.  Both builds
 # have to be silent: unlike the AES pair below there is no variable-time
 # implementation here for one of them to expose.
-mlkem_native_src="$mlkem_src cbits/mlkem/crypton_mlkem_asm.S"
+#
+# crypton_cpu.c because the backend asks it whether this processor has the
+# instructions before using them; without it the link fails on the
+# capability function in cbits/mlkem/crypton_mlkem.h.
+mlkem_native_src="$mlkem_src cbits/mlkem/crypton_mlkem_asm.S cbits/crypton_cpu.c"
 mlkem_native_inc="$mlkem_inc -DCRYPTON_MLKEM_NATIVE_BACKEND"
 
 
@@ -189,12 +193,22 @@
 
 # The ML-KEM backend, on both architectures that have one.  x86-64 wants the
 # flags its own build system passes, or the AVX2 sources do not assemble.
+#
+# The backend now asks the processor before it runs, so on x86-64 without
+# AVX2 this build measures the portable C under a name that says otherwise.
+# Say so and skip, rather than pass while checking the thing the plain
+# mlkem build above has already checked.
 case $(uname -m) in
 aarch64 | arm64)
 	run_one mlkem_native "$mlkem_native_src" "$mlkem_native_inc" mlkem
 	;;
 x86_64 | amd64)
-	run_one mlkem_native "$mlkem_native_src" "$mlkem_native_inc -mavx2 -mbmi2" mlkem
+	if grep -qw avx2 "${CRYPTON_CT_CPUINFO:-/proc/cpuinfo}" 2>/dev/null; then
+		run_one mlkem_native "$mlkem_native_src" "$mlkem_native_inc -mavx2 -mbmi2" mlkem
+	else
+		echo "skip mlkem_native: this processor has no AVX2, so the backend"
+		echo "     would fall back to the C the mlkem build above measured"
+	fi
 	;;
 esac
 
diff --git a/crypton.cabal b/crypton.cabal
--- a/crypton.cabal
+++ b/crypton.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.0
 name:               crypton
-version:            2.1.10
+version:            2.1.11
 -- crypton's own code is BSD-3-Clause.  The parts of
 -- cbits/aes/gcm_fused_x86.c that follow picotls's fusion are MIT, and the
 -- vendored s2n-bignum assembly in cbits/s2n is taken under ISC; each has
@@ -716,11 +716,15 @@
             cbits/mldsa/crypton_mldsa_asm.S
 
         -- What the two packages' own build systems pass for the x86-64
-        -- backend; without them the AVX2 sources do not assemble.  AArch64
-        -- needs nothing beyond the baseline for the arithmetic, and takes
-        -- the armv8.4 Keccak from the -march the block below already sets.
+        -- backend, and only for the assembly: cc-options in a conditional
+        -- are the component's, not the block's, so -mavx2 here would raise
+        -- the baseline of every C file in the library -- Ed25519, Argon2,
+        -- P-256 and the rest -- and gcc would vectorise code that asks for
+        -- no such thing, with nothing to check at runtime.  The backends'
+        -- own AVX2 is all in the .S.  AArch64 needs nothing beyond the
+        -- baseline for the arithmetic, and takes the armv8.4 Keccak from
+        -- the -march the block below already sets.
         if arch(x86_64)
-            cc-options:  -mavx2 -mbmi2
             asm-options: -mavx2 -mbmi2
 
     if ((flag(support_rdrand) && (arch(i386) || arch(x86_64))) && !os(windows))
