Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions include/pybind11/iostream.h
Original file line number Diff line number Diff line change
Expand Up @@ -248,9 +248,16 @@ class OstreamRedirect {

void enter() {
if (do_stdout_) {
// Release any existing redirect first so the stream's original buffer is
// restored before the new redirect captures it. Otherwise a second enter()
// (e.g. re-entering the same context manager) would record a pointer to the
// buffer owned by the redirect that reset() is about to destroy, leaving a
// dangling streambuf behind on exit().
redirect_stdout.reset();
redirect_stdout.reset(new scoped_ostream_redirect());
}
if (do_stderr_) {
redirect_stderr.reset();
redirect_stderr.reset(new scoped_estream_redirect());
}
}
Expand Down
16 changes: 16 additions & 0 deletions tests/test_iostream.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,22 @@ def test_redirect_both(capfd):
assert stream2.getvalue() == msg2


def test_redirect_reentry(capfd):
# Re-entering the same ``ostream_redirect`` (``__enter__`` twice with no ``__exit__``
# in between, e.g. nesting the same context manager) must not leave std::cout bound
# to a freed streambuf. Before the fix the inner redirect recorded a pointer to the
# buffer owned by the outer redirect that was about to be destroyed, so a later write
# to std::cout was a use-after-free.
redirect = m.ostream_redirect(stdout=True, stderr=False)
redirect.__enter__()
redirect.__enter__()
redirect.__exit__()
m.raw_output("alive")
stdout, stderr = capfd.readouterr()
assert stdout == "alive"
assert not stderr


def test_move_redirect(capsys):
m.move_redirect_output("before_move", "after_move")
stdout, stderr = capsys.readouterr()
Expand Down
Loading