-
Notifications
You must be signed in to change notification settings - Fork 13.5k
add const_make_global
; err for const_allocate
ptrs if didn't call
#143595
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
67537c4
to
911c6a6
Compare
I'm really excited about this experiment and the movement on const alloc, so thanks for working on it <3 |
This comment has been minimized.
This comment has been minimized.
In terms of naming, I think we said that The original plan I proposed also makes the allocation immutable when it gets marked as global -- basically, this operation should indicate that you are done preparing this allocation and it is ready to be interned. (We actually intern it later as that's easier, but for the API this does not matter.) |
aa4e8a9
to
a1ef719
Compare
alloc.mutability = Mutability::Not; | ||
let alloc = self.tcx.mk_const_alloc(alloc); | ||
self.tcx.set_alloc_id_memory(alloc_id, alloc); | ||
interp_ok(()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This risks breaking key invariants like "tcx-managed allocations do not have any dangling pointers". So as nice as this is I think it won't work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm pretty sure both ptr_get_alloc_id
and self.memory.alloc_map.remove(&alloc_id)
calls above ensure that this allocation is valid, unless you mean that the allocation itself could contain values that are dangling pointers to other stuff?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes that's what I mean.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a bit easier in this situation than during interning, because we don't need to intern nested allocations, so you could just iterate over the relocation table and check if all of the relocations in it are already interned
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we want that, as already mentioned it would make it impossible to use this on cyclic structures.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm thinking of only marking it as immutable here and leaving it for the interning to actually make it into global.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please also add a test for a cyclic globalification
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please also add a test for a cyclic globalification
This turned out to be the hardest part that I haven't found enough time to come up with an adequate change to address. See the following snippet:
#![feature(core_intrinsics)]
#![feature(const_heap)]
use std::mem::{align_of, size_of};
use std::intrinsics;
struct SelfReferential {
me: *const SelfReferential,
}
const Y: &'static SelfReferential = unsafe {
let size = size_of::<SelfReferential>();
let align = align_of::<SelfReferential>();
let ptr_raw = intrinsics::const_allocate(size, align);
let ptr = ptr_raw as *mut SelfReferential;
let me_addr = &raw mut (*ptr).me;
*me_addr = ptr;
intrinsics::const_make_global(ptr_raw, size, align);
&*ptr
};
fn main() {}
me
's provenance says it wasn't derived from an immutable reference. We can't really change that because it can't be mutated after we callconst_make_global
.- The fact above hits "accepted a mutable pointer that should not have accepted" at
rust/compiler/rustc_const_eval/src/interpret/intern.rs
Lines 247 to 265 in 040e2f8
// Ensure that this is derived from a shared reference. Crucially, we check this *before* // checking whether the `alloc_id` has already been interned. The point of this check is to // ensure that when there are multiple pointers to the same allocation, they are *all* // derived from a shared reference. Therefore it would be bad if we only checked the first // pointer to any given allocation. // (It is likely not possible to actually have multiple pointers to the same allocation, // so alternatively we could also check that and ICE if there are multiple such pointers.) // See <https://github.com/rust-lang/rust/pull/128543> for why we are checking for "shared // reference" and not "immutable", i.e., for why we are allowing interior-mutable shared // references: they can actually be created in safe code while pointing to apparently // "immutable" values, via promotion or tail expression lifetime extension of // `&None::<Cell<T>>`. // We also exclude promoteds from this as `&mut []` can be promoted, which is a mutable // reference pointing to an immutable (zero-sized) allocation. We rely on the promotion // analysis not screwing up to ensure that it is sound to intern promoteds as immutable. if intern_kind != InternKind::Promoted && inner_mutability == Mutability::Not && !prov.shared_ref() { - We can check if
alloc_id
has kindHeap { was_made_immut: true }
and skip the error that way. intern_shallow
will keep returning alloc2 adding to our todo list being weird left and right. I added a.filter
inrust/compiler/rustc_const_eval/src/interpret/intern.rs
Lines 311 to 312 in 040e2f8
match intern_shallow(ecx, alloc_id, inner_mutability, Some(&mut disambiguator)) { Ok(nested) => todo.extend(nested), just_interned
.- Apparently that wasn't enough and rustc hits an infinite loop at some point. The stack trace isn't helpful enough and I didn't have enough time to find a way to debug the stack overflow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, we will probably have to relax that "derived from shared ref" check for heap allocations... which should be fine since those are all explicitly marked as immutable via make_global
so nobody can complain if it is UB to mutate them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we leave the cyclic part to a next PR, seems easier to review this one that way. At least the API is done in a way that cyclic globalification should be possible.
575f024
to
b1cc426
Compare
We already have a name for this: static promotion. What do you think about reusing that terminology here? |
alloc.mutability = Mutability::Not; | ||
let alloc = self.tcx.mk_const_alloc(alloc); | ||
self.tcx.set_alloc_id_memory(alloc_id, alloc); | ||
interp_ok(()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a bit easier in this situation than during interning, because we don't need to intern nested allocations, so you could just iterate over the relocation table and check if all of the relocations in it are already interned
So what would the API look like then, I think of static promotion as a MIR transformation. In that sense, it has very little to do with this PR, which is about achieving a similar result but via very different means. |
FWIW PR name, description, and commit message also still need updating for the new term. |
b1cc426
to
5badbfa
Compare
const_leak
and reject interning non-leaked const_allocate
ptrsconst_make_global
; err for const_allocate
ptrs if didn't call
This comment has been minimized.
This comment has been minimized.
6281681
to
0af0b13
Compare
Note that this actually doesn't fix #129233, and the crash doesn't go away, because the ICE code happens before On the other hand, a followup to indeed fix the issue (an escape hatch from interning yelling about |
MemoryKind::Machine(x) if let Some(reason) = x.disallows_intern() => match reason { | ||
// attempting to intern a `const_allocate`d pointer that was not made global via | ||
// `const_make_global`. We emit an error here but don't return an `Err` as if we | ||
// did the caller assumes we found a dangling pointer. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That seems very confusing to communicate different errors via entirely different channels. Can it be avoided?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It takes a lot of extra work when I try to avoid it. If I return an error immediately then it won't get interned - so we'd have to store it in the dead memory map - this leads to additional errors of dangling pointers if someone stores a non-made-global pointer in the final value (we'd detect the dead allocation).
If we don't return an error immediately then proceed with a shallow intern, then we won't be able to intern recursively (only the Ok
variant gives nested things to intern) without some more complex machinery or design.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm... I need find some time to take a closer look at the code then to understand why you'd add this to the dead alloc map etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It ICEs in get_alloc_info
because the alloc gets removed during interning - maybe I should try re-inserting it into the alloc_map? (wouldn't that hit another check because we're then not interning everything in the alloc_map..?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think ideally it is never removed from the alloc_map if it can't get interned.
However we have to worry about hitting the same allocation multiple times then, we should avoid duplicate errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking about it more, maybe the current logic is not so bad. It means we show the error only once for each allocation, but we do show it for all affected allocations. I still plan to take a closer look at the code.
What's not great is the resulting error -- ideally we'd delay showing these until after validation, so validation can show a better error if it finds the same allocation. Right now it can be quite hard to figure out where exactly there is a not-properly-global'd pointer. But we can leave better errors to a future PR.
compiler/rustc_const_eval/src/util/check_validity_requirement.rs
Outdated
Show resolved
Hide resolved
0af0b13
to
91ada06
Compare
Co-Authored-By: Ralf Jung <post@ralfj.de> Co-Authored-By: Oli Scherer <github333195615777966@oli-obk.de>
91ada06
to
c5ec462
Compare
This comment has been minimized.
This comment has been minimized.
9383ad8
to
6fddbb8
Compare
r? RalfJung since you looked at this extensively, lemme know if further changes are needed :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have some more suggestions that I wanted to push to your branch, which usually is permitted in PRs, but it seems you disabled that, so please take this diff instead:
diff --git a/compiler/rustc_const_eval/src/const_eval/machine.rs b/compiler/rustc_const_eval/src/const_eval/machine.rs
index 39791b7085d..ffb579aaab5 100644
--- a/compiler/rustc_const_eval/src/const_eval/machine.rs
+++ b/compiler/rustc_const_eval/src/const_eval/machine.rs
@@ -169,7 +169,11 @@ fn get_mut_or<E>(&mut self, k: K, vacant: impl FnOnce() -> Result<V, E>) -> Resu
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum MemoryKind {
- Heap { was_made_global: bool },
+ Heap {
+ /// Indicates whether `make_global` was called on this allocation.
+ /// If this is `true`, the allocation must be immutable.
+ was_made_global: bool,
+ },
}
impl fmt::Display for MemoryKind {
diff --git a/compiler/rustc_const_eval/src/interpret/intern.rs b/compiler/rustc_const_eval/src/interpret/intern.rs
index 85524949053..b8fcdc9b7ef 100644
--- a/compiler/rustc_const_eval/src/interpret/intern.rs
+++ b/compiler/rustc_const_eval/src/interpret/intern.rs
@@ -106,13 +106,12 @@ fn intern_shallow<'tcx, T: CanIntern, M: CompileTimeMachine<'tcx, T>>(
match kind {
MemoryKind::Machine(x) if let Some(reason) = x.disallows_intern() => match reason {
- // attempting to intern a `const_allocate`d pointer that was not made global via
- // `const_make_global`. We emit an error here but don't return an `Err`. The `Err`
- // is for pointers that we can't intern at all (i.e. dangling pointers). We still
- // (recursively) intern this pointer because we don't have to worry about the
- // additional paperwork involved with _not_ interning it, such as storing it in
- // the dead memory map and having to deal with additional "dangling pointer"
- // messages if someone tries to store the non-made-global ptr in the final value.
+ // Attempting to intern a `const_allocate`d pointer that was not made global via
+ // `const_make_global`. This is an error, but if we return `Err` now, things
+ // become inconsistent because we already removed the allocation from `alloc_map`.
+ // So instead we just emit an error and then continue interning as usual.
+ // We *could* do this check before removing the allocation from `alloc_map`, but then
+ // it would be much harder to ensure that we only error once for each allocation.
DisallowInternReason::ConstHeap => {
ecx.tcx.dcx().emit_err(ConstHeapPtrInFinal { span: ecx.tcx.span });
}
diff --git a/compiler/rustc_const_eval/src/interpret/memory.rs b/compiler/rustc_const_eval/src/interpret/memory.rs
index f52dc06f173..d855d5980ab 100644
--- a/compiler/rustc_const_eval/src/interpret/memory.rs
+++ b/compiler/rustc_const_eval/src/interpret/memory.rs
@@ -312,7 +312,7 @@ pub fn reallocate_ptr(
interp_ok(new_ptr)
}
- /// mark the `const_allocate`d pointer immutable so we can intern it.
+ /// Mark the `const_allocate`d allocation `ptr` points to as immutable so we can intern it.
pub fn make_const_heap_ptr_global(
&mut self,
ptr: Pointer<Option<CtfeProvenance>>,
@@ -325,20 +325,19 @@ pub fn make_const_heap_ptr_global(
return Err(ConstEvalErrKind::ConstMakeGlobalWithOffset(ptr)).into();
}
- let not_local_heap =
- matches!(self.tcx.try_get_global_alloc(alloc_id), Some(GlobalAlloc::Memory(_)));
-
- if not_local_heap {
+ if matches!(self.tcx.try_get_global_alloc(alloc_id), Some(_)) {
+ // This points to something outside the current interpreter.
return Err(ConstEvalErrKind::ConstMakeGlobalPtrIsNonHeap(ptr)).into();
}
+ // If we can't find it in `alloc_map` it must be dangling (because we don't use
+ // `extra_fn_ptr_map` in const-eval).
let (kind, alloc) = self
.memory
.alloc_map
.get_mut_or(alloc_id, || Err(ConstEvalErrKind::ConstMakeGlobalWithDanglingPtr(ptr)))?;
- alloc.mutability = Mutability::Not;
-
+ // Ensure this is actually a *heap* allocation, and record it as made-global.
match kind {
MemoryKind::Stack | MemoryKind::CallerLocation => {
return Err(ConstEvalErrKind::ConstMakeGlobalPtrIsNonHeap(ptr)).into();
@@ -352,6 +351,9 @@ pub fn make_const_heap_ptr_global(
}
}
+ // Prevent further mutation, this is now an immutable global.
+ alloc.mutability = Mutability::Not;
+
interp_ok(())
}
diff --git a/compiler/rustc_const_eval/src/util/check_validity_requirement.rs b/compiler/rustc_const_eval/src/util/check_validity_requirement.rs
index 1c2167a50fd..33478aa3a99 100644
--- a/compiler/rustc_const_eval/src/util/check_validity_requirement.rs
+++ b/compiler/rustc_const_eval/src/util/check_validity_requirement.rs
@@ -52,6 +52,7 @@ fn check_validity_requirement_strict<'tcx>(
let mut cx = InterpCx::new(cx.tcx(), DUMMY_SP, cx.typing_env, machine);
+ // It doesn't really matter which `MemoryKind` we use here, `Stack` is the least wrong.
let allocated =
cx.allocate(ty, MemoryKind::Stack).expect("OOM: failed to allocate for uninit check");
@@ -183,12 +184,10 @@ pub(crate) fn validate_scalar_in_layout<'tcx>(
let Ok(layout) = cx.layout_of(ty) else {
bug!("could not compute layout of {scalar:?}:{ty:?}")
};
- let allocated = cx
- .allocate(
- layout,
- MemoryKind::Machine(crate::const_eval::MemoryKind::Heap { was_made_global: false }),
- )
- .expect("OOM: failed to allocate for uninit check");
+
+ // It doesn't really matter which `MemoryKind` we use here, `Stack` is the least wrong.
+ let allocated =
+ cx.allocate(layout, MemoryKind::Stack).expect("OOM: failed to allocate for uninit check");
cx.write_scalar(scalar, &allocated).unwrap();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should calling it twice really be an error? Seems innocent enough to allow it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My thinking in making this an error is that the input to const_make_global
requires a mutable pointer and thus it must point to a mutable const_heap allocation. It's not an error if we allowed it but it might be indicative of errors in safe wrappers on unsafe code.
We could discuss this more and make a followup on this if this should be changed IMO.
@@ -0,0 +1,14 @@ | |||
#![feature(core_intrinsics)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#![feature(core_intrinsics)] | |
//! Ensure that once an allocation is "made global", we can no longer mutate it. | |
#![feature(core_intrinsics)] |
Some of the other tests could also use similar comments explaining their purpose.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this test still have any point? The comment at the top is already outdated, and ptr_not_made_global
checks the same thing more explicitly, doesn't it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, because if we don't unleash miri here it ICEs and needs fixing once #129233 is fixed.
ptr_not_made_global
is different because that has immutable pointers in the final value (unlike this file)
@rustbot author |
Also, could you file an issue about the problem with recursive globalification? We should look at that before stabilization, in particular if it ICEs. ;) |
Co-Authored-By: Ralf Jung <post@ralfj.de>
@rustbot ready |
Implements as discussed on Zulip: #t-compiler/const-eval > const heap
r? @rust-lang/wg-const-eval
cc #129233