-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
Drop impl of Vec is not idempotent and this is not documented #60822
Comments
cc @rust-lang/libs |
"It's not idempotent" should be the safe default assumption, and I know of no case so far where we've documented that a drop glue is idempotent. We might want to mention the general principle in suitable places (e.g., nomicon, UCG) but I don't think we should repeat "as usual, double-dropping this type causes UB" again on all specific types. We should only document the exceptions, if any. (I am also quite unsure whether going through the effort of guaranteeing that certain types' drop glue is idempotent is a good use of time, as it seems extremely niche and I don't know any use cases for that. But that just means I personally won't invest time in that discussion.) |
I think this is not an issue wit FWIW I’ve always assumed that double drop could be as much a memory safety issue as use-after-free. (“One and a half” drop even more so.) I don’t think |
Fully agreed. The best you can hope for, IMO, is that a type will make an effort to drop as much as possible even when there's a panic during dropping. I think it makes more sense to improve panic-resiliance of our drop impls than to make them idempotent. For example, AFAIK One interesting question I see here is the interaction with the pinning drop guarantee. Does |
Today double-drops are not a form of undefined behavior. They could, however, lead to undefined behavior depending on the types involved and how As part of the UCGs one could try to make double-drops UB per se. That would mean that unsafe code needs to make sure that double-drops don't happen, period. That might be a breaking change. If we don't do that, then we have some types for which double-drops are ok, and some types for which they are not, and the only way to tell is either by reading the documentation, or inspecting the type's source code. I'd rather not recommend people to rely on what the source code does. It suffices that one crate starts relying on some A "Unless stated otherwise,
I expect that to happen in an idempotent
Yes. When drop panics, the destructors of the fields are invoked. For Vec this means that the contained RawVec is dropped, which deallocates the backing storage of the Vec without setting the capacity of the Vec to zero.
AFAICT, no. If you have a |
https://rust-lang.github.io/rfcs/0320-nonzeroing-dynamic-drop.html (both the proposal that is now implemented an the description of the status quo before that) is relevant to the efforts that the language makes to not let double- Although calling Calling |
Drop is not the only operation that has such issues; pretty much any function you call can be UB or not under certain circumstances depending on implementation details. One example is e.g. relying on If we really consider such details to be stable just because they are observable by "this program was not UB but now it is", we'd have to bake an abstract model of all of these data structures into our operational semantics, just to make some more code UB. I think that is a bad approach. UB is for enabling compiler optimizations. It is not for catching clients that exploit unstable details of the current implementation of some library. Conflating these two problems makes the definition of UB much, much more complicated, I think that's a mistake. Just because some interaction with
I don't understand why you'd even want to call Seems like you want to write generic code that drops stuff again if the first drop panicked. But what would be a situation where that is ever a good idea? From all I can see, the only place where this can help is if the second
This misses the point, because The interesting case is a |
How can you drop the
The documentation of |
I was talking about the implicit drop that happens when the vector goes out of scope.
I am aware. When libraries decide to document such details, clients may of course rely on them. This matches the situation for idempotent drop: clients may rely on this if and only if the library decides to document this as a guarantee. I don't think we should change our language spec to detect clients relying on |
@SimonSapin The problem is that safe Rust can be called from |
Are you saying that this is analogous to whether users should be able to rely on double-drops invoking / not invoking UB? These data-structure properties feel quite obvious to me, but I have no idea how users can today learn that, at least when using the libstd types, they should always assume that |
Both seem equally obvious to me. We also don't document that you cannot Surely we don't have to exhaustively list everything we do not guarantee. That could get exhausting indeed... I'm all for being more explicit, and in that sense I am okay with noting this explicitly somewhere. But in no way does that mean that other undocumented assumptions can randomly be conjured. (This is how the discussion at hand feels to me: coming up with a random assumption and then arguing that just because nothing says that you can not assume this, hence you can. I understand you do not consider it random, but I see nothing distinguishing this from any other not-documented-not-to-hold assumption that people might come up with.) |
drop leaves the data structure is a "deinitialized" state in which no
further operation can be performed on it.
AFAICT a second attempt to drop a value only makes sense if the first one
failed (which is what the issue cited in the top comment does).
You seem to be arguing that a drop that fails (panics) actually
deinitializes the value and therefore trying to re-drop isn’t a logical
operation to perform. If this is the case, to me this imply that a drop
that fails actually succeeds, such that drops are infallible, which makes
me wonder why do we allow drop to panic at all?
|
I mostly agree with @RalfJung, especially wrt. to turning "unstated non-guarantees" through random assumptions into guarantees. However, I think if there are reasonable assumptions that could be made that we don't want to guarantee, it's a practical idea to disabuse users of those assumptions. |
Note however that @gnzlbg themselves demonstrated that currently double-drops (of Vecs) don't work -- elements get double-dropped on the second drop. That means if a hypothetical users actually tries to determine this behavior experimentally, they'd quickly find double frees and similar issues if they are at all diligent. |
@rkruppe that holds as long as users try it with types for which this is the case.
@Centril maybe you can explain to me how this assumption feels random to you (and maybe the others). Rust allows fallible drop, that is, drop can fail. I don't understand how from this it follows that, because drop failed, one cannot attempt to drop the value again. The only idea I have about how this could be the case is if drop is infallible, that is, even if drop fails, the value is dropped, but then I don't understand why we allow drops to fail in the first place. AFAICT, drop failed, therefore the value was not dropped, therefore I can try to drop it again, and that is not a double-drop because the value wasn't dropped in the first place. It is just a second attempt to drop a value, and this issue is about whether attempting to drop should be idempotent until drop succeeds (once the value is dropped, you don't longer own it, so attempting to drop further is a use-after-move kind of UB AFAICT). |
My impression is that it's been considered bad form in general to panic in a |
...however, this could certainly be documented better. Currently the documentation for
I think it's trying to explain that double panic = abort, but as worded it doesn't really make sense. It should probably explain that more clearly, and also explicitly state that panicking in |
It should also cover the case in which the double panic does not happen, and suggest what users should assume if the details of a particular drop implementation are not guaranteed. For example, that a dropped value for which drop fails must, in general, be leaked, because it is, in general, left in an unspecified partially-dropped state which is neither "initialized" nor "deinitialized" and which does not, in general, support an attempt to re-drop the value or any other operation.
How bad would it be to make drop impls |
Well, at least I am arguing that that might be the case. You have no way to tell. Would you expect it to be legal to
"panic" doesn't mean "I failed in a clean way and nothing happened", it means "something went wrong half-way through this operation and any part of it might or might not have happened". |
Why not? If the value was not dropped, then I'd expect that to work just like pushing into an empty vector would. |
For |
"panic during drop" is very different from "not dropped". This is true for any operation. I don't understand why you would assume that an operation that panicked had no effect and thus can be considered as having not happened. You keep just changing my words "drop panicked" into "not dropped". Please don't. :)
I think it is okay to leave safe abstrcations in an invalid state when
|
Where are you getting the impression that I assume that? How could you make
How can we rely on this? The example I show using |
You keep saying "not dropped" for "drop panicked", making it sound as if a drop-that-panicked was the same thing as if drop had never been called. I am sorry if I misunderstood you here, but I also don't know any other way to interpret your words.
The Rust compiler ensures that Sure, unsafe code can circumvent this. Needless to say, unsafe code can circumvent any compiler-enforced restriction. But "drop is called exactly once" has been deeply entrenched in the guarantees and considerations around dropping in Rust as far back as I can remember. There is plenty of unsafe code in libstd that relies on this. This is as close to a guarantee as we get without formal proofs. The docs for |
I am repeating myself here, but just to be clear: I am all for improving the docs. But I find it implausible to assume that doing double-drops could ever be legal (in generic code). There is plenty of evidence for code relying on no double drops happening, there is plenty of mechanism to make sure this doesn't happen in safe code, The run-time tracking of the "move" state that the compiler does would literally be useless if drop was idempotent! Heck, before we had proper move tracking we even had a compiler-implemented mechanism to make drop idempotent (overwriting memory with some bit pattern, I forgot the details) and we got rid of it because of its unnecessary run-time cost. It feels to me like you are altering the deal here, and the deal has been set and clear for many years. We should discuss how to improve the docs because clearly it is still possible to assume that drop might be idempotent , but there is no point discussing the deal itself. Unfortunately, I do not understand at all where you got the idea from that drop might be idempotent, so I don't know where the docs could be improved to fix this. This issue has nothing to do with |
By "not dropped" I precisely mean "a value was not dropped". A drop-that-panics is a drop that fails, which is not the same as if drop was never called (how could it panic if it was never called?).
I assumed a couple of things. First, that a drop that fails does not successfully drop a value, and since the value has not been dropped, code is allowed to try to drop it again. Again, this does not mean that I thought that a failed call to Second, I assumed that correct safe abstractions over unsafe code made sure this works. My own abstractions do this, e.g., I then asked on Discord why was that, and whether the Drop impl for Vec has a bug, or whether Drop impls never have to account for that, and the discussion was that |
@gnzlbg It seems, in other words, that you assumed the drop glue is exception safe in a fairly strong way (even when it fails, it upholds the safety invariants necessary for dropping). Of course, as you might guess from my phrasing, that seems like a pretty big and unwarranted assumption to me. We don't generally expect any Rust code to be very exception safe (that's why we have marker traits like |
@rkruppe The whole safe |
The entire safe API, yes, by necessity (and only as exception safe as required to maintain memory safety -- e.g., it will happily leak memory). Though there don't seem to be any unsafe APIs (or methods at least) that have to worry about panics, so I guess it's true that distinction might not be apparent just from looking at Vec. |
From the point-of-view of safe code, it does not matter whether From the POV of unsafe code, I knew that double-drops cannot happen, but what I did not know is that what also cannot happen is |
I would like to close this issue. We have rust-lang/reference#348 open to document the exact guarantees around drop and panicking, and it seems like that would be the place for this; I agree with others on this thread that I don't really see how one could assume that initiating drop twice in any way (whether it be due to unwinding or otherwise) is safe. Specifically, any method which conceptually has a signature of
This sentence is confusing to me -- double-drops cannot happen sounds equivalent to Drop::drop getting called twice; Drop::drop is the drop operation? Specifically, I think the confusion lies here: " I thought, of course that can happen, if unsafe code calls drop_in_place, and that panics, and then Drop::drop is called again, that will call Drop::drop twice, but is not a double-drop, because the first time the value failed to drop" -- this is a double drop. When drop_in_place panics, it cannot assumed to have done nothing, so calling drop again means that you're violating the safety contract of drop_in_place. This is why Presuming there's not further discussion here I will aim to close this issue in a week or so. |
In the context of this discussion, the question that needs to be properly answered somewhere in the reference is: If a call to Looking at the comments here there is a certain amount of consensus that a
Which part of the reference issue summarizes / super-seeds this discussion ? If there isn't any, could you post a summary of this discussion there before closing this, so that documenting this still gets tracked somewhere? |
#60611 shows that the
Drop
impl ofVec
is not idempotent. That is, ifDrop
fails, theVec
is left in an "un-droppable" state, and trying to re-drop the vector invokes undefined behavior - that is, the vector must be leaked.It might be possible to make it idempotent without adding significant overhead [0], but I don't know whether we should do this. I think we should be clearer about whether the
Drop
impl of a type is idempotent or not, since making the wrong assumption can lead to UB, so I believe we should document this somewhere.We could document this for the
Vec
type, but maybe this can also be something that can be documented at the top of thelibcore
/liballoc
/libstd
crates for all types (e.g.Drop
impls of standard types are not idempotent).[0] Maybe setting the vector
len
field to zero before dropping the slice elements and having theDrop
impl ofRawVec
set the capacity to zero before exiting is enough to avoid trying to re-drop the elements or trying to deallocate the memory (which is always freed on the first drop attempt).The text was updated successfully, but these errors were encountered: