python / python/cpython

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

Đang mở
#127,050 1 bình luận 1 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

stdlib type-bug
Ngôn ngữ chính
Python
Star
77.2k
Fork
35.9k
Chỉ số merge pull request
Chỉ số pull request đang chờ

Mô tả

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

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Hướng nghiên cứu

Issue trỏ đến các dòng 1989-1992 trong Lib/subprocess.py; hãy bắt đầu từ đó và chạy reproducer ThreadPoolExecutor được cung cấp trên Linux. Đọc các cuộc thảo luận về việc khóa wait/poll trong GH-82811 và GH-86724, sau đó xác minh rằng poll không còn trả về None cho một tiến trình con đã thoát và vẫn không blocking.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
python
Lĩnh vực
operating-systems
Loại issue
Lỗi
Độ khó
5/5
Thời gian dự kiến
Hơn một tuần
Mức độ hoạt động
Ít trao đổi
Độ rõ ràng
Khá rõ ràng
Mức phù hợp với người mới
35/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.