Skip to content

gh-130163: Fix usage of borrow references from _PySys_GetAttr #130282

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

Closed
320 changes: 320 additions & 0 deletions Lib/test/test_sys_getattr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
from pathlib import Path
import tempfile
import test.support
from test.support.script_helper import assert_python_failure, assert_python_ok
import textwrap
import unittest




@test.support.cpython_only
class PySysGetAttrTest(unittest.TestCase):

common_faulthandler_code = textwrap.dedent('''
import sys
from faulthandler import dump_traceback, enable, dump_traceback_later

# The faulthandler_get_fileno calls stderr functions twice - the first
# call is a fileno and the second one is a flush. So, if the stderr
# changed while fileno is called, then we get a segfault when we
# call flush after that.
class FakeIO:
def __init__(self, what):
self.what = what
def write(self, str):
pass
def flush(self):
pass
def fileno(self):
self.restore_std('stderr')
return 0

@staticmethod
def restore_std(what):
stdfile = getattr(sys, what)
setattr(sys, what, getattr(sys, "__" + what + "__"))
del stdfile

@staticmethod
def set_std(what):
setattr(sys, what, FakeIO(what))

def main():
enable(None, True)
FakeIO.set_std('stderr')
{0}

if __name__ == "__main__":
main()
''')

common_warnings_code = textwrap.dedent('''
from io import StringIO
import sys
import warnings

# First of all, we have to delete _showwarnmsg to get into show_warning
# function. The show_warning do series of calls of PyFile_WriteObject
# and PyFile_WriteString functions. And one of the call is for __repr__
# of warning's message. So if we change stderr while calling __repr__
# (or concurently) we can get segfault from one of the consequence call
# to write functions.
class Foo:
def __init__(self):
self.x = sys.stderr
setattr(sys, "stderr", StringIO())

def __repr__(self):
x = sys.stderr
setattr(sys, "stderr", self.x)
del x
return "Foo"

def main():
del warnings._showwarnmsg
{0}

if __name__ == "__main__":
main()
exit(0)
''')

common_input_code = textwrap.dedent('''
import sys

class FakeIO:
def write(self, str):
pass
def flush(self):
pass

# The input function gets borrowed refs for stdin, stdout and stderr.
# As we use FakeIO without fileno the input functions thinks that we
# are not tty and following happens:
# audit is called, stderr is flushed, prompt's __repr__ is printed to
# stdout and line is read from stdin. For stdin and stdout we can just
# replace stdin and stdout from prompt's __repr__ and get segfault. But
# for stderr we should do this from audit function.
class CrashWhat:
def __init__(self, what):
self.what = what
self.std = getattr(sys, what)
setattr(sys, what, FakeIO())

def __repr__(self):
std = getattr(sys, self.what)
setattr(sys, self.what, self.std)
del std
return "Crash"

def audit(event, args):
repr(args)

def main():
{0}
input({1})

if __name__ == "__main__":
main()

''')

unraisable_hook_code = textwrap.dedent('''
import sys

class UnraisableHookInitiator:
def __del__(self):
raise Exception('1')

# To get into unraisablehook we need to raise an exception from __del__
# function. So, format_unraisable_v gets hook, calls audit
# function and calls hook. If we revert back unraisablehook from audit
# function we will get segfault when calling hook.
class UnraisableHook:
def __call__(self, *args, **kwds):
print('X', *args)

def __repr__(self):
h = sys.unraisablehook
setattr(sys, 'unraisablehook', sys.__unraisablehook__)
del h
return 'UnraisableHook'

def audit(event, args):
repr(args)

def main():
sys.addaudithook(audit)
setattr(sys, 'unraisablehook', UnraisableHook())
x = UnraisableHookInitiator()
del x

if __name__ == "__main__":
main()

''')

flush_std_files_common_code = textwrap.dedent('''
import sys

# The flush_std_files function gets stdout and stderr. And then checks
# if both of them are closed. And if so calls flush for them.
# If we restore stdfile from FakeIO.closed property we can get segfault.
class FakeIO:
def __init__(self, what):
self.what = what
def write(self, str):
pass
def flush(self):
pass

@property
def closed(self):
stdfile = getattr(sys, self.what)
setattr(sys, self.what, getattr(sys, "__" + self.what + "__"))
del stdfile
return False

def main():
setattr(sys, '{0}', FakeIO('{0}'))

if __name__ == "__main__":
main()
''')

pyerr_printex_code = textwrap.dedent('''
import sys

# To get into excepthook we should run invalid statement.
# Then _PyErr_PrintEx gets excepthook, calls audit function and tries
# to call hook. If we replace hook from audit (or concurently) we get
# segfault.
class Hook:
def __call__(self, *args, **kwds):
pass

def __repr__(self):
h = sys.excepthook
setattr(sys, 'excepthook', sys.__excepthook__)
del h
return 'Hook'

def audit(event, args):
repr(args)

def main():
sys.addaudithook(audit)
setattr(sys, 'excepthook', Hook())
raise

if __name__ == "__main__":
main()
''')

print_code = textwrap.dedent('''
from io import StringIO
import sys

# The print function gets stdout and does a series of calls write
# functions. One of the function calls __repr__ and if we replace
# stdout from __repr__ (or concurently) we get segfault.
class Bar:
def __init__(self):
self.x = sys.stdout
setattr(sys, "stdout", StringIO())

def __repr__(self):
x = sys.stdout
setattr(sys, "stdout", self.x)
del x
return "Bar"

def main():
print(Bar())

if __name__ == "__main__":
main()
exit(0)
''')

def _check(self, code):
rc, out, err = assert_python_ok('-c', code)
self.assertEqual(rc, 0)
self.assertNotIn(b"Segmentation fault", err)
self.assertNotIn(b"access violation", err)

def test_print_deleted_stdout(self):
# print should use strong reference to the stdout.
self._check(self.print_code)

def test_faulthandler_enable_deleted_stderr(self):
# faulthandler should use strong reference to the stderr
code = self.common_faulthandler_code.format(
"enable(None, True)"
)
self._check(code)

def test_faulthandler_dump_traceback_deleted_stderr(self):
# faulthandler should use strong reference to the stderr
code = self.common_faulthandler_code.format(
"dump_traceback(None, False)"
)
self._check(code)

def test_faulthandler_dump_traceback_later_deleted_stderr(self):
# faulthandler should use strong reference to the stderr
code = self.common_faulthandler_code.format(
"dump_traceback_later(0.1, True, None, False)"
)
self._check(code)

def test_warnings_warn(self):
code = self.common_warnings_code.format(
"warnings.warn(Foo())"
)
self._check(code)

def test_warnings_warn_explicit(self):
code = self.common_warnings_code.format(
"warnings.warn_explicit(Foo(), UserWarning, 'filename', 0)"
)
self._check(code)

def test_input_stdin(self):
code = self.common_input_code.format(
"",
"CrashWhat('stdin')"
)
self._check(code)

def test_input_stdout(self):
code = self.common_input_code.format(
"",
"CrashWhat('stdout')"
)
self._check(code)

def test_input_stderr(self):
code = self.common_input_code.format(
"sys.addaudithook(audit)",
"CrashWhat('stderr')"
)
self._check(code)

def test_errors_unraisablehook(self):
self._check(self.unraisable_hook_code)

def test_py_finalize_flush_std_files_stdout(self):
code = self.flush_std_files_common_code.format("stdout")
self._check(code)

def test_py_finalize_flush_std_files_stderr(self):
code = self.flush_std_files_common_code.format("stderr")
self._check(code)

def test_pyerr_printex_excepthook(self):
self._check(self.pyerr_printex_code)

if __name__ == "__main__":
unittest.main()
Loading