diff --git a/compiler/rustc_codegen_llvm/messages.ftl b/compiler/rustc_codegen_llvm/messages.ftl
index d14fe0299e64c..1c126e797621b 100644
--- a/compiler/rustc_codegen_llvm/messages.ftl
+++ b/compiler/rustc_codegen_llvm/messages.ftl
@@ -18,6 +18,8 @@ codegen_llvm_error_creating_import_library =
 codegen_llvm_error_writing_def_file =
     Error writing .DEF file: {$error}
 
+codegen_llvm_fixed_x18_invalid_arch = the `-Zfixed-x18` flag is not supported on the `{$arch}` architecture
+
 codegen_llvm_from_llvm_diag = {$message}
 
 codegen_llvm_from_llvm_optimization_diag = {$filename}:{$line}:{$column} {$pass_name} ({$kind}): {$message}
diff --git a/compiler/rustc_codegen_llvm/src/errors.rs b/compiler/rustc_codegen_llvm/src/errors.rs
index e15eda7c66c14..9d83dc811633b 100644
--- a/compiler/rustc_codegen_llvm/src/errors.rs
+++ b/compiler/rustc_codegen_llvm/src/errors.rs
@@ -254,3 +254,9 @@ pub struct MismatchedDataLayout<'a> {
 pub(crate) struct InvalidTargetFeaturePrefix<'a> {
     pub feature: &'a str,
 }
+
+#[derive(Diagnostic)]
+#[diag(codegen_llvm_fixed_x18_invalid_arch)]
+pub(crate) struct FixedX18InvalidArch<'a> {
+    pub arch: &'a str,
+}
diff --git a/compiler/rustc_codegen_llvm/src/llvm_util.rs b/compiler/rustc_codegen_llvm/src/llvm_util.rs
index 5552b38102511..53b9b530e9bd6 100644
--- a/compiler/rustc_codegen_llvm/src/llvm_util.rs
+++ b/compiler/rustc_codegen_llvm/src/llvm_util.rs
@@ -1,6 +1,6 @@
 use crate::back::write::create_informational_target_machine;
 use crate::errors::{
-    InvalidTargetFeaturePrefix, PossibleFeature, TargetFeatureDisableOrEnable,
+    FixedX18InvalidArch, InvalidTargetFeaturePrefix, PossibleFeature, TargetFeatureDisableOrEnable,
     UnknownCTargetFeature, UnknownCTargetFeaturePrefix, UnstableCTargetFeature,
 };
 use crate::llvm;
@@ -615,6 +615,15 @@ pub(crate) fn global_llvm_features(sess: &Session, diagnostics: bool) -> Vec<Str
         .flatten();
     features.extend(feats);
 
+    // -Zfixed-x18
+    if sess.opts.unstable_opts.fixed_x18 {
+        if sess.target.arch != "aarch64" {
+            sess.dcx().emit_fatal(FixedX18InvalidArch { arch: &sess.target.arch });
+        } else {
+            features.push("+reserve-x18".into());
+        }
+    }
+
     if diagnostics && let Some(f) = check_tied_features(sess, &featsmap) {
         sess.dcx().emit_err(TargetFeatureDisableOrEnable {
             features: f,
diff --git a/compiler/rustc_interface/src/tests.rs b/compiler/rustc_interface/src/tests.rs
index db150cc1f875d..12cbc72a82160 100644
--- a/compiler/rustc_interface/src/tests.rs
+++ b/compiler/rustc_interface/src/tests.rs
@@ -773,6 +773,7 @@ fn test_unstable_options_tracking_hash() {
     tracked!(emit_thin_lto, false);
     tracked!(export_executable_symbols, true);
     tracked!(fewer_names, Some(true));
+    tracked!(fixed_x18, true);
     tracked!(flatten_format_args, false);
     tracked!(force_unstable_if_unmarked, true);
     tracked!(fuel, Some(("abc".to_string(), 99)));
diff --git a/compiler/rustc_session/src/options.rs b/compiler/rustc_session/src/options.rs
index 7355e5b695347..48b6278f2f32d 100644
--- a/compiler/rustc_session/src/options.rs
+++ b/compiler/rustc_session/src/options.rs
@@ -1684,6 +1684,8 @@ options! {
     fewer_names: Option<bool> = (None, parse_opt_bool, [TRACKED],
         "reduce memory use by retaining fewer names within compilation artifacts (LLVM-IR) \
         (default: no)"),
+    fixed_x18: bool = (false, parse_bool, [TRACKED],
+        "make the x18 register reserved on AArch64 (default: no)"),
     flatten_format_args: bool = (true, parse_bool, [TRACKED],
         "flatten nested format_args!() and literals into a simplified format_args!() call \
         (default: yes)"),
diff --git a/src/doc/unstable-book/src/compiler-flags/fixed-x18.md b/src/doc/unstable-book/src/compiler-flags/fixed-x18.md
new file mode 100644
index 0000000000000..8c8bff5fa296d
--- /dev/null
+++ b/src/doc/unstable-book/src/compiler-flags/fixed-x18.md
@@ -0,0 +1,32 @@
+# `fixed-x18`
+
+This option prevents the compiler from using the x18 register. It is only
+supported on aarch64.
+
+From the [ABI spec][arm-abi]:
+
+> X18 is the platform register and is reserved for the use of platform ABIs.
+> This is an additional temporary register on platforms that don't assign a
+> special meaning to it.
+
+This flag only has an effect when the x18 register would otherwise be considered
+a temporary register. When the flag is applied, x18 is always a reserved
+register.
+
+This flag is intended for use with the shadow call stack sanitizer. Generally,
+when that sanitizer is enabled, the x18 register is used to store a pointer to
+the shadow stack. Enabling this flag prevents the compiler from overwriting the
+shadow stack pointer with temporary data, which is necessary for the sanitizer
+to work correctly.
+
+Currently, the `-Zsanitizer=shadow-call-stack` flag is only supported on
+platforms that always treat x18 as a reserved register, and the `-Zfixed-x18`
+flag is not required to use the sanitizer on such platforms. However, the
+sanitizer may be supported on targets where this is not the case in the future.
+
+It is undefined behavior for `-Zsanitizer=shadow-call-stack` code to call into
+code where x18 is a temporary register. On the other hand, when you are *not*
+using the shadow call stack sanitizer, compilation units compiled with and
+without the `-Zfixed-x18` flag are compatible with each other.
+
+[arm-abi]: https://developer.arm.com/documentation/den0024/a/The-ABI-for-ARM-64-bit-Architecture/Register-use-in-the-AArch64-Procedure-Call-Standard/Parameters-in-general-purpose-registers
diff --git a/tests/codegen/fixed-x18.rs b/tests/codegen/fixed-x18.rs
new file mode 100644
index 0000000000000..4997a39a7263d
--- /dev/null
+++ b/tests/codegen/fixed-x18.rs
@@ -0,0 +1,22 @@
+// Test that the `reserve-x18` target feature is (not) emitted when
+// the `-Zfixed-x18` flag is (not) set.
+
+//@ revisions: unset set
+//@ needs-llvm-components: aarch64
+//@ compile-flags: --target aarch64-unknown-none
+//@ [set] compile-flags: -Zfixed-x18
+
+#![crate_type = "lib"]
+#![feature(no_core, lang_items)]
+#![no_core]
+
+#[lang = "sized"]
+trait Sized {}
+
+#[no_mangle]
+pub fn foo() {
+    // CHECK: @foo() unnamed_addr #0
+
+    // unset-NOT: attributes #0 = { {{.*}}"target-features"="{{[^"]*}}+reserve-x18{{.*}} }
+    // set: attributes #0 = { {{.*}}"target-features"="{{[^"]*}}+reserve-x18{{.*}} }
+}
diff --git a/tests/ui/abi/fixed_x18.rs b/tests/ui/abi/fixed_x18.rs
new file mode 100644
index 0000000000000..f1ff3e1d53418
--- /dev/null
+++ b/tests/ui/abi/fixed_x18.rs
@@ -0,0 +1,25 @@
+// This tests that -Zfixed-x18 causes a compilation failure on targets other than aarch64.
+// Behavior on aarch64 is tested by tests/codegen/fixed-x18.rs.
+//
+//@ revisions: x64 i686 arm riscv32 riscv64
+//@ error-pattern: the `-Zfixed-x18` flag is not supported
+//@ dont-check-compiler-stderr
+//
+//@ compile-flags: -Zfixed-x18
+//@ [x64] needs-llvm-components: x86
+//@ [x64] compile-flags: --target=x86_64-unknown-linux-gnu --crate-type=rlib
+//@ [i686] needs-llvm-components: x86
+//@ [i686] compile-flags: --target=i686-unknown-linux-gnu --crate-type=rlib
+//@ [arm] needs-llvm-components: arm
+//@ [arm] compile-flags: --target=armv7-unknown-linux-gnueabihf --crate-type=rlib
+//@ [riscv32] needs-llvm-components: riscv
+//@ [riscv32] compile-flags: --target=riscv32i-unknown-none-elf --crate-type=rlib
+//@ [riscv64] needs-llvm-components: riscv
+//@ [riscv64] compile-flags: --target=riscv64gc-unknown-none-elf --crate-type=rlib
+
+#![crate_type = "lib"]
+#![feature(no_core, lang_items)]
+#![no_core]
+
+#[lang = "sized"]
+trait Sized {}