diff --git a/compiler/rustc_driver/src/lib.rs b/compiler/rustc_driver/src/lib.rs
index f5b059793cf4b..53ae913f94f12 100644
--- a/compiler/rustc_driver/src/lib.rs
+++ b/compiler/rustc_driver/src/lib.rs
@@ -123,8 +123,7 @@ impl Callbacks for TimePassesCallbacks {
     fn config(&mut self, config: &mut interface::Config) {
         // If a --prints=... option has been given, we don't print the "total"
         // time because it will mess up the --prints output. See #64339.
-        self.time_passes = config.opts.prints.is_empty()
-            && (config.opts.unstable_opts.time_passes || config.opts.unstable_opts.time);
+        self.time_passes = config.opts.prints.is_empty() && config.opts.time_passes();
         config.opts.trimmed_def_paths = TrimmedDefPaths::GoodPath;
     }
 }
@@ -249,7 +248,7 @@ fn run_compiler(
                     if sopts.describe_lints {
                         let mut lint_store = rustc_lint::new_lint_store(
                             sopts.unstable_opts.no_interleave_lints,
-                            compiler.session().unstable_options(),
+                            compiler.session().enable_internal_lints(),
                         );
                         let registered_lints =
                             if let Some(register_lints) = compiler.register_lints() {
diff --git a/compiler/rustc_error_messages/locales/en-US/passes.ftl b/compiler/rustc_error_messages/locales/en-US/passes.ftl
index f95e33cd16a7d..b17eb9c2d260e 100644
--- a/compiler/rustc_error_messages/locales/en-US/passes.ftl
+++ b/compiler/rustc_error_messages/locales/en-US/passes.ftl
@@ -256,3 +256,9 @@ passes-unused-duplicate = unused attribute
 passes-unused-multiple = multiple `{$name}` attributes
     .suggestion = remove this attribute
     .note = attribute also specified here
+
+passes-rustc-lint-opt-ty = `#[rustc_lint_opt_ty]` should be applied to a struct
+    .label = not a struct
+
+passes-rustc-lint-opt-deny-field-access = `#[rustc_lint_opt_deny_field_access]` should be applied to a field
+    .label = not a field
diff --git a/compiler/rustc_feature/src/builtin_attrs.rs b/compiler/rustc_feature/src/builtin_attrs.rs
index 7b540e67aab3d..0e73d8fd7f600 100644
--- a/compiler/rustc_feature/src/builtin_attrs.rs
+++ b/compiler/rustc_feature/src/builtin_attrs.rs
@@ -619,6 +619,12 @@ pub const BUILTIN_ATTRIBUTES: &[BuiltinAttribute] = &[
     // Used by the `rustc::untranslatable_diagnostic` and `rustc::diagnostic_outside_of_impl` lints
     // to assist in changes to diagnostic APIs.
     rustc_attr!(rustc_lint_diagnostics, Normal, template!(Word), WarnFollowing, INTERNAL_UNSTABLE),
+    // Used by the `rustc::bad_opt_access` lint to identify `DebuggingOptions` and `CodegenOptions`
+    // types (as well as any others in future).
+    rustc_attr!(rustc_lint_opt_ty, Normal, template!(Word), WarnFollowing, INTERNAL_UNSTABLE),
+    // Used by the `rustc::bad_opt_access` lint on fields
+    // types (as well as any others in future).
+    rustc_attr!(rustc_lint_opt_deny_field_access, Normal, template!(List: "message"), WarnFollowing, INTERNAL_UNSTABLE),
 
     // ==========================================================================
     // Internal attributes, Const related:
diff --git a/compiler/rustc_interface/src/interface.rs b/compiler/rustc_interface/src/interface.rs
index 6c7ddb4531ef8..94f81b6607798 100644
--- a/compiler/rustc_interface/src/interface.rs
+++ b/compiler/rustc_interface/src/interface.rs
@@ -329,6 +329,8 @@ pub fn create_compiler_and_run<R>(config: Config, f: impl FnOnce(&Compiler) -> R
     })
 }
 
+// JUSTIFICATION: before session exists, only config
+#[cfg_attr(not(bootstrap), allow(rustc::bad_opt_access))]
 pub fn run_compiler<R: Send>(config: Config, f: impl FnOnce(&Compiler) -> R + Send) -> R {
     tracing::trace!("run_compiler");
     util::run_in_thread_pool_with_globals(
diff --git a/compiler/rustc_interface/src/passes.rs b/compiler/rustc_interface/src/passes.rs
index 334a595a88ae6..8f0835917861a 100644
--- a/compiler/rustc_interface/src/passes.rs
+++ b/compiler/rustc_interface/src/passes.rs
@@ -210,7 +210,7 @@ pub fn register_plugins<'a>(
 
     let mut lint_store = rustc_lint::new_lint_store(
         sess.opts.unstable_opts.no_interleave_lints,
-        sess.unstable_options(),
+        sess.enable_internal_lints(),
     );
     register_lints(sess, &mut lint_store);
 
diff --git a/compiler/rustc_interface/src/tests.rs b/compiler/rustc_interface/src/tests.rs
index 0a0eb99cd9266..21d9eaccf67e7 100644
--- a/compiler/rustc_interface/src/tests.rs
+++ b/compiler/rustc_interface/src/tests.rs
@@ -1,3 +1,4 @@
+#![cfg_attr(not(bootstrap), allow(rustc::bad_opt_access))]
 use crate::interface::parse_cfgspecs;
 
 use rustc_data_structures::fx::FxHashSet;
diff --git a/compiler/rustc_interface/src/util.rs b/compiler/rustc_interface/src/util.rs
index 97856ecf22c66..4c64e679b9571 100644
--- a/compiler/rustc_interface/src/util.rs
+++ b/compiler/rustc_interface/src/util.rs
@@ -559,6 +559,8 @@ pub fn collect_crate_types(session: &Session, attrs: &[ast::Attribute]) -> Vec<C
     // Only check command line flags if present. If no types are specified by
     // command line, then reuse the empty `base` Vec to hold the types that
     // will be found in crate attributes.
+    // JUSTIFICATION: before wrapper fn is available
+    #[cfg_attr(not(bootstrap), allow(rustc::bad_opt_access))]
     let mut base = session.opts.crate_types.clone();
     if base.is_empty() {
         base.extend(attr_types);
diff --git a/compiler/rustc_lint/src/internal.rs b/compiler/rustc_lint/src/internal.rs
index 738f475983e9b..c26d7824758ef 100644
--- a/compiler/rustc_lint/src/internal.rs
+++ b/compiler/rustc_lint/src/internal.rs
@@ -51,20 +51,6 @@ fn typeck_results_of_method_fn<'tcx>(
     cx: &LateContext<'tcx>,
     expr: &Expr<'_>,
 ) -> Option<(Span, DefId, ty::subst::SubstsRef<'tcx>)> {
-    // FIXME(rustdoc): Lints which use this function use typecheck results which can cause
-    // `rustdoc` to error if there are resolution failures.
-    //
-    // As internal lints are currently always run if there are `unstable_options`, they are added
-    // to the lint store of rustdoc. Internal lints are also not used via the `lint_mod` query.
-    // Crate lints run outside of a query so rustdoc currently doesn't disable them.
-    //
-    // Instead of relying on this, either change crate lints to a query disabled by rustdoc, only
-    // run internal lints if the user is explicitly opting in or figure out a different way to
-    // avoid running lints for rustdoc.
-    if cx.tcx.sess.opts.actually_rustdoc {
-        return None;
-    }
-
     match expr.kind {
         ExprKind::MethodCall(segment, _, _)
             if let Some(def_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id) =>
@@ -446,3 +432,38 @@ impl LateLintPass<'_> for Diagnostics {
         }
     }
 }
+
+declare_tool_lint! {
+    pub rustc::BAD_OPT_ACCESS,
+    Deny,
+    "prevent using options by field access when there is a wrapper function",
+    report_in_external_macro: true
+}
+
+declare_lint_pass!(BadOptAccess => [ BAD_OPT_ACCESS ]);
+
+impl LateLintPass<'_> for BadOptAccess {
+    fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) {
+        let ExprKind::Field(base, target) = expr.kind else { return };
+        let Some(adt_def) = cx.typeck_results().expr_ty(base).ty_adt_def() else { return };
+        // Skip types without `#[rustc_lint_opt_ty]` - only so that the rest of the lint can be
+        // avoided.
+        if !cx.tcx.has_attr(adt_def.did(), sym::rustc_lint_opt_ty) {
+            return;
+        }
+
+        for field in adt_def.all_fields() {
+            if field.name == target.name &&
+                let Some(attr) = cx.tcx.get_attr(field.did, sym::rustc_lint_opt_deny_field_access) &&
+                let Some(items) = attr.meta_item_list()  &&
+                let Some(item) = items.first()  &&
+                let Some(literal) = item.literal()  &&
+                let ast::LitKind::Str(val, _) = literal.kind
+            {
+                cx.struct_span_lint(BAD_OPT_ACCESS, expr.span, |lint| {
+                    lint.build(val.as_str()).emit(); }
+                );
+            }
+        }
+    }
+}
diff --git a/compiler/rustc_lint/src/lib.rs b/compiler/rustc_lint/src/lib.rs
index 8726d36498bed..7b0702dad75de 100644
--- a/compiler/rustc_lint/src/lib.rs
+++ b/compiler/rustc_lint/src/lib.rs
@@ -509,8 +509,14 @@ fn register_internals(store: &mut LintStore) {
     store.register_late_pass(|| Box::new(TyTyKind));
     store.register_lints(&Diagnostics::get_lints());
     store.register_late_pass(|| Box::new(Diagnostics));
+    store.register_lints(&BadOptAccess::get_lints());
+    store.register_late_pass(|| Box::new(BadOptAccess));
     store.register_lints(&PassByValue::get_lints());
     store.register_late_pass(|| Box::new(PassByValue));
+    // FIXME(davidtwco): deliberately do not include `UNTRANSLATABLE_DIAGNOSTIC` and
+    // `DIAGNOSTIC_OUTSIDE_OF_IMPL` here because `-Wrustc::internal` is provided to every crate and
+    // these lints will trigger all of the time - change this once migration to diagnostic structs
+    // and translation is completed
     store.register_group(
         false,
         "rustc::internal",
@@ -523,6 +529,7 @@ fn register_internals(store: &mut LintStore) {
             LintId::of(LINT_PASS_IMPL_WITHOUT_MACRO),
             LintId::of(USAGE_OF_QUALIFIED_TY),
             LintId::of(EXISTING_DOC_KEYWORD),
+            LintId::of(BAD_OPT_ACCESS),
         ],
     );
 }
diff --git a/compiler/rustc_mir_transform/src/lower_slice_len.rs b/compiler/rustc_mir_transform/src/lower_slice_len.rs
index 47848cfa497f3..2f02d00ec9fb0 100644
--- a/compiler/rustc_mir_transform/src/lower_slice_len.rs
+++ b/compiler/rustc_mir_transform/src/lower_slice_len.rs
@@ -11,7 +11,7 @@ pub struct LowerSliceLenCalls;
 
 impl<'tcx> MirPass<'tcx> for LowerSliceLenCalls {
     fn is_enabled(&self, sess: &rustc_session::Session) -> bool {
-        sess.opts.mir_opt_level() > 0
+        sess.mir_opt_level() > 0
     }
 
     fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
diff --git a/compiler/rustc_mir_transform/src/reveal_all.rs b/compiler/rustc_mir_transform/src/reveal_all.rs
index 8ea550fa123b6..4919ad40098cb 100644
--- a/compiler/rustc_mir_transform/src/reveal_all.rs
+++ b/compiler/rustc_mir_transform/src/reveal_all.rs
@@ -9,7 +9,7 @@ pub struct RevealAll;
 
 impl<'tcx> MirPass<'tcx> for RevealAll {
     fn is_enabled(&self, sess: &rustc_session::Session) -> bool {
-        sess.opts.mir_opt_level() >= 3 || super::inline::Inline.is_enabled(sess)
+        sess.mir_opt_level() >= 3 || super::inline::Inline.is_enabled(sess)
     }
 
     fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
diff --git a/compiler/rustc_passes/src/check_attr.rs b/compiler/rustc_passes/src/check_attr.rs
index f3ccbbb56792d..fde12b9eee6b9 100644
--- a/compiler/rustc_passes/src/check_attr.rs
+++ b/compiler/rustc_passes/src/check_attr.rs
@@ -121,6 +121,10 @@ impl CheckAttrVisitor<'_> {
                 sym::rustc_lint_diagnostics => {
                     self.check_rustc_lint_diagnostics(&attr, span, target)
                 }
+                sym::rustc_lint_opt_ty => self.check_rustc_lint_opt_ty(&attr, span, target),
+                sym::rustc_lint_opt_deny_field_access => {
+                    self.check_rustc_lint_opt_deny_field_access(&attr, span, target)
+                }
                 sym::rustc_clean
                 | sym::rustc_dirty
                 | sym::rustc_if_this_changed
@@ -1382,6 +1386,35 @@ impl CheckAttrVisitor<'_> {
         self.check_applied_to_fn_or_method(attr, span, target)
     }
 
+    /// Checks that the `#[rustc_lint_opt_ty]` attribute is only applied to a struct.
+    fn check_rustc_lint_opt_ty(&self, attr: &Attribute, span: Span, target: Target) -> bool {
+        match target {
+            Target::Struct => true,
+            _ => {
+                self.tcx.sess.emit_err(errors::RustcLintOptTy { attr_span: attr.span, span });
+                false
+            }
+        }
+    }
+
+    /// Checks that the `#[rustc_lint_opt_deny_field_access]` attribute is only applied to a field.
+    fn check_rustc_lint_opt_deny_field_access(
+        &self,
+        attr: &Attribute,
+        span: Span,
+        target: Target,
+    ) -> bool {
+        match target {
+            Target::Field => true,
+            _ => {
+                self.tcx
+                    .sess
+                    .emit_err(errors::RustcLintOptDenyFieldAccess { attr_span: attr.span, span });
+                false
+            }
+        }
+    }
+
     /// Checks that the dep-graph debugging attributes are only present when the query-dep-graph
     /// option is passed to the compiler.
     fn check_rustc_dirty_clean(&self, attr: &Attribute) -> bool {
diff --git a/compiler/rustc_passes/src/errors.rs b/compiler/rustc_passes/src/errors.rs
index 0d4317f6b8881..5feb0e2956b74 100644
--- a/compiler/rustc_passes/src/errors.rs
+++ b/compiler/rustc_passes/src/errors.rs
@@ -625,3 +625,21 @@ pub struct UnusedMultiple {
     pub other: Span,
     pub name: Symbol,
 }
+
+#[derive(SessionDiagnostic)]
+#[error(passes::rustc_lint_opt_ty)]
+pub struct RustcLintOptTy {
+    #[primary_span]
+    pub attr_span: Span,
+    #[label]
+    pub span: Span,
+}
+
+#[derive(SessionDiagnostic)]
+#[error(passes::rustc_lint_opt_deny_field_access)]
+pub struct RustcLintOptDenyFieldAccess {
+    #[primary_span]
+    pub attr_span: Span,
+    #[label]
+    pub span: Span,
+}
diff --git a/compiler/rustc_session/src/config.rs b/compiler/rustc_session/src/config.rs
index 55307b9cebb70..fe9ef6045415e 100644
--- a/compiler/rustc_session/src/config.rs
+++ b/compiler/rustc_session/src/config.rs
@@ -948,6 +948,8 @@ fn default_configuration(sess: &Session) -> CrateConfig {
     if sess.opts.debug_assertions {
         ret.insert((sym::debug_assertions, None));
     }
+    // JUSTIFICATION: before wrapper fn is available
+    #[cfg_attr(not(bootstrap), allow(rustc::bad_opt_access))]
     if sess.opts.crate_types.contains(&CrateType::ProcMacro) {
         ret.insert((sym::proc_macro, None));
     }
@@ -2196,6 +2198,8 @@ fn parse_remap_path_prefix(
     mapping
 }
 
+// JUSTIFICATION: before wrapper fn is available
+#[cfg_attr(not(bootstrap), allow(rustc::bad_opt_access))]
 pub fn build_session_options(matches: &getopts::Matches) -> Options {
     let color = parse_color(matches);
 
diff --git a/compiler/rustc_session/src/options.rs b/compiler/rustc_session/src/options.rs
index 28e2e0db89a1f..501997679f4bf 100644
--- a/compiler/rustc_session/src/options.rs
+++ b/compiler/rustc_session/src/options.rs
@@ -102,28 +102,6 @@ macro_rules! top_level_options {
     );
 }
 
-impl Options {
-    pub fn mir_opt_level(&self) -> usize {
-        self.unstable_opts
-            .mir_opt_level
-            .unwrap_or_else(|| if self.optimize != OptLevel::No { 2 } else { 1 })
-    }
-
-    pub fn instrument_coverage(&self) -> bool {
-        self.cg.instrument_coverage.unwrap_or(InstrumentCoverage::Off) != InstrumentCoverage::Off
-    }
-
-    pub fn instrument_coverage_except_unused_generics(&self) -> bool {
-        self.cg.instrument_coverage.unwrap_or(InstrumentCoverage::Off)
-            == InstrumentCoverage::ExceptUnusedGenerics
-    }
-
-    pub fn instrument_coverage_except_unused_functions(&self) -> bool {
-        self.cg.instrument_coverage.unwrap_or(InstrumentCoverage::Off)
-            == InstrumentCoverage::ExceptUnusedFunctions
-    }
-}
-
 top_level_options!(
     /// The top-level command-line options struct.
     ///
@@ -149,9 +127,11 @@ top_level_options!(
     /// `CodegenOptions`, think about how it influences incremental compilation. If in
     /// doubt, specify `[TRACKED]`, which is always "correct" but might lead to
     /// unnecessary re-compilation.
+    #[cfg_attr(not(bootstrap), rustc_lint_opt_ty)]
     pub struct Options {
         /// The crate config requested for the session, which may be combined
         /// with additional crate configurations during the compile process.
+        #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::crate_types` instead of this field"))]
         crate_types: Vec<CrateType> [TRACKED],
         optimize: OptLevel [TRACKED],
         /// Include the `debug_assertions` flag in dependency tracking, since it
@@ -198,7 +178,9 @@ top_level_options!(
         /// what rustc was invoked with, but massaged a bit to agree with
         /// commands like `--emit llvm-ir` which they're often incompatible with
         /// if we otherwise use the defaults of rustc.
+        #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::codegen_units` instead of this field"))]
         cli_forced_codegen_units: Option<usize> [UNTRACKED],
+        #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::lto` instead of this field"))]
         cli_forced_thinlto_off: bool [UNTRACKED],
 
         /// Remap source path prefixes in all output (messages, object files, debug, etc.).
@@ -249,11 +231,12 @@ macro_rules! options {
      ),* ,) =>
 (
     #[derive(Clone)]
-    pub struct $struct_name { $(pub $opt: $t),* }
+    #[cfg_attr(not(bootstrap), rustc_lint_opt_ty)]
+    pub struct $struct_name { $( $( #[$attr] )* pub $opt: $t),* }
 
     impl Default for $struct_name {
         fn default() -> $struct_name {
-            $struct_name { $( $( #[$attr] )* $opt: $init),* }
+            $struct_name { $($opt: $init),* }
         }
     }
 
@@ -297,6 +280,22 @@ macro_rules! options {
 
 ) }
 
+impl Options {
+    // JUSTIFICATION: defn of the suggested wrapper fn
+    #[cfg_attr(not(bootstrap), allow(rustc::bad_opt_access))]
+    pub fn time_passes(&self) -> bool {
+        self.unstable_opts.time_passes || self.unstable_opts.time
+    }
+}
+
+impl CodegenOptions {
+    // JUSTIFICATION: defn of the suggested wrapper fn
+    #[cfg_attr(not(bootstrap), allow(rustc::bad_opt_access))]
+    pub fn instrument_coverage(&self) -> InstrumentCoverage {
+        self.instrument_coverage.unwrap_or(InstrumentCoverage::Off)
+    }
+}
+
 // Sometimes different options need to build a common structure.
 // That structure can be kept in one of the options' fields, the others become dummy.
 macro_rules! redirect_field {
@@ -1076,6 +1075,7 @@ options! {
 
     ar: String = (String::new(), parse_string, [UNTRACKED],
         "this option is deprecated and does nothing"),
+    #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::code_model` instead of this field"))]
     code_model: Option<CodeModel> = (None, parse_code_model, [TRACKED],
         "choose the code model to use (`rustc --print code-models` for details)"),
     codegen_units: Option<usize> = (None, parse_opt_number, [UNTRACKED],
@@ -1095,12 +1095,14 @@ options! {
         "extra data to put in each output filename"),
     force_frame_pointers: Option<bool> = (None, parse_opt_bool, [TRACKED],
         "force use of the frame pointers"),
+    #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::must_emit_unwind_tables` instead of this field"))]
     force_unwind_tables: Option<bool> = (None, parse_opt_bool, [TRACKED],
         "force use of unwind tables"),
     incremental: Option<String> = (None, parse_opt_string, [UNTRACKED],
         "enable incremental compilation"),
     inline_threshold: Option<u32> = (None, parse_opt_number, [TRACKED],
         "set the threshold for inlining a function"),
+    #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::instrument_coverage` instead of this field"))]
     instrument_coverage: Option<InstrumentCoverage> = (None, parse_instrument_coverage, [TRACKED],
         "instrument the generated code to support LLVM source-based code coverage \
         reports (note, the compiler build config must include `profiler = true`); \
@@ -1113,6 +1115,7 @@ options! {
         "a single extra argument to append to the linker invocation (can be used several times)"),
     link_args: Vec<String> = (Vec::new(), parse_list, [UNTRACKED],
         "extra arguments to append to the linker invocation (space separated)"),
+    #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::link_dead_code` instead of this field"))]
     link_dead_code: Option<bool> = (None, parse_opt_bool, [TRACKED],
         "keep dead code at link time (useful for code coverage) (default: no)"),
     link_self_contained: Option<bool> = (None, parse_opt_bool, [UNTRACKED],
@@ -1127,6 +1130,7 @@ options! {
         "generate build artifacts that are compatible with linker-based LTO"),
     llvm_args: Vec<String> = (Vec::new(), parse_list, [TRACKED],
         "a list of arguments to pass to LLVM (space separated)"),
+    #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::lto` instead of this field"))]
     lto: LtoCli = (LtoCli::Unspecified, parse_lto, [TRACKED],
         "perform LLVM link-time optimizations"),
     metadata: Vec<String> = (Vec::new(), parse_list, [TRACKED],
@@ -1143,8 +1147,10 @@ options! {
         "disable LLVM's SLP vectorization pass"),
     opt_level: String = ("0".to_string(), parse_string, [TRACKED],
         "optimization level (0-3, s, or z; default: 0)"),
+    #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::overflow_checks` instead of this field"))]
     overflow_checks: Option<bool> = (None, parse_opt_bool, [TRACKED],
         "use overflow checks for integer arithmetic"),
+    #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::panic_strategy` instead of this field"))]
     panic: Option<PanicStrategy> = (None, parse_opt_panic_strategy, [TRACKED],
         "panic strategy to compile crate with"),
     passes: Vec<String> = (Vec::new(), parse_list, [TRACKED],
@@ -1156,6 +1162,7 @@ options! {
         "compile the program with profiling instrumentation"),
     profile_use: Option<PathBuf> = (None, parse_opt_pathbuf, [TRACKED],
         "use the given `.profdata` file for profile-guided optimization"),
+    #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::relocation_model` instead of this field"))]
     relocation_model: Option<RelocModel> = (None, parse_relocation_model, [TRACKED],
         "control generation of position-independent code (PIC) \
         (`rustc --print relocation-models` for details)"),
@@ -1167,6 +1174,7 @@ options! {
         "save all temporary output files during compilation (default: no)"),
     soft_float: bool = (false, parse_bool, [TRACKED],
         "use soft float ABI (*eabihf targets only) (default: no)"),
+    #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::split_debuginfo` instead of this field"))]
     split_debuginfo: Option<SplitDebuginfo> = (None, parse_split_debuginfo, [TRACKED],
         "how to handle split-debuginfo, a platform-specific option"),
     strip: Strip = (Strip::None, parse_strip, [UNTRACKED],
@@ -1202,11 +1210,13 @@ options! {
         "encode MIR of all functions into the crate metadata (default: no)"),
     assume_incomplete_release: bool = (false, parse_bool, [TRACKED],
         "make cfg(version) treat the current version as incomplete (default: no)"),
+    #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::asm_comments` instead of this field"))]
     asm_comments: bool = (false, parse_bool, [TRACKED],
         "generate comments into the assembly (may change behavior) (default: no)"),
     assert_incr_state: Option<String> = (None, parse_opt_string, [UNTRACKED],
         "assert that the incremental cache is in given state: \
          either `loaded` or `not-loaded`."),
+    #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::binary_dep_depinfo` instead of this field"))]
     binary_dep_depinfo: bool = (false, parse_bool, [TRACKED],
         "include artifacts (sysroot, crate dependencies) used during compilation in dep-info \
         (default: no)"),
@@ -1284,6 +1294,7 @@ options! {
         "emit the bc module with thin LTO info (default: yes)"),
     export_executable_symbols: bool = (false, parse_bool, [TRACKED],
         "export symbols from executables, as if they were dynamic libraries"),
+    #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::fewer_names` instead of this field"))]
     fewer_names: Option<bool> = (None, parse_opt_bool, [TRACKED],
         "reduce memory use by retaining fewer names within compilation artifacts (LLVM-IR) \
         (default: no)"),
@@ -1326,6 +1337,7 @@ options! {
         "control whether `#[inline]` functions are in all CGUs"),
     input_stats: bool = (false, parse_bool, [UNTRACKED],
         "gather statistics about the input (default: no)"),
+    #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::instrument_coverage` instead of this field"))]
     instrument_coverage: Option<InstrumentCoverage> = (None, parse_instrument_coverage, [TRACKED],
         "instrument the generated code to support LLVM source-based code coverage \
         reports (note, the compiler build config must include `profiler = true`); \
@@ -1334,6 +1346,7 @@ options! {
         `=except-unused-generics`
         `=except-unused-functions`
         `=off` (default)"),
+    #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::instrument_mcount` instead of this field"))]
     instrument_mcount: bool = (false, parse_bool, [TRACKED],
         "insert function instrument code for mcount-based tracing (default: no)"),
     keep_hygiene_data: bool = (false, parse_bool, [UNTRACKED],
@@ -1356,6 +1369,7 @@ options! {
     merge_functions: Option<MergeFunctions> = (None, parse_merge_functions, [TRACKED],
         "control the operation of the MergeFunctions LLVM pass, taking \
         the same values as the target option of the same name"),
+    #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::meta_stats` instead of this field"))]
     meta_stats: bool = (false, parse_bool, [UNTRACKED],
         "gather metadata statistics (default: no)"),
     mir_emit_retag: bool = (false, parse_bool, [TRACKED],
@@ -1365,6 +1379,7 @@ options! {
         "use like `-Zmir-enable-passes=+DestProp,-InstCombine`. Forces the specified passes to be \
         enabled, overriding all other checks. Passes that are not specified are enabled or \
         disabled by other flags as usual."),
+    #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::mir_opt_level` instead of this field"))]
     mir_opt_level: Option<usize> = (None, parse_opt_number, [TRACKED],
         "MIR optimization level (0-4; default: 1 in non optimized builds and 2 in optimized builds)"),
     move_size_limit: Option<usize> = (None, parse_opt_number, [TRACKED],
@@ -1431,6 +1446,7 @@ options! {
         See #77382 and #74551."),
     print_fuel: Option<String> = (None, parse_opt_string, [TRACKED],
         "make rustc print the total optimization fuel used by a crate"),
+    #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::print_llvm_passes` instead of this field"))]
     print_llvm_passes: bool = (false, parse_bool, [UNTRACKED],
         "print the LLVM optimization passes being run (default: no)"),
     print_mono_items: Option<String> = (None, parse_opt_string, [UNTRACKED],
@@ -1505,6 +1521,7 @@ options! {
         "exclude spans when debug-printing compiler state (default: no)"),
     src_hash_algorithm: Option<SourceFileHashAlgorithm> = (None, parse_src_file_hash, [TRACKED],
         "hash algorithm of source files in debug info (`md5`, `sha1`, or `sha256`)"),
+    #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::stack_protector` instead of this field"))]
     stack_protector: StackProtector = (StackProtector::None, parse_stack_protector, [TRACKED],
         "control stack smash protection strategy (`rustc --print stack-protector-strategies` for details)"),
     strict_init_checks: bool = (false, parse_bool, [TRACKED],
@@ -1525,6 +1542,7 @@ options! {
     symbol_mangling_version: Option<SymbolManglingVersion> = (None,
         parse_symbol_mangling_version, [TRACKED],
         "which mangling version to use for symbol names ('legacy' (default) or 'v0')"),
+    #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::teach` instead of this field"))]
     teach: bool = (false, parse_bool, [TRACKED],
         "show extended diagnostic help (default: no)"),
     temps_dir: Option<String> = (None, parse_opt_string, [UNTRACKED],
@@ -1540,6 +1558,7 @@ options! {
         "emit directionality isolation markers in translated diagnostics"),
     tune_cpu: Option<String> = (None, parse_opt_string, [TRACKED],
         "select processor to schedule for (`rustc --print target-cpus` for details)"),
+    #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::lto` instead of this field"))]
     thinlto: Option<bool> = (None, parse_opt_bool, [TRACKED],
         "enable ThinLTO when possible"),
     thir_unsafeck: bool = (false, parse_bool, [TRACKED],
@@ -1548,14 +1567,19 @@ options! {
     /// a sequential compiler for now. This'll likely be adjusted
     /// in the future. Note that -Zthreads=0 is the way to get
     /// the num_cpus behavior.
+    #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::threads` instead of this field"))]
     threads: usize = (1, parse_threads, [UNTRACKED],
         "use a thread pool with N threads"),
+    #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::time_passes` instead of this field"))]
     time: bool = (false, parse_bool, [UNTRACKED],
         "measure time of rustc processes (default: no)"),
+    #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::time_llvm_passes` instead of this field"))]
     time_llvm_passes: bool = (false, parse_bool, [UNTRACKED],
         "measure time of each LLVM pass (default: no)"),
+    #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::time_passes` instead of this field"))]
     time_passes: bool = (false, parse_bool, [UNTRACKED],
         "measure time of each rustc pass (default: no)"),
+    #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::tls_model` instead of this field"))]
     tls_model: Option<TlsModel> = (None, parse_tls_model, [TRACKED],
         "choose the TLS model to use (`rustc --print tls-models` for details)"),
     trace_macros: bool = (false, parse_bool, [UNTRACKED],
@@ -1590,14 +1614,17 @@ options! {
         "enable unsound and buggy MIR optimizations (default: no)"),
     /// This name is kind of confusing: Most unstable options enable something themselves, while
     /// this just allows "normal" options to be feature-gated.
+    #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::unstable_options` instead of this field"))]
     unstable_options: bool = (false, parse_bool, [UNTRACKED],
         "adds unstable command line options to rustc interface (default: no)"),
     use_ctors_section: Option<bool> = (None, parse_opt_bool, [TRACKED],
         "use legacy .ctors section for initializers rather than .init_array"),
     validate_mir: bool = (false, parse_bool, [UNTRACKED],
         "validate MIR after each transformation"),
+    #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::verbose` instead of this field"))]
     verbose: bool = (false, parse_bool, [UNTRACKED],
         "in general, enable more debug printouts (default: no)"),
+    #[cfg_attr(not(bootstrap), rustc_lint_opt_deny_field_access("use `Session::verify_llvm_ir` instead of this field"))]
     verify_llvm_ir: bool = (false, parse_bool, [TRACKED],
         "verify LLVM IR (default: no)"),
     virtual_function_elimination: bool = (false, parse_bool, [TRACKED],
diff --git a/compiler/rustc_session/src/session.rs b/compiler/rustc_session/src/session.rs
index 854cad79a2040..9669287b3f370 100644
--- a/compiler/rustc_session/src/session.rs
+++ b/compiler/rustc_session/src/session.rs
@@ -1,7 +1,7 @@
 use crate::cgu_reuse_tracker::CguReuseTracker;
 use crate::code_stats::CodeStats;
 pub use crate::code_stats::{DataTypeKind, FieldInfo, SizeKind, VariantInfo};
-use crate::config::{self, CrateType, OutputType, SwitchWithOptPath};
+use crate::config::{self, CrateType, InstrumentCoverage, OptLevel, OutputType, SwitchWithOptPath};
 use crate::parse::{add_feature_diagnostics, ParseSess};
 use crate::search_paths::{PathKind, SearchPath};
 use crate::{filesearch, lint};
@@ -583,50 +583,356 @@ impl Session {
     pub fn source_map(&self) -> &SourceMap {
         self.parse_sess.source_map()
     }
+
+    pub fn time_passes(&self) -> bool {
+        self.opts.time_passes()
+    }
+
+    /// Returns `true` if internal lints should be added to the lint store - i.e. if
+    /// `-Zunstable-options` is provided and this isn't rustdoc (internal lints can trigger errors
+    /// to be emitted under rustdoc).
+    pub fn enable_internal_lints(&self) -> bool {
+        self.unstable_options() && !self.opts.actually_rustdoc
+    }
+
+    pub fn instrument_coverage(&self) -> bool {
+        self.opts.cg.instrument_coverage() != InstrumentCoverage::Off
+    }
+
+    pub fn instrument_coverage_except_unused_generics(&self) -> bool {
+        self.opts.cg.instrument_coverage() == InstrumentCoverage::ExceptUnusedGenerics
+    }
+
+    pub fn instrument_coverage_except_unused_functions(&self) -> bool {
+        self.opts.cg.instrument_coverage() == InstrumentCoverage::ExceptUnusedFunctions
+    }
+
+    /// Gets the features enabled for the current compilation session.
+    /// DO NOT USE THIS METHOD if there is a TyCtxt available, as it circumvents
+    /// dependency tracking. Use tcx.features() instead.
+    #[inline]
+    pub fn features_untracked(&self) -> &rustc_feature::Features {
+        self.features.get().unwrap()
+    }
+
+    pub fn init_features(&self, features: rustc_feature::Features) {
+        match self.features.set(features) {
+            Ok(()) => {}
+            Err(_) => panic!("`features` was initialized twice"),
+        }
+    }
+
+    pub fn is_sanitizer_cfi_enabled(&self) -> bool {
+        self.opts.unstable_opts.sanitizer.contains(SanitizerSet::CFI)
+    }
+
+    /// Check whether this compile session and crate type use static crt.
+    pub fn crt_static(&self, crate_type: Option<CrateType>) -> bool {
+        if !self.target.crt_static_respected {
+            // If the target does not opt in to crt-static support, use its default.
+            return self.target.crt_static_default;
+        }
+
+        let requested_features = self.opts.cg.target_feature.split(',');
+        let found_negative = requested_features.clone().any(|r| r == "-crt-static");
+        let found_positive = requested_features.clone().any(|r| r == "+crt-static");
+
+        // JUSTIFICATION: necessary use of crate_types directly (see FIXME below)
+        #[cfg_attr(not(bootstrap), allow(rustc::bad_opt_access))]
+        if found_positive || found_negative {
+            found_positive
+        } else if crate_type == Some(CrateType::ProcMacro)
+            || crate_type == None && self.opts.crate_types.contains(&CrateType::ProcMacro)
+        {
+            // FIXME: When crate_type is not available,
+            // we use compiler options to determine the crate_type.
+            // We can't check `#![crate_type = "proc-macro"]` here.
+            false
+        } else {
+            self.target.crt_static_default
+        }
+    }
+
+    pub fn is_wasi_reactor(&self) -> bool {
+        self.target.options.os == "wasi"
+            && matches!(
+                self.opts.unstable_opts.wasi_exec_model,
+                Some(config::WasiExecModel::Reactor)
+            )
+    }
+
+    pub fn target_can_use_split_dwarf(&self) -> bool {
+        !self.target.is_like_windows && !self.target.is_like_osx
+    }
+
+    pub fn generate_proc_macro_decls_symbol(&self, stable_crate_id: StableCrateId) -> String {
+        format!("__rustc_proc_macro_decls_{:08x}__", stable_crate_id.to_u64())
+    }
+
+    pub fn target_filesearch(&self, kind: PathKind) -> filesearch::FileSearch<'_> {
+        filesearch::FileSearch::new(
+            &self.sysroot,
+            self.opts.target_triple.triple(),
+            &self.opts.search_paths,
+            &self.target_tlib_path,
+            kind,
+        )
+    }
+    pub fn host_filesearch(&self, kind: PathKind) -> filesearch::FileSearch<'_> {
+        filesearch::FileSearch::new(
+            &self.sysroot,
+            config::host_triple(),
+            &self.opts.search_paths,
+            &self.host_tlib_path,
+            kind,
+        )
+    }
+
+    /// Returns a list of directories where target-specific tool binaries are located.
+    pub fn get_tools_search_paths(&self, self_contained: bool) -> Vec<PathBuf> {
+        let rustlib_path = rustc_target::target_rustlib_path(&self.sysroot, &config::host_triple());
+        let p = PathBuf::from_iter([
+            Path::new(&self.sysroot),
+            Path::new(&rustlib_path),
+            Path::new("bin"),
+        ]);
+        if self_contained { vec![p.clone(), p.join("self-contained")] } else { vec![p] }
+    }
+
+    pub fn init_incr_comp_session(
+        &self,
+        session_dir: PathBuf,
+        lock_file: flock::Lock,
+        load_dep_graph: bool,
+    ) {
+        let mut incr_comp_session = self.incr_comp_session.borrow_mut();
+
+        if let IncrCompSession::NotInitialized = *incr_comp_session {
+        } else {
+            panic!("Trying to initialize IncrCompSession `{:?}`", *incr_comp_session)
+        }
+
+        *incr_comp_session =
+            IncrCompSession::Active { session_directory: session_dir, lock_file, load_dep_graph };
+    }
+
+    pub fn finalize_incr_comp_session(&self, new_directory_path: PathBuf) {
+        let mut incr_comp_session = self.incr_comp_session.borrow_mut();
+
+        if let IncrCompSession::Active { .. } = *incr_comp_session {
+        } else {
+            panic!("trying to finalize `IncrCompSession` `{:?}`", *incr_comp_session);
+        }
+
+        // Note: this will also drop the lock file, thus unlocking the directory.
+        *incr_comp_session = IncrCompSession::Finalized { session_directory: new_directory_path };
+    }
+
+    pub fn mark_incr_comp_session_as_invalid(&self) {
+        let mut incr_comp_session = self.incr_comp_session.borrow_mut();
+
+        let session_directory = match *incr_comp_session {
+            IncrCompSession::Active { ref session_directory, .. } => session_directory.clone(),
+            IncrCompSession::InvalidBecauseOfErrors { .. } => return,
+            _ => panic!("trying to invalidate `IncrCompSession` `{:?}`", *incr_comp_session),
+        };
+
+        // Note: this will also drop the lock file, thus unlocking the directory.
+        *incr_comp_session = IncrCompSession::InvalidBecauseOfErrors { session_directory };
+    }
+
+    pub fn incr_comp_session_dir(&self) -> cell::Ref<'_, PathBuf> {
+        let incr_comp_session = self.incr_comp_session.borrow();
+        cell::Ref::map(incr_comp_session, |incr_comp_session| match *incr_comp_session {
+            IncrCompSession::NotInitialized => panic!(
+                "trying to get session directory from `IncrCompSession`: {:?}",
+                *incr_comp_session,
+            ),
+            IncrCompSession::Active { ref session_directory, .. }
+            | IncrCompSession::Finalized { ref session_directory }
+            | IncrCompSession::InvalidBecauseOfErrors { ref session_directory } => {
+                session_directory
+            }
+        })
+    }
+
+    pub fn incr_comp_session_dir_opt(&self) -> Option<cell::Ref<'_, PathBuf>> {
+        self.opts.incremental.as_ref().map(|_| self.incr_comp_session_dir())
+    }
+
+    pub fn print_perf_stats(&self) {
+        eprintln!(
+            "Total time spent computing symbol hashes:      {}",
+            duration_to_secs_str(*self.perf_stats.symbol_hash_time.lock())
+        );
+        eprintln!(
+            "Total queries canonicalized:                   {}",
+            self.perf_stats.queries_canonicalized.load(Ordering::Relaxed)
+        );
+        eprintln!(
+            "normalize_generic_arg_after_erasing_regions:   {}",
+            self.perf_stats.normalize_generic_arg_after_erasing_regions.load(Ordering::Relaxed)
+        );
+        eprintln!(
+            "normalize_projection_ty:                       {}",
+            self.perf_stats.normalize_projection_ty.load(Ordering::Relaxed)
+        );
+    }
+
+    /// We want to know if we're allowed to do an optimization for crate foo from -z fuel=foo=n.
+    /// This expends fuel if applicable, and records fuel if applicable.
+    pub fn consider_optimizing<T: Fn() -> String>(&self, crate_name: &str, msg: T) -> bool {
+        let mut ret = true;
+        if let Some((ref c, _)) = self.opts.unstable_opts.fuel {
+            if c == crate_name {
+                assert_eq!(self.threads(), 1);
+                let mut fuel = self.optimization_fuel.lock();
+                ret = fuel.remaining != 0;
+                if fuel.remaining == 0 && !fuel.out_of_fuel {
+                    if self.diagnostic().can_emit_warnings() {
+                        // We only call `msg` in case we can actually emit warnings.
+                        // Otherwise, this could cause a `delay_good_path_bug` to
+                        // trigger (issue #79546).
+                        self.warn(&format!("optimization-fuel-exhausted: {}", msg()));
+                    }
+                    fuel.out_of_fuel = true;
+                } else if fuel.remaining > 0 {
+                    fuel.remaining -= 1;
+                }
+            }
+        }
+        if let Some(ref c) = self.opts.unstable_opts.print_fuel {
+            if c == crate_name {
+                assert_eq!(self.threads(), 1);
+                self.print_fuel.fetch_add(1, SeqCst);
+            }
+        }
+        ret
+    }
+
+    pub fn rust_2015(&self) -> bool {
+        self.edition() == Edition::Edition2015
+    }
+
+    /// Are we allowed to use features from the Rust 2018 edition?
+    pub fn rust_2018(&self) -> bool {
+        self.edition() >= Edition::Edition2018
+    }
+
+    /// Are we allowed to use features from the Rust 2021 edition?
+    pub fn rust_2021(&self) -> bool {
+        self.edition() >= Edition::Edition2021
+    }
+
+    /// Are we allowed to use features from the Rust 2024 edition?
+    pub fn rust_2024(&self) -> bool {
+        self.edition() >= Edition::Edition2024
+    }
+
+    /// Returns `true` if we cannot skip the PLT for shared library calls.
+    pub fn needs_plt(&self) -> bool {
+        // Check if the current target usually needs PLT to be enabled.
+        // The user can use the command line flag to override it.
+        let needs_plt = self.target.needs_plt;
+
+        let dbg_opts = &self.opts.unstable_opts;
+
+        let relro_level = dbg_opts.relro_level.unwrap_or(self.target.relro_level);
+
+        // Only enable this optimization by default if full relro is also enabled.
+        // In this case, lazy binding was already unavailable, so nothing is lost.
+        // This also ensures `-Wl,-z,now` is supported by the linker.
+        let full_relro = RelroLevel::Full == relro_level;
+
+        // If user didn't explicitly forced us to use / skip the PLT,
+        // then try to skip it where possible.
+        dbg_opts.plt.unwrap_or(needs_plt || !full_relro)
+    }
+
+    /// Checks if LLVM lifetime markers should be emitted.
+    pub fn emit_lifetime_markers(&self) -> bool {
+        self.opts.optimize != config::OptLevel::No
+        // AddressSanitizer uses lifetimes to detect use after scope bugs.
+        // MemorySanitizer uses lifetimes to detect use of uninitialized stack variables.
+        // HWAddressSanitizer will use lifetimes to detect use after scope bugs in the future.
+        || self.opts.unstable_opts.sanitizer.intersects(SanitizerSet::ADDRESS | SanitizerSet::MEMORY | SanitizerSet::HWADDRESS)
+    }
+
+    pub fn is_proc_macro_attr(&self, attr: &Attribute) -> bool {
+        [sym::proc_macro, sym::proc_macro_attribute, sym::proc_macro_derive]
+            .iter()
+            .any(|kind| attr.has_name(*kind))
+    }
+
+    pub fn contains_name(&self, attrs: &[Attribute], name: Symbol) -> bool {
+        attrs.iter().any(|item| item.has_name(name))
+    }
+
+    pub fn find_by_name<'a>(
+        &'a self,
+        attrs: &'a [Attribute],
+        name: Symbol,
+    ) -> Option<&'a Attribute> {
+        attrs.iter().find(|attr| attr.has_name(name))
+    }
+
+    pub fn filter_by_name<'a>(
+        &'a self,
+        attrs: &'a [Attribute],
+        name: Symbol,
+    ) -> impl Iterator<Item = &'a Attribute> {
+        attrs.iter().filter(move |attr| attr.has_name(name))
+    }
+
+    pub fn first_attr_value_str_by_name(
+        &self,
+        attrs: &[Attribute],
+        name: Symbol,
+    ) -> Option<Symbol> {
+        attrs.iter().find(|at| at.has_name(name)).and_then(|at| at.value_str())
+    }
+}
+
+// JUSTIFICATION: defn of the suggested wrapper fns
+#[cfg_attr(not(bootstrap), allow(rustc::bad_opt_access))]
+impl Session {
     pub fn verbose(&self) -> bool {
         self.opts.unstable_opts.verbose
     }
-    pub fn time_passes(&self) -> bool {
-        self.opts.unstable_opts.time_passes || self.opts.unstable_opts.time
-    }
+
     pub fn instrument_mcount(&self) -> bool {
         self.opts.unstable_opts.instrument_mcount
     }
+
     pub fn time_llvm_passes(&self) -> bool {
         self.opts.unstable_opts.time_llvm_passes
     }
+
     pub fn meta_stats(&self) -> bool {
         self.opts.unstable_opts.meta_stats
     }
+
     pub fn asm_comments(&self) -> bool {
         self.opts.unstable_opts.asm_comments
     }
+
     pub fn verify_llvm_ir(&self) -> bool {
         self.opts.unstable_opts.verify_llvm_ir || option_env!("RUSTC_VERIFY_LLVM_IR").is_some()
     }
+
     pub fn print_llvm_passes(&self) -> bool {
         self.opts.unstable_opts.print_llvm_passes
     }
+
     pub fn binary_dep_depinfo(&self) -> bool {
         self.opts.unstable_opts.binary_dep_depinfo
     }
-    pub fn mir_opt_level(&self) -> usize {
-        self.opts.mir_opt_level()
-    }
-
-    /// Gets the features enabled for the current compilation session.
-    /// DO NOT USE THIS METHOD if there is a TyCtxt available, as it circumvents
-    /// dependency tracking. Use tcx.features() instead.
-    #[inline]
-    pub fn features_untracked(&self) -> &rustc_feature::Features {
-        self.features.get().unwrap()
-    }
 
-    pub fn init_features(&self, features: rustc_feature::Features) {
-        match self.features.set(features) {
-            Ok(()) => {}
-            Err(_) => panic!("`features` was initialized twice"),
-        }
+    pub fn mir_opt_level(&self) -> usize {
+        self.opts
+            .unstable_opts
+            .mir_opt_level
+            .unwrap_or_else(|| if self.opts.optimize != OptLevel::No { 2 } else { 1 })
     }
 
     /// Calculates the flavor of LTO to use for this compilation.
@@ -702,6 +1008,7 @@ impl Session {
     pub fn panic_strategy(&self) -> PanicStrategy {
         self.opts.cg.panic.unwrap_or(self.target.panic_strategy)
     }
+
     pub fn fewer_names(&self) -> bool {
         if let Some(fewer_names) = self.opts.unstable_opts.fewer_names {
             fewer_names
@@ -713,43 +1020,17 @@ impl Session {
             !more_names
         }
     }
-
-    pub fn unstable_options(&self) -> bool {
-        self.opts.unstable_opts.unstable_options
-    }
-    pub fn is_nightly_build(&self) -> bool {
-        self.opts.unstable_features.is_nightly_build()
-    }
-    pub fn is_sanitizer_cfi_enabled(&self) -> bool {
-        self.opts.unstable_opts.sanitizer.contains(SanitizerSet::CFI)
-    }
-    pub fn overflow_checks(&self) -> bool {
-        self.opts.cg.overflow_checks.unwrap_or(self.opts.debug_assertions)
-    }
-
-    /// Check whether this compile session and crate type use static crt.
-    pub fn crt_static(&self, crate_type: Option<CrateType>) -> bool {
-        if !self.target.crt_static_respected {
-            // If the target does not opt in to crt-static support, use its default.
-            return self.target.crt_static_default;
-        }
-
-        let requested_features = self.opts.cg.target_feature.split(',');
-        let found_negative = requested_features.clone().any(|r| r == "-crt-static");
-        let found_positive = requested_features.clone().any(|r| r == "+crt-static");
-
-        if found_positive || found_negative {
-            found_positive
-        } else if crate_type == Some(CrateType::ProcMacro)
-            || crate_type == None && self.opts.crate_types.contains(&CrateType::ProcMacro)
-        {
-            // FIXME: When crate_type is not available,
-            // we use compiler options to determine the crate_type.
-            // We can't check `#![crate_type = "proc-macro"]` here.
-            false
-        } else {
-            self.target.crt_static_default
-        }
+
+    pub fn unstable_options(&self) -> bool {
+        self.opts.unstable_opts.unstable_options
+    }
+
+    pub fn is_nightly_build(&self) -> bool {
+        self.opts.unstable_features.is_nightly_build()
+    }
+
+    pub fn overflow_checks(&self) -> bool {
+        self.opts.cg.overflow_checks.unwrap_or(self.opts.debug_assertions)
     }
 
     pub fn relocation_model(&self) -> RelocModel {
@@ -764,14 +1045,6 @@ impl Session {
         self.opts.unstable_opts.tls_model.unwrap_or(self.target.tls_model)
     }
 
-    pub fn is_wasi_reactor(&self) -> bool {
-        self.target.options.os == "wasi"
-            && matches!(
-                self.opts.unstable_opts.wasi_exec_model,
-                Some(config::WasiExecModel::Reactor)
-            )
-    }
-
     pub fn split_debuginfo(&self) -> SplitDebuginfo {
         self.opts.cg.split_debuginfo.unwrap_or(self.target.split_debuginfo)
     }
@@ -784,10 +1057,6 @@ impl Session {
         }
     }
 
-    pub fn target_can_use_split_dwarf(&self) -> bool {
-        !self.target.is_like_windows && !self.target.is_like_osx
-    }
-
     pub fn must_emit_unwind_tables(&self) -> bool {
         // This is used to control the emission of the `uwtable` attribute on
         // LLVM functions.
@@ -815,151 +1084,6 @@ impl Session {
             )
     }
 
-    pub fn generate_proc_macro_decls_symbol(&self, stable_crate_id: StableCrateId) -> String {
-        format!("__rustc_proc_macro_decls_{:08x}__", stable_crate_id.to_u64())
-    }
-
-    pub fn target_filesearch(&self, kind: PathKind) -> filesearch::FileSearch<'_> {
-        filesearch::FileSearch::new(
-            &self.sysroot,
-            self.opts.target_triple.triple(),
-            &self.opts.search_paths,
-            &self.target_tlib_path,
-            kind,
-        )
-    }
-    pub fn host_filesearch(&self, kind: PathKind) -> filesearch::FileSearch<'_> {
-        filesearch::FileSearch::new(
-            &self.sysroot,
-            config::host_triple(),
-            &self.opts.search_paths,
-            &self.host_tlib_path,
-            kind,
-        )
-    }
-
-    /// Returns a list of directories where target-specific tool binaries are located.
-    pub fn get_tools_search_paths(&self, self_contained: bool) -> Vec<PathBuf> {
-        let rustlib_path = rustc_target::target_rustlib_path(&self.sysroot, &config::host_triple());
-        let p = PathBuf::from_iter([
-            Path::new(&self.sysroot),
-            Path::new(&rustlib_path),
-            Path::new("bin"),
-        ]);
-        if self_contained { vec![p.clone(), p.join("self-contained")] } else { vec![p] }
-    }
-
-    pub fn init_incr_comp_session(
-        &self,
-        session_dir: PathBuf,
-        lock_file: flock::Lock,
-        load_dep_graph: bool,
-    ) {
-        let mut incr_comp_session = self.incr_comp_session.borrow_mut();
-
-        if let IncrCompSession::NotInitialized = *incr_comp_session {
-        } else {
-            panic!("Trying to initialize IncrCompSession `{:?}`", *incr_comp_session)
-        }
-
-        *incr_comp_session =
-            IncrCompSession::Active { session_directory: session_dir, lock_file, load_dep_graph };
-    }
-
-    pub fn finalize_incr_comp_session(&self, new_directory_path: PathBuf) {
-        let mut incr_comp_session = self.incr_comp_session.borrow_mut();
-
-        if let IncrCompSession::Active { .. } = *incr_comp_session {
-        } else {
-            panic!("trying to finalize `IncrCompSession` `{:?}`", *incr_comp_session);
-        }
-
-        // Note: this will also drop the lock file, thus unlocking the directory.
-        *incr_comp_session = IncrCompSession::Finalized { session_directory: new_directory_path };
-    }
-
-    pub fn mark_incr_comp_session_as_invalid(&self) {
-        let mut incr_comp_session = self.incr_comp_session.borrow_mut();
-
-        let session_directory = match *incr_comp_session {
-            IncrCompSession::Active { ref session_directory, .. } => session_directory.clone(),
-            IncrCompSession::InvalidBecauseOfErrors { .. } => return,
-            _ => panic!("trying to invalidate `IncrCompSession` `{:?}`", *incr_comp_session),
-        };
-
-        // Note: this will also drop the lock file, thus unlocking the directory.
-        *incr_comp_session = IncrCompSession::InvalidBecauseOfErrors { session_directory };
-    }
-
-    pub fn incr_comp_session_dir(&self) -> cell::Ref<'_, PathBuf> {
-        let incr_comp_session = self.incr_comp_session.borrow();
-        cell::Ref::map(incr_comp_session, |incr_comp_session| match *incr_comp_session {
-            IncrCompSession::NotInitialized => panic!(
-                "trying to get session directory from `IncrCompSession`: {:?}",
-                *incr_comp_session,
-            ),
-            IncrCompSession::Active { ref session_directory, .. }
-            | IncrCompSession::Finalized { ref session_directory }
-            | IncrCompSession::InvalidBecauseOfErrors { ref session_directory } => {
-                session_directory
-            }
-        })
-    }
-
-    pub fn incr_comp_session_dir_opt(&self) -> Option<cell::Ref<'_, PathBuf>> {
-        self.opts.incremental.as_ref().map(|_| self.incr_comp_session_dir())
-    }
-
-    pub fn print_perf_stats(&self) {
-        eprintln!(
-            "Total time spent computing symbol hashes:      {}",
-            duration_to_secs_str(*self.perf_stats.symbol_hash_time.lock())
-        );
-        eprintln!(
-            "Total queries canonicalized:                   {}",
-            self.perf_stats.queries_canonicalized.load(Ordering::Relaxed)
-        );
-        eprintln!(
-            "normalize_generic_arg_after_erasing_regions:   {}",
-            self.perf_stats.normalize_generic_arg_after_erasing_regions.load(Ordering::Relaxed)
-        );
-        eprintln!(
-            "normalize_projection_ty:                       {}",
-            self.perf_stats.normalize_projection_ty.load(Ordering::Relaxed)
-        );
-    }
-
-    /// We want to know if we're allowed to do an optimization for crate foo from -z fuel=foo=n.
-    /// This expends fuel if applicable, and records fuel if applicable.
-    pub fn consider_optimizing<T: Fn() -> String>(&self, crate_name: &str, msg: T) -> bool {
-        let mut ret = true;
-        if let Some((ref c, _)) = self.opts.unstable_opts.fuel {
-            if c == crate_name {
-                assert_eq!(self.threads(), 1);
-                let mut fuel = self.optimization_fuel.lock();
-                ret = fuel.remaining != 0;
-                if fuel.remaining == 0 && !fuel.out_of_fuel {
-                    if self.diagnostic().can_emit_warnings() {
-                        // We only call `msg` in case we can actually emit warnings.
-                        // Otherwise, this could cause a `delay_good_path_bug` to
-                        // trigger (issue #79546).
-                        self.warn(&format!("optimization-fuel-exhausted: {}", msg()));
-                    }
-                    fuel.out_of_fuel = true;
-                } else if fuel.remaining > 0 {
-                    fuel.remaining -= 1;
-                }
-            }
-        }
-        if let Some(ref c) = self.opts.unstable_opts.print_fuel {
-            if c == crate_name {
-                assert_eq!(self.threads(), 1);
-                self.print_fuel.fetch_add(1, SeqCst);
-            }
-        }
-        ret
-    }
-
     /// Returns the number of query threads that should be used for this
     /// compilation
     pub fn threads(&self) -> usize {
@@ -1040,109 +1164,17 @@ impl Session {
         self.opts.unstable_opts.teach && self.diagnostic().must_teach(code)
     }
 
-    pub fn rust_2015(&self) -> bool {
-        self.opts.edition == Edition::Edition2015
-    }
-
-    /// Are we allowed to use features from the Rust 2018 edition?
-    pub fn rust_2018(&self) -> bool {
-        self.opts.edition >= Edition::Edition2018
-    }
-
-    /// Are we allowed to use features from the Rust 2021 edition?
-    pub fn rust_2021(&self) -> bool {
-        self.opts.edition >= Edition::Edition2021
-    }
-
-    /// Are we allowed to use features from the Rust 2024 edition?
-    pub fn rust_2024(&self) -> bool {
-        self.opts.edition >= Edition::Edition2024
-    }
-
     pub fn edition(&self) -> Edition {
         self.opts.edition
     }
 
-    /// Returns `true` if we cannot skip the PLT for shared library calls.
-    pub fn needs_plt(&self) -> bool {
-        // Check if the current target usually needs PLT to be enabled.
-        // The user can use the command line flag to override it.
-        let needs_plt = self.target.needs_plt;
-
-        let dbg_opts = &self.opts.unstable_opts;
-
-        let relro_level = dbg_opts.relro_level.unwrap_or(self.target.relro_level);
-
-        // Only enable this optimization by default if full relro is also enabled.
-        // In this case, lazy binding was already unavailable, so nothing is lost.
-        // This also ensures `-Wl,-z,now` is supported by the linker.
-        let full_relro = RelroLevel::Full == relro_level;
-
-        // If user didn't explicitly forced us to use / skip the PLT,
-        // then try to skip it where possible.
-        dbg_opts.plt.unwrap_or(needs_plt || !full_relro)
-    }
-
-    /// Checks if LLVM lifetime markers should be emitted.
-    pub fn emit_lifetime_markers(&self) -> bool {
-        self.opts.optimize != config::OptLevel::No
-        // AddressSanitizer uses lifetimes to detect use after scope bugs.
-        // MemorySanitizer uses lifetimes to detect use of uninitialized stack variables.
-        // HWAddressSanitizer will use lifetimes to detect use after scope bugs in the future.
-        || self.opts.unstable_opts.sanitizer.intersects(SanitizerSet::ADDRESS | SanitizerSet::MEMORY | SanitizerSet::HWADDRESS)
-    }
-
     pub fn link_dead_code(&self) -> bool {
         self.opts.cg.link_dead_code.unwrap_or(false)
     }
-
-    pub fn instrument_coverage(&self) -> bool {
-        self.opts.instrument_coverage()
-    }
-
-    pub fn instrument_coverage_except_unused_generics(&self) -> bool {
-        self.opts.instrument_coverage_except_unused_generics()
-    }
-
-    pub fn instrument_coverage_except_unused_functions(&self) -> bool {
-        self.opts.instrument_coverage_except_unused_functions()
-    }
-
-    pub fn is_proc_macro_attr(&self, attr: &Attribute) -> bool {
-        [sym::proc_macro, sym::proc_macro_attribute, sym::proc_macro_derive]
-            .iter()
-            .any(|kind| attr.has_name(*kind))
-    }
-
-    pub fn contains_name(&self, attrs: &[Attribute], name: Symbol) -> bool {
-        attrs.iter().any(|item| item.has_name(name))
-    }
-
-    pub fn find_by_name<'a>(
-        &'a self,
-        attrs: &'a [Attribute],
-        name: Symbol,
-    ) -> Option<&'a Attribute> {
-        attrs.iter().find(|attr| attr.has_name(name))
-    }
-
-    pub fn filter_by_name<'a>(
-        &'a self,
-        attrs: &'a [Attribute],
-        name: Symbol,
-    ) -> impl Iterator<Item = &'a Attribute> {
-        attrs.iter().filter(move |attr| attr.has_name(name))
-    }
-
-    pub fn first_attr_value_str_by_name(
-        &self,
-        attrs: &[Attribute],
-        name: Symbol,
-    ) -> Option<Symbol> {
-        attrs.iter().find(|at| at.has_name(name)).and_then(|at| at.value_str())
-    }
 }
 
+// JUSTIFICATION: part of session construction
+#[cfg_attr(not(bootstrap), allow(rustc::bad_opt_access))]
 fn default_emitter(
     sopts: &config::Options,
     registry: rustc_errors::registry::Registry,
@@ -1227,6 +1259,8 @@ pub enum DiagnosticOutput {
     Raw(Box<dyn Write + Send>),
 }
 
+// JUSTIFICATION: literally session construction
+#[cfg_attr(not(bootstrap), allow(rustc::bad_opt_access))]
 pub fn build_session(
     sopts: config::Options,
     local_crate_source_file: Option<PathBuf>,
@@ -1348,11 +1382,8 @@ pub fn build_session(
         CguReuseTracker::new_disabled()
     };
 
-    let prof = SelfProfilerRef::new(
-        self_profiler,
-        sopts.unstable_opts.time_passes || sopts.unstable_opts.time,
-        sopts.unstable_opts.time_passes,
-    );
+    let prof =
+        SelfProfilerRef::new(self_profiler, sopts.time_passes(), sopts.unstable_opts.time_passes);
 
     let ctfe_backtrace = Lock::new(match env::var("RUSTC_CTFE_BACKTRACE") {
         Ok(ref val) if val == "immediate" => CtfeBacktrace::Immediate,
@@ -1401,8 +1432,12 @@ pub fn build_session(
     sess
 }
 
-// If it is useful to have a Session available already for validating a
-// commandline argument, you can do so here.
+/// Validate command line arguments with a `Session`.
+///
+/// If it is useful to have a Session available already for validating a commandline argument, you
+/// can do so here.
+// JUSTIFICATION: needs to access args to validate them
+#[cfg_attr(not(bootstrap), allow(rustc::bad_opt_access))]
 fn validate_commandline_args_with_session_available(sess: &Session) {
     // Since we don't know if code in an rlib will be linked to statically or
     // dynamically downstream, rustc generates `__imp_` symbols that help linkers
diff --git a/compiler/rustc_span/src/symbol.rs b/compiler/rustc_span/src/symbol.rs
index c75b6772487f9..060e7a7b90aee 100644
--- a/compiler/rustc_span/src/symbol.rs
+++ b/compiler/rustc_span/src/symbol.rs
@@ -1238,6 +1238,8 @@ symbols! {
         rustc_layout_scalar_valid_range_start,
         rustc_legacy_const_generics,
         rustc_lint_diagnostics,
+        rustc_lint_opt_deny_field_access,
+        rustc_lint_opt_ty,
         rustc_lint_query_instability,
         rustc_macro_transparency,
         rustc_main,
diff --git a/src/librustdoc/lib.rs b/src/librustdoc/lib.rs
index 7429f2b6ab148..3a96884d45d9f 100644
--- a/src/librustdoc/lib.rs
+++ b/src/librustdoc/lib.rs
@@ -788,7 +788,7 @@ fn main_options(options: config::Options) -> MainResult {
         if sess.opts.describe_lints {
             let mut lint_store = rustc_lint::new_lint_store(
                 sess.opts.unstable_opts.no_interleave_lints,
-                sess.unstable_options(),
+                sess.enable_internal_lints(),
             );
             let registered_lints = if let Some(register_lints) = compiler.register_lints() {
                 register_lints(sess, &mut lint_store);
diff --git a/src/test/ui-fulldeps/internal-lints/bad_opt_access.rs b/src/test/ui-fulldeps/internal-lints/bad_opt_access.rs
new file mode 100644
index 0000000000000..d6bd6945e150a
--- /dev/null
+++ b/src/test/ui-fulldeps/internal-lints/bad_opt_access.rs
@@ -0,0 +1,22 @@
+// compile-flags: -Z unstable-options
+
+// Test that accessing command line options by field access triggers a lint for those fields
+// that have wrapper functions which should be used.
+
+#![crate_type = "lib"]
+#![feature(rustc_private)]
+#![deny(rustc::bad_opt_access)]
+
+extern crate rustc_session;
+use rustc_session::Session;
+
+pub fn access_bad_option(sess: Session) {
+    let _ = sess.opts.cg.split_debuginfo;
+    //~^ ERROR use `Session::split_debuginfo` instead of this field
+
+    let _ = sess.opts.crate_types;
+    //~^ ERROR use `Session::crate_types` instead of this field
+
+    let _ = sess.opts.crate_name;
+    // okay!
+}
diff --git a/src/test/ui-fulldeps/internal-lints/bad_opt_access.stderr b/src/test/ui-fulldeps/internal-lints/bad_opt_access.stderr
new file mode 100644
index 0000000000000..e4145bff8bee9
--- /dev/null
+++ b/src/test/ui-fulldeps/internal-lints/bad_opt_access.stderr
@@ -0,0 +1,20 @@
+error: use `Session::split_debuginfo` instead of this field
+  --> $DIR/bad_opt_access.rs:14:13
+   |
+LL |     let _ = sess.opts.cg.split_debuginfo;
+   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+   |
+note: the lint level is defined here
+  --> $DIR/bad_opt_access.rs:8:9
+   |
+LL | #![deny(rustc::bad_opt_access)]
+   |         ^^^^^^^^^^^^^^^^^^^^^
+
+error: use `Session::crate_types` instead of this field
+  --> $DIR/bad_opt_access.rs:17:13
+   |
+LL |     let _ = sess.opts.crate_types;
+   |             ^^^^^^^^^^^^^^^^^^^^^
+
+error: aborting due to 2 previous errors
+
diff --git a/src/tools/clippy/src/driver.rs b/src/tools/clippy/src/driver.rs
index c219c7de830ef..c1ec2bd5bd665 100644
--- a/src/tools/clippy/src/driver.rs
+++ b/src/tools/clippy/src/driver.rs
@@ -94,6 +94,8 @@ struct ClippyCallbacks {
 }
 
 impl rustc_driver::Callbacks for ClippyCallbacks {
+    // JUSTIFICATION: necessary in clippy driver to set `mir_opt_level`
+    #[cfg_attr(not(bootstrap), allow(rustc::bad_opt_access))]
     fn config(&mut self, config: &mut interface::Config) {
         let previous = config.register_lints.take();
         let clippy_args_var = self.clippy_args_var.take();