fix issue 17108 Associative array byKeyValue is unsafe#1944
fix issue 17108 Associative array byKeyValue is unsafe#1944andralex merged 1 commit intodlang:masterfrom
Conversation
|
Thanks for your pull request, @somzzz! 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
|
f550561 to
00b147a
Compare
src/object.d
Outdated
| { | ||
| import core.internal.traits : substInout; | ||
|
|
||
| static auto safeAARangeEmpty(AARange rr) @safe { return () @trusted { return _aaRangeEmpty(rr); } (); } |
There was a problem hiding this comment.
I'm not sure it's necessary to have these functions as they're only used once. I think an @trusted block should suffice.
src/object.d
Outdated
| } | ||
|
|
||
| return Result(_aaRange(cast(void*)aa)); | ||
| return () @trusted {return Result(_aaRange(cast(void*)aa)); } (); |
00b147a to
84af68f
Compare
| return (*aa).get(key, defaultValue); | ||
| } | ||
|
|
||
| unittest |
There was a problem hiding this comment.
Why don't you simply make the unittest @safe?
__compiles is an anti-pattern and should only be used if absolutely necessary.
src/object.d
Outdated
| } | ||
|
|
||
| return Result(_aaRange(cast(void*)aa)); | ||
| return () @trusted { return Result(_aaRange(cast(void*)aa)); } (); |
There was a problem hiding this comment.
The constructor of Result should be safe, so you could try to move the lamda inside.
src/object.d
Outdated
|
|
||
| pure nothrow @nogc: | ||
| @property bool empty() { return _aaRangeEmpty(r); } | ||
| @property bool empty() @safe { return () @trusted { return _aaRangeEmpty(r); } (); } |
There was a problem hiding this comment.
For DMD there is an overhead in these lamdas and in this case there is no need for the lamda if you replace @safe with @trusted
There was a problem hiding this comment.
I thought Kenji fixed the compiler so when the compiler detects this pattern it always inlines the lambda.
There was a problem hiding this comment.
This should simply be marked @safe, and the implementation of _aaRangeEmpty marked @safe. We should avoid @trusted wherever we can.
schveiguy
left a comment
There was a problem hiding this comment.
I think you can also instrument byKey and byValue while you are at it, as all of them do pretty much the same thing as byKeyValue.
| @property auto value() inout | ||
| { | ||
| return () @trusted { return *cast(substInout!V*)valp; } (); | ||
| }; |
There was a problem hiding this comment.
There is a problem here in that the AA can rehash the buckets, making any existing range point at invalid elements. I think in order to make this @safe we need to verify the range is still pointing at a valid element.
In particular the fetching of the value returns the pointer + key.sizeof, which even if the element pointer is null is going to be sketchy.
I still prefer to see this not being a @trusted escape, but instead mark the implementation as @safe and have the implementation use an escape.
We also have to be careful here of any kind of postblit in K or V. If you trust the whole thing, then it is going to inadvertently trust that part too.
There was a problem hiding this comment.
@schveiguy the "abandoned" elements will be still valid memory. All we need to make sure is they are cleared properly (by means of move in all likelihood). That would make old ranges iterate the wrong elements but not unsafe.
There was a problem hiding this comment.
The abandoned elements themselves are valid, and are set to null.
However, the code for fetching the value looks like this (from here: https://github.com/somzzz/druntime/blob/84af68f87d077d9632e7dc58da04670316d4219c/src/rt/aaA.d#L705):
void* _aaRangeFrontValue(Range r)
{
return r.buckets[r.idx].entry + r.valoff;
}Here, r.buckets is really r.impl.buckets (impl points at the actual AA instance, so we are OK there). The r.idx is the range's local copy of which bucket it is currently looking at.
entry is possibly null (shouldn't be on a valid AA range, but if a rehash happens, it could be). So we are adding an arbitrary amount to a null pointer, and dereferencing that without considering it (unsafe).
Another consideration that I had figured out in the past but forgot to mention it here, it's very possible r.idx >= r.impl.buckets.length if the rehash has shrunk the array. Now we are definitely in unsafe territory.
All this can be fixed by returning null if r.idx >= r.buckets.length or the appropriate bucket pointer is null (just don't add the offset). And these aren't bounds-checked, since druntime is compiled in release mode.
There was a problem hiding this comment.
Looking at this more closely, it's really the generation of the Pair struct that is where the safety issues may lie.
| } | ||
| } | ||
| )); | ||
| } |
There was a problem hiding this comment.
I'd like to also make sure that the following doesn't compile:
struct BadValue
{
int x;
this(this) { *cast(ubyte*)(null + 100000) = 5; } // not @safe
alias x this;
}
BadValue[int] aa;
auto x = aa.byKeyValue.front;
() @safe {auto y = x.value; }
src/object.d
Outdated
| @property ref value() inout { return *cast(substInout!V*)valp; } | ||
| @property auto key() inout | ||
| { | ||
| return () @trusted { return *cast(substInout!K*)keyp; } (); |
There was a problem hiding this comment.
auto p = @trusted { return cast(substInout!K *) keyp; }();
return *p;
src/object.d
Outdated
| }; | ||
| @property auto value() inout | ||
| { | ||
| return () @trusted { return *cast(substInout!V*)valp; } (); |
84af68f to
7afc355
Compare
|
Sorry to be nitpicking.
Aside from the nits, LGTM. |
7afc355 to
99e701c
Compare
FYI: atm there is no style checking in place at druntime and DMD :/ |
|
@schveiguy @andralex ping |
|
|
||
| void* _aaRangeFrontKey(Range r) | ||
| { | ||
| if (r.idx >= r.dim) |
There was a problem hiding this comment.
Marking the function @safe enables bounds checking on the buckets-access, so I think the test here is unnecessary. A range error is probably better than an access violation later.
Interestingly, x64 code elides the bounds check if this condition exists, but keeps both for 32-bit code.
src/object.d
Outdated
| @property ref front() { return *cast(substInout!K*)_aaRangeFrontKey(r); } | ||
| void popFront() { _aaRangePopFront(r); } | ||
| @property bool empty() @safe { return _aaRangeEmpty(r); } | ||
| @property auto front() |
There was a problem hiding this comment.
This changes the return value from a reference to a copy. Is this expected? Can it break code that has been using ref for iteration?
While a mutable reference is not a good idea to begin with, using a const reference might be a bit more conservative.
There was a problem hiding this comment.
An intermediate version of this PR didn't allow ref as return type so at that point I changed it to auto.
But yes, it's better to preserve the original. Thanks for pointing it out!
99e701c to
51003a3
Compare
|
@schveiguy I'll merge this since your concern has been addressed, holler if there's anything left |
|
Ehm just copy/pasting @klickverbot's examples of what shouldn't be allowed from #1644 int main() @safe {
auto a = ["foo": 1];
auto r = a.byValue;
r.popFront;
return r.front; // oops
}and another one struct Foo {
int[int] aa;
auto opCast() pure nothrow @nogc { *cast(uint*)0xdeadbeef = 0xcafebabe; return null; }
alias aa this;
}
int main() @safe {
Foo f;
return !f.byKey.empty;
}While 2.077.1 correctly emits warnings that this isn't |
|
The first one, I think is expected to segfault, as you are dereferencing a null pointer (which is allowed in void foo() @safe
{
int *x = null;
int y = *x; // oops, but it's ok.
}The second, I think can be fixed by assigning to a local AA before casting. I'll look at making a PR. Again IFTI is confusing here, as we really are trying to add a member to the AA, but instead end up adding members to items that aren't really AAs. |
No description provided.