Skip to content

[Linux] Extension host crashes because POSIX test-result FIFO file descriptor is closed twice #26173

Description

@Weidav

Type: Bug

Behaviour

When Python testing is active on Linux, the VS Code extension host can crash during cleanup of a python-test-results-* FIFO.

Expected: the FIFO descriptor is closed exactly once and the extension host continues running.

Actual: the FIFO is first closed by its net.Socket. The socket's close handler then schedules another asynchronous fs.close(fd). If Linux reuses that descriptor number before the asynchronous close executes, the second close closes an unrelated descriptor. libuv subsequently receives EBADF from epoll_ctl and aborts the extension host.

The failure is timing-dependent.

Steps to reproduce:

  1. Open a Linux remote workspace with Python extension 2026.4.0.
  2. Enable pytest and project-based testing.
  3. Run tests through the VS Code Testing view.
  4. Continue working while the test-result pipe is being disposed. Concurrent descriptor activity, such as Git operations, makes the race more likely.
  5. Eventually the extension host disconnects and restarts.

I do not yet have a deterministic UI reproduction, but strace -f -k captured the complete descriptor lifecycle described below.

Diagnostic data

Output for Python in the Output panel (View→Output, change the drop-down in the upper-right of the Output panel to Python)

[info] [test-by-project] Activating project-based testing mode
[info] [test-by-project] Discovering projects for workspace
[info] [test-by-project] Registered 4 project(s)
[info] [test-by-project] Activated 4 project(s)

No Python error was logged when the crash occurred because the extension host aborted in native libuv code.

Environment

  • Python extension: ms-python.python 2026.4.0
  • VS Code server: 1.127.0 (4fe60c8b1cdac1c4c174f2fb180d0d758272d713)
  • Node.js: 24.15.0
  • libuv: 1.51.0
  • OS: Ubuntu 24.04.5, x86-64
  • Kernel: 7.0.0-1012-aws
  • Environment: remote Dev Container

Native trace

The relevant strace -f -k -yy sequence is:

# The Python test-result FIFO owns fd 31.
openat(..., "/tmp/python-test-results-<redacted>",
       O_RDONLY|O_NONBLOCK|O_CLOEXEC) = 31

# net.Socket/libuv closes the FIFO.
epoll_ctl(14, EPOLL_CTL_DEL, 31, ...) = 0
close(31</tmp/python-test-results-<redacted>>) = 0
  uv__stream_close
  uv_close
  node::HandleWrap::Close

# Linux reuses fd 31 for an unrelated child-process stdout socket.
socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0,
           [31<UNIX-STREAM>, 82<UNIX-STREAM>]) = 0
  uv_socketpair
  uv_spawn
  node::ProcessWrap::Spawn

# A libuv filesystem worker performs the second asynchronous close.
close(31<UNIX-STREAM>) = 0
  uv__close_nocancel
  uv__fs_work
  worker

# The event loop tries to register the newly created socket.
epoll_ctl(14, EPOLL_CTL_ADD, 31, {events=EPOLLIN, ...})
    = -1 EBADF (Bad file descriptor)
  uv__io_poll
  uv_run
  node::SpinEventLoopInternal

The first and second closes were ~ 177 ms apart. The new socket was created between them.

Suspected source

The current POSIX reader appears to assign ownership of the same descriptor to both net.Socket and the explicit close handler:

https://github.com/microsoft/vscode-python/blob/main/src/client/common/pipes/namedPipes.ts#L191-L197

const fd = await fs.open(pipeName, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK);
const socket = new net.Socket({ fd });
const reader = new rpc.SocketMessageReader(socket, "utf-8");

socket.on("close", () => {
    fs.close(fd).catch(noop);
    reader.dispose();
});

The trace shows that net.Socket has already closed the descriptor through uv__stream_close before the handler's fs.close(fd) runs.

Draft PR #26082 replaces this implementation with a directly owned FIFO reader and guarded disposal, so it may also fix this crash.

However, those currently describe the five-second FIFO drain delay rather than this extension-host crash.

Extension version: 2026.4.0
VS Code version: Code 1.139.1 (04c0d99f4fb0d8afe6ce4f0c58e31e183ac3e4b1, 2026-09-25T03:40:57Z)
OS version: Darwin arm64 27.0.0
Modes:

  • Python version (& distribution if applicable, e.g. Anaconda): 3.14.2
  • Type of virtual environment used (e.g. conda, venv, virtualenv, etc.): Pyenv
  • Value of the python.languageServer setting: Default
User Settings


languageServer: "Pylance"

Installed Extensions
Extension Name Extension Id Version
code-spell-checker str 4.9.3
copilot-chat Git 0.67.0
debugpy ms- 2026.6.0
even-better-toml tam 0.21.2
gitlens eam 19.2.0
indent-rainbow ode 8.3.1
js-debug ms- 1.117.0
js-debug-companion ms- 1.1.3
jsts-chat-features Typ 0.0.4
markdown-all-in-one yzh 3.6.3
markdown-preview-enhanced shd 0.8.39
python ms- 2026.4.0
rainbow-csv mec 3.24.1
remote-ssh ms- 0.128.0
shell-format fox 7.2.8
vscode-database-client2 cwe 9.0.2
vscode-docker ms- 2.0.0
vscode-js-profile-table ms- 1.0.11
vscode-markdownlint Dav 0.62.1
vscode-pylance ms- 2026.4.1
vscode-theme-onedark aka 2.3.0
vscode-yaml red 1.24.0
A/B Experiments
permission_prompt_control:1332567
ccr_pr_nudge_control:1319470
vsc_wsm_c:1340633
ce50c479:1417073
1g5a5742:1379227
3aced641:1389836
jc553158:1416789

Activity

  1. added 2 commits that reference this issue on Sep 29, 2026
    e226c00
    6e247d3
  2. zuckerruebe commented on Oct 19, 2026

    @zuckerruebe

    I'm hitting this too, but during test discovery, and frequently: the extension host aborts 20–70 s after
    nearly every window reload (10+ times in a few minutes).

    Environment:

    • Python extension 2026.10.0, Python Environments 1.40.0 (project-based testing active), VS Code 1.142.0
    • WSL2 remote (Ubuntu), kernel 6.6.87.2 and 6.18.54.1 (both affected)
    • Multi-root workspace with three pytest-enabled folders, so many discoveries run concurrently at startup

    Core dumps show libuv aborting in the extension host's main loop:

    • With io_uring (default): abort() in uv__epoll_ctl_flush (libuv 1.52.1, src/unix/linux.c:1304)
    • With io_uring disabled (kernel.io_uring_disabled=2): assert(errno == EEXIST) in uv__io_poll
      (linux.c:1427) with errno == EPERM. The watcher belongs to a UV_NAMED_PIPE whose fd number now refers to a
      non-pollable file, consistent with the fd having been closed and reused.

    In 2026.10.0 the POSIX reader still does

    const fd = await fs.open(pipe, O_RDONLY | O_NONBLOCK);
    const socket = new net.Socket({ fd });
    socket.on('close', () => { fs.close(fd).catch(noop); ... });
    

    net.Socket owns and closes the fd, so the fs.close(fd) is a second close that can hit a reused descriptor (e.g.
    another discovery's FIFO). Crashes coincide with bursts of python-test-discovery-* FIFOs being created.
    Dropping the fs.close(fd) (letting the socket own the fd) should fix it.

  3. Weidav commented on Oct 19, 2026

    @Weidav
    Author

    Damian Birchler (@zuckerruebe) try the linked pull request #26174 and built the extension from it. That should fix the issue, if it works for you leave a comment or like there, so that this gets attention from Micrososft

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions