diff --git a/compiler/rustc_attr/src/builtin.rs b/compiler/rustc_attr/src/builtin.rs
index c1db4d07dfc86..3220e337db713 100644
--- a/compiler/rustc_attr/src/builtin.rs
+++ b/compiler/rustc_attr/src/builtin.rs
@@ -47,6 +47,34 @@ pub enum InlineAttr {
     Hint,
     Always,
     Never,
+    /// `#[inline(must)]` always attempts to inline in the MIR inliner, regardless of optimisation
+    /// level, and emits a warn-by-default lint if this is not possible.
+    Must {
+        attr_span: Span,
+        reason: Option<Symbol>,
+    },
+    /// `#[inline(required)]` always attempts to inline in the MIR inliner, regardless of
+    /// optimisation level, and emits a error-by-default lint if this is not possible.
+    Required {
+        attr_span: Span,
+        reason: Option<Symbol>,
+    },
+}
+
+impl InlineAttr {
+    pub fn required(&self) -> bool {
+        match self {
+            InlineAttr::Must { .. } | InlineAttr::Required { .. } => true,
+            InlineAttr::None | InlineAttr::Hint | InlineAttr::Always | InlineAttr::Never => false,
+        }
+    }
+
+    pub fn always(&self) -> bool {
+        match self {
+            InlineAttr::Always | InlineAttr::Must { .. } | InlineAttr::Required { .. } => true,
+            InlineAttr::None | InlineAttr::Hint | InlineAttr::Never => false,
+        }
+    }
 }
 
 #[derive(Clone, Encodable, Decodable, Debug, PartialEq, Eq, HashStable_Generic)]
diff --git a/compiler/rustc_codegen_gcc/src/attributes.rs b/compiler/rustc_codegen_gcc/src/attributes.rs
index d20e13e15b944..7c54ada7ece6e 100644
--- a/compiler/rustc_codegen_gcc/src/attributes.rs
+++ b/compiler/rustc_codegen_gcc/src/attributes.rs
@@ -20,7 +20,9 @@ fn inline_attr<'gcc, 'tcx>(
 ) -> Option<FnAttribute<'gcc>> {
     match inline {
         InlineAttr::Hint => Some(FnAttribute::Inline),
-        InlineAttr::Always => Some(FnAttribute::AlwaysInline),
+        InlineAttr::Always | InlineAttr::Required { .. } | InlineAttr::Must { .. } => {
+            Some(FnAttribute::AlwaysInline)
+        }
         InlineAttr::Never => {
             if cx.sess().target.arch != "amdgpu" {
                 Some(FnAttribute::NoInline)
diff --git a/compiler/rustc_codegen_llvm/src/attributes.rs b/compiler/rustc_codegen_llvm/src/attributes.rs
index 2c5ec9dad59f1..bbd5e7e7a93b1 100644
--- a/compiler/rustc_codegen_llvm/src/attributes.rs
+++ b/compiler/rustc_codegen_llvm/src/attributes.rs
@@ -37,7 +37,9 @@ fn inline_attr<'ll>(cx: &CodegenCx<'ll, '_>, inline: InlineAttr) -> Option<&'ll
     }
     match inline {
         InlineAttr::Hint => Some(AttributeKind::InlineHint.create_attr(cx.llcx)),
-        InlineAttr::Always => Some(AttributeKind::AlwaysInline.create_attr(cx.llcx)),
+        InlineAttr::Always | InlineAttr::Required { .. } | InlineAttr::Must { .. } => {
+            Some(AttributeKind::AlwaysInline.create_attr(cx.llcx))
+        }
         InlineAttr::Never => {
             if cx.sess().target.arch != "amdgpu" {
                 Some(AttributeKind::NoInline.create_attr(cx.llcx))
diff --git a/compiler/rustc_codegen_ssa/src/codegen_attrs.rs b/compiler/rustc_codegen_ssa/src/codegen_attrs.rs
index d536419ab3c20..b2e6549f8b065 100644
--- a/compiler/rustc_codegen_ssa/src/codegen_attrs.rs
+++ b/compiler/rustc_codegen_ssa/src/codegen_attrs.rs
@@ -1,4 +1,4 @@
-use rustc_ast::{MetaItemInner, MetaItemKind, ast, attr};
+use rustc_ast::{MetaItem, MetaItemInner, MetaItemKind, ast, attr};
 use rustc_attr::{InlineAttr, InstructionSetAttr, OptimizeAttr, list_contains_name};
 use rustc_data_structures::fx::FxHashMap;
 use rustc_errors::codes::*;
@@ -17,7 +17,7 @@ use rustc_middle::ty::{self as ty, TyCtxt};
 use rustc_session::parse::feature_err;
 use rustc_session::{Session, lint};
 use rustc_span::symbol::Ident;
-use rustc_span::{Span, sym};
+use rustc_span::{Span, Symbol, sym};
 use rustc_target::spec::{SanitizerSet, abi};
 
 use crate::errors::{self, MissingFeatures, TargetFeatureDisableOrEnable};
@@ -525,10 +525,36 @@ fn codegen_fn_attrs(tcx: TyCtxt<'_>, did: LocalDefId) -> CodegenFnAttrs {
                     struct_span_code_err!(tcx.dcx(), attr.span, E0534, "expected one argument")
                         .emit();
                     InlineAttr::None
+                } else if list_contains_name(items, sym::must) && tcx.features().required_inlining {
+                    parse_inline_must_required(
+                        tcx,
+                        items,
+                        attr.span,
+                        sym::must,
+                        |attr_span, reason| InlineAttr::Must { attr_span, reason },
+                    )
+                } else if list_contains_name(items, sym::required)
+                    && tcx.features().required_inlining
+                {
+                    parse_inline_must_required(
+                        tcx,
+                        items,
+                        attr.span,
+                        sym::required,
+                        |attr_span, reason| InlineAttr::Required { attr_span, reason },
+                    )
                 } else if list_contains_name(items, sym::always) {
                     InlineAttr::Always
                 } else if list_contains_name(items, sym::never) {
                     InlineAttr::Never
+                } else if tcx.features().required_inlining {
+                    struct_span_code_err!(tcx.dcx(), items[0].span(), E0535, "invalid argument")
+                        .with_help(
+                            "valid inline arguments are `required`, `must`, `always` and `never`",
+                        )
+                        .emit();
+
+                    InlineAttr::None
                 } else {
                     struct_span_code_err!(tcx.dcx(), items[0].span(), E0535, "invalid argument")
                         .with_help("valid inline arguments are `always` and `never`")
@@ -586,7 +612,7 @@ fn codegen_fn_attrs(tcx: TyCtxt<'_>, did: LocalDefId) -> CodegenFnAttrs {
     // is probably a poor usage of `#[inline(always)]` and easily avoided by not using the attribute.
     if tcx.features().target_feature_11
         && tcx.is_closure_like(did.to_def_id())
-        && codegen_fn_attrs.inline != InlineAttr::Always
+        && !codegen_fn_attrs.inline.always()
     {
         let owner_id = tcx.parent(did.to_def_id());
         if tcx.def_kind(owner_id).has_codegen_attrs() {
@@ -600,8 +626,7 @@ fn codegen_fn_attrs(tcx: TyCtxt<'_>, did: LocalDefId) -> CodegenFnAttrs {
     // purpose functions as they wouldn't have the right target features
     // enabled. For that reason we also forbid #[inline(always)] as it can't be
     // respected.
-    if !codegen_fn_attrs.target_features.is_empty() && codegen_fn_attrs.inline == InlineAttr::Always
-    {
+    if !codegen_fn_attrs.target_features.is_empty() && codegen_fn_attrs.inline.always() {
         if let Some(span) = inline_span {
             tcx.dcx().span_err(
                 span,
@@ -611,7 +636,7 @@ fn codegen_fn_attrs(tcx: TyCtxt<'_>, did: LocalDefId) -> CodegenFnAttrs {
         }
     }
 
-    if !codegen_fn_attrs.no_sanitize.is_empty() && codegen_fn_attrs.inline == InlineAttr::Always {
+    if !codegen_fn_attrs.no_sanitize.is_empty() && codegen_fn_attrs.inline.always() {
         if let (Some(no_sanitize_span), Some(inline_span)) = (no_sanitize_span, inline_span) {
             let hir_id = tcx.local_def_id_to_hir_id(did);
             tcx.node_span_lint(
@@ -706,6 +731,37 @@ pub fn check_tied_features(
     None
 }
 
+fn parse_inline_must_required<'tcx>(
+    tcx: TyCtxt<'tcx>,
+    items: &[MetaItemInner],
+    attr_span: Span,
+    expected_symbol: Symbol,
+    create: impl Fn(Span, Option<Symbol>) -> InlineAttr,
+) -> InlineAttr {
+    match items.iter().find(|i| i.has_name(expected_symbol)).expect("called on items w/out sym") {
+        MetaItemInner::MetaItem(mi @ MetaItem { kind: MetaItemKind::Word, .. }) => {
+            debug_assert!(mi.has_name(expected_symbol));
+            create(attr_span, None)
+        }
+        nested => {
+            if let Some((found_symbol, reason)) = nested.singleton_lit_list()
+                && reason.kind.is_str()
+            {
+                debug_assert_eq!(found_symbol, expected_symbol);
+                create(attr_span, reason.kind.str())
+            } else {
+                struct_span_code_err!(tcx.dcx(), attr_span, E0535, "invalid argument")
+                    .with_help(format!(
+                        "expected one string argument to `#[inline({})]`",
+                        expected_symbol.as_str()
+                    ))
+                    .emit();
+                create(attr_span, None)
+            }
+        }
+    }
+}
+
 /// Checks if the provided DefId is a method in a trait impl for a trait which has track_caller
 /// applied to the method prototype.
 fn should_inherit_track_caller(tcx: TyCtxt<'_>, def_id: DefId) -> bool {
diff --git a/compiler/rustc_feature/src/unstable.rs b/compiler/rustc_feature/src/unstable.rs
index c0398db9c101f..7b476ee9c58db 100644
--- a/compiler/rustc_feature/src/unstable.rs
+++ b/compiler/rustc_feature/src/unstable.rs
@@ -221,6 +221,8 @@ declare_features! (
     (internal, prelude_import, "1.2.0", None),
     /// Used to identify crates that contain the profiler runtime.
     (internal, profiler_runtime, "1.18.0", None),
+    /// Allows using `#[inline(required)]`/`#[inline(must)]`
+    (internal, required_inlining, "CURRENT_RUSTC_VERSION", None),
     /// Allows using `rustc_*` attributes (RFC 572).
     (internal, rustc_attrs, "1.0.0", None),
     /// Allows using the `#[stable]` and `#[unstable]` attributes.
diff --git a/compiler/rustc_lint_defs/src/builtin.rs b/compiler/rustc_lint_defs/src/builtin.rs
index 23dc5214fe2ea..edfab278e274a 100644
--- a/compiler/rustc_lint_defs/src/builtin.rs
+++ b/compiler/rustc_lint_defs/src/builtin.rs
@@ -67,6 +67,7 @@ declare_lint_pass! {
         MISSING_ABI,
         MISSING_FRAGMENT_SPECIFIER,
         MISSING_UNSAFE_ON_EXTERN,
+        MUST_INLINE,
         MUST_NOT_SUSPEND,
         NAMED_ARGUMENTS_USED_POSITIONALLY,
         NEVER_TYPE_FALLBACK_FLOWING_INTO_UNSAFE,
@@ -88,6 +89,7 @@ declare_lint_pass! {
         REFINING_IMPL_TRAIT_REACHABLE,
         RENAMED_AND_REMOVED_LINTS,
         REPR_TRANSPARENT_EXTERNAL_PRIVATE_FIELDS,
+        REQUIRED_INLINE,
         RUST_2021_INCOMPATIBLE_CLOSURE_CAPTURES,
         RUST_2021_INCOMPATIBLE_OR_PATTERNS,
         RUST_2021_PREFIXES_INCOMPATIBLE_SYNTAX,
@@ -5082,3 +5084,39 @@ declare_lint! {
     };
     crate_level_only
 }
+
+declare_lint! {
+    /// The `must_inline` lint is emitted when a function annotated with
+    /// `#[inline(must)]` was not able to be inlined.
+    ///
+    /// ### Explanation
+    ///
+    /// Functions can be marked as `#[inline(must)]` in the standard
+    /// library if they must be inlined in order to guarantee performance
+    /// characteristics or some other similar guarantee.
+    ///
+    /// In some circumstances, these functions cannot be inlined and a
+    /// reason will be provided, this can either be rectified or the
+    /// lint can be silenced if the risk is acceptable.
+    pub MUST_INLINE,
+    Warn,
+    "`#[inline(must)]`-annotated function could not be inlined"
+}
+
+declare_lint! {
+    /// The `required_inline` lint is emitted when a function annotated with
+    /// `#[inline(required)]` was not able to be inlined.
+    ///
+    /// ### Explanation
+    ///
+    /// Functions can be marked as `#[inline(required)]` in the standard
+    /// library if they are required to be inlined in order to uphold
+    /// security properties or some other similar guarantee.
+    ///
+    /// In some circumstances, these functions cannot be inlined and a
+    /// reason will be provided, this can either be rectified or the
+    /// lint can be silenced if the risk is acceptable.
+    pub REQUIRED_INLINE,
+    Deny,
+    "`#[inline(required)]`-annotated function could not be inlined"
+}
diff --git a/compiler/rustc_middle/src/mir/mono.rs b/compiler/rustc_middle/src/mir/mono.rs
index 56ca9167d4daa..ba241a6775e7e 100644
--- a/compiler/rustc_middle/src/mir/mono.rs
+++ b/compiler/rustc_middle/src/mir/mono.rs
@@ -1,7 +1,6 @@
 use std::fmt;
 use std::hash::Hash;
 
-use rustc_attr::InlineAttr;
 use rustc_data_structures::base_n::{BaseNString, CASE_INSENSITIVE, ToBaseN};
 use rustc_data_structures::fingerprint::Fingerprint;
 use rustc_data_structures::fx::FxIndexMap;
@@ -138,9 +137,10 @@ impl<'tcx> MonoItem<'tcx> {
                 // creating one copy of this `#[inline]` function which may
                 // conflict with upstream crates as it could be an exported
                 // symbol.
-                match tcx.codegen_fn_attrs(instance.def_id()).inline {
-                    InlineAttr::Always => InstantiationMode::LocalCopy,
-                    _ => InstantiationMode::GloballyShared { may_conflict: true },
+                if tcx.codegen_fn_attrs(instance.def_id()).inline.always() {
+                    InstantiationMode::LocalCopy
+                } else {
+                    InstantiationMode::GloballyShared { may_conflict: true }
                 }
             }
             MonoItem::Static(..) | MonoItem::GlobalAsm(..) => {
diff --git a/compiler/rustc_mir_transform/messages.ftl b/compiler/rustc_mir_transform/messages.ftl
index c8992b8b83430..a24b8b501d1f8 100644
--- a/compiler/rustc_mir_transform/messages.ftl
+++ b/compiler/rustc_mir_transform/messages.ftl
@@ -25,6 +25,17 @@ mir_transform_must_not_suspend = {$pre}`{$def_path}`{$post} held across a suspen
     .help = consider using a block (`{"{ ... }"}`) to shrink the value's scope, ending before the suspend point
 mir_transform_operation_will_panic = this operation will panic at runtime
 
+mir_transform_required_inline =
+    `{$callee}` could not be inlined into `{$caller}` but {$requires_or_must} inlined
+    .call = ...`{$callee}` called here
+    .attr = inlining due to this annotation
+    .caller = within `{$caller}`...
+    .callee = `{$callee}` defined here
+    .note = could not be inlined due to: {$reason}
+
+mir_transform_required_inline_justification =
+    `{$callee}` {$requires_or_must} inlined to: {$sym}
+
 mir_transform_unaligned_packed_ref = reference to packed field is unaligned
     .note = packed structs are only aligned by one byte, and many modern architectures penalize unaligned field accesses
     .note_ub = creating a misaligned reference is undefined behavior (even if that reference is never dereferenced)
diff --git a/compiler/rustc_mir_transform/src/cross_crate_inline.rs b/compiler/rustc_mir_transform/src/cross_crate_inline.rs
index 42cbece32d8c9..f69e8fe61ef64 100644
--- a/compiler/rustc_mir_transform/src/cross_crate_inline.rs
+++ b/compiler/rustc_mir_transform/src/cross_crate_inline.rs
@@ -8,8 +8,6 @@ use rustc_middle::ty::TyCtxt;
 use rustc_session::config::{InliningThreshold, OptLevel};
 use rustc_span::sym;
 
-use crate::{inline, pass_manager as pm};
-
 pub(super) fn provide(providers: &mut Providers) {
     providers.cross_crate_inlinable = cross_crate_inlinable;
 }
@@ -46,7 +44,10 @@ fn cross_crate_inlinable(tcx: TyCtxt<'_>, def_id: LocalDefId) -> bool {
     // #[inline(never)] to force code generation.
     match codegen_fn_attrs.inline {
         InlineAttr::Never => return false,
-        InlineAttr::Hint | InlineAttr::Always => return true,
+        InlineAttr::Hint
+        | InlineAttr::Always
+        | InlineAttr::Required { .. }
+        | InlineAttr::Must { .. } => return true,
         _ => {}
     }
 
@@ -59,7 +60,8 @@ fn cross_crate_inlinable(tcx: TyCtxt<'_>, def_id: LocalDefId) -> bool {
     // Don't do any inference if codegen optimizations are disabled and also MIR inlining is not
     // enabled. This ensures that we do inference even if someone only passes -Zinline-mir,
     // which is less confusing than having to also enable -Copt-level=1.
-    if matches!(tcx.sess.opts.optimize, OptLevel::No) && !pm::should_run_pass(tcx, &inline::Inline)
+    if matches!(tcx.sess.opts.optimize, OptLevel::No)
+        && !crate::inline::should_run_pass_for_item(tcx, def_id.to_def_id())
     {
         return false;
     }
diff --git a/compiler/rustc_mir_transform/src/errors.rs b/compiler/rustc_mir_transform/src/errors.rs
index 8b309147c64cd..971031ae32d18 100644
--- a/compiler/rustc_mir_transform/src/errors.rs
+++ b/compiler/rustc_mir_transform/src/errors.rs
@@ -4,8 +4,8 @@ use rustc_macros::{Diagnostic, LintDiagnostic, Subdiagnostic};
 use rustc_middle::mir::AssertKind;
 use rustc_middle::ty::TyCtxt;
 use rustc_session::lint::{self, Lint};
-use rustc_span::Span;
 use rustc_span::def_id::DefId;
+use rustc_span::{Span, Symbol};
 
 use crate::fluent_generated as fluent;
 
@@ -136,3 +136,29 @@ pub(crate) struct MustNotSuspendReason {
 #[note(mir_transform_note2)]
 #[help]
 pub(crate) struct UndefinedTransmute;
+
+#[derive(LintDiagnostic)]
+#[diag(mir_transform_required_inline)]
+#[note]
+pub(crate) struct RequiredInline {
+    pub requires_or_must: &'static str,
+    #[label(mir_transform_caller)]
+    pub caller_span: Span,
+    #[label(mir_transform_callee)]
+    pub callee_span: Span,
+    #[label(mir_transform_attr)]
+    pub attr_span: Span,
+    #[label(mir_transform_call)]
+    pub call_span: Span,
+    pub callee: String,
+    pub caller: String,
+    pub reason: &'static str,
+    #[subdiagnostic]
+    pub justification: Option<RequiredInlineJustification>,
+}
+
+#[derive(Subdiagnostic)]
+#[note(mir_transform_required_inline_justification)]
+pub(crate) struct RequiredInlineJustification {
+    pub sym: Symbol,
+}
diff --git a/compiler/rustc_mir_transform/src/inline.rs b/compiler/rustc_mir_transform/src/inline.rs
index c9f24764cc2a1..33abec90f87a2 100644
--- a/compiler/rustc_mir_transform/src/inline.rs
+++ b/compiler/rustc_mir_transform/src/inline.rs
@@ -5,7 +5,7 @@ use std::ops::{Range, RangeFrom};
 
 use rustc_attr::InlineAttr;
 use rustc_hir::def::DefKind;
-use rustc_hir::def_id::DefId;
+use rustc_hir::def_id::{DefId, LocalDefId};
 use rustc_index::Idx;
 use rustc_index::bit_set::BitSet;
 use rustc_middle::bug;
@@ -16,6 +16,7 @@ use rustc_middle::ty::{
     self, Instance, InstanceKind, ParamEnv, Ty, TyCtxt, TypeFlags, TypeVisitableExt,
 };
 use rustc_session::config::{DebugInfo, OptLevel};
+use rustc_session::lint::builtin::{MUST_INLINE, REQUIRED_INLINE};
 use rustc_span::source_map::Spanned;
 use rustc_span::sym;
 use rustc_target::abi::FieldIdx;
@@ -24,6 +25,7 @@ use tracing::{debug, instrument, trace, trace_span};
 
 use crate::cost_checker::CostChecker;
 use crate::deref_separator::deref_finder;
+use crate::errors::RequiredInlineJustification;
 use crate::simplify::simplify_cfg;
 use crate::util;
 use crate::validate::validate_types;
@@ -45,26 +47,8 @@ struct CallSite<'tcx> {
 }
 
 impl<'tcx> crate::MirPass<'tcx> for Inline {
-    fn is_enabled(&self, sess: &rustc_session::Session) -> bool {
-        // FIXME(#127234): Coverage instrumentation currently doesn't handle inlined
-        // MIR correctly when Modified Condition/Decision Coverage is enabled.
-        if sess.instrument_coverage_mcdc() {
-            return false;
-        }
-
-        if let Some(enabled) = sess.opts.unstable_opts.inline_mir {
-            return enabled;
-        }
-
-        match sess.mir_opt_level() {
-            0 | 1 => false,
-            2 => {
-                (sess.opts.optimize == OptLevel::Default
-                    || sess.opts.optimize == OptLevel::Aggressive)
-                    && sess.opts.incremental == None
-            }
-            _ => true,
-        }
+    fn is_enabled(&self, _: &rustc_session::Session) -> bool {
+        true
     }
 
     fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
@@ -78,6 +62,30 @@ impl<'tcx> crate::MirPass<'tcx> for Inline {
     }
 }
 
+/// Is this pass enabled for a given item? Can't just rely on `MirPass::is_enabled` as a body having
+/// `#[inline(required)]` should be inlined even if every other item isn't.
+pub fn should_run_pass_for_item<'tcx>(tcx: TyCtxt<'tcx>, did: DefId) -> bool {
+    if crate::pass_manager::pass_is_overridden(tcx, &Inline) {
+        return true;
+    }
+
+    if let Some(enabled) = tcx.sess.opts.unstable_opts.inline_mir {
+        return enabled;
+    }
+
+    let codegen_fn_attrs = tcx.codegen_fn_attrs(did);
+    codegen_fn_attrs.inline.required()
+        || match tcx.sess.mir_opt_level() {
+            0 | 1 => false,
+            2 => {
+                (tcx.sess.opts.optimize == OptLevel::Default
+                    || tcx.sess.opts.optimize == OptLevel::Aggressive)
+                    && tcx.sess.opts.incremental == None
+            }
+            _ => true,
+        }
+}
+
 fn inline<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) -> bool {
     let def_id = body.source.def_id().expect_local();
 
@@ -101,12 +109,16 @@ fn inline<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) -> bool {
     let mut this = Inliner {
         tcx,
         param_env,
+        def_id,
         codegen_fn_attrs,
         history: Vec::new(),
         changed: false,
         caller_is_inline_forwarder: matches!(
             codegen_fn_attrs.inline,
-            InlineAttr::Hint | InlineAttr::Always
+            InlineAttr::Hint
+                | InlineAttr::Always
+                | InlineAttr::Required { .. }
+                | InlineAttr::Must { .. }
         ) && body_is_forwarder(body),
     };
     let blocks = START_BLOCK..body.basic_blocks.next_index();
@@ -117,6 +129,8 @@ fn inline<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) -> bool {
 struct Inliner<'tcx> {
     tcx: TyCtxt<'tcx>,
     param_env: ParamEnv<'tcx>,
+    /// `DefId` of caller.
+    def_id: LocalDefId,
     /// Caller codegen attributes.
     codegen_fn_attrs: &'tcx CodegenFnAttrs,
     /// Stack of inlined instances.
@@ -155,9 +169,20 @@ impl<'tcx> Inliner<'tcx> {
             let span = trace_span!("process_blocks", %callsite.callee, ?bb);
             let _guard = span.enter();
 
+            if !should_run_pass_for_item(self.tcx, callsite.callee.def_id()) {
+                debug!("not enabled");
+                continue;
+            }
+
             match self.try_inlining(caller_body, &callsite) {
                 Err(reason) => {
                     debug!("not-inlined {} [{}]", callsite.callee, reason);
+                    if matches!(
+                        self.tcx.codegen_fn_attrs(callsite.callee.def_id()).inline,
+                        InlineAttr::Required { .. } | InlineAttr::Must { .. }
+                    ) {
+                        self.report_required_error(&callsite, reason);
+                    }
                 }
                 Ok(new_blocks) => {
                     debug!("inlined {}", callsite.callee);
@@ -194,8 +219,10 @@ impl<'tcx> Inliner<'tcx> {
         // Intrinsic fallback bodies are automatically made cross-crate inlineable,
         // but at this stage we don't know whether codegen knows the intrinsic,
         // so just conservatively don't inline it.
-        if self.tcx.has_attr(callsite.callee.def_id(), sym::rustc_intrinsic) {
-            return Err("Callee is an intrinsic, do not inline fallback bodies");
+        if self.tcx.has_attr(callsite.callee.def_id(), sym::rustc_intrinsic)
+            && !callee_attrs.inline.required()
+        {
+            return Err("callee is an intrinsic");
         }
 
         let terminator = caller_body[callsite.block].terminator.as_ref().unwrap();
@@ -205,7 +232,7 @@ impl<'tcx> Inliner<'tcx> {
             if !arg.node.ty(&caller_body.local_decls, self.tcx).is_sized(self.tcx, self.param_env) {
                 // We do not allow inlining functions with unsized params. Inlining these functions
                 // could create unsized locals, which are unsound and being phased out.
-                return Err("Call has unsized argument");
+                return Err("call has unsized argument");
             }
         }
 
@@ -214,7 +241,8 @@ impl<'tcx> Inliner<'tcx> {
 
         if !self.tcx.consider_optimizing(|| {
             format!("Inline {:?} into {:?}", callsite.callee, caller_body.source)
-        }) {
+        }) && !callee_attrs.inline.required()
+        {
             return Err("optimization fuel exhausted");
         }
 
@@ -223,7 +251,8 @@ impl<'tcx> Inliner<'tcx> {
             self.param_env,
             ty::EarlyBinder::bind(callee_body.clone()),
         ) else {
-            return Err("failed to normalize callee body");
+            debug!("failed to normalize callee body");
+            return Err("implementation limitation");
         };
 
         // Normally, this shouldn't be required, but trait normalization failure can create a
@@ -237,7 +266,8 @@ impl<'tcx> Inliner<'tcx> {
         )
         .is_empty()
         {
-            return Err("failed to validate callee body");
+            debug!("failed to validate callee body");
+            return Err("implementation limitation");
         }
 
         // Check call signature compatibility.
@@ -247,14 +277,15 @@ impl<'tcx> Inliner<'tcx> {
         if !util::relate_types(self.tcx, self.param_env, ty::Covariant, output_type, destination_ty)
         {
             trace!(?output_type, ?destination_ty);
-            return Err("failed to normalize return type");
+            debug!("failed to normalize return type");
+            return Err("implementation limitation");
         }
         if callsite.fn_sig.abi() == Abi::RustCall {
             // FIXME: Don't inline user-written `extern "rust-call"` functions,
             // since this is generally perf-negative on rustc, and we hope that
             // LLVM will inline these functions instead.
             if callee_body.spread_arg.is_some() {
-                return Err("do not inline user-written rust-call functions");
+                return Err("user-written rust-call functions");
             }
 
             let (self_arg, arg_tuple) = match &args[..] {
@@ -278,7 +309,8 @@ impl<'tcx> Inliner<'tcx> {
                 if !util::relate_types(self.tcx, self.param_env, ty::Covariant, input_type, arg_ty)
                 {
                     trace!(?arg_ty, ?input_type);
-                    return Err("failed to normalize tuple argument type");
+                    debug!("failed to normalize tuple argument type");
+                    return Err("implementation limitation");
                 }
             }
         } else {
@@ -288,7 +320,8 @@ impl<'tcx> Inliner<'tcx> {
                 if !util::relate_types(self.tcx, self.param_env, ty::Covariant, input_type, arg_ty)
                 {
                     trace!(?arg_ty, ?input_type);
-                    return Err("failed to normalize argument type");
+                    debug!("failed to normalize argument type");
+                    return Err("implementation limitation");
                 }
             }
         }
@@ -317,12 +350,14 @@ impl<'tcx> Inliner<'tcx> {
                 // because it has no MIR because it's an extern function), then the inliner
                 // won't cause cycles on this.
                 if !self.tcx.is_mir_available(callee_def_id) {
-                    return Err("item MIR unavailable");
+                    debug!("item MIR unavailable");
+                    return Err("implementation limitation");
                 }
             }
             // These have no own callable MIR.
             InstanceKind::Intrinsic(_) | InstanceKind::Virtual(..) => {
-                return Err("instance without MIR (intrinsic / virtual)");
+                debug!("instance without MIR (intrinsic / virtual)");
+                return Err("implementation limitation");
             }
 
             // FIXME(#127030): `ConstParamHasTy` has bad interactions with
@@ -331,7 +366,8 @@ impl<'tcx> Inliner<'tcx> {
             // the MIR for this instance until all of its const params are
             // substituted.
             InstanceKind::DropGlue(_, Some(ty)) if ty.has_type_flags(TypeFlags::HAS_CT_PARAM) => {
-                return Err("still needs substitution");
+                debug!("still needs substitution");
+                return Err("implementation limitation");
             }
 
             // This cannot result in an immediate cycle since the callee MIR is a shim, which does
@@ -360,7 +396,8 @@ impl<'tcx> Inliner<'tcx> {
             // If we know for sure that the function we're calling will itself try to
             // call us, then we avoid inlining that function.
             if self.tcx.mir_callgraph_reachable((callee, caller_def_id.expect_local())) {
-                return Err("caller might be reachable from callee (query cycle avoidance)");
+                debug!("query cycle avoidance");
+                return Err("caller might be reachable from callee");
             }
 
             Ok(())
@@ -434,7 +471,13 @@ impl<'tcx> Inliner<'tcx> {
         }
 
         if let InlineAttr::Never = callee_attrs.inline {
-            return Err("never inline hint");
+            return Err("never inline attribute");
+        }
+
+        // FIXME(#127234): Coverage instrumentation currently doesn't handle inlined
+        // MIR correctly when Modified Condition/Decision Coverage is enabled.
+        if self.tcx.sess.instrument_coverage_mcdc() {
+            return Err("incompatible with MC/DC coverage");
         }
 
         // Reachability pass defines which functions are eligible for inlining. Generally inlining
@@ -497,7 +540,14 @@ impl<'tcx> Inliner<'tcx> {
         let tcx = self.tcx;
 
         if let Some(_) = callee_body.tainted_by_errors {
-            return Err("Body is tainted");
+            return Err("body has errors");
+        }
+
+        // Callees which require inlining must be inlined to maintain security properties or
+        // for performance reasons, so skip any heuristics.
+        if callee_attrs.inline.required() {
+            debug!("INLINING {:?} [required]", callsite);
+            return Ok(());
         }
 
         let mut threshold = if self.caller_is_inline_forwarder {
@@ -556,7 +606,7 @@ impl<'tcx> Inliner<'tcx> {
                 // assign one. However, during this stage we require an exact match when any
                 // inline-asm is detected. LLVM will still possibly do an inline later on
                 // if the no-attribute function ends up with the same instruction set anyway.
-                return Err("Cannot move inline-asm across instruction sets");
+                return Err("cannot move inline-asm across instruction sets");
             } else if let TerminatorKind::TailCall { .. } = term.kind {
                 // FIXME(explicit_tail_calls): figure out how exactly functions containing tail
                 // calls can be inlined (and if they even should)
@@ -893,6 +943,34 @@ impl<'tcx> Inliner<'tcx> {
 
         local
     }
+
+    fn report_required_error(&self, callsite: &CallSite<'tcx>, reason: &'static str) {
+        let (deny, attr_span, justification) =
+            match self.tcx.codegen_fn_attrs(callsite.callee.def_id()).inline {
+                InlineAttr::Required { attr_span, reason } => (true, attr_span, reason),
+                InlineAttr::Must { attr_span, reason } => (false, attr_span, reason),
+                _ => bug!("called on item without required inlining"),
+            };
+
+        let hir_id = self.tcx.local_def_id_to_hir_id(self.def_id);
+        let call_span = callsite.source_info.span;
+        self.tcx.emit_node_span_lint(
+            if deny { REQUIRED_INLINE } else { MUST_INLINE },
+            hir_id,
+            call_span,
+            crate::errors::RequiredInline {
+                requires_or_must: if deny { "is required to be" } else { "must be" },
+                call_span,
+                attr_span,
+                caller_span: self.tcx.def_span(self.def_id),
+                caller: self.tcx.def_path_str(self.def_id),
+                callee_span: self.tcx.def_span(callsite.callee.def_id()),
+                callee: self.tcx.def_path_str(callsite.callee.def_id()),
+                reason,
+                justification: justification.map(|sym| RequiredInlineJustification { sym }),
+            },
+        );
+    }
 }
 
 /**
diff --git a/compiler/rustc_mir_transform/src/pass_manager.rs b/compiler/rustc_mir_transform/src/pass_manager.rs
index 29f8b4f6e4ddf..b90bfa118d5b0 100644
--- a/compiler/rustc_mir_transform/src/pass_manager.rs
+++ b/compiler/rustc_mir_transform/src/pass_manager.rs
@@ -169,7 +169,7 @@ pub(super) fn run_passes<'tcx>(
     run_passes_inner(tcx, body, passes, phase_change, true);
 }
 
-pub(super) fn should_run_pass<'tcx, P>(tcx: TyCtxt<'tcx>, pass: &P) -> bool
+pub(super) fn pass_is_overridden<'tcx, P>(tcx: TyCtxt<'tcx>, pass: &P) -> bool
 where
     P: MirPass<'tcx> + ?Sized,
 {
@@ -185,7 +185,14 @@ where
             );
             *polarity
         });
-    overridden.unwrap_or_else(|| pass.is_enabled(tcx.sess))
+    overridden.unwrap_or(false)
+}
+
+pub(super) fn should_run_pass<'tcx, P>(tcx: TyCtxt<'tcx>, pass: &P) -> bool
+where
+    P: MirPass<'tcx> + ?Sized,
+{
+    pass_is_overridden(tcx, pass) || pass.is_enabled(tcx.sess)
 }
 
 fn run_passes_inner<'tcx>(
diff --git a/compiler/rustc_span/src/symbol.rs b/compiler/rustc_span/src/symbol.rs
index cc3bda99a117b..669e9e8346a16 100644
--- a/compiler/rustc_span/src/symbol.rs
+++ b/compiler/rustc_span/src/symbol.rs
@@ -1283,6 +1283,7 @@ symbols! {
         mul_assign,
         mul_with_overflow,
         multiple_supertrait_upcastable,
+        must,
         must_not_suspend,
         must_use,
         mut_preserve_binding_mode_2024,
@@ -1606,6 +1607,8 @@ symbols! {
         repr_simd,
         repr_transparent,
         require,
+        required,
+        required_inlining,
         residual,
         result,
         result_ffi_guarantees,
diff --git a/tests/mir-opt/inline/required_no_opt.caller.Inline.diff b/tests/mir-opt/inline/required_no_opt.caller.Inline.diff
new file mode 100644
index 0000000000000..1d0b3c59e679f
--- /dev/null
+++ b/tests/mir-opt/inline/required_no_opt.caller.Inline.diff
@@ -0,0 +1,30 @@
+- // MIR for `caller` before Inline
++ // MIR for `caller` after Inline
+  
+  fn caller() -> () {
+      let mut _0: ();
+      let _1: ();
+      let _2: ();
++     scope 1 (inlined callee_required) {
++     }
++     scope 2 (inlined callee_must) {
++     }
+  
+      bb0: {
+          StorageLive(_1);
+-         _1 = callee_required() -> [return: bb1, unwind continue];
+-     }
+- 
+-     bb1: {
+          StorageDead(_1);
+          StorageLive(_2);
+-         _2 = callee_must() -> [return: bb2, unwind continue];
+-     }
+- 
+-     bb2: {
+          StorageDead(_2);
+          _0 = const ();
+          return;
+      }
+  }
+  
diff --git a/tests/mir-opt/inline/required_no_opt.rs b/tests/mir-opt/inline/required_no_opt.rs
new file mode 100644
index 0000000000000..6069fcf4aba0e
--- /dev/null
+++ b/tests/mir-opt/inline/required_no_opt.rs
@@ -0,0 +1,18 @@
+// EMIT_MIT_FOR_EACH_PANIC_STRATEGY
+//@ compile-flags: -Copt-level=0 --crate-type=lib
+#![feature(required_inlining)]
+
+#[inline(required)]
+pub fn callee_required() {}
+
+#[inline(must)]
+pub fn callee_must() {}
+
+// EMIT_MIR required_no_opt.caller.Inline.diff
+pub fn caller() {
+    callee_required();
+    callee_must();
+    // CHECK-LABEL: fn caller(
+    // CHECK: (inlined callee_required)
+    // CHECK: (inlined callee_must)
+}
diff --git a/tests/ui/required-inlining/auxiliary/callees.rs b/tests/ui/required-inlining/auxiliary/callees.rs
new file mode 100644
index 0000000000000..70c96abc5ff89
--- /dev/null
+++ b/tests/ui/required-inlining/auxiliary/callees.rs
@@ -0,0 +1,10 @@
+//@ compile-flags: --crate-type=lib
+#![feature(required_inlining)]
+
+#[inline(required("maintain security properties"))]
+pub fn required() {
+}
+
+#[inline(must("maintain security properties"))]
+pub fn must() {
+}
diff --git a/tests/ui/required-inlining/cross-crate.rs b/tests/ui/required-inlining/cross-crate.rs
new file mode 100644
index 0000000000000..481ae9baed985
--- /dev/null
+++ b/tests/ui/required-inlining/cross-crate.rs
@@ -0,0 +1,14 @@
+//@ aux-build:callees.rs
+//@ build-pass
+//@ compile-flags: --crate-type=lib
+#![feature(required_inlining)]
+
+extern crate callees;
+
+// Test that required inlining across crates works as expected.
+
+pub fn caller() {
+    callees::required();
+
+    callees::must();
+}
diff --git a/tests/ui/required-inlining/deny.rs b/tests/ui/required-inlining/deny.rs
new file mode 100644
index 0000000000000..a49fbc427722d
--- /dev/null
+++ b/tests/ui/required-inlining/deny.rs
@@ -0,0 +1,34 @@
+//@ build-fail
+//@ compile-flags: --crate-type=lib
+#![allow(internal_features)]
+#![feature(rustc_attrs)]
+#![feature(required_inlining)]
+
+// Test that required inlining w/ errors works as expected.
+
+#[rustc_no_mir_inline]
+#[inline(required)]
+pub fn callee() {
+}
+
+#[rustc_no_mir_inline]
+#[inline(required("maintain security properties"))]
+pub fn callee_justified() {
+}
+
+pub fn caller() {
+    callee();
+//~^ ERROR `callee` could not be inlined into `caller` but is required to be inlined
+
+    callee_justified();
+//~^ ERROR `callee_justified` could not be inlined into `caller` but is required to be inlined
+}
+
+#[warn(required_inline)]
+pub fn caller_overridden() {
+    callee();
+//~^ WARN `callee` could not be inlined into `caller_overridden` but is required to be inlined
+
+    callee_justified();
+//~^ WARN `callee_justified` could not be inlined into `caller_overridden` but is required to be inlined
+}
diff --git a/tests/ui/required-inlining/deny.stderr b/tests/ui/required-inlining/deny.stderr
new file mode 100644
index 0000000000000..e4e92e525f3b2
--- /dev/null
+++ b/tests/ui/required-inlining/deny.stderr
@@ -0,0 +1,72 @@
+error: `callee` could not be inlined into `caller` but is required to be inlined
+  --> $DIR/deny.rs:20:5
+   |
+LL | #[inline(required)]
+   | ------------------- inlining due to this annotation
+LL | pub fn callee() {
+   | --------------- `callee` defined here
+...
+LL | pub fn caller() {
+   | --------------- within `caller`...
+LL |     callee();
+   |     ^^^^^^^^ ...`callee` called here
+   |
+   = note: could not be inlined due to: #[rustc_no_mir_inline]
+   = note: `#[deny(required_inline)]` on by default
+
+error: `callee_justified` could not be inlined into `caller` but is required to be inlined
+  --> $DIR/deny.rs:23:5
+   |
+LL | #[inline(required("maintain security properties"))]
+   | --------------------------------------------------- inlining due to this annotation
+LL | pub fn callee_justified() {
+   | ------------------------- `callee_justified` defined here
+...
+LL | pub fn caller() {
+   | --------------- within `caller`...
+...
+LL |     callee_justified();
+   |     ^^^^^^^^^^^^^^^^^^ ...`callee_justified` called here
+   |
+   = note: could not be inlined due to: #[rustc_no_mir_inline]
+   = note: `callee_justified` is required to be inlined to: maintain security properties
+
+warning: `callee` could not be inlined into `caller_overridden` but is required to be inlined
+  --> $DIR/deny.rs:29:5
+   |
+LL | #[inline(required)]
+   | ------------------- inlining due to this annotation
+LL | pub fn callee() {
+   | --------------- `callee` defined here
+...
+LL | pub fn caller_overridden() {
+   | -------------------------- within `caller_overridden`...
+LL |     callee();
+   |     ^^^^^^^^ ...`callee` called here
+   |
+   = note: could not be inlined due to: #[rustc_no_mir_inline]
+note: the lint level is defined here
+  --> $DIR/deny.rs:27:8
+   |
+LL | #[warn(required_inline)]
+   |        ^^^^^^^^^^^^^^^
+
+warning: `callee_justified` could not be inlined into `caller_overridden` but is required to be inlined
+  --> $DIR/deny.rs:32:5
+   |
+LL | #[inline(required("maintain security properties"))]
+   | --------------------------------------------------- inlining due to this annotation
+LL | pub fn callee_justified() {
+   | ------------------------- `callee_justified` defined here
+...
+LL | pub fn caller_overridden() {
+   | -------------------------- within `caller_overridden`...
+...
+LL |     callee_justified();
+   |     ^^^^^^^^^^^^^^^^^^ ...`callee_justified` called here
+   |
+   = note: could not be inlined due to: #[rustc_no_mir_inline]
+   = note: `callee_justified` is required to be inlined to: maintain security properties
+
+error: aborting due to 2 previous errors; 2 warnings emitted
+
diff --git a/tests/ui/required-inlining/gate.rs b/tests/ui/required-inlining/gate.rs
new file mode 100644
index 0000000000000..7eae08f6c246d
--- /dev/null
+++ b/tests/ui/required-inlining/gate.rs
@@ -0,0 +1,32 @@
+// gate-test-required_inlining
+//@ compile-flags: --crate-type=lib
+#![allow(internal_features)]
+#![feature(rustc_attrs)]
+
+#[rustc_no_mir_inline]
+#[inline(required)]
+//~^ ERROR invalid argument
+//~^^ HELP valid inline arguments are `always` and `never`
+pub fn bare_required() {
+}
+
+#[rustc_no_mir_inline]
+#[inline(required("justification"))]
+//~^ ERROR invalid argument
+//~^^ HELP valid inline arguments are `always` and `never`
+pub fn justified_required() {
+}
+
+#[rustc_no_mir_inline]
+#[inline(must)]
+//~^ ERROR invalid argument
+//~^^ HELP valid inline arguments are `always` and `never`
+pub fn bare_must() {
+}
+
+#[rustc_no_mir_inline]
+#[inline(must("justification"))]
+//~^ ERROR invalid argument
+//~^^ HELP valid inline arguments are `always` and `never`
+pub fn justified_must() {
+}
diff --git a/tests/ui/required-inlining/gate.stderr b/tests/ui/required-inlining/gate.stderr
new file mode 100644
index 0000000000000..f976aaf6120ea
--- /dev/null
+++ b/tests/ui/required-inlining/gate.stderr
@@ -0,0 +1,35 @@
+error[E0535]: invalid argument
+  --> $DIR/gate.rs:6:10
+   |
+LL | #[inline(required)]
+   |          ^^^^^^^^
+   |
+   = help: valid inline arguments are `always` and `never`
+
+error[E0535]: invalid argument
+  --> $DIR/gate.rs:13:10
+   |
+LL | #[inline(required("justification"))]
+   |          ^^^^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = help: valid inline arguments are `always` and `never`
+
+error[E0535]: invalid argument
+  --> $DIR/gate.rs:20:10
+   |
+LL | #[inline(must)]
+   |          ^^^^
+   |
+   = help: valid inline arguments are `always` and `never`
+
+error[E0535]: invalid argument
+  --> $DIR/gate.rs:27:10
+   |
+LL | #[inline(must("justification"))]
+   |          ^^^^^^^^^^^^^^^^^^^^^
+   |
+   = help: valid inline arguments are `always` and `never`
+
+error: aborting due to 4 previous errors
+
+For more information about this error, try `rustc --explain E0535`.
diff --git a/tests/ui/required-inlining/invalid.rs b/tests/ui/required-inlining/invalid.rs
new file mode 100644
index 0000000000000..87292c37a6193
--- /dev/null
+++ b/tests/ui/required-inlining/invalid.rs
@@ -0,0 +1,60 @@
+//@ compile-flags: --crate-type=lib
+#![allow(internal_features)]
+#![feature(rustc_attrs)]
+#![feature(required_inlining)]
+
+// Test that invalid required/must inlining attributes error as expected.
+
+#[inline(required = "bar")]
+//~^ ERROR invalid argument
+//~^^ HELP expected one string argument to `#[inline(required)]`
+pub fn reqd1() {
+}
+
+#[inline(required(bar, baz))]
+//~^ ERROR invalid argument
+//~^^ HELP expected one string argument to `#[inline(required)]`
+pub fn reqd2() {
+}
+
+#[inline(required(2))]
+//~^ ERROR invalid argument
+//~^^ HELP expected one string argument to `#[inline(required)]`
+pub fn reqd3() {
+}
+
+#[inline(required = 2)]
+//~^ ERROR invalid argument
+//~^^ HELP expected one string argument to `#[inline(required)]`
+pub fn reqd4() {
+}
+
+#[inline(must = "bar")]
+//~^ ERROR invalid argument
+//~^^ HELP expected one string argument to `#[inline(must)]`
+pub fn must1() {
+}
+
+#[inline(must(bar, baz))]
+//~^ ERROR invalid argument
+//~^^ HELP expected one string argument to `#[inline(must)]`
+pub fn must2() {
+}
+
+#[inline(must(2))]
+//~^ ERROR invalid argument
+//~^^ HELP expected one string argument to `#[inline(must)]`
+pub fn must3() {
+}
+
+#[inline(must = 2)]
+//~^ ERROR invalid argument
+//~^^ HELP expected one string argument to `#[inline(must)]`
+pub fn must4() {
+}
+
+#[inline(banana)]
+//~^ ERROR invalid argument
+//~^^ HELP valid inline arguments are `required`, `must`, `always` and `never`
+pub fn bare() {
+}
diff --git a/tests/ui/required-inlining/invalid.stderr b/tests/ui/required-inlining/invalid.stderr
new file mode 100644
index 0000000000000..95c5f9d747bb2
--- /dev/null
+++ b/tests/ui/required-inlining/invalid.stderr
@@ -0,0 +1,75 @@
+error[E0535]: invalid argument
+  --> $DIR/invalid.rs:8:1
+   |
+LL | #[inline(required = "bar")]
+   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = help: expected one string argument to `#[inline(required)]`
+
+error[E0535]: invalid argument
+  --> $DIR/invalid.rs:14:1
+   |
+LL | #[inline(required(bar, baz))]
+   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = help: expected one string argument to `#[inline(required)]`
+
+error[E0535]: invalid argument
+  --> $DIR/invalid.rs:20:1
+   |
+LL | #[inline(required(2))]
+   | ^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = help: expected one string argument to `#[inline(required)]`
+
+error[E0535]: invalid argument
+  --> $DIR/invalid.rs:26:1
+   |
+LL | #[inline(required = 2)]
+   | ^^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = help: expected one string argument to `#[inline(required)]`
+
+error[E0535]: invalid argument
+  --> $DIR/invalid.rs:32:1
+   |
+LL | #[inline(must = "bar")]
+   | ^^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = help: expected one string argument to `#[inline(must)]`
+
+error[E0535]: invalid argument
+  --> $DIR/invalid.rs:38:1
+   |
+LL | #[inline(must(bar, baz))]
+   | ^^^^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = help: expected one string argument to `#[inline(must)]`
+
+error[E0535]: invalid argument
+  --> $DIR/invalid.rs:44:1
+   |
+LL | #[inline(must(2))]
+   | ^^^^^^^^^^^^^^^^^^
+   |
+   = help: expected one string argument to `#[inline(must)]`
+
+error[E0535]: invalid argument
+  --> $DIR/invalid.rs:50:1
+   |
+LL | #[inline(must = 2)]
+   | ^^^^^^^^^^^^^^^^^^^
+   |
+   = help: expected one string argument to `#[inline(must)]`
+
+error[E0535]: invalid argument
+  --> $DIR/invalid.rs:56:10
+   |
+LL | #[inline(banana)]
+   |          ^^^^^^
+   |
+   = help: valid inline arguments are `required`, `must`, `always` and `never`
+
+error: aborting due to 9 previous errors
+
+For more information about this error, try `rustc --explain E0535`.
diff --git a/tests/ui/required-inlining/warn.rs b/tests/ui/required-inlining/warn.rs
new file mode 100644
index 0000000000000..08c87933ceef7
--- /dev/null
+++ b/tests/ui/required-inlining/warn.rs
@@ -0,0 +1,34 @@
+//@ build-fail
+//@ compile-flags: --crate-type=lib
+#![allow(internal_features)]
+#![feature(rustc_attrs)]
+#![feature(required_inlining)]
+
+// Test that required inlining w/ warnings works as expected.
+
+#[rustc_no_mir_inline]
+#[inline(must)]
+pub fn callee() {
+}
+
+#[rustc_no_mir_inline]
+#[inline(must("maintain performance characteristics"))]
+pub fn callee_justified() {
+}
+
+pub fn caller() {
+    callee();
+//~^ WARN `callee` could not be inlined into `caller` but must be inlined
+
+    callee_justified();
+//~^ WARN `callee_justified` could not be inlined into `caller` but must be inlined
+}
+
+#[deny(must_inline)]
+pub fn caller_overridden() {
+    callee();
+//~^ ERROR `callee` could not be inlined into `caller_overridden` but must be inlined
+
+    callee_justified();
+//~^ ERROR `callee_justified` could not be inlined into `caller_overridden` but must be inlined
+}
diff --git a/tests/ui/required-inlining/warn.stderr b/tests/ui/required-inlining/warn.stderr
new file mode 100644
index 0000000000000..aa748de95889b
--- /dev/null
+++ b/tests/ui/required-inlining/warn.stderr
@@ -0,0 +1,72 @@
+warning: `callee` could not be inlined into `caller` but must be inlined
+  --> $DIR/warn.rs:20:5
+   |
+LL | #[inline(must)]
+   | --------------- inlining due to this annotation
+LL | pub fn callee() {
+   | --------------- `callee` defined here
+...
+LL | pub fn caller() {
+   | --------------- within `caller`...
+LL |     callee();
+   |     ^^^^^^^^ ...`callee` called here
+   |
+   = note: could not be inlined due to: #[rustc_no_mir_inline]
+   = note: `#[warn(must_inline)]` on by default
+
+warning: `callee_justified` could not be inlined into `caller` but must be inlined
+  --> $DIR/warn.rs:23:5
+   |
+LL | #[inline(must("maintain performance characteristics"))]
+   | ------------------------------------------------------- inlining due to this annotation
+LL | pub fn callee_justified() {
+   | ------------------------- `callee_justified` defined here
+...
+LL | pub fn caller() {
+   | --------------- within `caller`...
+...
+LL |     callee_justified();
+   |     ^^^^^^^^^^^^^^^^^^ ...`callee_justified` called here
+   |
+   = note: could not be inlined due to: #[rustc_no_mir_inline]
+   = note: `callee_justified` must be inlined to: maintain performance characteristics
+
+error: `callee` could not be inlined into `caller_overridden` but must be inlined
+  --> $DIR/warn.rs:29:5
+   |
+LL | #[inline(must)]
+   | --------------- inlining due to this annotation
+LL | pub fn callee() {
+   | --------------- `callee` defined here
+...
+LL | pub fn caller_overridden() {
+   | -------------------------- within `caller_overridden`...
+LL |     callee();
+   |     ^^^^^^^^ ...`callee` called here
+   |
+   = note: could not be inlined due to: #[rustc_no_mir_inline]
+note: the lint level is defined here
+  --> $DIR/warn.rs:27:8
+   |
+LL | #[deny(must_inline)]
+   |        ^^^^^^^^^^^
+
+error: `callee_justified` could not be inlined into `caller_overridden` but must be inlined
+  --> $DIR/warn.rs:32:5
+   |
+LL | #[inline(must("maintain performance characteristics"))]
+   | ------------------------------------------------------- inlining due to this annotation
+LL | pub fn callee_justified() {
+   | ------------------------- `callee_justified` defined here
+...
+LL | pub fn caller_overridden() {
+   | -------------------------- within `caller_overridden`...
+...
+LL |     callee_justified();
+   |     ^^^^^^^^^^^^^^^^^^ ...`callee_justified` called here
+   |
+   = note: could not be inlined due to: #[rustc_no_mir_inline]
+   = note: `callee_justified` must be inlined to: maintain performance characteristics
+
+error: aborting due to 2 previous errors; 2 warnings emitted
+