Skip to content
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

Merged
merged 2 commits into from
Apr 6, 2018

Conversation

JackStouffer
Copy link
Member

@JackStouffer JackStouffer commented Mar 29, 2018

Running the following code

import std.algorithm;
import std.parallelism;
import std.range;
import std.stdio;

void main()
{
    stderr = File("/dev/null", "w");
    
    foreach(t; 1_000_000.iota.parallel)
    {
        stderr.write("aaa");
    }
}

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.

@dlang-bot
Copy link
Contributor

dlang-bot commented Mar 29, 2018

Thanks for your pull request, @JackStouffer!

Bugzilla references

Auto-close Bugzilla Severity Description
15768 critical std.stdio.File does not support __gshared semantics of stdout/err/in

⚠️⚠️⚠️ Warnings ⚠️⚠️⚠️

To target stable perform these two steps:

  1. Rebase your branch to upstream/stable:
git rebase --onto upstream/stable upstream/master
  1. Change the base branch of your PR to stable

Testing this PR locally

If you don't have a local development environment setup, you can use Digger to test this PR:

dub fetch digger
dub run digger -- build "master + phobos#6382"

@n8sh
Copy link
Member

n8sh commented Mar 29, 2018

    private void initImpl(FILE* handle, string name, uint refs = 1, bool isPopened = false)
    {
        assert(_p);
        _p.handle = handle;
        _p.refs = refs;

Needs atomicStore here.

    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 atomicLoad here.

    void detach() @safe
    {
        if (!_p) return;
        if (_p.refs == 1)

Needs atomicLoad here.

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)
Copy link
Member

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)
Copy link
Member

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)
Copy link
Member

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)
Copy link
Member

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)
Copy link
Member

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.

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member

@n8sh n8sh Mar 29, 2018

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)

@jercaianu
Copy link
Contributor

@JackStouffer
For testing, can't we redirect stdout to a file, write something in there from a couple of threads and then restore stdout?

@wilzbach wilzbach added this to the 2.080.0 milestone Apr 1, 2018
@wilzbach
Copy link
Member

wilzbach commented Apr 1, 2018

For testing, can't we redirect stdout to a file, write something in there from a couple of threads and then restore stdout?

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. stresstests), s.t. these tests aren't run by default for the unittest target?

@JackStouffer JackStouffer force-pushed the safe-file branch 2 times, most recently from 7b18d4c to 7d4037e Compare April 2, 2018 12:25
@JackStouffer
Copy link
Member Author

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)
Copy link
Member

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

@JackStouffer
Copy link
Member Author

Can anyone that uses Windows confirm or deny that 13727 is fixed by this?

@wilzbach
Copy link
Member

wilzbach commented Apr 2, 2018

Maybe add the test case from dlang/dmd#5747?

@JackStouffer
Copy link
Member Author

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.

@wilzbach
Copy link
Member

wilzbach commented Apr 2, 2018

Well, we can definitely run it once here for validation?
(also the test case that Walter wanted to add it in dlang/dmd#5747 only uses 1000 runs.)

@anon17
Copy link

anon17 commented Apr 3, 2018

13727 has a C test, it doesn't depend on phobos.

@anon17
Copy link

anon17 commented Apr 3, 2018

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.

@JackStouffer
Copy link
Member Author

How so? Writing is done with LockingTextWriter, which uses OS file locks.

@schveiguy
Copy link
Member

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.

@JackStouffer
Copy link
Member Author

What specific race conditions are you talking about?

@schveiguy
Copy link
Member

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 stdout, but improperly implemented reference counting inside File.

My recommendation: split the bug into another report, as there are really 2 bugs here -- one is the File refcount implementation, and the other is the race condition you could have from trustedStdout returning stdout without locking.

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.

@schveiguy
Copy link
Member

What specific race conditions are you talking about?

Classic race condition with shared data. 2 or more threads writing the stdout handle without locking, or 1 thread writing the stdout handle while another thread is calling trustedStdout to read it without locking. In order to properly provide a way to modify the stdout handle, we would need locking.

The other option is to document that you can only change the handles from a single thread, or you get race/undefined behavior.

@JackStouffer
Copy link
Member Author

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.

@schveiguy
Copy link
Member

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.

Yes.

I really wouldn't call writing to stdout a "race condition" in the normal usage of the phrase because there are locks around writing

Again, I'm not talking about writing data to stdout, but reassigning stdout itself to a different handle (e.g. stdout = File("output.txt");). There are no protections around this.

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.

@JackStouffer JackStouffer added the Merge:72h no objection -> merge The PR will be merged if there are no objections raised. label Apr 3, 2018
Copy link
Member

@andralex andralex left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@schveiguy
Copy link
Member

schveiguy commented Apr 5, 2018

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 ;)

@JackStouffer
Copy link
Member Author

15768 specifically mentions the ref count being the heart of the issue. The reassignment race conditions are really not part of this.

@schveiguy
Copy link
Member

schveiguy commented Apr 5, 2018

The title of the bug report is the problem (it's talking about trustedStdout which simply returns stdout). I'll just change it, so it doesn't confuse people.

@anon17
Copy link

anon17 commented Apr 6, 2018

$(REF stdin,core,stdout) you probably meant $(REF stdin,core,stdio). Same for stderr.

@JackStouffer JackStouffer added Merge:auto-merge and removed Merge:72h no objection -> merge The PR will be merged if there are no objections raised. labels Apr 6, 2018
@dlang-bot dlang-bot merged commit 7762418 into dlang:master Apr 6, 2018
@JackStouffer JackStouffer deleted the safe-file branch April 6, 2018 15:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants