python / python/cpython

`subprocess.Popen.poll` race condition returns without polling the child

Abierto
#127,050 1 comentario 1 reacción 0 asignados Ver en GitHub

Nadie ha tomado este issue todavía.

stdlib type-bug
Lenguaje dominante
Python
Estrellas
77.2k
Forks
35.9k
Métricas de merge de PR
Métricas de PR pendientes

Descripción

Bug report

Bug description:

While working on GH-127049, I noticed that on non-Windows platforms Popen.poll can return None even for an exited child when Popen.poll races against Popen.[poll | wait] in another thread.

I think Popen.poll is violating its docs when this happens, because it returns without checking the child.

Writing a reproducer for this is tricky, but the following hits the race condition more than half the time on my machine:

from __future__ import annotations

import os
import subprocess
from concurrent.futures import ThreadPoolExecutor

with ThreadPoolExecutor(1) as executor:
    process = subprocess.Popen(
        (
            "python",
            "-c",
            "import time; time.sleep(1)",
        ),
        stdin=subprocess.DEVNULL,
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
    )
    executor.submit(process.wait)

    try:
        os.waitid(os.P_PID, process.pid, os.WEXITED | os.WNOWAIT)
    except ChildProcessError:
        # P_PIDFD would avoid this ECHILD, but writing the reproducer this way for
        # portability.
        pass
    assert process.poll() is not None

The culprit is https://github.com/python/cpython/blob/v3.14.0a1/Lib/subprocess.py#L1989-L1992, from d65ba51e245ffdd155bc1e7b8884fc943048111f.

IIUC the reason that poll can't block on _waitpid_lock.acquire here is because poll must be non-blocking API, but a wait call can make a blocking call (waitpid without WNOHANG) while holding that lock.

IIUC this should be fixable with the two-step (1) wait-without-reaping and (2) hold a lock to atomically reap-without-waiting & set .returncode approach described at https://github.com/python/cpython/issues/82811#issuecomment-1093845693 and https://github.com/python/cpython/issues/86724#issuecomment-1093894017 and used by Trio. That approach should also be able to fix case 1 of the thread-unsafety ignored in GH-20010, but not case 2[^1]. It could be a bit of a pain though[^2].

To be clear about impact, though, I have only seen this poll retval bug happen while testing a fix for GH-127049. And in that situation, a very slight modification to said fix for GH-127049 can easily avoid this bug (as well as case 1 from GH-20010).

[^1]: Tangent: Case 2 should be fixable on Linux >= 5.4 (via pidfd_open & P_PIDFD). (It's possibly also fixable on BSDs & macOS, though I am not currently familiar there. What happens to an already-existing kevent for a PID after that PID has been freed? Does it still work, like a pidfd? It looks like Trio assumes that it does.)

[^2]: Does the "fail gracefully when SIGCLD is set to be ignored or waiting for child processes has otherwise been disabled for our process" case need different handling than the "an external caller reaped the PID and stole the return code from the Popen" case? From Popen's perspective (assuming a platform without pidfd/kqueue), both of these cases look like an ECHILD (either from "WNOWAIT waitid (wait-without-reaping) or WNOHANG wait[p]id (reap-without-waiting)). Is it okay if we can't disambiguate between these two cases?

CPython versions tested on:

3.9, 3.10, 3.11, 3.12, 3.13, 3.14, CPython main branch

Operating systems tested on:

Linux

Guía de contribución

Abrir la guía de contribución

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Línea de trabajo

El issue apunta a las líneas 1989-1992 de Lib/subprocess.py; empieza allí y ejecuta en Linux el reproductor proporcionado de ThreadPoolExecutor. Lee las discusiones sobre el bloqueo de wait/poll en GH-82811 y GH-86724 y, después, verifica que poll ya no devuelva None para un proceso hijo terminado y que siga siendo no bloqueante.

Escrito por el modelo de indexación a partir del texto del issue.

Evaluación

Stack tecnológico
python
Área
operating-systems
Tipo de issue
Error
Dificultad
5/5
Tiempo estimado
Más de una semana
Estado de actividad
Tranquilo
Claridad
Bastante claro
Aptitud para principiantes
35/100

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.