-
-
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
Fix Issue 15768 - std.stdio.trustedStdout accesses __gshared data wit… #6382
Conversation
Thanks for your pull request, @JackStouffer! Bugzilla references
|
private void initImpl(FILE* handle, string name, uint refs = 1, bool isPopened = false)
{
assert(_p);
_p.handle = handle;
_p.refs = refs; Needs private void resetFile(string name, in char[] stdioOpenmode, bool isPopened) @trusted
{
import core.stdc.stdlib : malloc;
import std.exception : enforce;
import std.conv : text;
import std.exception : errnoEnforce;
if (_p is null)
{
_p = cast(Impl*) enforce(malloc(Impl.sizeof), "Out of memory");
}
else if (_p.refs == 1) Needs void detach() @safe
{
if (!_p) return;
if (_p.refs == 1) Needs |
std/stdio.d
Outdated
@@ -843,12 +844,12 @@ Throws: $(D ErrnoException) on failure if closing the file. | |||
void detach() @safe | |||
{ | |||
if (!_p) return; | |||
if (_p.refs == 1) | |||
if (atomicLoad(_p.refs) == 1) |
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 potential resource leak. Example:
Two threads have references. Both check count, and see that it is 2. Both skip this branch and so the file is not closed. What needs to instead happen is unconditional atomic!"-="
and check the result of that.
So in the new example:
Two threads have references. Both atomically decrement. One sees 0
as the new value and the other sees 1
as the new value. Whichever sees 0
then calls close.
std/stdio.d
Outdated
@@ -513,13 +514,13 @@ Throws: $(D ErrnoException) in case of error. | |||
{ | |||
_p = cast(Impl*) enforce(malloc(Impl.sizeof), "Out of memory"); | |||
} | |||
else if (_p.refs == 1) | |||
else if (atomicLoad(_p.refs) == 1) |
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.
Same concurrency story here as in detatch
. Instead of "close if it's 1, otherwise decrement" you need to "decrement, then close if the result was 0."
std/stdio.d
Outdated
@@ -843,14 +846,11 @@ Throws: $(D ErrnoException) on failure if closing the file. | |||
void detach() @safe | |||
{ | |||
if (!_p) return; | |||
if (_p.refs == 1) | |||
atomicOp!"-="(_p.refs, 1); | |||
if (atomicLoad(_p.refs) == 0) |
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.
You can save an atomic operation: if (atomicOp!"-="(_p.refs, 1) == 0)
std/stdio.d
Outdated
_p.refs--; | ||
_p = cast(Impl*) enforce(malloc(Impl.sizeof), "Out of memory"); | ||
atomicOp!"-="(_p.refs, 1); | ||
if (atomicLoad(_p.refs) == 0) |
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.
You can save an atomic operation: if (atomicOp!"-="(_p.refs, 1) == 0)
std/stdio.d
Outdated
@@ -887,8 +881,7 @@ Throws: $(D ErrnoException) on error. | |||
if (!_p) return; // succeed vacuously | |||
scope(exit) | |||
{ | |||
assert(_p.refs); | |||
if (!--_p.refs) | |||
if (atomicLoad(_p.refs) == 0) |
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 different from the existing code - it doesn't decrement anymore.
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.
It looks like it's been changed to balance out a change in detatch
. The problem is that this is a public function that can be called from places other than detatch
.
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 don't see how I can fix this without running into the issue you pointed out earlier.
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.
You could refactor the code so detatch
doesn't call close
(perhaps both of them call a third, private function) or in the branch of detatch
where _p.refs
is 0 you could change it back to 1 before calling close
.
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.
close
already calls closeHandles
so you could change detatch
to this:
void detach() @safe
{
if (!_p) return;
scope(exit) _p = null;
if (atomicOp!"-="(_p.refs, 1) == 0)
{
scope(exit) free(_p);
closeHandles();
}
}
Then you could change close
so it decrements again.
EDIT: put free(_p)
in another scope(exit)
@JackStouffer |
Well, the test that Jack posted above allows to perfectly reproduce this bug. It just takes ~0.3s on my system. Maybe we can have a special folder (e.g. |
7b18d4c
to
7d4037e
Compare
Added test under a new version branch. |
std/stdio.d
Outdated
auto deleteme = testFilename(); | ||
stderr = File(deleteme, "w"); | ||
|
||
foreach(t; 1_000_000.iota.parallel) |
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.
std/stdio.d:3439: foreach(t; 1_000_000.iota.parallel)
posix.mak:544: recipe for target 'style_lint' failed
Can anyone that uses Windows confirm or deny that 13727 is fixed by this? |
Maybe add the test case from dlang/dmd#5747? |
I'd need to have the test be run on windows on the auto tester. And I'd assume we don't want to run that for every PR. |
Well, we can definitely run it once here for validation? |
13727 has a C test, it doesn't depend on phobos. |
Also don't remove comments about unsafe reassignment. You only fixed that reading them is thread-safe after single publishing, but concurrent writing is still subject to race condition. |
…hout synchronization
How so? Writing is done with LockingTextWriter, which uses OS file locks. |
I think he means writing the handle itself, not writing data to the handle. But the warning needs to be reworded -- the handle is subject to race conditions after it's initialized, for reading OR writing. It has nothing to do with the issue this PR is fixing. |
What specific race conditions are you talking about? |
Let me back up a second here -- this PR is fixing an issue, but it doesn't appear to be the issue identified by the bug report's description. It appears that the test case given in the bug report has nothing to do with a race condition on modifying My recommendation: split the bug into another report, as there are really 2 bugs here -- one is the I'm not sure we even want to add a mutex lock to stdout. Most of the time, it doesn't change ever, or if it does, you are doing it early on when only one thread exists. Maybe just updating the documentation to reflect the rules is the proper fix. |
Classic race condition with shared data. 2 or more threads writing the The other option is to document that you can only change the handles from a single thread, or you get race/undefined behavior. |
The test case shows the assignment outside of the threaded code because the bug in the bug report is specifically about the ref counting in File not working in a multi-threaded environment, and how that's an issue for stdout/in/err. It could possibly be an issue that a thread could re-assign stdout to another file instance in multi-threaded code, if that's what you're referring to. In that case, the nature of stdout/in/err being global due to the nature of C libraries should be documented, and the fact that calls to write/ln auto lock the file. I really wouldn't call writing to stdout a "race condition" in the normal usage of the phrase because there are locks around writing, and just like in any well designed multi-threaded program you'll need some queue equivalent to make sure data is written to the file in the correct order. Once a chunk of data starts to be written to the file, that entire chunk will successfully write. |
Yes.
Again, I'm not talking about writing data to stdout, but reassigning stdout itself to a different handle (e.g. The original warning was worded poorly anyway -- it said reassigning stdout was dangerous due to bug 15768, but really any use of any of the standard handles (regardless of assignment) was dangerous. In any case, your new information is good, so I think it's solved. |
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.
Thanks!
Not sure if issue 15768 should be reworded to reflect what this is actually fixing or this fix should target another bug? See my previous comment. Edit: Well, I guess we could consider the documentation "fixing" the race condition -- just don't do it, RTFM ;) |
15768 specifically mentions the ref count being the heart of the issue. The reassignment race conditions are really not part of this. |
The title of the bug report is the problem (it's talking about |
|
Running the following code
dies 100% of the time with v2.079.0, but with this fix it works every time I've tried.
I don't think there can be a test in the test suite for this, so please review carefully.