Skip to content

Windows + cx_Freeze GUI build: create_subprocess_exec() fails with OSError [Errno 9] bad file descriptor unless stdin=DEVNULL #126

@XiaoYouChR

Description

@XiaoYouChR

Summary

On Windows, winloop subprocess creation appears to fail inside a cx_Freeze GUI-frozen executable (base="gui" / no console), even for a minimal repro.

The failure happens when calling asyncio.create_subprocess_exec(...) with the usual defaults for stdin and with redirected stdout/stderr.

A reliable workaround on the application side is to explicitly pass:

stdin=asyncio.subprocess.DEVNULL

Once I do that, the frozen executable works.

This suggests winloop may be assuming that inherited/default stdio handles are valid in a no-console frozen process, while cx_Freeze GUI executables do not always provide valid default stdin/stdio state.


Environment

  • OS: Windows x64
  • Python: 3.11.15 (MSC v.1944 64 bit (AMD64))
  • winloop: 0.5.0
  • cx_Freeze: 8.6.1

Real-world trigger

This first showed up in my application when I was doing:

process = await asyncio.create_subprocess_exec(
    ffmpegPath,
    "-version",
    stdout=asyncio.subprocess.PIPE,
    stderr=asyncio.subprocess.DEVNULL,
)

Inside a cx_Freeze GUI-frozen executable, that crashes with:

OSError: [Errno 9] bad file descriptor

Minimal reproduction

smoke.py

import asyncio
import os
import winloop


async def main():
    cmd = os.environ.get("ComSpec") or r"C:\Windows\System32\cmd.exe"

    proc = await asyncio.create_subprocess_exec(
        cmd,
        "/c",
        "echo",
        "123",
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.DEVNULL,
    )

    stdout, _ = await proc.communicate()
    print(proc.returncode, stdout)


if __name__ == "__main__":
    winloop.install()
    asyncio.run(main())

setup_cxfreeze.py

from cx_Freeze import Executable, setup

setup(
    name="winloop-cxfreeze-smoke",
    version="0",
    options={
        "build_exe": {
            "packages": ["winloop"],
        }
    },
    executables=[
        Executable(
            script="smoke.py",
            base="gui",
            target_name="winloop-cxfreeze-smoke.exe",
        )
    ],
)

Build and run

python setup_cxfreeze.py build_exe
.\build\exe.win-amd64-3.11\winloop-cxfreeze-smoke.exe

Actual result

The frozen executable crashes with:

Traceback (most recent call last):
  File "_tmp_cxfreeze_minimal.py", line 34, in <module>
  File "_tmp_cxfreeze_minimal.py", line 29, in run
  File "C:\Develop Tools\Python311\Lib\asyncio\runners.py", line 190, in run
    return runner.run(main)
  File "C:\Develop Tools\Python311\Lib\asyncio\runners.py", line 118, in run
    return self._loop.run_until_complete(task)
  File "winloop/loop.pyx", line 1615, in winloop.loop.Loop.run_until_complete
  File "_tmp_cxfreeze_minimal.py", line 14, in main
  File "C:\Develop Tools\Python311\Lib\asyncio\subprocess.py", line 223, in create_subprocess_exec
    transport, protocol = await loop.subprocess_exec(
  File "winloop/loop.pyx", line 2979, in subprocess_exec
  File "winloop/loop.pyx", line 2910, in __subprocess_run
  File "winloop/handles/process.pyx", line 673, in winloop.loop.UVProcessTransport.new
  File "winloop/handles/process.pyx", line 151, in winloop.loop.UVProcess._init
OSError: [Errno 9] bad file descriptor

The important part is that the failure is happening inside winloop subprocess setup / spawn on Windows in the frozen GUI process.


Expected result

The frozen GUI executable should be able to start subprocesses successfully, even when the parent process does not have a normal console/stdin handle.

At minimum, I would expect behavior similar to “invalid default stdio is ignored or replaced with NUL/DEVNULL” rather than surfacing EBADF from process creation.


Workaround

If I change the subprocess call to explicitly set stdin to DEVNULL, the same frozen executable works:

proc = await asyncio.create_subprocess_exec(
    cmd,
    "/c",
    "echo",
    "123",
    stdin=asyncio.subprocess.DEVNULL,
    stdout=asyncio.subprocess.PIPE,
    stderr=asyncio.subprocess.DEVNULL,
)

This succeeds in the frozen cx_Freeze GUI executable.

I applied the same workaround in my real app for ffmpeg, ffprobe, and N_m3u8DL-RE, and that avoided the crash.


Additional investigation

I also reproduced a related Windows issue outside of cx_Freeze by manually closing parent fd 0 before calling create_subprocess_exec(). That made subprocess setup fail with EBADF too, which makes me suspect the same family of problem:

  • invalid/default stdio inheritance on Windows
  • especially in no-console / GUI / frozen parent processes

So this may be in the winloop Windows subprocess stdio handling path rather than in cx_Freeze itself.

The traceback points at UVProcess._init() where uv_spawn() errors are converted, which makes me think one of the inherited/default stdio descriptors/handles is invalid in the frozen GUI process.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Programmers WantedAdditional Programmers wanted for writing a fix or helping with the review process.bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions