gh-139894: fix incorrect sharing of current task while forking in asyncio (#139897)
Some checks are pending
Tests / (push) Blocked by required conditions
Tests / Change detection (push) Waiting to run
Tests / Docs (push) Blocked by required conditions
Tests / Check if Autoconf files are up to date (push) Blocked by required conditions
Tests / Windows MSI (push) Blocked by required conditions
Tests / Check if generated files are up to date (push) Blocked by required conditions
Tests / Ubuntu SSL tests with OpenSSL (push) Blocked by required conditions
Tests / Ubuntu SSL tests with AWS-LC (push) Blocked by required conditions
Tests / Android (aarch64) (push) Blocked by required conditions
Tests / Android (x86_64) (push) Blocked by required conditions
Tests / WASI (push) Blocked by required conditions
Tests / Hypothesis tests on Ubuntu (push) Blocked by required conditions
Tests / Address sanitizer (push) Blocked by required conditions
Tests / Sanitizers (push) Blocked by required conditions
Tests / Cross build Linux (push) Blocked by required conditions
Tests / CIFuzz (push) Blocked by required conditions
Tests / All required checks pass (push) Blocked by required conditions
Lint / lint (push) Waiting to run
mypy / Run mypy on Lib/_pyrepl (push) Waiting to run
mypy / Run mypy on Lib/test/libregrtest (push) Waiting to run
mypy / Run mypy on Lib/tomllib (push) Waiting to run
mypy / Run mypy on Tools/build (push) Waiting to run
mypy / Run mypy on Tools/cases_generator (push) Waiting to run
mypy / Run mypy on Tools/clinic (push) Waiting to run
mypy / Run mypy on Tools/jit (push) Waiting to run
mypy / Run mypy on Tools/peg_generator (push) Waiting to run

Fix incorrect sharing of current task with the forked child process by clearing thread state's current task and current loop in `PyOS_AfterFork_Child`.
This commit is contained in:
Kumar Aditya 2025-10-10 21:58:23 +05:30 committed by GitHub
parent c7f1da97eb
commit b881df47ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 72 additions and 25 deletions

View file

@ -1180,32 +1180,68 @@ class TestFunctional(unittest.TestCase):
@support.requires_fork()
class TestFork(unittest.IsolatedAsyncioTestCase):
class TestFork(unittest.TestCase):
async def test_fork_not_share_event_loop(self):
with warnings_helper.ignore_fork_in_thread_deprecation_warnings():
# The forked process should not share the event loop with the parent
loop = asyncio.get_running_loop()
r, w = os.pipe()
self.addCleanup(os.close, r)
self.addCleanup(os.close, w)
pid = os.fork()
if pid == 0:
# child
try:
loop = asyncio.get_event_loop()
os.write(w, b'LOOP:' + str(id(loop)).encode())
except RuntimeError:
os.write(w, b'NO LOOP')
except BaseException as e:
os.write(w, b'ERROR:' + ascii(e).encode())
finally:
os._exit(0)
else:
# parent
result = os.read(r, 100)
self.assertEqual(result, b'NO LOOP')
wait_process(pid, exitcode=0)
@warnings_helper.ignore_fork_in_thread_deprecation_warnings()
def test_fork_not_share_current_task(self):
loop = object()
task = object()
asyncio._set_running_loop(loop)
self.addCleanup(asyncio._set_running_loop, None)
asyncio.tasks._enter_task(loop, task)
self.addCleanup(asyncio.tasks._leave_task, loop, task)
self.assertIs(asyncio.current_task(), task)
r, w = os.pipe()
self.addCleanup(os.close, r)
self.addCleanup(os.close, w)
pid = os.fork()
if pid == 0:
# child
try:
asyncio._set_running_loop(loop)
current_task = asyncio.current_task()
if current_task is None:
os.write(w, b'NO TASK')
else:
os.write(w, b'TASK:' + str(id(current_task)).encode())
except BaseException as e:
os.write(w, b'ERROR:' + ascii(e).encode())
finally:
asyncio._set_running_loop(None)
os._exit(0)
else:
# parent
result = os.read(r, 100)
self.assertEqual(result, b'NO TASK')
wait_process(pid, exitcode=0)
@warnings_helper.ignore_fork_in_thread_deprecation_warnings()
def test_fork_not_share_event_loop(self):
# The forked process should not share the event loop with the parent
loop = object()
asyncio._set_running_loop(loop)
self.assertIs(asyncio.get_running_loop(), loop)
self.addCleanup(asyncio._set_running_loop, None)
r, w = os.pipe()
self.addCleanup(os.close, r)
self.addCleanup(os.close, w)
pid = os.fork()
if pid == 0:
# child
try:
loop = asyncio.get_event_loop()
os.write(w, b'LOOP:' + str(id(loop)).encode())
except RuntimeError:
os.write(w, b'NO LOOP')
except BaseException as e:
os.write(w, b'ERROR:' + ascii(e).encode())
finally:
os._exit(0)
else:
# parent
result = os.read(r, 100)
self.assertEqual(result, b'NO LOOP')
wait_process(pid, exitcode=0)
@warnings_helper.ignore_fork_in_thread_deprecation_warnings()
@hashlib_helper.requires_hashdigest('md5')

View file

@ -0,0 +1 @@
Fix incorrect sharing of current task with the child process while forking in :mod:`asyncio`. Patch by Kumar Aditya.

View file

@ -689,6 +689,14 @@ reset_remotedebug_data(PyThreadState *tstate)
_Py_MAX_SCRIPT_PATH_SIZE);
}
static void
reset_asyncio_state(_PyThreadStateImpl *tstate)
{
llist_init(&tstate->asyncio_tasks_head);
tstate->asyncio_running_loop = NULL;
tstate->asyncio_running_task = NULL;
}
void
PyOS_AfterFork_Child(void)
@ -725,6 +733,8 @@ PyOS_AfterFork_Child(void)
reset_remotedebug_data(tstate);
reset_asyncio_state((_PyThreadStateImpl *)tstate);
// Remove the dead thread states. We "start the world" once we are the only
// thread state left to undo the stop the world call in `PyOS_BeforeFork`.
// That needs to happen before `_PyThreadState_DeleteList`, because that