Skip to content

Commit 9b72238

Browse files
committed
Auto merge of #128543 - RalfJung:const-interior-mut, r=fee1-dead
const-eval interning: accept interior mutable pointers in final value …but keep rejecting mutable references This fixes #121610 by no longer firing the lint when there is a pointer with interior mutability in the final value of the constant. On stable, such pointers can be created with code like: ```rust pub enum JsValue { Undefined, Object(Cell<bool>), } impl Drop for JsValue { fn drop(&mut self) {} } // This does *not* get promoted since `JsValue` has a destructor. // However, the outer scope rule applies, still giving this 'static lifetime. const UNDEFINED: &JsValue = &JsValue::Undefined; ``` It's not great to accept such values since people *might* think that it is legal to mutate them with unsafe code. (This is related to how "infectious" `UnsafeCell` is, which is a [wide open question](rust-lang/unsafe-code-guidelines#236).) However, we [explicitly document](https://doc.rust-lang.org/reference/behavior-considered-undefined.html) that things created by `const` are immutable. Furthermore, we also accept the following even more questionable code without any lint today: ```rust let x: &'static Option<Cell<i32>> = &None; ``` This is even more questionable since it does *not* involve a `const`, and yet still puts the data into immutable memory. We could view this as promotion [potentially introducing UB](rust-lang/unsafe-code-guidelines#493). However, we've accepted this since ~forever and it's [too late to reject this now](#122789); the pattern is just too useful. So basically, if you think that `UnsafeCell` should be tracked fully precisely, then you should want the lint we currently emit to be removed, which this PR does. If you think `UnsafeCell` should "infect" surrounding `enum`s, the big problem is really rust-lang/unsafe-code-guidelines#493 which does not trigger the lint -- the cases the lint triggers on are actually the "harmless" ones as there is an explicit surrounding `const` explaining why things end up being immutable. What all this goes to show is that the hard error added in #118324 (later turned into the future-compat lint that I am now suggesting we remove) was based on some wrong assumptions, at least insofar as it concerns shared references. Furthermore, that lint does not help at all for the most problematic case here where the potential UB is completely implicit. (In fact, the lint is actively in the way of [my preferred long-term strategy](rust-lang/unsafe-code-guidelines#493 (comment)) for dealing with this UB.) So I think we should go back to square one and remove that error/lint for shared references. For mutable references, it does seem to work as intended, so we can keep it. Here it serves as a safety net in case the static checks that try to contain mutable references to the inside of a const initializer are not working as intended; I therefore made the check ICE to encourage users to tell us if that safety net is triggered. Closes #122153 by removing the lint. Cc `@rust-lang/opsem` `@rust-lang/lang`
2 parents 5e3ede2 + 123757a commit 9b72238

23 files changed

+225
-578
lines changed

compiler/rustc_const_eval/src/check_consts/check.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -538,8 +538,9 @@ impl<'tcx> Visitor<'tcx> for Checker<'_, 'tcx> {
538538
// final value.
539539
// Note: This is only sound if every local that has a `StorageDead` has a
540540
// `StorageDead` in every control flow path leading to a `return` terminator.
541-
// The good news is that interning will detect if any unexpected mutable
542-
// pointer slips through.
541+
// If anything slips through, there's no safety net -- safe code can create
542+
// references to variants of `!Freeze` enums as long as that variant is `Freeze`,
543+
// so interning can't protect us here.
543544
if self.local_is_transient(place.local) {
544545
self.check_op(ops::TransientCellBorrow);
545546
} else {

compiler/rustc_const_eval/src/const_eval/eval_queries.rs

+13-13
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,19 @@ use rustc_middle::traits::Reveal;
1010
use rustc_middle::ty::layout::LayoutOf;
1111
use rustc_middle::ty::print::with_no_trimmed_paths;
1212
use rustc_middle::ty::{self, Ty, TyCtxt};
13-
use rustc_session::lint;
1413
use rustc_span::def_id::LocalDefId;
1514
use rustc_span::{Span, DUMMY_SP};
1615
use rustc_target::abi::{self, Abi};
1716
use tracing::{debug, instrument, trace};
1817

1918
use super::{CanAccessMutGlobal, CompileTimeInterpCx, CompileTimeMachine};
2019
use crate::const_eval::CheckAlignment;
21-
use crate::errors::{self, ConstEvalError, DanglingPtrInFinal};
2220
use crate::interpret::{
2321
create_static_alloc, eval_nullary_intrinsic, intern_const_alloc_recursive, throw_exhaust,
2422
CtfeValidationMode, GlobalId, Immediate, InternKind, InternResult, InterpCx, InterpError,
2523
InterpResult, MPlaceTy, MemoryKind, OpTy, RefTracking, StackPopCleanup,
2624
};
27-
use crate::CTRL_C_RECEIVED;
25+
use crate::{errors, CTRL_C_RECEIVED};
2826

2927
// Returns a pointer to where the result lives
3028
#[instrument(level = "trace", skip(ecx, body))]
@@ -100,18 +98,15 @@ fn eval_body_using_ecx<'tcx, R: InterpretationResult<'tcx>>(
10098
return Err(ecx
10199
.tcx
102100
.dcx()
103-
.emit_err(DanglingPtrInFinal { span: ecx.tcx.span, kind: intern_kind })
101+
.emit_err(errors::DanglingPtrInFinal { span: ecx.tcx.span, kind: intern_kind })
104102
.into());
105103
}
106104
Err(InternResult::FoundBadMutablePointer) => {
107-
// only report mutable pointers if there were no dangling pointers
108-
let err_diag = errors::MutablePtrInFinal { span: ecx.tcx.span, kind: intern_kind };
109-
ecx.tcx.emit_node_span_lint(
110-
lint::builtin::CONST_EVAL_MUTABLE_PTR_IN_FINAL_VALUE,
111-
ecx.machine.best_lint_scope(*ecx.tcx),
112-
err_diag.span,
113-
err_diag,
114-
)
105+
return Err(ecx
106+
.tcx
107+
.dcx()
108+
.emit_err(errors::MutablePtrInFinal { span: ecx.tcx.span, kind: intern_kind })
109+
.into());
115110
}
116111
}
117112

@@ -443,7 +438,12 @@ fn report_eval_error<'tcx>(
443438
error,
444439
DUMMY_SP,
445440
|| super::get_span_and_frames(ecx.tcx, ecx.stack()),
446-
|span, frames| ConstEvalError { span, error_kind: kind, instance, frame_notes: frames },
441+
|span, frames| errors::ConstEvalError {
442+
span,
443+
error_kind: kind,
444+
instance,
445+
frame_notes: frames,
446+
},
447447
)
448448
}
449449

compiler/rustc_const_eval/src/const_eval/machine.rs

+18-5
Original file line numberDiff line numberDiff line change
@@ -718,16 +718,29 @@ impl<'tcx> interpret::Machine<'tcx> for CompileTimeMachine<'tcx> {
718718
_kind: mir::RetagKind,
719719
val: &ImmTy<'tcx, CtfeProvenance>,
720720
) -> InterpResult<'tcx, ImmTy<'tcx, CtfeProvenance>> {
721-
// If it's a frozen shared reference that's not already immutable, make it immutable.
721+
// If it's a frozen shared reference that's not already immutable, potentially make it immutable.
722722
// (Do nothing on `None` provenance, that cannot store immutability anyway.)
723723
if let ty::Ref(_, ty, mutbl) = val.layout.ty.kind()
724724
&& *mutbl == Mutability::Not
725-
&& val.to_scalar_and_meta().0.to_pointer(ecx)?.provenance.is_some_and(|p| !p.immutable())
726-
// That next check is expensive, that's why we have all the guards above.
727-
&& ty.is_freeze(*ecx.tcx, ecx.param_env)
725+
&& val
726+
.to_scalar_and_meta()
727+
.0
728+
.to_pointer(ecx)?
729+
.provenance
730+
.is_some_and(|p| !p.immutable())
728731
{
732+
// That next check is expensive, that's why we have all the guards above.
733+
let is_immutable = ty.is_freeze(*ecx.tcx, ecx.param_env);
729734
let place = ecx.ref_to_mplace(val)?;
730-
let new_place = place.map_provenance(CtfeProvenance::as_immutable);
735+
let new_place = if is_immutable {
736+
place.map_provenance(CtfeProvenance::as_immutable)
737+
} else {
738+
// Even if it is not immutable, remember that it is a shared reference.
739+
// This allows it to become part of the final value of the constant.
740+
// (See <https://github.com/rust-lang/rust/pull/128543> for why we allow this
741+
// even when there is interior mutability.)
742+
place.map_provenance(CtfeProvenance::as_shared_ref)
743+
};
731744
Ok(ImmTy::from_immediate(new_place.to_ref(ecx), val.layout))
732745
} else {
733746
Ok(val.clone())

compiler/rustc_const_eval/src/errors.rs

+2-5
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,10 @@ pub(crate) struct NestedStaticInThreadLocal {
3535
pub span: Span,
3636
}
3737

38-
#[derive(LintDiagnostic)]
38+
#[derive(Diagnostic)]
3939
#[diag(const_eval_mutable_ptr_in_final)]
4040
pub(crate) struct MutablePtrInFinal {
41-
// rust-lang/rust#122153: This was marked as `#[primary_span]` under
42-
// `derive(Diagnostic)`. Since we expect we may hard-error in future, we are
43-
// keeping the field (and skipping it under `derive(LintDiagnostic)`).
44-
#[skip_arg]
41+
#[primary_span]
4542
pub span: Span,
4643
pub kind: InternKind,
4744
}

compiler/rustc_const_eval/src/interpret/intern.rs

+37-21
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use rustc_hir as hir;
2020
use rustc_middle::middle::codegen_fn_attrs::CodegenFnAttrs;
2121
use rustc_middle::mir::interpret::{ConstAllocation, CtfeProvenance, InterpResult};
2222
use rustc_middle::query::TyCtxtAt;
23+
use rustc_middle::span_bug;
2324
use rustc_middle::ty::layout::TyAndLayout;
2425
use rustc_span::def_id::LocalDefId;
2526
use rustc_span::sym;
@@ -223,45 +224,59 @@ pub fn intern_const_alloc_recursive<'tcx, M: CompileTimeMachine<'tcx, const_eval
223224
continue;
224225
}
225226

226-
// Crucially, we check this *before* checking whether the `alloc_id`
227-
// has already been interned. The point of this check is to ensure that when
228-
// there are multiple pointers to the same allocation, they are *all* immutable.
229-
// Therefore it would be bad if we only checked the first pointer to any given
230-
// allocation.
227+
// Ensure that this is derived from a shared reference. Crucially, we check this *before*
228+
// checking whether the `alloc_id` has already been interned. The point of this check is to
229+
// ensure that when there are multiple pointers to the same allocation, they are *all*
230+
// derived from a shared reference. Therefore it would be bad if we only checked the first
231+
// pointer to any given allocation.
231232
// (It is likely not possible to actually have multiple pointers to the same allocation,
232233
// so alternatively we could also check that and ICE if there are multiple such pointers.)
234+
// See <https://github.com/rust-lang/rust/pull/128543> for why we are checking for "shared
235+
// reference" and not "immutable", i.e., for why we are allowing interior-mutable shared
236+
// references: they can actually be created in safe code while pointing to apparently
237+
// "immutable" values, via promotion or tail expression lifetime extension of
238+
// `&None::<Cell<T>>`.
239+
// We also exclude promoteds from this as `&mut []` can be promoted, which is a mutable
240+
// reference pointing to an immutable (zero-sized) allocation. We rely on the promotion
241+
// analysis not screwing up to ensure that it is sound to intern promoteds as immutable.
233242
if intern_kind != InternKind::Promoted
234243
&& inner_mutability == Mutability::Not
235-
&& !prov.immutable()
244+
&& !prov.shared_ref()
236245
{
237-
if ecx.tcx.try_get_global_alloc(alloc_id).is_some()
238-
&& !just_interned.contains(&alloc_id)
239-
{
246+
let is_already_global = ecx.tcx.try_get_global_alloc(alloc_id).is_some();
247+
if is_already_global && !just_interned.contains(&alloc_id) {
240248
// This is a pointer to some memory from another constant. We encounter mutable
241249
// pointers to such memory since we do not always track immutability through
242250
// these "global" pointers. Allowing them is harmless; the point of these checks
243251
// during interning is to justify why we intern the *new* allocations immutably,
244-
// so we can completely ignore existing allocations. We also don't need to add
245-
// this to the todo list, since after all it is already interned.
252+
// so we can completely ignore existing allocations.
253+
// We can also skip the rest of this loop iteration, since after all it is already
254+
// interned.
246255
continue;
247256
}
248-
// Found a mutable pointer inside a const where inner allocations should be
249-
// immutable. We exclude promoteds from this, since things like `&mut []` and
250-
// `&None::<Cell<i32>>` lead to promotion that can produce mutable pointers. We rely
251-
// on the promotion analysis not screwing up to ensure that it is sound to intern
252-
// promoteds as immutable.
253-
trace!("found bad mutable pointer");
254-
// Prefer dangling pointer errors over mutable pointer errors
255-
if result.is_ok() {
256-
result = Err(InternResult::FoundBadMutablePointer);
257+
// If this is a dangling pointer, that's actually fine -- the problematic case is
258+
// when there is memory there that someone might expect to be mutable, but we make it immutable.
259+
let dangling = !is_already_global && !ecx.memory.alloc_map.contains_key(&alloc_id);
260+
if !dangling {
261+
// Found a mutable reference inside a const where inner allocations should be
262+
// immutable.
263+
if !ecx.tcx.sess.opts.unstable_opts.unleash_the_miri_inside_of_you {
264+
span_bug!(
265+
ecx.tcx.span,
266+
"the static const safety checks accepted mutable references they should not have accepted"
267+
);
268+
}
269+
// Prefer dangling pointer errors over mutable pointer errors
270+
if result.is_ok() {
271+
result = Err(InternResult::FoundBadMutablePointer);
272+
}
257273
}
258274
}
259275
if ecx.tcx.try_get_global_alloc(alloc_id).is_some() {
260276
// Already interned.
261277
debug_assert!(!ecx.memory.alloc_map.contains_key(&alloc_id));
262278
continue;
263279
}
264-
just_interned.insert(alloc_id);
265280
// We always intern with `inner_mutability`, and furthermore we ensured above that if
266281
// that is "immutable", then there are *no* mutable pointers anywhere in the newly
267282
// interned memory -- justifying that we can indeed intern immutably. However this also
@@ -272,6 +287,7 @@ pub fn intern_const_alloc_recursive<'tcx, M: CompileTimeMachine<'tcx, const_eval
272287
// pointers before deciding which allocations can be made immutable; but for now we are
273288
// okay with losing some potential for immutability here. This can anyway only affect
274289
// `static mut`.
290+
just_interned.insert(alloc_id);
275291
match intern_shallow(ecx, alloc_id, inner_mutability) {
276292
Ok(nested) => todo.extend(nested),
277293
Err(()) => {

compiler/rustc_const_eval/src/interpret/validity.rs

+8-1
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@ use hir::def::DefKind;
1414
use rustc_ast::Mutability;
1515
use rustc_data_structures::fx::FxHashSet;
1616
use rustc_hir as hir;
17-
use rustc_middle::bug;
1817
use rustc_middle::mir::interpret::ValidationErrorKind::{self, *};
1918
use rustc_middle::mir::interpret::{
2019
alloc_range, ExpectedKind, InterpError, InvalidMetaKind, Misalignment, PointerKind, Provenance,
2120
UnsupportedOpInfo, ValidationErrorInfo,
2221
};
2322
use rustc_middle::ty::layout::{LayoutCx, LayoutOf, TyAndLayout};
2423
use rustc_middle::ty::{self, Ty, TyCtxt};
24+
use rustc_middle::{bug, span_bug};
2525
use rustc_span::symbol::{sym, Symbol};
2626
use rustc_target::abi::{
2727
Abi, FieldIdx, FieldsShape, Scalar as ScalarAbi, Size, VariantIdx, Variants, WrappingRange,
@@ -617,6 +617,13 @@ impl<'rt, 'tcx, M: Machine<'tcx>> ValidityVisitor<'rt, 'tcx, M> {
617617
if ptr_expected_mutbl == Mutability::Mut
618618
&& alloc_actual_mutbl == Mutability::Not
619619
{
620+
if !self.ecx.tcx.sess.opts.unstable_opts.unleash_the_miri_inside_of_you
621+
{
622+
span_bug!(
623+
self.ecx.tcx.span,
624+
"the static const safety checks accepted mutable references they should not have accepted"
625+
);
626+
}
620627
throw_validation_failure!(self.path, MutableRefToImmutable);
621628
}
622629
// In a const, everything must be completely immutable.

compiler/rustc_lint/src/lib.rs

+4
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,10 @@ fn register_builtins(store: &mut LintStore) {
579579
<https://github.com/rust-lang/rust/issues/107457> for more information",
580580
);
581581
store.register_removed("writes_through_immutable_pointer", "converted into hard error");
582+
store.register_removed(
583+
"const_eval_mutable_ptr_in_final_value",
584+
"partially allowed now, otherwise turned into a hard error",
585+
);
582586
}
583587

584588
fn register_internals(store: &mut LintStore) {

compiler/rustc_lint_defs/src/builtin.rs

-46
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ declare_lint_pass! {
2929
CENUM_IMPL_DROP_CAST,
3030
COHERENCE_LEAK_CHECK,
3131
CONFLICTING_REPR_HINTS,
32-
CONST_EVAL_MUTABLE_PTR_IN_FINAL_VALUE,
3332
CONST_EVALUATABLE_UNCHECKED,
3433
CONST_ITEM_MUTATION,
3534
DEAD_CODE,
@@ -2804,51 +2803,6 @@ declare_lint! {
28042803
@feature_gate = strict_provenance;
28052804
}
28062805

2807-
declare_lint! {
2808-
/// The `const_eval_mutable_ptr_in_final_value` lint detects if a mutable pointer
2809-
/// has leaked into the final value of a const expression.
2810-
///
2811-
/// ### Example
2812-
///
2813-
/// ```rust
2814-
/// pub enum JsValue {
2815-
/// Undefined,
2816-
/// Object(std::cell::Cell<bool>),
2817-
/// }
2818-
///
2819-
/// impl ::std::ops::Drop for JsValue {
2820-
/// fn drop(&mut self) {}
2821-
/// }
2822-
///
2823-
/// const UNDEFINED: &JsValue = &JsValue::Undefined;
2824-
///
2825-
/// fn main() {
2826-
/// }
2827-
/// ```
2828-
///
2829-
/// {{produces}}
2830-
///
2831-
/// ### Explanation
2832-
///
2833-
/// In the 1.77 release, the const evaluation machinery adopted some
2834-
/// stricter rules to reject expressions with values that could
2835-
/// end up holding mutable references to state stored in static memory
2836-
/// (which is inherently immutable).
2837-
///
2838-
/// This is a [future-incompatible] lint to ease the transition to an error.
2839-
/// See [issue #122153] for more details.
2840-
///
2841-
/// [issue #122153]: https://github.com/rust-lang/rust/issues/122153
2842-
/// [future-incompatible]: ../index.md#future-incompatible-lints
2843-
pub CONST_EVAL_MUTABLE_PTR_IN_FINAL_VALUE,
2844-
Warn,
2845-
"detects a mutable pointer that has leaked into final value of a const expression",
2846-
@future_incompatible = FutureIncompatibleInfo {
2847-
reason: FutureIncompatibilityReason::FutureReleaseErrorReportInDeps,
2848-
reference: "issue #122153 <https://github.com/rust-lang/rust/issues/122153>",
2849-
};
2850-
}
2851-
28522806
declare_lint! {
28532807
/// The `const_evaluatable_unchecked` lint detects a generic constant used
28542808
/// in a type.

0 commit comments

Comments
 (0)