-
-
Notifications
You must be signed in to change notification settings - Fork 706
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
Make std.stdio.File.readln @safe #8623
Conversation
|
Thanks for your pull request and interest in making D better, @ntrel! We are looking forward to reviewing it, and you should be hearing from a maintainer soon.
Please see CONTRIBUTING.md for more information. If you have addressed all reviews or aren't sure how to proceed, don't hesitate to ping us with a simple comment. Bugzilla referencesYour PR doesn't reference any Bugzilla issue. If your PR contains non-trivial changes, please reference a Bugzilla issue or create a manual changelog. Testing this PR locallyIf you don't have a local development environment setup, you can use Digger to test this PR: dub run digger -- build "master + phobos#8623" |
|
Unrelated ae failure: |
|
I'm a bit reluctant to trust such a large function. Are there many operations that prevent it from being @safe? |
|
@RazvanN7 Only 34 ;-) I have made |
std/stdio.d
Outdated
| { | ||
| alias trusted_FLOCK = (fps) @trusted => _FLOCK(fps); |
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 is a controversial pattern, because even though it's inside a function, it doesn't have a @safe interface and can cause memory corruption when called wrongly inside this @safe function body. (@aG0aep6G)
Without refactoring, the correct thing to do is mark the function @trusted. The @trusted lambda's may give the illusion that the surface of @trusted code is smaller, but you could still escape FILE* fps or add a wrong FLOCK/FUNLOCK call anywhere in the @safe body.
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 is a controversial pattern, because even though it's inside a function, it doesn't have a
@safeinterface and can cause memory corruption when called wrongly inside this@safefunction body. (@aG0aep6G)
I am pleased to see that my constant nagging is having an effect :)
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.
One way to fix this is to encapsulate all the necessary trusted functionality into a static struct that is non-copyable, and use that instead.
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.
Ideally that struct's data would be @system, but that's not in the language yet.
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.
@schveiguy It works with -preview=systemVariables (dlang/dmd#14478). I have implemented this. There is still one hole though, the user can call destroy on the struct instance. That will unlock even though an unshared file pointer may still be active (though access is protected with scope, the struct is still in scope).
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.
The destructor should null the 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.
Why? destroy (or move) already does that.
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.
OK, I misunderstood. I didn't realize you were exposing the pointer elsewhere.
|
I'm sorry if I wasn't clear, but I wasn't asking to put all unsafe operations in trusted lambdas. I was just asking how prevalent are the unsafe patterns in the idea that maybe we could get rid of them. |
Instead of wrong trusted_FUNLOCK. Note: trusted_FGETC does have a @safe interface.
std/stdio.d
Outdated
| version (DIGITAL_MARS_STDIO) | ||
| { | ||
| auto lf = LockedFile(fps); | ||
| auto fp = lf.fp; |
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 defeats the purpose of the struct. The struct can't just be a wrapper for the type itself, it needs to be a wrapper for the functionality that should be trusted. So like instead of trusted_FGETWC, the struct should have a method on it that is trusted.
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.
Ok, but I'm not sure how this defeats the purpose of the struct. What is actually unsafe if fp can only be accessed when the file is locked (aside from the destroy issue)?
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.
FILE * fp;
{ auto lf = LockedFile(fps); fp = lf.fp; }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.
In other words, you should not be able to use the unlocked FILE * if the struct no longer exists.
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 plan to move trusted_FGETC to a struct method as that's better anyway).
lf.fp does not return a FILE*, it's a _iobuf*. On Windows I get:
Error: cannot implicitly convert expression `lf.fp()` of type `_iobuf*` to `shared(_iobuf)*`
But all the other code paths use a cast too so I assume they would trigger the same error. Aside from the type error, the assignment fp = lf.fp should fail due to -dip1000 but it doesn't:
https://forum.dlang.org/post/cvrzlycycprixbadbpgg@forum.dlang.org
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.
OK, I didn't know what exact type fp was returning, this wasn't my point though, I should have been more general in the example:
typeof(LockedFile.fp) fp;
{auto lf = LockedFile(fps); fp = lf.fp; }The point is, if you just expose the pointer then it defeats the purpose of encapsulating the trusted behavior.
We can use `fps` for `ferror` calls instead.
std/stdio.d
Outdated
| @disable void opAssign(LockedFile); | ||
|
|
||
| // Since fps is now locked, we can cast away shared | ||
| @trusted fp() return scope => cast(_iobuf*) fps; |
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 method should be @system
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've removed this method now. I assume you said that because return scope doesn't enforce that the result doesn't live longer than the struct. That seems to be a significant missing piece with -dip1000.
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 mean the point is to encapsulate the unlocked pointer away from usage.
I didn't realize you weren't actually storing the unlocked pointer, but using this to cast it. I was expecting the LockedFile to be storing the unlocked pointer, and protect it from incorrect usage.
std/stdio.d
Outdated
| { | ||
| auto lf = LockedFile(fps); | ||
| // Since fps is now locked, we can cast away shared | ||
| auto fp = (() @trusted => cast(_iobuf*) fps)(); |
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 shouldn't be done at all, use the LockedFile. Just add any needed trusted methods to the struct.
If all trusted operations are encapsulated in the struct, then you don't have to worry about this function in terms of safety (you do right now, because @system data isn't yet a thing, but eventually).
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.
Just add any needed trusted methods to the struct
Well, they're fields:
stdio.d:5509: if (__fhnd_info[fp._file] & FHND_WCHAR)
stdio.d:5541: else if (fp._flag & _IONBF)
stdio.d:5565: int u = fp._cnt;
stdio.d:5566: char* p = fp._ptr;
stdio.d:5568: if (fp._flag & _IOTRAN)
stdio.d:5610: fp._cnt -= i;
stdio.d:5611: () @trusted { fp._ptr += i; }();
Maybe I could add an opDispatch.
If all trusted operations are encapsulated in the struct, then you don't have to worry about this function in terms of safety
I already need to call a trusted lambda to call free. I don't really see what's wrong with immediately calling a trusted lambda, the @trustedness is not being hidden. It's interface is safe at that point in the execution.
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.
OK, I suppose if there's a lf.destroy call added then the fp is not safe.
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.
Most of this does not need to happen on the structure itself, use locals (it's actually faster anyway to manipulate local pointers/counters than use an indirection, from my experience in iopipe). Also, if needed you can encapsulate the whole functionality into the struct itself, or make this section a trusted lambda.
Yeah, I'm looking at the code now, and actually I think this isn't getting the picture across, I'm sorry, trusted code is just so hard to get right. The opDispatch is incorrectly marked safe, because it's treating the pointer as a safe pointer, when it should be a system pointer.
When I get a few minutes, I'll rewrite what I think it should be.
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.
@schveiguy I think I've got it right now, thanks for your help. I put all the digital mars stdio code in a single trusted lambda and did the same for the getdelim code too.
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.
Looks a lot better! One thing I would recommend, store the actual pointer under a more obscure name, and access via a @system accessor, which will enable proper safety checking even without the @system data DIP implemented. Currently the simple access of the private member is still considered @safe by the compiler, and this will at least involve the enforcement, even if the requirement is artificial.
e.g.:
private struct LockedFile {
@system private _iobuf* _fp; // obscure name to avoid accidental usage
@system _iobuf* fp() { return _fp; } // use this in all @trusted calls
...
}When the system variable DIP is default, this can be changed to just a field.
Use @trusted lambda for DIGITAL_MARS_STDIO impl
|
Is this ready to go? |
|
@dkorpel Yes IMO. Steven recommended a workaround until |
I don't think so. It's internal / private anyway, and accessing system variables by default already outputs deprecation messages. |
|
Does anyone know if it's safe to make |
Make
readlnImpl@trusted.Make
File.readlnoverloads@safe.