allocator: refactor for stabilisation#157428
Conversation
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
| /// - the allocator is mutated through public API taking `&mut` access (notably, | ||
| /// running the allocator's destructor is such a mutation), or |
There was a problem hiding this comment.
This guarantee seems fine on the surface, but I'm trying to wrap my head around what's actually being guaranteed here. Like, clearly, it'd be wildly unsafe to offer an invalidate_everything method on an allocator that just deletes the backing memory without requiring any of the things that are using it to be dropped, but this feels like it's opening the door for that kind of method "as long as you're careful" which, doesn't make a lot of sense.
Like, I'm trying to gauge what value is being gained by this guarantee and it mostly just feels like it's making things more confusing without actually helping.
There was a problem hiding this comment.
we're saying that such an invalidate_everything method is allowed to exist, and you can't rely on the allocator having not yet been dropped for soundness. in other words, so long as you hold a &alloc (thus preventing a &mut alloc from being created), you can trust the memory you have is fine; but if you lose the &alloc and get a new one back, your memory might have been scribbled over and you must act as such
There was a problem hiding this comment.
Okay, but wouldn't that be the same thing as the lifetime expiring as before? Technically, even though both of them are written as &A, you've gotten a new &A lifetime in that case.
There was a problem hiding this comment.
i think the extra guarantee here is that if you do hold a &mut alloc, you can call methods that take alloc by-shared-ref without worry but you can't pass the actual &mut to an untrusted function and expect your allocator to be okay at the end. but i agree that's not obviously guaranteed
There was a problem hiding this comment.
Isn't that just totally breaking the aliasing guarantees, though? Since that &mut reference wouldn't be unique.
There was a problem hiding this comment.
...you know, you make a good point. i'll revisit the reasoning for this, i recall adding this in response to something being brought up
There was a problem hiding this comment.
nvm, i'm being stupid. the following is the reason:
let mut alloc = SomeAllocator::new();
let ptr = alloc.allocate(...);
alloc.something_by_shared_ref();
// ptr is still guaranteed to be valid
alloc.trusted_method_by_unique_ref();
// ptr is still valid because we know for sure the method is trusted not to mess w/ allocator state
alloc.untrusted_method_by_unique_ref();
// ptr must be assumed to be maybe-invalid even if the lifetime of alloc is not expired and ptr hasn't yet been deallocatedThere was a problem hiding this comment.
I guess that I was technically thinking of Box whose lifecycle is intrinsically tied to the lifetime of the allocator parameter, whereas in this case if you just call alloc and dealloc manually the "lifetime" is not really tracked at all. So, yes, mutable borrows can happen on the allocator and it's fine, and you guarantee this doesn't happen by taking a non-mutable borrow.
There was a problem hiding this comment.
Seeing this thread from @RalfJung: #157428 (comment)
I think we probably also need to be careful about how we define the relationship between these rules and StaticAllocator, since "lifetime expiration" in those cases refers to the allocator value and not references in that case.
|
@rustbot author (mostly so you can more clearly signal when you think things are ready; I've commented here already so I'll see any additional changes for review as they're made) |
| /// | ||
| /// [`Pin`]: ../../core/pin/struct.Pin.html | ||
| #[unstable(feature = "allocator_api", issue = "32838")] | ||
| pub unsafe trait StaticAllocator: Allocator {} |
There was a problem hiding this comment.
I dropped the 'static bound here (and thus implicitly in Box::pin_in, etc.); since this trait is about being able to be lifetime-subtyped safely, it would mean that you need to be able to coerce to a StaticAllocator + 'a so the whole guarantee about "this is Actually Static I Promise" has weight. cc @rust-lang/opsem in case i did a bad here
There was a problem hiding this comment.
That's more of a @rust-lang/types question
This comment has been minimized.
This comment has been minimized.
|
If |
|
|
|
There is currently no way for it to unwind, but that may change with features like Just to be clear: I mean the global |
|
Yeah, but I believe that it's been deemed unsound if See #51245 |
|
That is the status quo, but that only matters to the standard library. Noone else can rely on this; a downstream user who wants to allocate in the That is why I consider this a huge footgun. |
…e, r=Amanieu Rewrite safety requirements for `Allocator` impls This PR supersedes rust-lang#156544. cc rust-lang#157428, rust-lang#156920 cc @nia-e r? @Amanieu (reviewer of rust-lang#156544) I define the concept of "equivalent allocators", in order to be able to talk about cloning allocators, and to give commonsense guarantees about stdlib `Allocator` impls. I define the concept of "invalidating a memory block" in order to be able to talk about users not being allowed to use a block of allocated memory after its allocator is "gone". An `Allocator` implementation is now allowed to invalidate its allocations when the allocator is mutated or when a lifetime in the allocator type expires. Mutation of an `Allocator` should sensibly be allowed to invalidate its allocations. For example, the `bumpalo` crates has a `Bump::reset` method that takes `&mut self` and invalidates all past allocations. Accesses via `&` still must not invalidate past allocations since, for example, `Box` provides `&` access to the allocator. I still had the "allocator destructor" clause as a separate clause from the `&mut` clause, to avoid questions about whether drop glue of types that don't implement `Drop` but have fields that implement `Drop` counts as creating a `&mut` to the whole thing. The "lifetime expiry" clause closes a hole/ambiguity on when an allocator is considered to be "dropped" if it does not have a destructor. Additionally, this clause matches what is required for `Box::into_pin` and `{Rc, Arc}::pin` to be sound. (Those methods have an `A: 'static` bound to prevent allocating via a `&MyAllocator` and then running `MyAllocator`'s destructor.)
…e, r=Amanieu Rewrite safety requirements for `Allocator` impls This PR supersedes rust-lang#156544. cc rust-lang#157428, rust-lang#156920 cc @nia-e r? @Amanieu (reviewer of rust-lang#156544) I define the concept of "equivalent allocators", in order to be able to talk about cloning allocators, and to give commonsense guarantees about stdlib `Allocator` impls. I define the concept of "invalidating a memory block" in order to be able to talk about users not being allowed to use a block of allocated memory after its allocator is "gone". An `Allocator` implementation is now allowed to invalidate its allocations when the allocator is mutated or when a lifetime in the allocator type expires. Mutation of an `Allocator` should sensibly be allowed to invalidate its allocations. For example, the `bumpalo` crates has a `Bump::reset` method that takes `&mut self` and invalidates all past allocations. Accesses via `&` still must not invalidate past allocations since, for example, `Box` provides `&` access to the allocator. I still had the "allocator destructor" clause as a separate clause from the `&mut` clause, to avoid questions about whether drop glue of types that don't implement `Drop` but have fields that implement `Drop` counts as creating a `&mut` to the whole thing. The "lifetime expiry" clause closes a hole/ambiguity on when an allocator is considered to be "dropped" if it does not have a destructor. Additionally, this clause matches what is required for `Box::into_pin` and `{Rc, Arc}::pin` to be sound. (Those methods have an `A: 'static` bound to prevent allocating via a `&MyAllocator` and then running `MyAllocator`'s destructor.)
…e, r=Amanieu Rewrite safety requirements for `Allocator` impls This PR supersedes rust-lang#156544. cc rust-lang#157428, rust-lang#156920 cc @nia-e r? @Amanieu (reviewer of rust-lang#156544) I define the concept of "equivalent allocators", in order to be able to talk about cloning allocators, and to give commonsense guarantees about stdlib `Allocator` impls. I define the concept of "invalidating a memory block" in order to be able to talk about users not being allowed to use a block of allocated memory after its allocator is "gone". An `Allocator` implementation is now allowed to invalidate its allocations when the allocator is mutated or when a lifetime in the allocator type expires. Mutation of an `Allocator` should sensibly be allowed to invalidate its allocations. For example, the `bumpalo` crates has a `Bump::reset` method that takes `&mut self` and invalidates all past allocations. Accesses via `&` still must not invalidate past allocations since, for example, `Box` provides `&` access to the allocator. I still had the "allocator destructor" clause as a separate clause from the `&mut` clause, to avoid questions about whether drop glue of types that don't implement `Drop` but have fields that implement `Drop` counts as creating a `&mut` to the whole thing. The "lifetime expiry" clause closes a hole/ambiguity on when an allocator is considered to be "dropped" if it does not have a destructor. Additionally, this clause matches what is required for `Box::into_pin` and `{Rc, Arc}::pin` to be sound. (Those methods have an `A: 'static` bound to prevent allocating via a `&MyAllocator` and then running `MyAllocator`'s destructor.)
|
☔ The latest upstream changes (presumably #158924) made this pull request unmergeable. Please resolve the merge conflicts by rebasing. |
Rollup merge of #157801 - theemathas:allocator-safety-rewrite, r=Amanieu Rewrite safety requirements for `Allocator` impls This PR supersedes #156544. cc #157428, #156920 cc @nia-e r? @Amanieu (reviewer of #156544) I define the concept of "equivalent allocators", in order to be able to talk about cloning allocators, and to give commonsense guarantees about stdlib `Allocator` impls. I define the concept of "invalidating a memory block" in order to be able to talk about users not being allowed to use a block of allocated memory after its allocator is "gone". An `Allocator` implementation is now allowed to invalidate its allocations when the allocator is mutated or when a lifetime in the allocator type expires. Mutation of an `Allocator` should sensibly be allowed to invalidate its allocations. For example, the `bumpalo` crates has a `Bump::reset` method that takes `&mut self` and invalidates all past allocations. Accesses via `&` still must not invalidate past allocations since, for example, `Box` provides `&` access to the allocator. I still had the "allocator destructor" clause as a separate clause from the `&mut` clause, to avoid questions about whether drop glue of types that don't implement `Drop` but have fields that implement `Drop` counts as creating a `&mut` to the whole thing. The "lifetime expiry" clause closes a hole/ambiguity on when an allocator is considered to be "dropped" if it does not have a destructor. Additionally, this clause matches what is required for `Box::into_pin` and `{Rc, Arc}::pin` to be sound. (Those methods have an `A: 'static` bound to prevent allocating via a `&MyAllocator` and then running `MyAllocator`'s destructor.)
|
Allocators themselves should not use |
|
The discussion was about clone calling |
|
That might be a valid reason to create custom methods for allocator cloning instead of using |
|
I think the restriction on panicking should be lifted regardless of the mechanism used to duplicate allocators. |
|
The fact that even std, which has a pretty high review bar, is unsound under panicking allocation indicates that this is too high a bar for the entire ecosystem to uphold, and we'd be better off saying that allocation must never panic. |
|
Well, the code in The other one was in |
|
More generally, code that deals with allocations, unless it uses |
|
i see. in that case i'd nominate the tracking issue for alloc_error_hook for discussion on whether we want to add the requirement that it never unwind (and, implicitly, add that guarantee to handle_alloc_error). there was some discussion about this on zulip already i believe |
|
Note that there's also the default error hook defined in |
|
Never mind, that is already using a non-unwinding panic. rust/library/alloc/src/alloc.rs Lines 572 to 577 in 4294891 And setting a custom |
This comment was marked as outdated.
This comment was marked as outdated.
|
What about allocators panicking in their |
Note: this is why I mentioned that the status quo is always an abort, since libstd uses this handler as well.
I could have sworn that panics in destructors were always converted to unconditional aborts, but I can't find any link in the reference that says this. At least, panics in destructors are risky because they're run during unwinding, and a panic while unwinding is always an abort. There is pretty strong justification for wanting an unwinding |
The RFC didn't make it (yet): rust-lang/rfcs#3288 |
I think the way to deal with that is to use APIs that don't call |
I keep mistakenly thinking the same thing, even though I have used panicking destructors for soundness holes in crates in the past. This is one of the biggest footguns in unsafe code; you need to make sure that you do not drop values of generic type unless you can deal with an unwind. |
|
And in my PR to fix |
While I agree, I also know that catch+unwind is exactly how threads work, and this is the basis for why async runtimes like tokio also depend on this model. I would imagine that most cases that depend on catch+unwind here are not planning to actually mitigate the error, but require a bare-minimum cleanup in the error handler, like saving some important data or logging the state during the issue. You could argue that these things are best put in the alloc error handler, but since it's hard to ensure a program is panic-free without some cursed linker shenanigans or daemonic rituals, folks might just argue for being able to catch the allocation error instead, with the caveat that any issues during unwinding still cause an abort anyway. |
|
Some methods also don't have |
View all comments
Adds my current proposal per the doc in #156882 and follow-up Zulip conversations (notably for dyn-compat) unstably.
r? libs