Fix Issue 18163 - std.container.array should be usable in @safe#6001
Fix Issue 18163 - std.container.array should be usable in @safe#6001n8sh wants to merge 2 commits intodlang:masterfrom
Conversation
|
Thanks for your pull request, @n8sh! We are looking forward to reviewing it, and you should be hearing from a maintainer soon. Some tips to help speed things up:
Bear in mind that large or tricky changes may require multiple rounds of review and revision. Please see CONTRIBUTING.md for more information. Bugzilla references
|
|
Marking RefCounted as trusted is very unsafe. See http://forum.dlang.org/post/n0nnu0$1tth$1@digitalmars.com |
|
Thanks for the link @JackStouffer. I'm not making the entirety of RefCounted trusted, just certain things in |
| import std.conv : emplace; | ||
|
|
||
| _store = cast(Impl*) pureMalloc(Impl.sizeof); | ||
| _store = (() @trusted => cast(Impl*) pureMalloc(Impl.sizeof))(); |
There was a problem hiding this comment.
@trusted for the cast from void* to Impl*. Could only be unsafe if _store were dereferenced before being initialized, which cannot happen since we next call emplace.
There was a problem hiding this comment.
These comments should be put in the source code, s.t. they are easy to catch by future readers ;-)
| onOutOfMemoryError(); | ||
| static if (hasIndirections!T) | ||
| pureGcAddRange(&_store._payload, T.sizeof); | ||
| (() @trusted => pureGcAddRange(&_store._payload, T.sizeof))(); |
There was a problem hiding this comment.
@trusted for the pureGcAddRange. Okay because this cannot cannot conceivably cause memory corruption.
| import core.stdc.string : memcpy, memset; | ||
|
|
||
| _store = cast(Impl*) pureMalloc(Impl.sizeof); | ||
| _store = (() @trusted => cast(Impl*) pureMalloc(Impl.sizeof))(); |
There was a problem hiding this comment.
@trusted for the cast from void* to Impl*. Could only be unsafe if _store were dereferenced before being initialized, which cannot happen since we next either initialize with memcpy or with a blit.
| onOutOfMemoryError(); | ||
| static if (hasIndirections!T) | ||
| pureGcAddRange(&_store._payload, T.sizeof); | ||
| (() @trusted => pureGcAddRange(&_store._payload, T.sizeof))(); |
There was a problem hiding this comment.
@trusted for the pureGcAddRange. Okay because this cannot cannot conceivably cause memory corruption (pointer is non-null and size is correct).
| // Can avoid destructing result. | ||
| static if (hasElaborateAssign!T || !isAssignable!T) | ||
| memcpy(&_store._payload, &source, T.sizeof); | ||
| (() @trusted => memcpy(&_store._payload, &source, T.sizeof))(); |
There was a problem hiding this comment.
@trusted for the memcpy. Okay because this cannot cannot cause memory corruption barring unsafe previous operations. By specification it is not safe for any structure to contain a pointer to itself, so the change in address is allowed; _store is non-null; and _store has the correct size.
| auto init = typeid(T).initializer(); | ||
| if (init.ptr is null) // null ptr means initialize to 0s | ||
| memset(&source, 0, sz); | ||
| (() @trusted => memset(&source, 0, sz))(); |
There was a problem hiding this comment.
@trusted for memset. Address is valid and size is valid.
| (() @trusted => memset(&source, 0, sz))(); | ||
| else | ||
| memcpy(&source, init.ptr, sz); | ||
| (() @trusted => memcpy(&source, init.ptr, sz))(); |
There was a problem hiding this comment.
@trusted for memcpy. Destination is non-null, source is valid if there has not already been memory corruption, and size does not overrun.
| static if (hasIndirections!T) | ||
| { | ||
| pureGcRemoveRange(&_refCounted._store._payload); | ||
| (() @trusted => pureGcRemoveRange(&_refCounted._store._payload))(); |
There was a problem hiding this comment.
@trusted for pureGcRemoveRange. Cannot possibly cause memory corruption unless the payload has pointers to garbage-collected memory which is referred to by pointers elsewhere not in a location known to the garbage collector, and there is no safe way to obtain such pointers.
|
I would feel slightly less uncomfortable at the number of The point being, ensure that the new |
| } | ||
|
|
||
| pureFree(_refCounted._store); | ||
| (() @trusted => pureFree(_refCounted._store))(); |
There was a problem hiding this comment.
@trusted for pureFree. The pointer is known not to be null due to isInitialized. Is this a bit that could cause problems if an "alias" to the stored object is captured elsewhere as in http://forum.dlang.org/post/n0nnu0$1tth$1@digitalmars.com?
There was a problem hiding this comment.
Yeah, this is where it breaks down. Because you can return a raw reference to the payload, you could potentially have RefCounted free the memory from underneath you.
There was a problem hiding this comment.
If the change were made in Array instead, the RefCounted payload is private to Array so a reference to the payload itself couldn't escape, but we're still left with the problem of an escaping reference to one of the items contained in the Array whose space will be freed when the reference count hits zero. Is there a way forward that doesn't rely on DIP-1000?
There was a problem hiding this comment.
One approach that comes to mind is making Array's opIndex return by value instead of by reference. To avoid breaking existing code or giving unpleasant surprises we'd have to limit this to types that are reasonably small and don't have a complex destructor and don't have a complex copy constructor. (Of course for those types there would need to be an opIndexAssign.) Then we know that no references to the to-be-freed memory could have escaped without unsafe shenanigans.
There was a problem hiding this comment.
If there was a way to say "in safe mode, only return values, not references", that would be ideal. I'm unaware if there is such a thing.
There was a problem hiding this comment.
One thought that occurs to me is to use std.traits.isSafe with a suitably-crafted lambda / function in a template sig constraint, and thereby alter the function's refness depending on where it's called from. Unfortunately, since we can't overload on @safe/@system, I'm not sure if this is achievable.
There was a problem hiding this comment.
What I am thinking of now is adding a Flag!"returnByValue" parameter to the Array template that defaults to !hasElaborateDestructor!T && !hasElaborateCopyConstructor!T && T.sizeof <= (uint[]).sizeof but can be manually specified if desired.
EDIT: Defaulting to anything but return-by-reference seems to be infeasible because it would break anything that had been relying on hasSwappableElements.
| Assignment operators | ||
| */ | ||
| void opAssign(typeof(this) rhs) | ||
| void opAssign(typeof(this) rhs) @trusted |
There was a problem hiding this comment.
@trusted for std.algorithm.swap(_refCounted._store, rhs._refCounted._store). I believe this is safe due to the prohibition against structs holding pointers to themselves and due to the lack of any safe way for anyone else to have the address of either _store (meaning there is no need to account for this).
Okay, will add a test in a few hours. EDIT: On hold while seeing if fundamental problems can be addressed. |
|
BTW, I acknowledge that there are a lot of |
There was a problem hiding this comment.
I think the trusted escapes are OK, but see comment on the swap.
Edit: I forgot about the fact that RefCounted can return a reference to its payload, which means you could have a dangling pointer after the last RefCounted got destroyed. I think this likely isn't viable as a way to make Array safe, you would have to add all the appropriate escapes inside there.
| Assignment operators | ||
| */ | ||
| void opAssign(typeof(this) rhs) | ||
| void opAssign(typeof(this) rhs) @trusted |
There was a problem hiding this comment.
This should not need marking, you sure it needs it? swap should be safe for swapping 2 pointers (which is what I think this is doing).
There was a problem hiding this comment.
Oddly it does. Just verified.
There was a problem hiding this comment.
Weird. I tested locally, and swap with 2 pointers works in @safe. I would have expected this to infer that.
In any case, this should be marked @safe, not @trusted I would think.
And, even if they aren't (and I don't think they are), make sure future improvements don't! |
|
I am closing this pull request because I currently don't see a way to deal with the lifetime problems associated with return by reference (see above comments). |
I think that standard library Array should be just as
@safeas built-in dynamic arrays. Thanks to @wilzbach for opening this issue. This patch requires marking some code@trustedbut in each case I believe it is defensible. Some changes were required in RefCounted because it is used in Array.