diff --git a/compiler/rustc_codegen_llvm/messages.ftl b/compiler/rustc_codegen_llvm/messages.ftl
index 17f2e7ca9f702..41391b096cca8 100644
--- a/compiler/rustc_codegen_llvm/messages.ftl
+++ b/compiler/rustc_codegen_llvm/messages.ftl
@@ -56,6 +56,8 @@ codegen_llvm_prepare_thin_lto_module_with_llvm_err = failed to prepare thin LTO
 codegen_llvm_run_passes = failed to run LLVM passes
 codegen_llvm_run_passes_with_llvm_err = failed to run LLVM passes: {$llvm_err}
 
+codegen_llvm_sanitizer_kcfi_arity_requires_llvm_21_0_0 = `-Zsanitizer-kcfi-arity` requires LLVM 21.0.0 or later.
+
 codegen_llvm_sanitizer_memtag_requires_mte =
     `-Zsanitizer=memtag` requires `-Ctarget-feature=+mte`
 
diff --git a/compiler/rustc_codegen_llvm/src/context.rs b/compiler/rustc_codegen_llvm/src/context.rs
index 9f8ec0ea526af..ca434f4cf0053 100644
--- a/compiler/rustc_codegen_llvm/src/context.rs
+++ b/compiler/rustc_codegen_llvm/src/context.rs
@@ -326,6 +326,22 @@ pub(crate) unsafe fn create_module<'ll>(
                 pfe.prefix().into(),
             );
         }
+
+        // Add "kcfi-arity" module flag if KCFI arity indicator is enabled. (See
+        // https://github.com/llvm/llvm-project/pull/117121.)
+        if sess.is_sanitizer_kcfi_arity_enabled() {
+            // KCFI arity indicator requires LLVM 21.0.0 or later.
+            if llvm_version < (21, 0, 0) {
+                tcx.dcx().emit_err(crate::errors::SanitizerKcfiArityRequiresLLVM2100);
+            }
+
+            llvm::add_module_flag_u32(
+                llmod,
+                llvm::ModuleFlagMergeBehavior::Override,
+                "kcfi-arity",
+                1,
+            );
+        }
     }
 
     // Control Flow Guard is currently only supported by MSVC and LLVM on Windows.
diff --git a/compiler/rustc_codegen_llvm/src/errors.rs b/compiler/rustc_codegen_llvm/src/errors.rs
index 4c5a78ca74fe4..ecf108f988f0d 100644
--- a/compiler/rustc_codegen_llvm/src/errors.rs
+++ b/compiler/rustc_codegen_llvm/src/errors.rs
@@ -217,3 +217,7 @@ pub(crate) struct MismatchedDataLayout<'a> {
 pub(crate) struct FixedX18InvalidArch<'a> {
     pub arch: &'a str,
 }
+
+#[derive(Diagnostic)]
+#[diag(codegen_llvm_sanitizer_kcfi_arity_requires_llvm_21_0_0)]
+pub(crate) struct SanitizerKcfiArityRequiresLLVM2100;
diff --git a/compiler/rustc_interface/src/tests.rs b/compiler/rustc_interface/src/tests.rs
index aabd235bcabed..0743656497062 100644
--- a/compiler/rustc_interface/src/tests.rs
+++ b/compiler/rustc_interface/src/tests.rs
@@ -852,6 +852,7 @@ fn test_unstable_options_tracking_hash() {
     tracked!(sanitizer_cfi_generalize_pointers, Some(true));
     tracked!(sanitizer_cfi_normalize_integers, Some(true));
     tracked!(sanitizer_dataflow_abilist, vec![String::from("/rustc/abc")]);
+    tracked!(sanitizer_kcfi_arity, Some(true));
     tracked!(sanitizer_memory_track_origins, 2);
     tracked!(sanitizer_recover, SanitizerSet::ADDRESS);
     tracked!(saturating_float_casts, Some(true));
diff --git a/compiler/rustc_session/messages.ftl b/compiler/rustc_session/messages.ftl
index 74b8087e07769..a0f873f3cf66b 100644
--- a/compiler/rustc_session/messages.ftl
+++ b/compiler/rustc_session/messages.ftl
@@ -94,6 +94,8 @@ session_sanitizer_cfi_requires_lto = `-Zsanitizer=cfi` requires `-Clto` or `-Cli
 
 session_sanitizer_cfi_requires_single_codegen_unit = `-Zsanitizer=cfi` with `-Clto` requires `-Ccodegen-units=1`
 
+session_sanitizer_kcfi_arity_requires_kcfi = `-Zsanitizer-kcfi-arity` requires `-Zsanitizer=kcfi`
+
 session_sanitizer_kcfi_requires_panic_abort = `-Z sanitizer=kcfi` requires `-C panic=abort`
 
 session_sanitizer_not_supported = {$us} sanitizer is not supported for this target
diff --git a/compiler/rustc_session/src/errors.rs b/compiler/rustc_session/src/errors.rs
index 71d8dbe44fed0..3979aea70d33d 100644
--- a/compiler/rustc_session/src/errors.rs
+++ b/compiler/rustc_session/src/errors.rs
@@ -147,6 +147,10 @@ pub(crate) struct SanitizerCfiGeneralizePointersRequiresCfi;
 #[diag(session_sanitizer_cfi_normalize_integers_requires_cfi)]
 pub(crate) struct SanitizerCfiNormalizeIntegersRequiresCfi;
 
+#[derive(Diagnostic)]
+#[diag(session_sanitizer_kcfi_arity_requires_kcfi)]
+pub(crate) struct SanitizerKcfiArityRequiresKcfi;
+
 #[derive(Diagnostic)]
 #[diag(session_sanitizer_kcfi_requires_panic_abort)]
 pub(crate) struct SanitizerKcfiRequiresPanicAbort;
diff --git a/compiler/rustc_session/src/options.rs b/compiler/rustc_session/src/options.rs
index 8977365ee73d9..01c614f48f281 100644
--- a/compiler/rustc_session/src/options.rs
+++ b/compiler/rustc_session/src/options.rs
@@ -2433,6 +2433,8 @@ written to standard error output)"),
         "enable normalizing integer types (default: no)"),
     sanitizer_dataflow_abilist: Vec<String> = (Vec::new(), parse_comma_list, [TRACKED],
         "additional ABI list files that control how shadow parameters are passed (comma separated)"),
+    sanitizer_kcfi_arity: Option<bool> = (None, parse_opt_bool, [TRACKED],
+        "enable KCFI arity indicator (default: no)"),
     sanitizer_memory_track_origins: usize = (0, parse_sanitizer_memory_track_origins, [TRACKED],
         "enable origins tracking in MemorySanitizer"),
     sanitizer_recover: SanitizerSet = (SanitizerSet::empty(), parse_sanitizers, [TRACKED],
diff --git a/compiler/rustc_session/src/session.rs b/compiler/rustc_session/src/session.rs
index bcd9a73d9d30a..57072132555ae 100644
--- a/compiler/rustc_session/src/session.rs
+++ b/compiler/rustc_session/src/session.rs
@@ -382,6 +382,10 @@ impl Session {
         self.opts.unstable_opts.sanitizer_cfi_normalize_integers == Some(true)
     }
 
+    pub fn is_sanitizer_kcfi_arity_enabled(&self) -> bool {
+        self.opts.unstable_opts.sanitizer_kcfi_arity == Some(true)
+    }
+
     pub fn is_sanitizer_kcfi_enabled(&self) -> bool {
         self.opts.unstable_opts.sanitizer.contains(SanitizerSet::KCFI)
     }
@@ -1204,6 +1208,11 @@ fn validate_commandline_args_with_session_available(sess: &Session) {
         }
     }
 
+    // KCFI arity indicator requires KCFI.
+    if sess.is_sanitizer_kcfi_arity_enabled() && !sess.is_sanitizer_kcfi_enabled() {
+        sess.dcx().emit_err(errors::SanitizerKcfiArityRequiresKcfi);
+    }
+
     // LLVM CFI pointer generalization requires CFI or KCFI.
     if sess.is_sanitizer_cfi_generalize_pointers_enabled() {
         if !(sess.is_sanitizer_cfi_enabled() || sess.is_sanitizer_kcfi_enabled()) {
diff --git a/tests/assembly/sanitizer/kcfi/emit-arity-indicator.rs b/tests/assembly/sanitizer/kcfi/emit-arity-indicator.rs
new file mode 100644
index 0000000000000..b3b623b509b4b
--- /dev/null
+++ b/tests/assembly/sanitizer/kcfi/emit-arity-indicator.rs
@@ -0,0 +1,61 @@
+// Verifies that KCFI arity indicator is emitted.
+//
+//@ add-core-stubs
+//@ revisions: x86_64
+//@ assembly-output: emit-asm
+//@[x86_64] compile-flags: --target x86_64-unknown-linux-gnu -Cllvm-args=-x86-asm-syntax=intel -Ctarget-feature=-crt-static -Cpanic=abort -Zsanitizer=kcfi -Zsanitizer-kcfi-arity -Copt-level=0
+//@ [x86_64] needs-llvm-components: x86
+//@ min-llvm-version: 21.0.0
+
+#![crate_type = "lib"]
+
+pub fn add_one(x: i32) -> i32 {
+    // CHECK-LABEL: __cfi__{{.*}}7add_one{{.*}}:
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  mov ecx, 2628068948
+    x + 1
+}
+
+pub fn add_two(x: i32, _y: i32) -> i32 {
+    // CHECK-LABEL: __cfi__{{.*}}7add_two{{.*}}:
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  mov edx, 2505940310
+    x + 2
+}
+
+pub fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
+    // CHECK-LABEL: __cfi__{{.*}}8do_twice{{.*}}:
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  nop
+    // CHECK-NEXT:  mov edx, 653723426
+    f(arg) + f(arg)
+}
diff --git a/tests/codegen/sanitizer/kcfi/add-kcfi-arity-flag.rs b/tests/codegen/sanitizer/kcfi/add-kcfi-arity-flag.rs
new file mode 100644
index 0000000000000..f65f767be986e
--- /dev/null
+++ b/tests/codegen/sanitizer/kcfi/add-kcfi-arity-flag.rs
@@ -0,0 +1,18 @@
+// Verifies that "kcfi-arity" module flag is added.
+//
+//@ add-core-stubs
+//@ revisions: x86_64
+//@ [x86_64] compile-flags: --target x86_64-unknown-none
+//@ [x86_64] needs-llvm-components: x86
+//@ compile-flags: -Ctarget-feature=-crt-static -Cpanic=abort -Zsanitizer=kcfi -Zsanitizer-kcfi-arity
+
+#![feature(no_core, lang_items)]
+#![crate_type = "lib"]
+#![no_core]
+
+extern crate minicore;
+use minicore::*;
+
+pub fn foo() {}
+
+// CHECK: !{{[0-9]+}} = !{i32 4, !"kcfi-arity", i32 1}
diff --git a/tests/ui/sanitizer/kcfi-arity-requires-kcfi.rs b/tests/ui/sanitizer/kcfi-arity-requires-kcfi.rs
new file mode 100644
index 0000000000000..0939181e9a351
--- /dev/null
+++ b/tests/ui/sanitizer/kcfi-arity-requires-kcfi.rs
@@ -0,0 +1,8 @@
+// Verifies that `-Zsanitizer-kcfi-arity` requires `-Zsanitizer=kcfi`.
+//
+//@ needs-sanitizer-kcfi
+//@ compile-flags: -Cno-prepopulate-passes -Ctarget-feature=-crt-static -Zsanitizer-kcfi-arity
+
+#![feature(no_core)]
+#![no_core]
+#![no_main]
diff --git a/tests/ui/sanitizer/kcfi-arity-requires-kcfi.stderr b/tests/ui/sanitizer/kcfi-arity-requires-kcfi.stderr
new file mode 100644
index 0000000000000..4ed1b754fd431
--- /dev/null
+++ b/tests/ui/sanitizer/kcfi-arity-requires-kcfi.stderr
@@ -0,0 +1,4 @@
+error: `-Zsanitizer-kcfi-arity` requires `-Zsanitizer=kcfi`
+
+error: aborting due to 1 previous error
+
diff --git a/tests/ui/sanitizer/kcfi-arity-requires-llvm-21-0-0.rs b/tests/ui/sanitizer/kcfi-arity-requires-llvm-21-0-0.rs
new file mode 100644
index 0000000000000..49d9381227982
--- /dev/null
+++ b/tests/ui/sanitizer/kcfi-arity-requires-llvm-21-0-0.rs
@@ -0,0 +1,10 @@
+// Verifies that `-Zsanitizer-kcfi-arity` requires LLVM 21.0.0 or later.
+//
+//@ needs-sanitizer-kcfi
+//@ compile-flags: -Cno-prepopulate-passes -Ctarget-feature=-crt-static -Cpanic=abort -Zsanitizer=kcfi -Zsanitizer-kcfi-arity
+//@ build-fail
+//@ max-llvm-major-version: 20
+
+#![feature(no_core)]
+#![no_core]
+#![no_main]
diff --git a/tests/ui/sanitizer/kcfi-arity-requires-llvm-21-0-0.stderr b/tests/ui/sanitizer/kcfi-arity-requires-llvm-21-0-0.stderr
new file mode 100644
index 0000000000000..ac6bd7411fd81
--- /dev/null
+++ b/tests/ui/sanitizer/kcfi-arity-requires-llvm-21-0-0.stderr
@@ -0,0 +1,4 @@
+error: `-Zsanitizer-kcfi-arity` requires LLVM 21.0.0 or later.
+
+error: aborting due to 1 previous error
+