Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Lib/asyncio/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def run(self):

console.write(banner)

if startup_path := os.getenv("PYTHONSTARTUP"):
if not sys.flags.isolated and (startup_path := os.getenv("PYTHONSTARTUP")):
sys.audit("cpython.run_startup", startup_path)

import tokenize
Expand Down
4 changes: 4 additions & 0 deletions Lib/asyncio/futures.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ def __init__(self, *, loop=None):
loop object used by the future. If it's not provided, the future uses
the default event loop.
"""
if self._loop is not None:
raise RuntimeError(f"{self.__class__.__name__} object is already "
"initialized")

if loop is None:
self._loop = events.get_event_loop()
else:
Expand Down
31 changes: 4 additions & 27 deletions Lib/test/test_asyncio/test_futures.py
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,10 @@ def test_future_cancelled_exception_refcycles(self):
self.assertIsNotNone(exc)
self.assertListEqual(gc.get_referrers(exc), [])

def test_future_disallow_multiple_initialization(self):
f = self._new_future(loop=self.loop)
with self.assertRaises(RuntimeError, msg="is already initialized"):
f.__init__(loop=self.loop)

@unittest.skipUnless(hasattr(futures, '_CFuture'),
'requires the C _asyncio module')
Expand Down Expand Up @@ -1091,33 +1095,6 @@ def __getattribute__(self, name):
fut.add_done_callback(fut_callback_0)
self.assertRaises(ReachableCode, fut.set_result, "boom")

def test_use_after_free_on_fut_context_0_with_evil__getattribute__(self):
# see: https://github.com/python/cpython/issues/125984

class EvilEventLoop(SimpleEvilEventLoop):
def call_soon(self, *args, **kwargs):
super().call_soon(*args, **kwargs)
raise ReachableCode

def __getattribute__(self, name):
if name == 'call_soon':
# resets the future's event loop
fut.__init__(loop=SimpleEvilEventLoop())
return object.__getattribute__(self, name)

evil_loop = EvilEventLoop()
with mock.patch.object(self, 'loop', evil_loop):
fut = self._new_future()
self.assertIs(fut.get_loop(), evil_loop)

fut_callback_0 = mock.Mock()
fut_context_0 = mock.Mock()
fut.add_done_callback(fut_callback_0, context=fut_context_0)
del fut_context_0
del fut_callback_0
self.assertRaises(ReachableCode, fut.set_result, "boom")


@unittest.skipUnless(hasattr(futures, '_CFuture'),
'requires the C _asyncio module')
class CFutureDoneCallbackTests(BaseFutureDoneCallbackTests,
Expand Down
42 changes: 9 additions & 33 deletions Lib/test/test_asyncio/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2776,28 +2776,17 @@ def test_get_context(self):
finally:
loop.close()

def test_proper_refcounts(self):
# see: https://github.com/python/cpython/issues/126083
class Break:
def __str__(self):
raise RuntimeError("break")

obj = object()
initial_refcount = sys.getrefcount(obj)

coro = coroutine_function()
with contextlib.closing(asyncio.EventLoop()) as loop:
task = asyncio.Task.__new__(asyncio.Task)
for _ in range(5):
with self.assertRaisesRegex(RuntimeError, 'break'):
task.__init__(coro, loop=loop, context=obj, name=Break())

coro.close()
task._log_destroy_pending = False
del task
def test_task_disallow_multiple_initialization(self):
async def foo():
pass

self.assertEqual(sys.getrefcount(obj), initial_refcount)
coro = foo()
self.addCleanup(coro.close)
task = self.new_task(self.loop, coro)
task._log_destroy_pending = False

with self.assertRaises(RuntimeError, msg="is already initialized"):
task.__init__(coro, loop=self.loop)

def add_subclass_tests(cls):
BaseTask = cls.Task
Expand Down Expand Up @@ -2921,19 +2910,6 @@ class CTask_CFuture_Tests(BaseTaskTests, SetMethodsTest,
all_tasks = getattr(tasks, '_c_all_tasks', None)
current_task = staticmethod(getattr(tasks, '_c_current_task', None))

@support.refcount_test
def test_refleaks_in_task___init__(self):
gettotalrefcount = support.get_attribute(sys, 'gettotalrefcount')
async def coro():
pass
task = self.new_task(self.loop, coro())
self.loop.run_until_complete(task)
refs_before = gettotalrefcount()
for i in range(100):
task.__init__(coro(), loop=self.loop)
self.loop.run_until_complete(task)
self.assertAlmostEqual(gettotalrefcount() - refs_before, 0, delta=10)

def test_del__log_destroy_pending_segfault(self):
async def coro():
pass
Expand Down
35 changes: 23 additions & 12 deletions Lib/test/test_repl.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
raise unittest.SkipTest("test module requires subprocess")


def spawn_repl(*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, custom=False, **kw):
def spawn_repl(*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, custom=False, isolated=True, **kw):
"""Run the Python REPL with the given arguments.
kw is extra keyword args to pass to subprocess.Popen. Returns a Popen
Expand All @@ -42,7 +42,10 @@ def spawn_repl(*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, custom=F
# path may be used by PyConfig_Get("module_search_paths") to build the
# default module search path.
stdin_fname = os.path.join(os.path.dirname(sys.executable), "<stdin>")
cmd_line = [stdin_fname, '-I']
cmd_line = [stdin_fname]
# Isolated mode implies -EPs and ignores PYTHON* variables.
if isolated:
cmd_line.append('-I')
# Don't re-run the built-in REPL from interactive mode
# if we're testing a custom REPL (such as the asyncio REPL).
if not custom:
Expand Down Expand Up @@ -215,7 +218,7 @@ def make_repl(env):
with os_helper.temp_dir() as tmpdir:
script = os.path.join(tmpdir, "pythonstartup.py")
with open(script, "w") as f:
f.write("print('from pythonstartup')" + os.linesep)
f.write("print('from pythonstartup')\n")

env = os.environ.copy()
env['PYTHONSTARTUP'] = script
Expand Down Expand Up @@ -296,19 +299,27 @@ def test_asyncio_repl_reaches_python_startup_script(self):
with os_helper.temp_dir() as tmpdir:
script = os.path.join(tmpdir, "pythonstartup.py")
with open(script, "w") as f:
f.write("print('pythonstartup done!')" + os.linesep)
f.write("exit(0)" + os.linesep)
f.write("print('pythonstartup done!')\n")
env = os.environ.copy()
env["PYTHON_HISTORY"] = os.path.join(tmpdir, ".asyncio_history")
env["PYTHONSTARTUP"] = script
p = spawn_asyncio_repl(isolated=False, env=env)
output = kill_python(p)
self.assertEqual(p.returncode, 0)
self.assertIn("pythonstartup done!", output)

def test_asyncio_repl_respects_isolated_mode(self):
with os_helper.temp_dir() as tmpdir:
script = os.path.join(tmpdir, "pythonstartup.py")
with open(script, "w") as f:
f.write("print('should not print')\n")
env = os.environ.copy()
env["PYTHON_HISTORY"] = os.path.join(tmpdir, ".asyncio_history")
env["PYTHONSTARTUP"] = script
subprocess.check_call(
[sys.executable, "-m", "asyncio"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env,
timeout=SHORT_TIMEOUT,
)
p = spawn_asyncio_repl(isolated=True, env=env)
output = kill_python(p)
self.assertEqual(p.returncode, 0)
self.assertNotIn("should not print", output)

@unittest.skipUnless(pty, "requires pty")
def test_asyncio_repl_is_ok(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix possible crashes when initializing :class:`asyncio.Task` or :class:`asyncio.Future` multiple times.
These classes can now be initialized only once and any subsequent initialization attempt will raise a RuntimeError.
Patch by Kumar Aditya.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
The :mod:`asyncio` REPL now respects the :option:`-I` flag (isolated mode).
Previously, it would load and execute :envvar:`PYTHONSTARTUP` even if the
flag was set. Contributed by Bartosz Sławecki.
24 changes: 6 additions & 18 deletions Modules/_asynciomodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -498,21 +498,13 @@ future_schedule_callbacks(asyncio_state *state, FutureObj *fut)
static int
future_init(FutureObj *fut, PyObject *loop)
{
if (fut->fut_loop != NULL) {
PyErr_Format(PyExc_RuntimeError, "%T object is already initialized", fut);
return -1;
}

PyObject *res;
int is_true;

Py_CLEAR(fut->fut_loop);
Py_CLEAR(fut->fut_callback0);
Py_CLEAR(fut->fut_context0);
Py_CLEAR(fut->fut_callbacks);
Py_CLEAR(fut->fut_result);
Py_CLEAR(fut->fut_exception);
Py_CLEAR(fut->fut_exception_tb);
Py_CLEAR(fut->fut_source_tb);
Py_CLEAR(fut->fut_cancel_msg);
Py_CLEAR(fut->fut_cancelled_exc);
Py_CLEAR(fut->fut_awaited_by);

fut->fut_state = STATE_PENDING;
fut->fut_log_tb = 0;
fut->fut_blocking = 0;
Expand Down Expand Up @@ -3008,11 +3000,7 @@ task_call_step_soon(asyncio_state *state, TaskObj *task, PyObject *arg)
return -1;
}

// Beware: An evil call_soon could alter task_context.
// See: https://github.com/python/cpython/issues/126080.
PyObject *task_context = Py_NewRef(task->task_context);
int ret = call_soon(state, task->task_loop, cb, NULL, task_context);
Py_DECREF(task_context);
int ret = call_soon(state, task->task_loop, cb, NULL, task->task_context);
Py_DECREF(cb);
return ret;
}
Expand Down
2 changes: 1 addition & 1 deletion Python/compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -634,7 +634,7 @@ _PyCompile_EnterScope(compiler *c, identifier name, int scope_type,
}
}
if (u->u_ste->ste_has_conditional_annotations) {
/* Cook up an implicit __conditional__annotations__ cell */
/* Cook up an implicit __conditional_annotations__ cell */
Py_ssize_t res;
assert(u->u_scope_type == COMPILE_SCOPE_CLASS || u->u_scope_type == COMPILE_SCOPE_MODULE);
res = _PyCompile_DictAddObj(u->u_metadata.u_cellvars, &_Py_ID(__conditional_annotations__));
Expand Down
Loading