From 754554cb1eeff5999b1f27538c971cd45c6dfc84 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 20 Feb 2026 20:55:12 +0100 Subject: [PATCH 01/28] ui-tests: offer a more robust way to capture the terminal text The existing method to capture text from Windows Terminal emulates mouse movements, dragging across the entire window with finicky pixel calculations for title bar height, scroll bar width and padding, then right-clicks to copy. This is fragile: if the window geometry changes, if another window gets focus, or if the title bar height differs between OS versions, the capture silently gets the wrong text. Windows Terminal's exportBuffer action avoids all of that by writing the complete scrollback buffer to a file on a keybinding, with no dependence on pixel positions or window focus. To use it, WT must run in portable mode with a settings.json that defines the action and keybinding. Add setup-portable-wt.ps1, which downloads WT (when not already present), creates the .portable marker and writes settings.json with Ctrl+Shift+F12 bound to exportBuffer. It accepts a -DestDir parameter so CI can use $RUNNER_TEMP while local development uses $TEMP. When running inside GitHub Actions it also appends the WT directory to $GITHUB_PATH. In the CI workflow, replace the inline "Install Windows Terminal" step with a call to the setup script (which is available after checkout). In the AHK test library, add CaptureBufferFromWindowsTerminal() which triggers the keybinding, waits for the export file, and returns the buffer contents. The export file is written into the script directory so it gets uploaded as a build artifact on failure. Assisted-by: Claude Opus 4.6 Signed-off-by: Johannes Schindelin --- .github/workflows/ui-tests.yml | 23 +++----- ui-tests/.gitattributes | 1 + ui-tests/setup-portable-wt.ps1 | 96 ++++++++++++++++++++++++++++++++++ ui-tests/ui-test-library.ahk | 21 ++++++++ 4 files changed, 126 insertions(+), 15 deletions(-) create mode 100644 ui-tests/setup-portable-wt.ps1 diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 0a73526e35..8208dbfad8 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -32,28 +32,25 @@ jobs: $p = Get-ChildItem -Recurse "${env:RUNNER_TEMP}\artifacts" | where {$_.Name -eq "msys-2.0.dll"} | Select -ExpandProperty VersionInfo | Select -First 1 -ExpandProperty FileName cp $p "c:/Program Files/Git/usr/bin/msys-2.0.dll" + - uses: actions/checkout@v6 + with: + sparse-checkout: | + ui-tests + - uses: actions/cache/restore@v5 id: restore-wt with: key: wt-${{ env.WT_VERSION }} path: ${{ runner.temp }}/wt.zip - - name: Download Windows Terminal - if: steps.restore-wt.outputs.cache-hit != 'true' - shell: bash + - name: Install and configure portable Windows Terminal + working-directory: ui-tests run: | - curl -fLo "$RUNNER_TEMP/wt.zip" \ - https://github.com/microsoft/terminal/releases/download/v$WT_VERSION/Microsoft.WindowsTerminal_${WT_VERSION}_x64.zip + powershell -File setup-portable-wt.ps1 -WtVersion $env:WT_VERSION -DestDir $env:RUNNER_TEMP - uses: actions/cache/save@v5 if: steps.restore-wt.outputs.cache-hit != 'true' with: key: wt-${{ env.WT_VERSION }} path: ${{ runner.temp }}/wt.zip - - name: Install Windows Terminal - shell: bash - working-directory: ${{ runner.temp }} - run: | - "$WINDIR/system32/tar.exe" -xf "$RUNNER_TEMP/wt.zip" && - cygpath -aw terminal-$WT_VERSION >>$GITHUB_PATH - uses: actions/cache/restore@v5 id: restore-ahk with: @@ -99,10 +96,6 @@ jobs: "$WINDIR/system32/tar.exe" -C "$RUNNER_TEMP" -xvf "$RUNNER_TEMP/win32-openssh.zip" && echo "OPENSSH_FOR_WINDOWS_DIRECTORY=$(cygpath -aw "$RUNNER_TEMP/OpenSSH-Win64")" >>$GITHUB_ENV - - uses: actions/checkout@v6 - with: - sparse-checkout: | - ui-tests - name: Minimize existing Log window working-directory: ui-tests run: | diff --git a/ui-tests/.gitattributes b/ui-tests/.gitattributes index 4dd1b9375b..7d5ccef0ca 100644 --- a/ui-tests/.gitattributes +++ b/ui-tests/.gitattributes @@ -1 +1,2 @@ *.ahk eol=lf +*.ps1 eol=lf diff --git a/ui-tests/setup-portable-wt.ps1 b/ui-tests/setup-portable-wt.ps1 new file mode 100644 index 0000000000..572e6bc119 --- /dev/null +++ b/ui-tests/setup-portable-wt.ps1 @@ -0,0 +1,96 @@ +# Configures a portable Windows Terminal for the UI tests. +# +# Downloads WT if needed, then creates .portable marker and settings.json +# with exportBuffer bound to Ctrl+Shift+F12. The export file lands in the +# script's own directory (ui-tests/) so it gets uploaded as build artifact. +# +# The portable WT uses its own settings directory (next to the executable) +# so it never touches the user's installed Windows Terminal configuration. + +param( + [string]$WtVersion = $env:WT_VERSION, + [string]$DestDir = $env:TEMP +) + +if (-not $WtVersion) { $WtVersion = '1.22.11141.0' } + +$wtDir = "$DestDir\terminal-$WtVersion" +$wtExe = "$wtDir\wt.exe" + +# Download if the directory doesn't contain wt.exe yet +if (-not (Test-Path $wtExe)) { + $wtZip = "$DestDir\wt.zip" + if (-not (Test-Path $wtZip)) { + $url = "https://github.com/microsoft/terminal/releases/download/v$WtVersion/Microsoft.WindowsTerminal_${WtVersion}_x64.zip" + Write-Host "Downloading Windows Terminal $WtVersion ..." + curl.exe -fLo $wtZip $url + if ($LASTEXITCODE -ne 0) { throw "Download failed" } + } + Write-Host "Extracting ..." + & "$env:WINDIR\system32\tar.exe" -C $DestDir -xf $wtZip + if ($LASTEXITCODE -ne 0) { throw "Extract failed" } +} + +# Create .portable marker so WT reads settings from settings\ next to wt.exe +$portableMarker = "$wtDir\.portable" +if (-not (Test-Path $portableMarker)) { + Set-Content -Path $portableMarker -Value "" +} + +# Write settings.json with exportBuffer action +$settingsDir = "$wtDir\settings" +if (-not (Test-Path $settingsDir)) { New-Item -ItemType Directory -Path $settingsDir -Force | Out-Null } + +$bufferExportPath = ($PSScriptRoot + '\wt-buffer-export.txt') -replace '\\', '/' + +$settings = @" +{ + "`$schema": "https://aka.ms/terminal-profiles-schema", + "actions": [ + { + "command": { + "action": "exportBuffer", + "path": "$bufferExportPath" + }, + "id": "User.TestExportBuffer" + }, + { + "command": { "action": "copy", "singleLine": false }, + "id": "User.copy" + }, + { "command": "paste", "id": "User.paste" } + ], + "copyFormatting": "none", + "copyOnSelect": false, + "defaultProfile": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", + "keybindings": [ + { "id": "User.TestExportBuffer", "keys": "ctrl+shift+f12" }, + { "id": null, "keys": "ctrl+v" }, + { "id": null, "keys": "ctrl+c" } + ], + "profiles": { + "defaults": {}, + "list": [ + { + "commandline": "%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", + "guid": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", + "hidden": false, + "name": "Windows PowerShell" + } + ] + }, + "schemes": [], + "themes": [] +} +"@ + +Set-Content -Path "$settingsDir\settings.json" -Value $settings + +# Add WT to PATH if running in GitHub Actions +if ($env:GITHUB_PATH) { + $wtDir | Out-File -Append -FilePath $env:GITHUB_PATH +} + +Write-Host "Portable WT ready at: $wtDir" +Write-Host " exportBuffer path: $bufferExportPath" +Write-Host " exportBuffer key: Ctrl+Shift+F12" diff --git a/ui-tests/ui-test-library.ahk b/ui-tests/ui-test-library.ahk index 120e85ed76..eea0570ebd 100644 --- a/ui-tests/ui-test-library.ahk +++ b/ui-tests/ui-test-library.ahk @@ -104,6 +104,27 @@ CaptureTextFromWindowsTerminal(winTitle := '') { return Result } +; Capture the Windows Terminal buffer via the exportBuffer action (Ctrl+Shift+F12). +; Requires a portable WT with settings.json that maps Ctrl+Shift+F12 to exportBuffer +; writing to /wt-buffer-export.txt. Unlike CaptureTextFromWindowsTerminal(), +; this does not depend on mouse position or window focus quirks. +CaptureBufferFromWindowsTerminal(winTitle := '') { + static exportFile := A_ScriptDir . '\wt-buffer-export.txt' + if FileExist(exportFile) + FileDelete exportFile + if winTitle != '' + WinActivate winTitle + Sleep 200 + Send '^+{F12}' + deadline := A_TickCount + 3000 + while !FileExist(exportFile) && A_TickCount < deadline + Sleep 50 + if !FileExist(exportFile) + return '' + Sleep 100 + return FileRead(exportFile) +} + WaitForRegExInWindowsTerminal(regex, errorMessage, successMessage, timeout := 5000, winTitle := '') { timeout := timeout + A_TickCount ; Wait for the regex to match in the terminal output From 5061115194d991b1ccedbd1b821561774530ae6c Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 20 Feb 2026 21:35:47 +0100 Subject: [PATCH 02/28] fixup! Start implementing UI-based tests by adding an AutoHotKey library Now that CaptureBufferFromWindowsTerminal() is available, switch WaitForRegExInWindowsTerminal() to use it instead of the mouse-drag based CaptureTextFromWindowsTerminal(). This also lets us drop the WheelDown scrolling that was needed because the mouse-drag method could only capture the visible portion of the terminal: exportBuffer writes the entire scrollback, so there is nothing to scroll to. Assisted-by: Claude Opus 4.6 Signed-off-by: Johannes Schindelin --- ui-tests/ui-test-library.ahk | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ui-tests/ui-test-library.ahk b/ui-tests/ui-test-library.ahk index eea0570ebd..d62ea2eff4 100644 --- a/ui-tests/ui-test-library.ahk +++ b/ui-tests/ui-test-library.ahk @@ -130,7 +130,7 @@ WaitForRegExInWindowsTerminal(regex, errorMessage, successMessage, timeout := 50 ; Wait for the regex to match in the terminal output while true { - capturedText := CaptureTextFromWindowsTerminal(winTitle) + capturedText := CaptureBufferFromWindowsTerminal(winTitle) if RegExMatch(capturedText, regex) break Sleep 100 @@ -138,9 +138,6 @@ WaitForRegExInWindowsTerminal(regex, errorMessage, successMessage, timeout := 50 Info('Captured text:`n' . capturedText) ExitWithError errorMessage } - if winTitle != '' - WinActivate winTitle - MouseClick 'WheelDown', , , 20 } Info(successMessage) } \ No newline at end of file From 60caca8b188b8ca944b6fd4df001f34960902831 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 20 Feb 2026 21:41:27 +0100 Subject: [PATCH 03/28] amend! ci: add an AutoHotKey-based integration test ci: add an AutoHotKey-based integration test The issue reported in https://github.com/microsoft/git/issues/730 was fixed, but due to missing tests for the issue a regression slipped in within mere weeks. Let's add an integration test that will (hopefully) prevent this issue from regressing again. This integration test is implement as an AutoHotKey script. It might look unnatural to use a script language designed to implement global keyboard shortcuts, but it is a quite powerful approach. While there are miles between the ease of developing AutoHotKey scripts and developing, say, Playwright tests, there is a decent integration into VS Code (including single-step debugging), and AutoHotKey's own development and community are quite vibrant and friendly. I had looked at alternatives to AutoHotKey, such as WinAppDriver, SikuliX, nut.js and AutoIt, in particular searching for a solution that would have a powerful recording feature similar to Playwright, but did not find any that is 1) mature, 2) well-maintained, 3) open source and 4) would be easy to integrate into a GitHub workflow. In the end, AutoHotKey appeared my clearest preference. So how is the test implemented? It lives in `ui-test/` and requires AutoHotKey v2 as well as Windows Terminal (the Legacy Prompt would not reproduce the problem). It then follows the reproducer I gave to the Cygwin team: 1. initialize a Git repository 2. install a `pre-commit` hook 3. this hook shall spawn a non-Cygwin/MSYS2 process in the background 4. that background process shall print to the console after Git exits 5. open a Command Prompt in Windows Terminal 6. run `git commit` 7. wait until the background process is done printing 8. press the Cursor Up key 9. observe that the Command Prompt does not react (in the test, it _does_ expect a reaction: the previous command in the command history should be shown, i.e. `git commit`) In my reproducer, I then also suggested to press the Enter key and to observe that now the "More ?" prompt is shown, but no input is accepted, until Ctrl+Z is pressed. Naturally, the test should not expect _that_ ;-) There were a couple of complications I needed to face when developing this test: - I did not find any easy macro recorder for AutoHotKey that I liked. It would not have helped much, anyway, because intentions are hard to record. - Before I realized that there is excellent AutoHotKey support in VS Code via the AutoHotKey++ and AutoHotKey Debug extensions, I struggled quite a bit to get the syntax right. - Windows Terminal does not use classical Win32 controls that AutoHotKey knows well. To capture the terminal text, we use Windows Terminal's exportBuffer action, which writes the entire scrollback to a file on a keybinding (Ctrl+Shift+F12). This requires running WT in portable mode with a settings.json that defines the action, which the setup script takes care of. - Despite my expectations, `ExitApp` would not actually exit AutoHotKey before the spawned process exits and/or the associated window is closed. For good measure, run this test both on windows-2022 (corresponding to Windows 10) and on windows-2025 (corresponding to Windows 11). Co-authored-by: Eu-Pin Tien Assisted-by: Claude Opus 4.6 Signed-off-by: Johannes Schindelin --- ui-tests/background-hook.ahk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-tests/background-hook.ahk b/ui-tests/background-hook.ahk index af7c27d313..76d04708d2 100755 --- a/ui-tests/background-hook.ahk +++ b/ui-tests/background-hook.ahk @@ -43,7 +43,7 @@ WaitForRegExInWindowsTerminal('`n49$', 'Timed out waiting for commit to finish', ; Verify that CursorUp shows the previous command Send('{Up}') Sleep 150 -Text := CaptureTextFromWindowsTerminal() +Text := CaptureBufferFromWindowsTerminal() if not RegExMatch(Text, 'git commit --allow-empty -m zOMG *$') ExitWithError 'Cursor Up did not work: ' Text Info('Match!') From 370c3a669154c6c0095529d4f90bf5025b691dd5 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 20 Feb 2026 22:01:18 +0100 Subject: [PATCH 04/28] fixup! Start implementing UI-based tests by adding an AutoHotKey library Now that all callers have been switched to CaptureBufferFromWindowsTerminal(), remove the old CaptureTextFromWindowsTerminal() function entirely. It relied on emulating mouse movements to drag-select the visible portion of the terminal and copy it via right-click, which was fragile and required hard-coded pixel offsets for the title bar height, scroll bar width, and padding. None of that complexity is needed anymore. Assisted-by: Claude Opus 4.6 Signed-off-by: Johannes Schindelin --- ui-tests/ui-test-library.ahk | 40 +----------------------------------- 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/ui-tests/ui-test-library.ahk b/ui-tests/ui-test-library.ahk index d62ea2eff4..d1a240e331 100644 --- a/ui-tests/ui-test-library.ahk +++ b/ui-tests/ui-test-library.ahk @@ -67,47 +67,9 @@ RunWaitOne(command) { return Result } -; This function is quite the hack. It assumes that the Windows Terminal is the active window, -; then drags the mouse diagonally across the window to select all text and then copies it. -; -; This is fragile! If any other window becomes active, or if the mouse is moved, -; the function will not work as intended. -; -; An alternative would be to use `ControlSend`, e.g. -; `ControlSend '+^a', 'Windows.UI.Input.InputSite.WindowClass1', 'ahk_id ' . hwnd -; This _kinda_ works, the text is selected (all text, in fact), but the PowerShell itself -; _also_ processes the keyboard events and therefore they leave ugly and unintended -; `^Ac` characters in the prompt. So that alternative is not really usable. -CaptureTextFromWindowsTerminal(winTitle := '') { - if winTitle != '' - WinActivate winTitle - ControlGetPos &cx, &cy, &cw, &ch, 'Windows.UI.Composition.DesktopWindowContentBridge1', "A" - titleBarHeight := 54 - scrollBarWidth := 28 - pad := 8 - - SavedClipboard := ClipboardAll - A_Clipboard := '' - SendMode('Event') - if winTitle != '' - WinActivate winTitle - MouseMove cx + pad, cy + titleBarHeight + pad - if winTitle != '' - WinActivate winTitle - MouseClickDrag 'Left', , , cx + cw - scrollBarWidth, cy + ch - pad, , '' - if winTitle != '' - WinActivate winTitle - MouseClick 'Right' - ClipWait() - Result := A_Clipboard - Clipboard := SavedClipboard - return Result -} - ; Capture the Windows Terminal buffer via the exportBuffer action (Ctrl+Shift+F12). ; Requires a portable WT with settings.json that maps Ctrl+Shift+F12 to exportBuffer -; writing to /wt-buffer-export.txt. Unlike CaptureTextFromWindowsTerminal(), -; this does not depend on mouse position or window focus quirks. +; writing to /wt-buffer-export.txt. CaptureBufferFromWindowsTerminal(winTitle := '') { static exportFile := A_ScriptDir . '\wt-buffer-export.txt' if FileExist(exportFile) From 1e0ff3720728a1bd8788a836476a3e83665aa9aa Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 20 Feb 2026 12:17:50 +0100 Subject: [PATCH 05/28] Add AGENTS.md with comprehensive project context for AI agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This file documents the layered fork structure of this repository (Cygwin → MSYS2 → Git for Windows), the merging-rebase strategy that keeps the main branch fast-forwarding, the build system and its bootstrap chicken-and-egg nature (msys-2.0.dll is the POSIX emulation layer that its own GCC depends on), the CI pipeline, key directories and files, development guidelines, and external resources. The intent is to give AI coding agents enough context to work competently on this codebase without hallucinating about its structure or purpose. Assisted-by: Claude Opus 4.6 Signed-off-by: Johannes Schindelin --- AGENTS.md | 298 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..177fb5ef44 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,298 @@ +# Guidelines for AI Agents Working on This Codebase + +## Project Overview + +This repository is the **Git for Windows fork** of the **MSYS2 runtime**, which is itself a fork of the **Cygwin runtime**. The runtime provides a POSIX emulation layer on Windows, producing `msys-2.0.dll` (analogous to Cygwin's `cygwin1.dll`). It is the foundational component that allows Unix-style programs (bash, coreutils, etc.) to run on Windows within the MSYS2 and Git for Windows ecosystems. + +### The Layered Fork Structure + +There are three layers of this project, each building on the one below: + +1. **Cygwin** (`git://sourceware.org/git/newlib-cygwin.git`, releases at https://cygwin.com): The upstream project. Cygwin is a POSIX-compatible environment for Windows consisting of a DLL (`cygwin1.dll`) that provides substantial POSIX API functionality, plus a collection of GNU and Open Source tools. The Cygwin project releases versioned tags (e.g., `cygwin-3.6.6`) from the `cygwin/cygwin` GitHub mirror. + +2. **MSYS2** (`https://github.com/msys2/msys2-runtime`): The MSYS2 project rebases its own patches on top of each Cygwin release. MSYS2 maintains branches named `msys2-X.Y.Z` (e.g., `msys2-3.6.6`) where the Cygwin code is the base and MSYS2-specific patches are applied on top. These patches implement features like POSIX-to-Windows path conversion (`msys2_path_conv.cc`), the `MSYS` environment variable for controlling runtime behavior, pseudo-console support toggling, and adaptations needed for MSYS2's focus on building native Windows software (as opposed to Cygwin's focus on running Unix software on Windows as-is). + +3. **Git for Windows** (`https://github.com/git-for-windows/msys2-runtime`, this repository): Git for Windows maintains a "merging rebase" on top of the MSYS2 patches. The `main` branch uses a special strategy where it always fast-forwards. Each rebase to a new upstream version starts with a "fake merge" commit (message: `Start the merging-rebase to cygwin-X.Y.Z`) that merges previous `main` using the `-s ours` strategy. This ensures the branch always fast-forwards despite being rebased. Git for Windows' own patches (on top of MSYS2's patches) address issues specific to Git's usage patterns, such as Ctrl+C signal handling, SSH hang fixes, and console output correctness. + +### Key Relationships + +- **Cygwin → MSYS2**: MSYS2 rebases onto each Cygwin release. When Cygwin releases version X.Y.Z, an `msys2-X.Y.Z` branch is created with MSYS2 patches rebased on top. +- **MSYS2 → Git for Windows**: Git for Windows performs a merging rebase that first merges in the MSYS2 patches, then rebases its own patches on top. +- The `main` branch in this repository (git-for-windows/msys2-runtime) is the Git for Windows branch, not Cygwin's or MSYS2's. + +## Repository Structure + +### Key Directories + +- **`winsup/cygwin/`**: The core of the Cygwin/MSYS2 runtime. This is where `msys-2.0.dll` (the POSIX emulation DLL) is built. Most development work happens here. Key files include: + - `dcrt0.cc`: Runtime initialization + - `spawn.cc`: Process spawning + - `path.cc`: Path handling + - `fork.cc`: fork() implementation + - `exceptions.cc`: Signal handling + - `msys2_path_conv.cc` / `msys2_path_conv.h`: MSYS2-specific POSIX-to-Windows path conversion (CC0-licensed) + - `environ.cc`: Environment variable handling, including the `MSYS` environment variable + - `fhandler/`: File handler implementations for various device types + - `local_includes/`: Internal headers + - `release/`: Version history files (one per Cygwin release version) +- **`winsup/utils/`**: Cygwin/MSYS2 utility programs (mount, cygpath, etc.) +- **`newlib/`**: The C library (newlib) used by the runtime +- **`ui-tests/`**: AutoHotKey-based integration tests that test the runtime in real terminal scenarios +- **`.github/workflows/`**: CI configuration + +## Build System + +### The Chicken-and-Egg Problem + +The MSYS2 runtime (`msys-2.0.dll`) is itself the POSIX emulation layer that the MSYS2 toolchain (GCC, binutils, etc.) depends on. The MSYS2 environment's own GCC links against `msys-2.0.dll` to provide POSIX semantics. This means you need a working MSYS2 runtime to compile a new MSYS2 runtime — a classic bootstrap problem. + +In practice, this is resolved by using an existing MSYS2 installation to build the new version. The CI workflow (`.github/workflows/build.yaml`) installs MSYS2 via the `msys2/setup-msys2` action, then builds the new runtime within that environment. + +### Build Dependencies + +Building requires MSYS2 packages: `msys2-devel`, `base-devel`, `autotools`, `cocom`, `gcc`, `gettext-devel`, `libiconv-devel`, `make`, `mingw-w64-cross-crt`, `mingw-w64-cross-gcc`, `mingw-w64-cross-zlib`, `perl`, `zlib-devel`. These are all **msys** packages (they link against `msys-2.0.dll`), not native MinGW packages. + +### Building in the Git for Windows SDK + +The Git for Windows SDK provides a complete MSYS2 environment with all necessary build dependencies pre-installed. The source tree is typically located at `/usr/src/MSYS2-packages/msys2-runtime/src/msys2-runtime` inside the SDK. + +**Critical: PATH ordering.** The build must use the MSYS2 toolchain, not any MinGW toolchain that might be on the PATH. Before building, ensure: + +```bash +export PATH=/usr/bin:/mingw64/bin:/mingw32/bin:$PATH +``` + +If MinGW's GCC is found first, the build will fail because MinGW tools do not link against `msys-2.0.dll` and cannot produce the runtime DLL. + +### Build Commands + +```bash +# Generate autotools files +(cd winsup && ./autogen.sh) + +# Configure (the --with-msys2-runtime-commit flag embeds the commit hash) +./configure --disable-dependency-tracking --with-msys2-runtime-commit="$(git rev-parse HEAD)" + +# Build +make -j8 +``` + +For quick rebuilds of just the DLL during development: +```bash +# Rebuild only msys-2.0.dll +make -C ../build-x86_64-pc-msys/x*/winsup/cygwin -j15 new-msys-2.0.dll +``` + +The build output is `new-msys-2.0.dll` in the build directory. This is a staging name to avoid overwriting the running DLL. + +### Testing a Locally-Built DLL + +You cannot replace the SDK's own `msys-2.0.dll` while running inside the SDK — the DLL is loaded by every MSYS2 process including your shell. Instead, copy the built DLL into a separate installation such as a Portable Git: + +```bash +cp new-msys-2.0.dll /path/to/PortableGit/usr/bin/msys-2.0.dll +``` + +Then run tests using that Portable Git's mintty/bash. Back up the original DLL first. + +The `build-and-copy.sh` helper script in the repository root can reconfigure, rebuild, and copy `msys-2.0.dll` to a target location. + +### Internal API Constraints + +Code inside `msys-2.0.dll` cannot use the full C runtime or C++ standard library freely. Key limitations: + +- **`__small_sprintf`** is used instead of `sprintf`. It does NOT support `%lld` (64-bit integers) or floating-point format specifiers. For 64-bit values, split into high/low 32-bit halves and print as two `%u` values. +- **Memory allocation** in low-level code (e.g., DLL initialization, atexit handlers) should use `HeapAlloc(GetProcessHeap(), ...)` to avoid circular dependencies with the Cygwin malloc. + +### CI Pipeline + +The CI (`.github/workflows/build.yaml`) does the following: +1. **Build**: Compiles the runtime on `windows-latest` using MSYS2 +2. **Minimal SDK artifact**: Creates a minimal Git for Windows SDK with the just-built runtime, used for testing Git itself +3. **Test minimal SDK**: Runs Git's test suite against the new runtime +4. **UI tests**: AutoHotKey-based integration tests for terminal behavior (Ctrl+C interrupts, SSH operations, etc.) +5. **MSYS2 tests**: Runs the MSYS2 project's own test suite across multiple environments and compilers + +## Git Branch and Rebase Workflow + +### The Merging Rebase Strategy + +Git for Windows uses a "merging rebase" to maintain a fast-forwarding `main` branch. The key insight is a "fake merge" commit that: + +1. Starts from the new upstream commit (Cygwin tag) +2. Merges in the previous `main` using `-s ours` (takes NO changes from previous main, only the tree from upstream) +3. This makes `main` a parent of the new commit, so the result is a fast-forward from previous `main` +4. Patches are then rebased on top of this fake merge + +The commit message follows a strict format: `Start the merging-rebase to cygwin-X.Y.Z`. This is machine-parseable — `git rev-parse 'main^{/^Start.the.merging-rebase}'` finds the most recent such commit. + +### History of Merging Rebases + +The repository has been continuously rebased through Cygwin versions from 3.3.x through the current 3.6.6. Each rebase is visible as a `Start the merging-rebase to cygwin-X.Y.Z` commit on `main`. + +### Key Branches + +- `main`: Git for Windows' branch (fast-forwarding, contains merging-rebase commits) +- `cygwin-X_Y-branch` (e.g., `cygwin-3_6-branch`): Tracking branches for upstream Cygwin +- `cygwin/main`: Upstream Cygwin's main branch +- Various feature branches for specific fixes (e.g., `fix-ctrl+c-again`, `fix-ssh-hangs-reloaded`) + +### Key Remotes + +- `cygwin`: The upstream Cygwin repository (`git://sourceware.org/git/newlib-cygwin.git`) +- `msys2`: The MSYS2 fork (`https://github.com/msys2/msys2-runtime`) +- `git-for-windows`: This repository (`https://github.com/git-for-windows/msys2-runtime`) +- `dscho`: Johannes Schindelin's fork (primary maintainer) + +## Development Guidelines + +### Language and Style + +The runtime is written in **C++** (with some C). The code uses Cygwin's existing coding conventions. When modifying files under `winsup/cygwin/`: +- Follow the existing indentation and brace style of each file +- Cygwin code uses 8-space tabs in many files +- MSYS2-specific additions (like `msys2_path_conv.cc`) may use different conventions + +### Making Changes + +Most changes for Git for Windows purposes are in `winsup/cygwin/`. Common areas of modification: +- Signal handling (`exceptions.cc`, `sigproc.cc`) +- Process spawning (`spawn.cc`) +- PTY/console handling (`fhandler/` directory, `termios.cc`) +- Path conversion (`msys2_path_conv.cc`, `path.cc`) +- Environment handling (`environ.cc`) + +### Testing + +- The CI builds the runtime and runs Git's entire test suite against it +- UI tests in `ui-tests/` test real terminal scenarios using AutoHotKey +- MSYS2's own test suite is run across multiple compiler/environment combinations +- For local testing, build the DLL and copy it to replace `msys-2.0.dll` in an MSYS2 installation + +### Commit Discipline + +- One logical change per commit +- Commit messages should explain context, intent, and justification in prose (not bullet points) +- For the rebase workflow, commit messages follow specific patterns (e.g., `Start the merging-rebase to ...`) that tooling depends on — do not alter these patterns + +## PTY Architecture — Pipes, State Machine, and Input Routing + +This section documents the internal architecture of the pseudo-terminal (PTY) implementation in `winsup/cygwin/fhandler/pty.cc`. Understanding this is essential for debugging any issue involving terminal input/output, keystroke handling, signal delivery, and process foreground/background transitions. + +### Background: Why This Matters + +The pseudo console support in the Cygwin runtime is one of the most intricate subsystems in this codebase. It bridges two fundamentally different models of terminal I/O — POSIX and Win32 console — across multiple processes that share state through shared memory. The implementation is ambitious and evolving; the complexity of the interactions between pipe switching, pseudo console lifecycle, cross-process mutexes, and foreground process detection means that changes in one area can have subtle, hard-to-diagnose effects elsewhere. Historically, bug fixes in this area have occasionally introduced new regressions, which is simply a reflection of how difficult the problem space is. Any AI agent working on PTY-related issues should take the time to understand the full picture before proposing changes, and should be especially careful about mutex acquisition order, state transitions that span process boundaries, and the distinction between the two pipe pairs described below. + +### The Two Pipe Pairs + +Each PTY has **two independent pipe pairs** for input, serving different consumers: + +1. **Cygwin (cyg) pipe**: `to_slave` / `from_master` + - Used when a **Cygwin/MSYS2 process** (e.g., bash) is in the foreground. + - Input goes through `line_edit()` (in `termios.cc`) which handles line discipline (echo, canonical mode, special characters) before being written via `accept_input()`. + - The slave reads from `from_master` (aliased as `get_handle()` on the slave side). + +2. **Native (nat) pipe**: `to_slave_nat` / `from_master_nat` + - Used when a **non-Cygwin (native Windows) process** (e.g., `powershell.exe`, `cmd.exe`, a MinGW program) is in the foreground. + - When the pseudo console (pcon) is active, `CreatePseudoConsole()` wraps this pipe pair. The Windows `conhost.exe` process reads from `from_master_nat` and provides console input semantics to the native app. + - The master writes directly to `to_slave_nat` via `WriteFile()`, bypassing `line_edit()`. + +For **output**, there is a corresponding pair (`to_master` / `to_master_nat`) plus a forwarding thread (`master_fwd_thread`) that copies output from the nat pipe's slave side (`from_slave_nat`) to the cyg pipe's master side (`to_master`), so the terminal emulator (mintty) always reads from one place. + +### The Pseudo Console (pcon) + +When `MSYS=disable_pcon` is NOT set (the default), the runtime uses Windows' `CreatePseudoConsole()` API to give native console applications a real console to talk to. The pseudo console is created on demand when a non-Cygwin process becomes the foreground process, and torn down when it exits. This is what allows programs like `cmd.exe`, `powershell.exe`, or any MinGW-built program to work correctly inside a mintty terminal, which has no native Win32 console of its own. + +The pcon lifecycle is managed across process boundaries: the slave process (running the non-Cygwin app) and the master process (the terminal emulator) both participate. This cross-process coordination is the source of much of the complexity. + +Key state fields in the `tty` structure (shared memory, in `tty.h`): + +- **`pcon_activated`** (`bool`): True when a pseudo console is currently active. +- **`pcon_start`** (`bool`): True during pseudo console initialization. +- **`pcon_start_pid`** (`pid_t`): PID of the process that initiated pcon setup. + +### The Input State Machine + +The field **`pty_input_state`** (type `xfer_dir`, in `tty.h:137`) tracks which pipe pair currently "owns" the input. It has two values: + +- **`to_cyg`**: Input is flowing to the Cygwin pipe. The master's `write()` uses the `line_edit()` → `accept_input()` path, which writes to `to_slave` (cyg pipe). +- **`to_nat`**: Input is flowing to the native pipe. The master's `write()` writes directly to `to_slave_nat` (nat pipe), or through the pseudo console. + +The state transitions happen via **`transfer_input()`** (pty.cc, around line 3905), which: +1. Reads all pending data from the "source" pipe (the one being abandoned). +2. Writes that data into the "destination" pipe (the one being switched to). +3. Sets `pty_input_state` to the new direction. + +This ensures data already buffered in one pipe is not lost when switching. **Any code that changes `pty_input_state` without calling `transfer_input()` risks losing or reordering data.** This invariant is critical and has been the root cause of past bugs. + +### Related State Fields + +- **`switch_to_nat_pipe`** (`bool`): Set to true when a non-Cygwin process is detected in the foreground. This is a prerequisite for `to_be_read_from_nat_pipe()` returning true. +- **`nat_pipe_owner_pid`** (`DWORD`): PID of the process that "owns" the nat pipe setup. Used to detect when the owner has exited (for cleanup). + +### The `to_be_read_from_nat_pipe()` Function + +This function (pty.cc, around line 1288) determines whether the current foreground process is a native (non-Cygwin) app. It checks: + +1. `switch_to_nat_pipe` must be true. +2. A named event `TTY_SLAVE_READING` must NOT exist (its existence means a Cygwin process is actively reading from the slave, indicating a Cygwin foreground). +3. `nat_fg(pgid)` returns true (the foreground process group contains a native process). + +**This function reads shared state without holding any mutex.** Its return value can therefore change between consecutive calls within the same function, which is an important consideration for callers that make multiple decisions based on the foreground state. + +### Mutexes and Synchronization + +Two cross-process named mutexes protect different aspects of the PTY state. Understanding which mutex protects what — and the fact that they are independent — is essential for diagnosing race conditions. + +- **`input_mutex`**: Protects the input data path. Held by `master::write()` while routing input to a pipe, by `transfer_input()` while moving data between pipes, and by `line_edit()` / `accept_input()`. +- **`pipe_sw_mutex`**: Protects pipe switching state — creation/destruction of the pseudo console, changes to `switch_to_nat_pipe`, `nat_pipe_owner_pid`. This is a DIFFERENT mutex from `input_mutex`. + +Because these are separate mutexes, it is possible for one process to modify the pipe switching state (under `pipe_sw_mutex`) while another process is in the middle of writing input (under `input_mutex`). Any code that modifies `pty_input_state` or `pcon_activated` must carefully consider whether it also needs `input_mutex` to avoid creating a window where the master's write path makes inconsistent decisions. + +Additionally, because these are **cross-process** named mutexes, they are shared via the kernel between the master (terminal emulator) and slave (bash and its children) processes. Operations that look local in the source code actually have system-wide synchronization effects. + +### The `master::write()` Input Routing (pty.cc, around line 2240) + +When the terminal emulator (mintty) sends a keystroke, it calls `master::write()`. After acquiring `input_mutex`, the function decides which path to take: + +1. **Path 1 — pcon+nat** (line ~2245): If `to_be_read_from_nat_pipe()` AND `pcon_activated` AND `pty_input_state == to_nat` → write directly to `to_slave_nat`. This is the fast path for native apps with pcon. + +2. **Path 2 — non-pcon transfer** (line ~2288): If `to_be_read_from_nat_pipe()` AND NOT `pcon_activated` AND `pty_input_state == to_cyg` → call `transfer_input(to_nat)` to move cyg pipe data to nat pipe, then fall through to line_edit. + +3. **Path 3 — line_edit** (line ~2300): The default/fallthrough path. Calls `line_edit()` which processes the input through terminal line discipline and then calls `accept_input()`, which writes to either the cyg or nat pipe based on the current `pty_input_state`. + +The conditions checked at each step involve multiple shared-memory fields (`to_be_read_from_nat_pipe()`, `pcon_activated`, `pty_input_state`). If any of these fields changes between consecutive calls to `master::write()` — or worse, between the check and the write within a single call — input can end up in the wrong pipe. + +### Key Functions for State Transitions + +- **`setup_for_non_cygwin_app()`** (~line 4150): Called when a non-Cygwin process becomes foreground. Sets up the pseudo console and switches input to nat pipe. +- **`cleanup_for_non_cygwin_app()`** (~line 4184): Called when the non-Cygwin process exits. Tears down pcon, transfers input back to cyg pipe. +- **`reset_switch_to_nat_pipe()`** (~line 1091): Cleanup function called from various slave-side operations (e.g., `bg_check()`, `setpgid_aux()`). Detects when the nat pipe owner has exited and resets state. This function is particularly subtle because it runs in the slave process and modifies shared state that the master relies on. +- **`mask_switch_to_nat_pipe()`** (~line 1249): Temporarily masks/unmasks the nat pipe switching. Used when a Cygwin process starts/stops reading from the slave. +- **`setpgid_aux()`** (~line 4214): Called when the foreground process group changes. May trigger pipe switching. + +### Debugging Tips + +When investigating PTY-related bugs, keep these patterns in mind: + +- **Data in two pipes**: If characters are lost, duplicated, or reordered, check whether data ended up split across the cyg and nat pipes due to a state transition during input. +- **Cross-process state changes**: The master and slave processes share state through the `tty` structure in shared memory. A state change in the slave (e.g., `reset_switch_to_nat_pipe()`) is immediately visible to the master, without any notification. Look for races where the master reads state, acts on it, but the state changed between the read and the action. +- **Mutex coverage gaps**: Check whether every modification of `pty_input_state`, `pcon_activated`, and `switch_to_nat_pipe` is protected by the appropriate mutex. The existence of two separate mutexes (`input_mutex` and `pipe_sw_mutex`) means that holding one does not protect against changes guarded by the other. +- **`transfer_input()` must accompany state changes**: Whenever `pty_input_state` is changed, any data buffered in the old pipe must be transferred to the new one. Forgetting this step causes data loss or reordering. +- **Tracing**: For timing-sensitive bugs, in-process tracing with lock-free per-thread buffers (using Windows TLS and `QueryPerformanceCounter`) is effective. Avoid file I/O during reproduction — accumulate in memory and dump at process exit. See the `ui-tests/` directory for AutoHotKey-based reproducers that can drive mintty programmatically. + +## Packaging + +The MSYS2 runtime is packaged as an **msys** package (`msys2-runtime`) using `makepkg` with a `PKGBUILD` recipe in the `msys2/MSYS2-packages` repository. The package definition lives at `msys2-runtime/PKGBUILD` in that repository. + +## External Resources + +- **Cygwin project**: https://cygwin.com — upstream source, FAQ, user's guide +- **Cygwin source**: https://github.com/cygwin/cygwin (mirror of `sourceware.org/git/newlib-cygwin.git`) +- **Cygwin announcements**: https://inbox.sourceware.org/cygwin-announce — release announcements +- **MSYS2 project**: https://www.msys2.org — documentation, package management +- **MSYS2 runtime source**: https://github.com/msys2/msys2-runtime +- **MSYS2 packages**: https://github.com/msys2/MSYS2-packages — package recipes including `msys2-runtime` +- **Git for Windows**: https://gitforwindows.org +- **Git for Windows runtime**: https://github.com/git-for-windows/msys2-runtime (this repository) +- **MSYS2 environments**: https://www.msys2.org/docs/environments/ — explains MSYS vs UCRT64 vs CLANG64 etc. From 0e6438b56ab1b679fab6f924416112c664179b5f Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 26 Feb 2026 16:12:30 +0100 Subject: [PATCH 06/28] fixup! Start implementing UI-based tests by adding an AutoHotKey library The SSH clone in the ctrl-c test occasionally fails with "early EOF" / "unexpected disconnect while reading sideband packet" on CI runners. This is a transient SSH/network issue unrelated to the MSYS2 runtime, but it causes the entire test to fail. Wrap the second clone (the one that verifies cloning completes successfully, as opposed to the one that tests Ctrl+C interruption) in a retry loop with up to five attempts. On each failure, clean up the partial clone directory and restart sshd (which may have exited after the broken connection), then try again. The regex pattern now accepts either "Receiving objects: .*, done." (success) or "fatal: early EOF" (transient failure) followed by the PowerShell prompt. Assisted-by: Claude Opus 4.6 Signed-off-by: Johannes Schindelin --- ui-tests/ctrl-c.ahk | 26 +++++++++++++++++++++----- ui-tests/ui-test-library.ahk | 8 +++++--- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/ui-tests/ctrl-c.ahk b/ui-tests/ctrl-c.ahk index 3ca8873de7..432c0b58c0 100644 --- a/ui-tests/ctrl-c.ahk +++ b/ui-tests/ctrl-c.ahk @@ -151,11 +151,27 @@ if (openSSHPath != '' and FileExist(openSSHPath . '\sshd.exe')) { Info('Started SSH server: ' sshdPID) Info('Starting clone') - Send('git -c core.sshCommand="ssh ' . sshOptions . '" clone ' . cloneOptions . '{Enter}') - Sleep 500 - Info('Waiting for clone to finish') - WinActivate('ahk_id ' . hwnd) - WaitForRegExInWindowsTerminal('Receiving objects: .*, done\.`r?`nPS .*>[ `n`r]*$', 'Timed out waiting for clone to finish', 'Clone finished', 15000, 'ahk_id ' . hwnd) + retries := 5 + Loop retries { + Send('git -c core.sshCommand="ssh ' . sshOptions . '" clone ' . cloneOptions . '{Enter}') + Sleep 500 + Info('Waiting for clone to finish (attempt ' . A_Index . '/' . retries . ')') + WinActivate('ahk_id ' . hwnd) + matchObj := WaitForRegExInWindowsTerminal('(Receiving objects: .*, done\.|fatal: early EOF)`r?`nPS .*>[ `n`r]*$', 'Timed out waiting for clone to finish', 'Clone command completed', 15000, 'ahk_id ' . hwnd) + + if InStr(matchObj[1], 'done.') + break + if A_Index == retries + ExitWithError('Clone failed after ' . retries . ' attempts (early EOF)') + Info('Clone failed (early EOF), restarting SSH server and retrying...') + if DirExist(largeGitClonePath) + DirDelete(largeGitClonePath, true) + ; Restart sshd for the next attempt (it may have exited after the failed connection) + Run(openSSHPath . '\sshd.exe ' . sshdOptions, '', 'Hide', &sshdPID) + if A_LastError + ExitWithError 'Error restarting SSH server: ' A_LastError + Info('Restarted SSH server: ' sshdPID) + } if not DirExist(largeGitClonePath) ExitWithError('`large-clone` did not work?!?') diff --git a/ui-tests/ui-test-library.ahk b/ui-tests/ui-test-library.ahk index d1a240e331..5a1b54bbd5 100644 --- a/ui-tests/ui-test-library.ahk +++ b/ui-tests/ui-test-library.ahk @@ -93,13 +93,15 @@ WaitForRegExInWindowsTerminal(regex, errorMessage, successMessage, timeout := 50 while true { capturedText := CaptureBufferFromWindowsTerminal(winTitle) - if RegExMatch(capturedText, regex) - break + if RegExMatch(capturedText, regex, &matchObj) + { + Info(successMessage) + return matchObj + } Sleep 100 if A_TickCount > timeout { Info('Captured text:`n' . capturedText) ExitWithError errorMessage } } - Info(successMessage) } \ No newline at end of file From 78d4be1b6e0459b2a006be1a7b5bfd0b1a7a89ec Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 6 Mar 2026 13:26:01 +0100 Subject: [PATCH 07/28] amend! Start implementing UI-based tests by adding an AutoHotKey library Start implementing UI-based tests by adding an AutoHotKey library AutoHotKey is not only a convenient way to add keyboard shortcuts for functionality (or applications) that does not come with shortcuts, but it is in general a powerful language to remote control GUI elements. We will use this language to implement a couple of automated tests that should hopefully prevent regressions as we have experienced in the past (for example, a regression that was fixed and immediately re-broken, which went unnoticed for months). So let's start by adding a library of useful functions, to be extended as needed. Note: As AutoHotKey is a GUI application, it does not expect to have stdout/stderr attached to it, therefore the `Info()` function added in this commit writes all the messages into `.log` files adjacent to the per-test working directories. But AutoHotKey _can_ have stdout/stderr attached to it, via redirection. In PowerShell, for example, appending `| Out-Default` to the invocation will make stdout/stderr available to AutoHotKey scripts (via the unintuitive syntax `FileAppend "text`n", "*"` (and `"**"` for stderr). The `Info()` function will detect when stdout is available and if it is, will also write to it, in addition to the `.log` file. Signed-off-by: Johannes Schindelin --- ui-tests/ui-test-library.ahk | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ui-tests/ui-test-library.ahk b/ui-tests/ui-test-library.ahk index 5a1b54bbd5..e6d261ed45 100644 --- a/ui-tests/ui-test-library.ahk +++ b/ui-tests/ui-test-library.ahk @@ -40,7 +40,19 @@ CleanUpWorkTree() { } Info(text) { + global workTree, cannotWriteToStdout FileAppend text '`n', workTree '.log' + if !IsSet(cannotWriteToStdout) + { + try + FileAppend text '`n', '*' + catch as e { + if e.__Class == 'OSError' && e.Number == 6 + cannotWriteToStdout:= false + else + throw e + } + } } closeWindow := false From 3d8be6bd4d3d9086c57856cf80c03258fdd16327 Mon Sep 17 00:00:00 2001 From: Takashi Yano Date: Tue, 3 Mar 2026 22:18:22 +0900 Subject: [PATCH 08/28] Cygwin: pty: Fix nat pipe hand-over when pcon is disabled The nat pipe ownership hand-over mechanism relies on the console process list - the set of processes attached to a console, enumerable via `GetConsoleProcessList()`. For non-cygwin process in pcon_activated case, this list contains all processes attached to the pseudo console. Otherwise, it contains all processes attached to the invisible console. 04f386e9af (Cygwin: console: Inherit pcon hand over from parent pty, 2024-10-31) added a last-resort fallback in `get_winpid_to_hand_over()` that hands nat pipe ownership to any process in the console process list, including Cygwin processes. This fallback is needed when a Cygwin process on the pseudo console (that might be exec'ed from non- cygwin process) must take over management of an active pseudo console after the original owner exits. When the pseudo console is disabled, this fallback incorrectly finds a Cygwin process (such as the shell) and assigns it nat pipe ownership, because both the original nat pipe owner and the shell are assosiated with the same invisible console. Since there is no console for that process to manage, ownership never gets released, input stays stuck on the nat pipe. Only the third (last-resort) call in the cascade needs guarding: the first two calls filter for native (non-Cygwin) processes via the `nat` parameter, and handing ownership to another native process is fine regardless of pcon state. It is only the fallback to Cygwin processes that is dangerous without an active pseudo console. Guard the fallback with a `pcon_activated` check, since handing nat pipe ownership to a Cygwin process only makes sense when there is an active pseudo console for it to manage. Fixes: 04f386e9af99 ("Cygwin: console: Inherit pcon hand over from parent pty") Signed-off-by: Takashi Yano Reviewed-by: Johannes Schindelin Cherry-picked-from: 699c6892f1 (Cygwin: pty: Fix nat pipe hand-over when pcon is disabled, 2026-03-03) Signed-off-by: Johannes Schindelin --- winsup/cygwin/fhandler/pty.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winsup/cygwin/fhandler/pty.cc b/winsup/cygwin/fhandler/pty.cc index 90f58671c2..e07bbfe31d 100644 --- a/winsup/cygwin/fhandler/pty.cc +++ b/winsup/cygwin/fhandler/pty.cc @@ -3559,7 +3559,7 @@ fhandler_pty_slave::get_winpid_to_hand_over (tty *ttyp, if (!switch_to) switch_to = get_console_process_id (current_pid, false, true, false, true); - if (!switch_to) + if (!switch_to && ttyp->pcon_activated) switch_to = get_console_process_id (current_pid, false, false, false, false); } From a0f8f2baa5454daaa3d751ab07bf0fee205c2b13 Mon Sep 17 00:00:00 2001 From: Takashi Yano Date: Tue, 24 Mar 2026 11:25:40 +0900 Subject: [PATCH 09/28] Cygwin: console: Release pipe_sw_mutex in pcon_hand_over_proc() Currently, pipe_sw_mutex is held in the process which is running in console inherited from pseudo console until the process ends. Due to this behaviour, the process may cause deadlock when it attempts to acquire input_mutex in set_input_mode() called via close_ctty(). This deadlock occurs because the pty master acquire input_mutex first and acquire pipe_sw_mutex next while the process exiting acquire pipe_sw_mutex first. To avoid this deadlock, this patch releases pipe_sw_mutex in pcon_hand_over_proc(). In addition, pointless pipe_sw_mutex acquire/release is drppped in pcon_hand_over_proc(). Fixes: 04f386e9af99 ("Cygwin: console: Inherit pcon hand over from parent pty") Signed-off-by: Takashi Yano Reviewed-by: Corinna Vinschen Cherry-picked-from: 9ef8e3ad3b (Cygwin: console: Release pipe_sw_mutex in pcon_hand_over_proc(), 2026-03-24) Signed-off-by: Johannes Schindelin --- winsup/cygwin/fhandler/console.cc | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/winsup/cygwin/fhandler/console.cc b/winsup/cygwin/fhandler/console.cc index 831df4f2cb..9e116118a4 100644 --- a/winsup/cygwin/fhandler/console.cc +++ b/winsup/cygwin/fhandler/console.cc @@ -1946,8 +1946,6 @@ fhandler_console::pcon_hand_over_proc (void) char buf[MAX_PATH]; shared_name (buf, PIPE_SW_MUTEX, parent_pty); HANDLE mtx = OpenMutex (MAXIMUM_ALLOWED, FALSE, buf); - WaitForSingleObject (mtx, INFINITE); - ReleaseMutex (mtx); DWORD res = WaitForSingleObject (mtx, INFINITE); if (res == WAIT_OBJECT_0 || res == WAIT_ABANDONED) { @@ -1958,8 +1956,7 @@ fhandler_console::pcon_hand_over_proc (void) } else system_printf("Acquiring pcon_ho_mutex failed."); - /* Do not release the mutex. - Hold onto the mutex until this process completes. */ + ReleaseMutex (mtx); } bool From 79ae2b09ed0b2dd826a333857246e7cb963922d8 Mon Sep 17 00:00:00 2001 From: Takashi Yano Date: Sat, 28 Mar 2026 19:59:29 +0900 Subject: [PATCH 10/28] Cygwin: pty: Fix input transfer when multiple non-cygwin apps exist Cygwin maintains POSIX line discipline for its own processes: input goes through `line_edit()` before reaching the reading process. Native (non-Cygwin) processes must not receive line-edited input; they expect raw console input instead. To support both, the PTY keeps two independent pipe pairs for input: a "cyg" pipe for Cygwin processes and a "nat" pipe for native ones. The runtime switches between the two as the foreground process changes. The PTY tracks which process "owns" the nat pipe session via the shared-memory field `nat_pipe_owner_pid`. Only one process is the owner at any time. When `setup_for_non_cygwin_app()` finds that the current owner is still alive, it leaves ownership with that process rather than claiming it for the new one. This means that a Cygwin-spawned native process can go through `cleanup_for_non_cygwin_app()` without being the nat pipe owner. Before this fix, that cleanup called `transfer_input(to_cyg)` unconditionally, draining the pseudo console's input buffer even though another process still owned the session. Keystrokes that the user had typed were moved to the cyg pipe prematurely, so the actual owner found an empty console input buffer and appeared to lose all input. When looking for the next owner of the console in `cleanup_for_non_cygwin_app()` (via `get_winpid_to_hand_over()`), and when transferring the input back to the cyg pipe, guard both with a `nat_pipe_owner_self()` check so that only the actual owner performs these operations. Non-owner processes skip straight to detaching from the pseudo console without disturbing the input buffer. Fixes: f9542a2e8e75 ("Cygwin: pty: Re-fix the last bug regarding nat-pipe.") Signed-off-by: Takashi Yano Reviewed-by: Johannes Schindelin Applied-from: https://inbox.sourceware.org/cygwin-patches/20260328110050.1928-1-takashi.yano@nifty.ne.jp/ Signed-off-by: Johannes Schindelin --- winsup/cygwin/fhandler/pty.cc | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/winsup/cygwin/fhandler/pty.cc b/winsup/cygwin/fhandler/pty.cc index e07bbfe31d..387fa178cf 100644 --- a/winsup/cygwin/fhandler/pty.cc +++ b/winsup/cygwin/fhandler/pty.cc @@ -4140,16 +4140,19 @@ fhandler_pty_slave::cleanup_for_non_cygwin_app (handle_set_t *p, tty *ttyp, { ttyp->wait_fwd (); WaitForSingleObject (p->pipe_sw_mutex, INFINITE); - DWORD switch_to = get_winpid_to_hand_over (ttyp, force_switch_to); - if ((!switch_to && (ttyp->pcon_activated || stdin_is_ptys)) - && ttyp->pty_input_state_eq (tty::to_nat)) + if (nat_pipe_owner_self (ttyp->nat_pipe_owner_pid)) { - WaitForSingleObject (p->input_mutex, mutex_timeout); - acquire_attach_mutex (mutex_timeout); - transfer_input (tty::to_cyg, p->from_master_nat, ttyp, - p->input_available_event); - release_attach_mutex (); - ReleaseMutex (p->input_mutex); + DWORD switch_to = get_winpid_to_hand_over (ttyp, force_switch_to); + if ((!switch_to && (ttyp->pcon_activated || stdin_is_ptys)) + && ttyp->pty_input_state_eq (tty::to_nat)) + { + WaitForSingleObject (p->input_mutex, mutex_timeout); + acquire_attach_mutex (mutex_timeout); + transfer_input (tty::to_cyg, p->from_master_nat, ttyp, + p->input_available_event); + release_attach_mutex (); + ReleaseMutex (p->input_mutex); + } } if (ttyp->pcon_activated) close_pseudoconsole (ttyp, force_switch_to); From 10aff7d8f7857e1385025d5b44aec6402543aa6e Mon Sep 17 00:00:00 2001 From: Takashi Yano Date: Sat, 28 Mar 2026 19:55:45 +0900 Subject: [PATCH 11/28] Cygwin: console: Fix master thread In Windows 11, key event with wRepeatCount == 0 is fixed-up to wRepeatCount == 1 in conhost.exe. https://github.com/microsoft/terminal/blob/v1.25.622.0/src/host/inputBuffer.cpp#L406 The console master thread (`cons_master_thread`) reads INPUT_RECORDs from the console input buffer, processes signal-generating events, and writes the remaining records back. After the writeback, it peeks the buffer and uses `inrec_eq()` to verify that conhost stored the records faithfully. On Windows 11, conhost normalizes `wRepeatCount` from 0 to 1 on readback, causing `inrec_eq()` to report a mismatch and triggering an unnecessary fixup path. Treat 0 and 1 as equivalent for comparison purposes. Addresses: https://github.com/git-for-windows/git/issues/5632 Fixes: ff4440fcf768 ("Cygwin: console: Introduce new thread which handles input signal.") Signed-off-by: Takashi Yano Reviewed-by: Johannes Schindelin Signed-off-by: Johannes Schindelin --- winsup/cygwin/fhandler/console.cc | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/winsup/cygwin/fhandler/console.cc b/winsup/cygwin/fhandler/console.cc index 9e116118a4..80d6e34fc7 100644 --- a/winsup/cygwin/fhandler/console.cc +++ b/winsup/cygwin/fhandler/console.cc @@ -318,9 +318,17 @@ inrec_eq (const INPUT_RECORD *a, const INPUT_RECORD *b, DWORD n) written event. Therefore they are ignored. */ const KEY_EVENT_RECORD *ak = &a[i].Event.KeyEvent; const KEY_EVENT_RECORD *bk = &b[i].Event.KeyEvent; + /* On Windows 11, conhost normalizes wRepeatCount from 0 to 1 + on readback. Treat them as equivalent for comparison. */ + WORD r1 = ak->wRepeatCount; + WORD r2 = bk->wRepeatCount; + if (r1 == 0) + r1 = 1; + if (r2 == 0) + r2 = 1; if (ak->bKeyDown != bk->bKeyDown || ak->uChar.UnicodeChar != bk->uChar.UnicodeChar - || ak->wRepeatCount != bk->wRepeatCount) + || r1 != r2) return false; } else if (a[i].EventType == MOUSE_EVENT) From 0c28b92d56e1d04847014f7e91f3cba95985cd92 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 6 Mar 2026 13:18:53 +0100 Subject: [PATCH 12/28] fixup! ci: add an AutoHotKey-based integration test Since the parent commit introduced logging to stdout as long as that handle is available, and since the GitHub workflow runs those tests by redirecting the output via `| Out-File` precisely so that that handle _is_ available, we no longer need to show the contents of that log file explicitly: It will have been shown already. Signed-off-by: Johannes Schindelin --- .github/workflows/ui-tests.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 8208dbfad8..10e76ccd1f 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -112,7 +112,6 @@ jobs: $exitCode = 0 & "${env:RUNNER_TEMP}\ahk\AutoHotKey64.exe" /ErrorStdOut /force background-hook.ahk "$PWD\bg-hook" 2>&1 | Out-Default if (!$?) { $exitCode = 1; echo "::error::Test failed!" } else { echo "::notice::Test log" } - type bg-hook.log $env:LARGE_FILES_DIRECTORY = "${env:RUNNER_TEMP}\large" & "${env:RUNNER_TEMP}\ahk\AutoHotKey64.exe" /ErrorStdOut /force ctrl-c.ahk "$PWD\ctrl-c" 2>&1 | Out-Default if (!$?) { $exitCode = 1; echo "::error::Ctrl+C Test failed!" } else { echo "::notice::Ctrl+C Test log" } From dc927cd9eb65852a0a2dc27466572adb481ecb42 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 6 Mar 2026 15:50:05 +0100 Subject: [PATCH 13/28] fixup! ui-tests: minimize Log window Since logging to `stdout` in CI runs now works, there is no need to write out the logs _again_. Signed-off-by: Johannes Schindelin --- .github/workflows/ui-tests.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 10e76ccd1f..4997d06c2e 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -100,7 +100,6 @@ jobs: working-directory: ui-tests run: | $exitCode = 0 - type minimize-log-window.ahk & "${env:RUNNER_TEMP}\ahk\AutoHotKey64.exe" /ErrorStdOut /force minimize-log-window.ahk "$PWD\minimize-log-window" 2>&1 | Out-Default if (!$?) { $exitCode = 1; echo "::error::Failed to minimize Log window!" } else { echo "::notice::Minimized Log window" } exit $exitCode From 50c1ac05553d5f73938b329ee15fe4d46bf4e3e7 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 6 Mar 2026 13:19:32 +0100 Subject: [PATCH 14/28] fixup! ui-tests: verify that a `sleep` in Windows Terminal can be interrupted Since logging to `stdout` in CI runs now works, there is no need to write out the logs _again_. Signed-off-by: Johannes Schindelin --- .github/workflows/ui-tests.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 4997d06c2e..f4ff52f161 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -114,7 +114,6 @@ jobs: $env:LARGE_FILES_DIRECTORY = "${env:RUNNER_TEMP}\large" & "${env:RUNNER_TEMP}\ahk\AutoHotKey64.exe" /ErrorStdOut /force ctrl-c.ahk "$PWD\ctrl-c" 2>&1 | Out-Default if (!$?) { $exitCode = 1; echo "::error::Ctrl+C Test failed!" } else { echo "::notice::Ctrl+C Test log" } - type ctrl-c.log exit $exitCode - name: Show logs if: always() From 4f08a7575975001f8e6f09f90d446f91a1b15c11 Mon Sep 17 00:00:00 2001 From: Takashi Yano Date: Sat, 28 Mar 2026 19:55:46 +0900 Subject: [PATCH 15/28] Cygwin: pty: Add workaround for handling of backspace when pcon enabled In Windows 11, pseudo console has an undesired key conversion that the Ctrl-H is translated into Ctrl-Backspace (not Backspace). The reverse VT input path in conhost's `_DoControlCharacter()` maps the byte 0x08 to a Ctrl+Backspace key event (VK_BACK with LEFT_CTRL_PRESSED and character 0x7F). This was introduced in PR #3935 (Jan 2020) to make Ctrl+Backspace delete whole words. In September 2022, PR #13894 rewrote the forward path to properly implement DECBKM (Backarrow Key Mode), but the reverse path was never updated to match, breaking the roundtrip. Due to this behaviour, inrec_eq() in cons_master_thread() fails to compare backspace/Ctrl-H events in the input record sequence. This patch is a workaround for the issue that replaces Ctrl-H with backspace (0x7f), which will be translated into Ctrl-H in pseudo console. Signed-off-by: Takashi Yano Reviewed-by: Johannes Schindelin Signed-off-by: Johannes Schindelin --- winsup/cygwin/fhandler/console.cc | 12 +++- winsup/cygwin/fhandler/pty.cc | 78 ++++++++++++++++++++++--- winsup/cygwin/local_includes/fhandler.h | 2 + 3 files changed, 82 insertions(+), 10 deletions(-) diff --git a/winsup/cygwin/fhandler/console.cc b/winsup/cygwin/fhandler/console.cc index 80d6e34fc7..20c20de526 100644 --- a/winsup/cygwin/fhandler/console.cc +++ b/winsup/cygwin/fhandler/console.cc @@ -318,6 +318,16 @@ inrec_eq (const INPUT_RECORD *a, const INPUT_RECORD *b, DWORD n) written event. Therefore they are ignored. */ const KEY_EVENT_RECORD *ak = &a[i].Event.KeyEvent; const KEY_EVENT_RECORD *bk = &b[i].Event.KeyEvent; + WCHAR c1 = ak->uChar.UnicodeChar; + WCHAR c2 = bk->uChar.UnicodeChar; + if (inside_pcon) + { + /* Workaround for pseudo console in Windows 11 */ + if (c1 == 8) /* Ctrl-H */ + c1 = 127; /* Backspace */ + if (c2 == 8) /* Ctrl-H */ + c2 = 127; /* Backspace */ + } /* On Windows 11, conhost normalizes wRepeatCount from 0 to 1 on readback. Treat them as equivalent for comparison. */ WORD r1 = ak->wRepeatCount; @@ -327,7 +337,7 @@ inrec_eq (const INPUT_RECORD *a, const INPUT_RECORD *b, DWORD n) if (r2 == 0) r2 = 1; if (ak->bKeyDown != bk->bKeyDown - || ak->uChar.UnicodeChar != bk->uChar.UnicodeChar + || c1 != c2 || r1 != r2) return false; } diff --git a/winsup/cygwin/fhandler/pty.cc b/winsup/cygwin/fhandler/pty.cc index 387fa178cf..292e0715dc 100644 --- a/winsup/cygwin/fhandler/pty.cc +++ b/winsup/cygwin/fhandler/pty.cc @@ -1933,7 +1933,8 @@ fhandler_pty_master::fhandler_pty_master (int unit, dev_t via) master_thread (NULL), from_master_nat (NULL), to_master_nat (NULL), from_slave_nat (NULL), to_slave_nat (NULL), echo_r (NULL), echo_w (NULL), dwProcessId (0), to_master (NULL), from_master (NULL), - master_fwd_thread (NULL) + master_fwd_thread (NULL), h_pcon_in_dupped (NULL), + nat_pipe_owner_pid_dupped (0) { dev_referred_via = via; if (unit >= 0) @@ -2114,6 +2115,10 @@ fhandler_pty_master::close (int flag) termios_printf ("error closing from_master %p, %E", from_master); from_master = NULL; + if (h_pcon_in_dupped) + ForceCloseHandle (h_pcon_in_dupped); + h_pcon_in_dupped = NULL; + return 0; } @@ -2215,28 +2220,77 @@ fhandler_pty_master::write (const void *ptr, size_t len) { /* Reaches here when non-cygwin app is foreground and pseudo console is activated. */ tmp_pathbuf tp; - char *buf = (char *) ptr; + char *buf = tp.c_get (); size_t nlen = len; if (get_ttyp ()->term_code_page != CP_UTF8) { static mbstate_t mbp; - buf = tp.c_get (); nlen = NT_MAX_PATH; convert_mb_str (CP_UTF8, buf, &nlen, get_ttyp ()->term_code_page, (const char *) ptr, len, &mbp); } + else + memcpy (buf, ptr, nlen); - for (size_t i = 0; i < nlen; i++) + if (get_ttyp ()->nat_pipe_owner_pid != nat_pipe_owner_pid_dupped) + { + if (!nat_pipe_owner_self (get_ttyp ()->nat_pipe_owner_pid)) + { + if (h_pcon_in_dupped) + ForceCloseHandle (h_pcon_in_dupped); + h_pcon_in_dupped = NULL; + nat_pipe_owner_pid_dupped = 0; + HANDLE pcon_owner = OpenProcess (PROCESS_DUP_HANDLE, FALSE, + get_ttyp ()->nat_pipe_owner_pid); + if (pcon_owner) + { + DuplicateHandle (pcon_owner, get_ttyp ()->h_pcon_in, + GetCurrentProcess (), &h_pcon_in_dupped, + 0, FALSE, DUPLICATE_SAME_ACCESS); + nat_pipe_owner_pid_dupped = get_ttyp ()->nat_pipe_owner_pid; + CloseHandle (pcon_owner); + } + } + else + { + h_pcon_in_dupped = get_ttyp ()->h_pcon_in; + nat_pipe_owner_pid_dupped = get_ttyp ()->nat_pipe_owner_pid; + } + } + + /* Retrieve console mode */ + DWORD cons_mode = ENABLE_VIRTUAL_TERMINAL_INPUT; + if (h_pcon_in_dupped && memchr (buf, '\010' /* Ctrl-H */, nlen)) + { + if (!nat_pipe_owner_self (nat_pipe_owner_pid_dupped)) + { + DWORD resume_pid = + attach_console_temporarily (nat_pipe_owner_pid_dupped); + GetConsoleMode (h_pcon_in_dupped, &cons_mode); + resume_from_temporarily_attach (resume_pid); + } + else + GetConsoleMode (h_pcon_in_dupped, &cons_mode); + } + + len = nlen; + for (size_t i = 0, j = 0; i < len; i++) { process_sig_state r = process_sigs (buf[i], get_ttyp (), this); - if (r == done_with_debugger) + if (r != done_with_debugger) { - for (size_t j = i; j < nlen - 1; j++) - buf[j] = buf[j + 1]; - nlen--; - i--; + char c = buf[i]; + /* Workaround for pseudo console in Windows 11 */ + if (!(cons_mode & ENABLE_VIRTUAL_TERMINAL_INPUT)) + /* Undesired backspace conversion in pseudo console does + not happen if ENABLE_VIRTUAL_TERMINAL_INPUT is set. */ + if (c == '\010') /* Ctrl-H */ + c = '\177'; /* Backspace */ + buf[j++] = c; } + else + nlen--; } DWORD n; @@ -3105,6 +3159,8 @@ fhandler_pty_master::fixup_after_fork (HANDLE parent) from_slave_nat = arch->from_slave_nat; to_slave_nat = arch->to_slave_nat; #endif + h_pcon_in_dupped = NULL; + nat_pipe_owner_pid_dupped = 0; report_tty_counts (this, "inherited master", ""); } @@ -3958,6 +4014,10 @@ fhandler_pty_slave::transfer_input (tty::xfer_dir dir, HANDLE from, tty *ttyp, if (r[i].EventType == KEY_EVENT && r[i].Event.KeyEvent.bKeyDown) { DWORD ctrl_key_state = r[i].Event.KeyEvent.dwControlKeyState; + if (r[i].Event.KeyEvent.uChar.AsciiChar == '\010' /* Ctrl-H */ + && !(ctrl_key_state & ALT_PRESSED)) + /* Workaround for pseudo console in Windows 11 */ + r[i].Event.KeyEvent.uChar.AsciiChar = '\177'; /* Backspace */ if (r[i].Event.KeyEvent.uChar.AsciiChar) { if ((ctrl_key_state & ALT_PRESSED) diff --git a/winsup/cygwin/local_includes/fhandler.h b/winsup/cygwin/local_includes/fhandler.h index 9fa73899c5..a4feeec247 100644 --- a/winsup/cygwin/local_includes/fhandler.h +++ b/winsup/cygwin/local_includes/fhandler.h @@ -2560,6 +2560,8 @@ class fhandler_pty_master: public fhandler_pty_common HANDLE thread_param_copied_event; HANDLE helper_goodbye; HANDLE helper_h_process; + HANDLE h_pcon_in_dupped; + DWORD nat_pipe_owner_pid_dupped; public: HANDLE get_echo_handle () const { return echo_r; } From b9d762742c58aa226f12346f1916488d482efa45 Mon Sep 17 00:00:00 2001 From: Takashi Yano Date: Sat, 28 Mar 2026 19:55:47 +0900 Subject: [PATCH 16/28] Cygwin: console: Use input_mutex in the parent PTY in master thread If the console is originating from pseudo console, the input into console is coming from PTY master. This is because: When the pseudo console is active, and a cygwin process is started from non-cygwin process, `cons_master_thread()` runs inside the Cygwin process that inherited the pseudo console from its parent PTY. It reads all `INPUT_RECORD`s from the console input buffer via `ReadConsoleInputW()`, processes signal-generating events (e.g. Ctrl+C), and writes the remaining records back via `WriteConsoleInputW()`. Meanwhile, the PTY master process (e.g. mintty) calls `fhandler_pty_master::write()`, which writes keystrokes to `to_slave_nat` (one end of the nat pipe). Conhost reads from the other end of that pipe, parses the byte stream through its VT input path, and inserts the resulting `INPUT_RECORD`s into the console input buffer. If `cons_master_thread()` reads the buffer and removes a signal record while conhost is simultaneously inserting new records from the PTY master's write, the verify step (`inrec_eq()`) finds records in the buffer that were not part of the original read, reports a mismatch, and enters the fixup path. That fixup path itself can disturb the record order, turning what was merely an interference into an actual problem. Acquiring the PTY's `input_mutex` in `cons_master_thread()` prevents `fhandler_pty_master::write()` from feeding new bytes into the pipe while the read-process-writeback-verify cycle is in progress. Use parent input_mutex as well as input_mutex in console device in cons_master_thread(). Fixes: 04f386e9af99 ("Cygwin: console: Inherit pcon hand over from parent pty") Signed-off-by: Takashi Yano Reviewed-by: Johannes Schindelin Signed-off-by: Johannes Schindelin --- winsup/cygwin/fhandler/console.cc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/winsup/cygwin/fhandler/console.cc b/winsup/cygwin/fhandler/console.cc index 20c20de526..6220a9142f 100644 --- a/winsup/cygwin/fhandler/console.cc +++ b/winsup/cygwin/fhandler/console.cc @@ -63,6 +63,7 @@ fhandler_console::console_state NO_COPY static bool NO_COPY inside_pcon_checked = false; static bool NO_COPY inside_pcon = false; static int NO_COPY parent_pty; +static HANDLE NO_COPY parent_pty_input_mutex = NULL; bool NO_COPY fhandler_console::invisible_console; @@ -465,6 +466,8 @@ fhandler_console::cons_master_thread (handle_set_t *p, tty *ttyp) continue; } total_read = 0; + if (inside_pcon && parent_pty_input_mutex) + WaitForSingleObject (parent_pty_input_mutex, mutex_timeout); switch (cygwait (p->input_handle, (DWORD) 0)) { case WAIT_OBJECT_0: @@ -489,6 +492,8 @@ fhandler_console::cons_master_thread (handle_set_t *p, tty *ttyp) default: /* Error */ free (input_rec); free (input_tmp); + if (inside_pcon && parent_pty_input_mutex) + ReleaseMutex (parent_pty_input_mutex); ReleaseMutex (p->input_mutex); return; } @@ -666,6 +671,8 @@ fhandler_console::cons_master_thread (handle_set_t *p, tty *ttyp) while (true); } skip_writeback: + if (inside_pcon && parent_pty_input_mutex) + ReleaseMutex (parent_pty_input_mutex); ReleaseMutex (p->input_mutex); cygwait (40); } @@ -1949,6 +1956,8 @@ fhandler_console::setup_pcon_hand_over () inside_pcon = true; atexit (fhandler_console::pcon_hand_over_proc); parent_pty = i; + parent_pty_input_mutex = + cygwin_shared->tty[i]->open_input_mutex (MAXIMUM_ALLOWED); break; } } @@ -1975,6 +1984,7 @@ fhandler_console::pcon_hand_over_proc (void) else system_printf("Acquiring pcon_ho_mutex failed."); ReleaseMutex (mtx); + ForceCloseHandle (parent_pty_input_mutex); } bool From 15c111b068c5b712ac0793a6e23557022c9761f1 Mon Sep 17 00:00:00 2001 From: Takashi Yano Date: Sat, 28 Mar 2026 19:55:48 +0900 Subject: [PATCH 17/28] Cygwin: pty: Apply line_edit() for transferred input to to_cyg When keystrokes travel through the nat pipe during a native process session, they bypass POSIX line discipline entirely. When they are transferred back to the cyg pipe at cleanup (via `transfer_input(to_cyg)`), they arrive as raw bytes. If the terminal is in canonical mode at that point, VERASE and VKILL characters in those raw bytes have no effect because `line_edit()` was never applied to them. The result: backspace typed while a native process was running fails to erase the preceding character once the input reaches bash's readline. The fix applies `line_edit()` to the transferred bytes before they reach the reading process. The right place to do this is the master's forward thread (`pty_master_fwd_thread()`), because it runs in the master process alongside `fhandler_pty_master::write()` and shares access to the readahead buffer and `line_edit()` state. Calling `line_edit()` from the slave (where `transfer_input()` runs) would not work because that state belongs to the master. To coordinate: `transfer_input(to_cyg)` writes the raw bytes to the cyg pipe's slave end (`to_slave`), then signals a new cross-process event (`input_transferred_to_cyg`) and spin-waits for the forward thread to clear it. The forward thread is converted from synchronous to overlapped I/O so it can wait on both the `from_slave_nat` read completion and the transfer event simultaneously. When the event fires, it reads the transferred bytes from the cyg pipe's master end (`from_master`), processes them through `line_edit()`, and clears the event. The spin-wait in `transfer_input()` holds `input_mutex` (from its caller), which blocks `fhandler_pty_master::write()` from injecting new keystrokes until the forward thread has finished applying `line_edit()` to the transferred bytes. Fixes: 10d083c745dd ("Cygwin: pty: Inherit typeahead data between two input pipes.") Signed-off-by: Takashi Yano Reviewed-by: Johannes Schindelin Signed-off-by: Johannes Schindelin --- winsup/cygwin/fhandler/pty.cc | 143 +++++++++++++++++------- winsup/cygwin/local_includes/fhandler.h | 10 +- winsup/cygwin/local_includes/tty.h | 1 + 3 files changed, 113 insertions(+), 41 deletions(-) diff --git a/winsup/cygwin/fhandler/pty.cc b/winsup/cygwin/fhandler/pty.cc index 292e0715dc..58fd996df5 100644 --- a/winsup/cygwin/fhandler/pty.cc +++ b/winsup/cygwin/fhandler/pty.cc @@ -209,6 +209,7 @@ atexit_func (void) { ptys->get_handle_nat (), ptys->get_input_available_event (), + ptys->input_transferred_to_cyg, ptys->input_mutex, ptys->pipe_sw_mutex }; @@ -738,7 +739,7 @@ fhandler_pty_slave::open (int flags, mode_t) { &from_master_nat_local, &input_available_event, &input_mutex, &inuse, &output_mutex, &to_master_nat_local, &pty_owner, &to_master_local, - &from_master_local, &pipe_sw_mutex, + &from_master_local, &pipe_sw_mutex, &input_transferred_to_cyg, NULL }; @@ -778,6 +779,12 @@ fhandler_pty_slave::open (int flags, mode_t) errmsg = "open input event failed, %E"; goto err; } + shared_name (buf, INPUT_TRANSFERRED_EVENT, get_minor ()); + if (!(input_transferred_to_cyg = OpenEvent (MAXIMUM_ALLOWED, TRUE, buf))) + { + errmsg = "open input transferred event failed, %E"; + goto err; + } /* FIXME: Needs a method to eliminate tty races */ { @@ -992,6 +999,8 @@ fhandler_pty_slave::close (int flag) termios_printf ("CloseHandle (inuse), %E"); if (!ForceCloseHandle (input_available_event)) termios_printf ("CloseHandle (input_available_event<%p>), %E", input_available_event); + if (!ForceCloseHandle (input_transferred_to_cyg)) + termios_printf ("CloseHandle (input_transferred_to_cyg<%p>), %E", input_transferred_to_cyg); if (!ForceCloseHandle (get_output_handle_nat ())) termios_printf ("CloseHandle (get_output_handle_nat ()<%p>), %E", get_output_handle_nat ()); @@ -1100,7 +1109,8 @@ fhandler_pty_slave::reset_switch_to_nat_pipe (void) WaitForSingleObject (input_mutex, mutex_timeout); acquire_attach_mutex (mutex_timeout); transfer_input (tty::to_cyg, get_handle_nat (), get_ttyp (), - input_available_event); + input_available_event, + input_transferred_to_cyg); release_attach_mutex (); ReleaseMutex (input_mutex); } @@ -1249,14 +1259,14 @@ fhandler_pty_slave::mask_switch_to_nat_pipe (bool mask, bool xfer) { acquire_attach_mutex (mutex_timeout); transfer_input (tty::to_cyg, get_handle_nat (), get_ttyp (), - input_available_event); + input_available_event, input_transferred_to_cyg); release_attach_mutex (); } else if (!mask && get_ttyp ()->pty_input_state_eq (tty::to_cyg)) { acquire_attach_mutex (mutex_timeout); transfer_input (tty::to_nat, get_handle (), get_ttyp (), - input_available_event); + input_available_event, input_transferred_to_cyg); release_attach_mutex (); } } @@ -1818,11 +1828,15 @@ fhandler_pty_slave::fch_open_handles (bool chown) shared_name (buf, INPUT_AVAILABLE_EVENT, get_minor ()); input_available_event = OpenEvent (READ_CONTROL | write_access, TRUE, buf); + shared_name (buf, INPUT_TRANSFERRED_EVENT, get_minor ()); + input_transferred_to_cyg = OpenEvent (READ_CONTROL | write_access, + TRUE, buf); output_mutex = get_ttyp ()->open_output_mutex (write_access); input_mutex = get_ttyp ()->open_input_mutex (write_access); pipe_sw_mutex = get_ttyp ()->open_mutex (PIPE_SW_MUTEX, write_access); inuse = get_ttyp ()->open_inuse (write_access); - if (!input_available_event || !output_mutex || !input_mutex || !inuse) + if (!input_available_event || !output_mutex || !input_mutex || !inuse + || !input_transferred_to_cyg) { __seterrno (); return false; @@ -1839,11 +1853,13 @@ fhandler_pty_slave::fch_set_sd (security_descriptor &sd, bool chown) get_object_sd (input_available_event, sd_old); if (!set_object_sd (input_available_event, sd, chown) + && !set_object_sd (input_transferred_to_cyg, sd, chown) && !set_object_sd (output_mutex, sd, chown) && !set_object_sd (input_mutex, sd, chown) && !set_object_sd (inuse, sd, chown)) return 0; set_object_sd (input_available_event, sd_old, chown); + set_object_sd (input_transferred_to_cyg, sd_old, chown); set_object_sd (output_mutex, sd_old, chown); set_object_sd (input_mutex, sd_old, chown); set_object_sd (inuse, sd_old, chown); @@ -1856,6 +1872,7 @@ void fhandler_pty_slave::fch_close_handles () { close_maybe (input_available_event); + close_maybe (input_transferred_to_cyg); close_maybe (output_mutex); close_maybe (input_mutex); close_maybe (inuse); @@ -2108,6 +2125,9 @@ fhandler_pty_master::close (int flag) if (!ForceCloseHandle (input_available_event)) termios_printf ("CloseHandle (input_available_event<%p>), %E", input_available_event); + if (!ForceCloseHandle (input_transferred_to_cyg)) + termios_printf ("CloseHandle (input_transferred_to_cyg<%p>), %E", + input_transferred_to_cyg); /* The from_master must be closed last so that the same pty is not allocated before cleaning up the other corresponding instances. */ @@ -2202,7 +2222,8 @@ fhandler_pty_master::write (const void *ptr, size_t len) acquire_attach_mutex (mutex_timeout); fhandler_pty_slave::transfer_input (tty::to_nat, from_master, get_ttyp (), - input_available_event); + input_available_event, + input_transferred_to_cyg); release_attach_mutex (); ReleaseMutex (input_mutex); } @@ -2311,7 +2332,8 @@ fhandler_pty_master::write (const void *ptr, size_t len) { acquire_attach_mutex (mutex_timeout); fhandler_pty_slave::transfer_input (tty::to_nat, from_master, - get_ttyp (), input_available_event); + get_ttyp (), input_available_event, + input_transferred_to_cyg); release_attach_mutex (); } @@ -2667,6 +2689,26 @@ fhandler_pty_master::pty_master_thread (const master_thread_param_t *p) return 0; } +void +fhandler_pty_master::apply_line_edit_to_transferred_input () +{ + /* cyg pipe is fhandler_pty_common::pipesize (128K) depth, so memory + allocated by w_get() (128K) is enough here. */ + tmp_pathbuf tp; + char *buf = (char *) tp.w_get (); + DWORD n; + ReadFile (from_master, buf, NT_MAX_PATH * 2, &n, NULL); + char *p = buf; + while (n) + { + ssize_t ret; + line_edit (p, n, get_ttyp ()->ti, &ret); + n -= ret; + p += ret; + } + SetEvent (input_available_event); +} + static DWORD pty_master_thread (VOID *arg) { @@ -2690,19 +2732,37 @@ fhandler_pty_master::pty_master_fwd_thread (const master_fwd_thread_param_t *p) char *outbuf = tp.c_get (); char *mbbuf = tp.c_get (); static mbstate_t mbp; + OVERLAPPED ov = {0, }; + ov.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); + HANDLE w[2] = {ov.hEvent, p->input_transferred_to_cyg}; termios_printf ("Started."); for (;;) { p->ttyp->fwd_last_time = GetTickCount64 (); - DWORD n; - p->ttyp->fwd_not_empty = - ::bytes_available (n, p->from_slave_nat) && n; - if (!ReadFile (p->from_slave_nat, outbuf, NT_MAX_PATH, &rlen, NULL)) + if (!ReadFile (p->from_slave_nat, outbuf, NT_MAX_PATH, NULL, &ov) + && GetLastError () != ERROR_IO_PENDING) { termios_printf ("ReadFile for forwarding failed, %E"); break; } +wait_event: + switch (WaitForMultipleObjects (2, w, FALSE, INFINITE)) + { + case WAIT_OBJECT_0: + GetOverlappedResult (p->from_slave_nat, &ov, &rlen, FALSE); + ResetEvent (ov.hEvent); + break; + case WAIT_OBJECT_0 + 1: + p->master->apply_line_edit_to_transferred_input (); + ResetEvent (p->input_transferred_to_cyg); + goto wait_event; + default: + /* Not expected to happen */ + debug_printf ("WaitForMultipleObjects() returns unexpectedly."); + Sleep (10); + goto wait_event; + } if (p->ttyp->stop_fwd_thread) break; ssize_t wlen = rlen; @@ -2981,7 +3041,8 @@ fhandler_pty_master::setup () char pipename[sizeof ("ptyNNNN-from-master-nat")]; __small_sprintf (pipename, "pty%d-to-master-nat", unit); res = fhandler_pipe::create (&sec_none, &from_slave_nat, &to_master_nat, - fhandler_pty_common::pipesize, pipename, 0); + fhandler_pty_common::pipesize, pipename, + FILE_FLAG_OVERLAPPED); if (res) { errstr = "output pipe for non-cygwin apps"; @@ -3042,6 +3103,10 @@ fhandler_pty_master::setup () &sa, TRUE)) || GetLastError () == ERROR_ALREADY_EXISTS) goto err; + if (!(input_transferred_to_cyg = t.get_event (errstr = INPUT_TRANSFERRED_EVENT, + &sa, TRUE)) + || GetLastError () == ERROR_ALREADY_EXISTS) + goto err; char buf[MAX_PATH]; errstr = shared_name (buf, OUTPUT_MUTEX, unit); @@ -3119,6 +3184,7 @@ fhandler_pty_master::setup () close_maybe (get_handle ()); close_maybe (get_output_handle ()); close_maybe (input_available_event); + close_maybe (input_transferred_to_cyg); close_maybe (output_mutex); close_maybe (input_mutex); close_maybe (from_master_nat); @@ -3924,6 +3990,8 @@ fhandler_pty_master::get_master_fwd_thread_param (master_fwd_thread_param_t *p) p->from_slave_nat = from_slave_nat; p->output_mutex = output_mutex; p->ttyp = get_ttyp (); + p->input_transferred_to_cyg = input_transferred_to_cyg; + p->master = this; SetEvent (thread_param_copied_event); } @@ -3931,7 +3999,8 @@ fhandler_pty_master::get_master_fwd_thread_param (master_fwd_thread_param_t *p) #define CTRL_PRESSED (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED) void fhandler_pty_slave::transfer_input (tty::xfer_dir dir, HANDLE from, tty *ttyp, - HANDLE input_available_event) + HANDLE input_available_event, + HANDLE input_transferred_to_cyg) { HANDLE to; if (dir == tty::to_nat) @@ -4053,26 +4122,8 @@ fhandler_pty_slave::transfer_input (tty::xfer_dir dir, HANDLE from, tty *ttyp, ptr = mbbuf; len = nlen; } - /* Call WriteFile() line by line */ - char *p0 = ptr; - char *p_cr = (char *) memchr (p0, '\r', len - (p0 - ptr)); - char *p_lf = (char *) memchr (p0, '\n', len - (p0 - ptr)); - while (p_cr || p_lf) - { - char *p1 = - p_cr ? (p_lf ? ((p_cr + 1 == p_lf) - ? p_lf : min(p_cr, p_lf)) : p_cr) : p_lf; - *p1 = '\n'; - n = p1 - p0 + 1; - if (n && WriteFile (to, p0, n, &n, NULL) && n) - transfered = true; - p0 = p1 + 1; - p_cr = (char *) memchr (p0, '\r', len - (p0 - ptr)); - p_lf = (char *) memchr (p0, '\n', len - (p0 - ptr)); - } - n = len - (p0 - ptr); - if (n && WriteFile (to, p0, n, &n, NULL) && n) - transfered = true; + if (len && WriteFile (to, ptr, len, &n, NULL) && n) + transfered = true;; } } else @@ -4111,13 +4162,20 @@ fhandler_pty_slave::transfer_input (tty::xfer_dir dir, HANDLE from, tty *ttyp, } CloseHandle (to); + ttyp->pty_input_state = dir; /* Fix input_available_event which indicates availability in cyg pipe. */ if (dir == tty::to_nat) /* all data is transfered to nat pipe, so no data available in cyg pipe. */ ResetEvent (input_available_event); else if (transfered) /* There is data transfered to cyg pipe. */ - SetEvent (input_available_event); - ttyp->pty_input_state = dir; + { + SetEvent (input_transferred_to_cyg); + /* Wait for line_edit() to be applied to the data in the cyg pipe. + Holding input mutex while waiting here is necessary to + prevent mixing transferred input and new master::write() input. */ + while (IsEventSignalled (input_transferred_to_cyg)) + yield (); + } ttyp->discard_input = false; } @@ -4137,6 +4195,9 @@ fhandler_pty_slave::get_duplicated_handle_set (handle_set_t *p) DuplicateHandle (GetCurrentProcess (), input_available_event, GetCurrentProcess (), &p->input_available_event, 0, 0, DUPLICATE_SAME_ACCESS); + DuplicateHandle (GetCurrentProcess (), input_transferred_to_cyg, + GetCurrentProcess (), &p->input_transferred_to_cyg, + 0, 0, DUPLICATE_SAME_ACCESS); DuplicateHandle (GetCurrentProcess (), input_mutex, GetCurrentProcess (), &p->input_mutex, 0, 0, DUPLICATE_SAME_ACCESS); @@ -4152,6 +4213,8 @@ fhandler_pty_slave::close_handle_set (handle_set_t *p) p->from_master_nat = NULL; CloseHandle (p->input_available_event); p->input_available_event = NULL; + CloseHandle (p->input_transferred_to_cyg); + p->input_transferred_to_cyg = NULL; CloseHandle (p->input_mutex); p->input_mutex = NULL; CloseHandle (p->pipe_sw_mutex); @@ -4187,7 +4250,7 @@ fhandler_pty_slave::setup_for_non_cygwin_app (bool nopcon, WaitForSingleObject (input_mutex, mutex_timeout); acquire_attach_mutex (mutex_timeout); transfer_input (tty::to_nat, get_handle (), get_ttyp (), - input_available_event); + input_available_event, input_transferred_to_cyg); release_attach_mutex (); ReleaseMutex (input_mutex); } @@ -4209,7 +4272,8 @@ fhandler_pty_slave::cleanup_for_non_cygwin_app (handle_set_t *p, tty *ttyp, WaitForSingleObject (p->input_mutex, mutex_timeout); acquire_attach_mutex (mutex_timeout); transfer_input (tty::to_cyg, p->from_master_nat, ttyp, - p->input_available_event); + p->input_available_event, + p->input_transferred_to_cyg); release_attach_mutex (); ReleaseMutex (p->input_mutex); } @@ -4235,7 +4299,7 @@ fhandler_pty_slave::setpgid_aux (pid_t pid) WaitForSingleObject (input_mutex, mutex_timeout); acquire_attach_mutex (mutex_timeout); transfer_input (tty::to_nat, get_handle (), get_ttyp (), - input_available_event); + input_available_event, input_transferred_to_cyg); release_attach_mutex (); ReleaseMutex (input_mutex); } @@ -4261,7 +4325,8 @@ fhandler_pty_slave::setpgid_aux (pid_t pid) } else acquire_attach_mutex (mutex_timeout); - transfer_input (tty::to_cyg, from, get_ttyp (), input_available_event); + transfer_input (tty::to_cyg, from, get_ttyp (), input_available_event, + input_transferred_to_cyg); if (attach_restore) resume_from_temporarily_attach (resume_pid); else diff --git a/winsup/cygwin/local_includes/fhandler.h b/winsup/cygwin/local_includes/fhandler.h index a4feeec247..95423ed0c0 100644 --- a/winsup/cygwin/local_includes/fhandler.h +++ b/winsup/cygwin/local_includes/fhandler.h @@ -2011,6 +2011,7 @@ class fhandler_termios: public fhandler_base { HANDLE from_master_nat; HANDLE input_available_event; + HANDLE input_transferred_to_cyg; HANDLE input_mutex; HANDLE pipe_sw_mutex; }; @@ -2382,13 +2383,14 @@ class fhandler_pty_common: public fhandler_termios fhandler_pty_common () : fhandler_termios (), output_mutex (NULL), input_mutex (NULL), pipe_sw_mutex (NULL), - input_available_event (NULL) + input_available_event (NULL), input_transferred_to_cyg (NULL) { pc.file_attributes (FILE_ATTRIBUTE_NORMAL); } static const unsigned pipesize = 128 * 1024; HANDLE output_mutex, input_mutex, pipe_sw_mutex; HANDLE input_available_event; + HANDLE input_transferred_to_cyg; bool use_archetype () const {return true;} DWORD __acquire_output_mutex (const char *fn, int ln, DWORD ms); @@ -2510,7 +2512,8 @@ class fhandler_pty_slave: public fhandler_pty_common void setup_locale (void); void create_invisible_console (void); static void transfer_input (tty::xfer_dir dir, HANDLE from, tty *ttyp, - HANDLE input_available_event); + HANDLE input_available_event, + HANDLE input_transferred_to_cyg); HANDLE get_input_available_event (void) { return input_available_event; } bool pcon_activated (void) { return get_ttyp ()->pcon_activated; } void cleanup_before_exit (); @@ -2545,8 +2548,10 @@ class fhandler_pty_master: public fhandler_pty_common struct master_fwd_thread_param_t { HANDLE to_master; HANDLE from_slave_nat; + HANDLE input_transferred_to_cyg; HANDLE output_mutex; tty *ttyp; + fhandler_pty_master *master; }; private: int pktmode; // non-zero if pty in a packet mode. @@ -2625,6 +2630,7 @@ class fhandler_pty_master: public fhandler_pty_common void get_master_thread_param (master_thread_param_t *p); void get_master_fwd_thread_param (master_fwd_thread_param_t *p); bool need_send_ctrl_c_event (); + void apply_line_edit_to_transferred_input (); }; class fhandler_dev_null: public fhandler_base diff --git a/winsup/cygwin/local_includes/tty.h b/winsup/cygwin/local_includes/tty.h index 754ee900e7..7d80ab401e 100644 --- a/winsup/cygwin/local_includes/tty.h +++ b/winsup/cygwin/local_includes/tty.h @@ -18,6 +18,7 @@ details. */ /* Input/Output/ioctl events */ #define INPUT_AVAILABLE_EVENT "cygtty.input.avail" +#define INPUT_TRANSFERRED_EVENT "cygtty.input.xfer" #define OUTPUT_MUTEX "cygtty.output.mutex" #define INPUT_MUTEX "cygtty.input.mutex" #define PIPE_SW_MUTEX "cygtty.pipe_sw.mutex" From 9e78b35e7566edad51c22de8688af3eaa23cb31b Mon Sep 17 00:00:00 2001 From: Takashi Yano Date: Sat, 28 Mar 2026 19:55:49 +0900 Subject: [PATCH 18/28] Cygwin: pty: Guard get_winpid_to_hand_over() with attach_mutex The master process (e.g. mintty) temporarily attaches to the pseudo console's conhost in `transfer_input()` so it can read INPUT_RECORDs via `ReadConsoleInputA()`. During that brief window, `get_console_process_id()` inside `get_winpid_to_hand_over()` calls `GetConsoleProcessList()`, which sees the master among the console's attached processes and may select it as the handover target. That is wrong because the master will detach immediately after the read. Until now, `attach_mutex` was a process-local unnamed mutex, so the slave's `get_winpid_to_hand_over()` could not serialize with the master's temporary attachment. Make `attach_mutex` a cross-process named mutex (`ATTACH_MUTEX`) shared within the PTY, and acquire it around the `get_console_process_id()` calls in `get_winpid_to_hand_over()`. This ensures the console process list enumeration never observes the master while it is temporarily attached. Fixes: 1e6c51d74136 ("Cygwin: pty: Reorganize the code path of setting up and closing pcon.") Signed-off-by: Takashi Yano Reviewed-by: Johannes Schindelin Signed-off-by: Johannes Schindelin --- winsup/cygwin/fhandler/pty.cc | 16 ++++++++++++++-- winsup/cygwin/local_includes/tty.h | 1 + 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/winsup/cygwin/fhandler/pty.cc b/winsup/cygwin/fhandler/pty.cc index 58fd996df5..d6817cbdac 100644 --- a/winsup/cygwin/fhandler/pty.cc +++ b/winsup/cygwin/fhandler/pty.cc @@ -773,6 +773,12 @@ fhandler_pty_slave::open (int flags, mode_t) errmsg = "open pipe switch mutex failed, %E"; goto err; } + if (!(attach_mutex + = get_ttyp ()->open_mutex (ATTACH_MUTEX, MAXIMUM_ALLOWED))) + { + errmsg = "open attach mutex failed, %E"; + goto err; + } shared_name (buf, INPUT_AVAILABLE_EVENT, get_minor ()); if (!(input_available_event = OpenEvent (MAXIMUM_ALLOWED, TRUE, buf))) { @@ -2493,6 +2499,9 @@ void fhandler_pty_slave::fixup_after_fork (HANDLE parent) { create_invisible_console (); + /* attach_mutex is initialized not only in the fork() case, but also in + the exec() case, since fixup_after_exec() calls fixup_after_fork(). */ + attach_mutex = get_ttyp ()->open_mutex (ATTACH_MUTEX, MAXIMUM_ALLOWED); // fork_fixup (parent, inuse, "inuse"); // fhandler_pty_common::fixup_after_fork (parent); @@ -3121,8 +3130,9 @@ fhandler_pty_master::setup () if (!(pipe_sw_mutex = CreateMutex (&sa, FALSE, buf))) goto err; - if (!attach_mutex) - attach_mutex = CreateMutex (&sec_none_nih, FALSE, NULL); + errstr = shared_name (buf, ATTACH_MUTEX, unit); + if (!(attach_mutex = CreateMutex (&sa, FALSE, buf))) + goto err; /* Create master control pipe which allows the master to duplicate the pty pipe handles to processes which deserve it. */ @@ -3676,6 +3686,7 @@ fhandler_pty_slave::get_winpid_to_hand_over (tty *ttyp, DWORD current_pid = myself->exec_dwProcessId ?: myself->dwProcessId; if (ttyp->nat_pipe_owner_pid == GetCurrentProcessId ()) current_pid = GetCurrentProcessId (); + acquire_attach_mutex (mutex_timeout); switch_to = get_console_process_id (current_pid, false, true, true, true); if (!switch_to) @@ -3684,6 +3695,7 @@ fhandler_pty_slave::get_winpid_to_hand_over (tty *ttyp, if (!switch_to && ttyp->pcon_activated) switch_to = get_console_process_id (current_pid, false, false, false, false); + release_attach_mutex (); } return switch_to; } diff --git a/winsup/cygwin/local_includes/tty.h b/winsup/cygwin/local_includes/tty.h index 7d80ab401e..6e70a74cd7 100644 --- a/winsup/cygwin/local_includes/tty.h +++ b/winsup/cygwin/local_includes/tty.h @@ -22,6 +22,7 @@ details. */ #define OUTPUT_MUTEX "cygtty.output.mutex" #define INPUT_MUTEX "cygtty.input.mutex" #define PIPE_SW_MUTEX "cygtty.pipe_sw.mutex" +#define ATTACH_MUTEX "cygtty.attach.mutex" #define TTY_SLAVE_ALIVE "cygtty.slave_alive" #define TTY_SLAVE_READING "cygtty.slave_reading" From d2355119f7c6fafc74a4c8595f15baffb6b5d364 Mon Sep 17 00:00:00 2001 From: Takashi Yano Date: Sat, 28 Mar 2026 19:55:50 +0900 Subject: [PATCH 19/28] Cygwin: pty: Guard to_be_read_from_nat_pipe() by pipe_sw_mutex `to_be_read_from_nat_pipe()` reads several shared-memory fields (`switch_to_nat_pipe`, `pcon_activated`, `pty_input_state`) to decide whether keystrokes should go to the nat pipe. It is called from `master::write()` on every keystroke. Without synchronization, the slave can be in the middle of a pipe switch (changing these fields in `setup_for_non_cygwin_app()`, `cleanup_for_non_cygwin_app()`, or `setpgid_aux()`) while the master reads a half-updated snapshot, making an inconsistent routing decision that sends keystrokes to the wrong pipe. Guard `to_be_read_from_nat_pipe()` with `pipe_sw_mutex` so it always reads a consistent state. The spin-wait at entry handles the pseudo console initialization case: when `pipe_sw_mutex` is held by the slave during `setup_pseudoconsole()` and `pcon_start` is set, the function returns false immediately, routing keystrokes to the cyg pipe through `line_edit()` where the CSI6n response handler expects them. Acquiring `pipe_sw_mutex` inside `to_be_read_from_nat_pipe()` creates a lock ordering constraint: `master::write()` holds `input_mutex` before calling `to_be_read_from_nat_pipe()`, so the master's lock order is `input_mutex` then `pipe_sw_mutex`. Previously, `cleanup_for_non_cygwin_app()` and `setpgid_aux()` acquired `pipe_sw_mutex` first and then `input_mutex` (for `transfer_input()`), which is the reverse order and would deadlock. Restructure both functions to release `pipe_sw_mutex` before acquiring `input_mutex`, maintaining a consistent lock order throughout. Fixes: bb4285206207 ("Cygwin: pty: Implement new pseudo console support.") Signed-off-by: Takashi Yano Reviewed-by: Johannes Schindelin Signed-off-by: Johannes Schindelin --- winsup/cygwin/fhandler/pty.cc | 48 +++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/winsup/cygwin/fhandler/pty.cc b/winsup/cygwin/fhandler/pty.cc index d6817cbdac..4a628ea0a5 100644 --- a/winsup/cygwin/fhandler/pty.cc +++ b/winsup/cygwin/fhandler/pty.cc @@ -1283,22 +1283,42 @@ fhandler_pty_slave::mask_switch_to_nat_pipe (bool mask, bool xfer) bool fhandler_pty_common::to_be_read_from_nat_pipe (void) { + /* If the slave is in setup_pseudoconsole(), pipe_sw_mutex cannot + be acquired because the slave has it. In this case pcon_start + will be asserted. During pcon_start, other input than response + to CSI6n should be go to cyg-pipe. So, wait for pcon_start and + return false. */ + while (WaitForSingleObject (pipe_sw_mutex, 0) == WAIT_TIMEOUT) + if (get_ttyp ()->pcon_start || get_ttyp ()->pcon_start_pid) + return false; + else + yield (); + + bool ret = false; if (!get_ttyp ()->switch_to_nat_pipe) - return false; + goto out; - char name[MAX_PATH]; - shared_name (name, TTY_SLAVE_READING, get_minor ()); - HANDLE masked = OpenEvent (READ_CONTROL, FALSE, name); - CloseHandle (masked); + { + char name[MAX_PATH]; + shared_name (name, TTY_SLAVE_READING, get_minor ()); + HANDLE masked = OpenEvent (READ_CONTROL, FALSE, name); + CloseHandle (masked); - if (masked) /* The foreground process is cygwin process */ - return false; + if (masked) /* The foreground process is cygwin process */ + goto out; + } if (!pinfo (get_ttyp ()->getpgid ())) /* GDB may set invalid process group for non-cygwin process. */ - return true; + { + ret = true; + goto out; + } - return get_ttyp ()->nat_fg (get_ttyp ()->getpgid ()); + ret = get_ttyp ()->nat_fg (get_ttyp ()->getpgid ()); +out: + ReleaseMutex (pipe_sw_mutex); + return ret; } void @@ -3901,7 +3921,6 @@ fhandler_pty_slave::term_has_pcon_cap (const WCHAR *env) goto maybe_dumb; /* Check if terminal has CSI6n */ - WaitForSingleObject (pipe_sw_mutex, INFINITE); WaitForSingleObject (input_mutex, mutex_timeout); /* Set pcon_activated and pcon_start so that the response will sent to io_handle_nat rather than io_handle. */ @@ -3937,7 +3956,6 @@ fhandler_pty_slave::term_has_pcon_cap (const WCHAR *env) while (len); get_ttyp ()->pcon_activated = false; get_ttyp ()->nat_pipe_owner_pid = 0; - ReleaseMutex (pipe_sw_mutex); if (len == 0) goto not_has_csi6n; @@ -3953,7 +3971,6 @@ fhandler_pty_slave::term_has_pcon_cap (const WCHAR *env) get_ttyp ()->pcon_start = false; get_ttyp ()->pcon_activated = false; ReleaseMutex (input_mutex); - ReleaseMutex (pipe_sw_mutex); maybe_dumb: get_ttyp ()->pcon_cap_checked = true; return false; @@ -4274,7 +4291,6 @@ fhandler_pty_slave::cleanup_for_non_cygwin_app (handle_set_t *p, tty *ttyp, DWORD force_switch_to) { ttyp->wait_fwd (); - WaitForSingleObject (p->pipe_sw_mutex, INFINITE); if (nat_pipe_owner_self (ttyp->nat_pipe_owner_pid)) { DWORD switch_to = get_winpid_to_hand_over (ttyp, force_switch_to); @@ -4290,6 +4306,7 @@ fhandler_pty_slave::cleanup_for_non_cygwin_app (handle_set_t *p, tty *ttyp, ReleaseMutex (p->input_mutex); } } + WaitForSingleObject (p->pipe_sw_mutex, INFINITE); if (ttyp->pcon_activated) close_pseudoconsole (ttyp, force_switch_to); else @@ -4308,6 +4325,7 @@ fhandler_pty_slave::setpgid_aux (pid_t pid) if (!was_nat_fg && nat_fg && get_ttyp ()->switch_to_nat_pipe && get_ttyp ()->pty_input_state_eq (tty::to_cyg)) { + ReleaseMutex (pipe_sw_mutex); WaitForSingleObject (input_mutex, mutex_timeout); acquire_attach_mutex (mutex_timeout); transfer_input (tty::to_nat, get_handle (), get_ttyp (), @@ -4318,6 +4336,7 @@ fhandler_pty_slave::setpgid_aux (pid_t pid) else if (was_nat_fg && !nat_fg && get_ttyp ()->switch_to_nat_pipe && get_ttyp ()->pty_input_state_eq (tty::to_nat)) { + ReleaseMutex (pipe_sw_mutex); bool attach_restore = false; HANDLE from = get_handle_nat (); DWORD resume_pid = 0; @@ -4345,7 +4364,8 @@ fhandler_pty_slave::setpgid_aux (pid_t pid) release_attach_mutex (); ReleaseMutex (input_mutex); } - ReleaseMutex (pipe_sw_mutex); + else + ReleaseMutex (pipe_sw_mutex); } bool From 76f644d90e44d4d41a69ab4ca38e98c64a9b67ec Mon Sep 17 00:00:00 2001 From: Takashi Yano Date: Sat, 28 Mar 2026 19:55:51 +0900 Subject: [PATCH 20/28] Cygwin: pty: Drop nat_fg() check from to_be_read_from_nat_pipe() While a non-cygwin app has exited but the stub process has not yet terminated, `nat_fg()` returns false because no non-cygwin app is running. In this window, pty input goes to the cyg pipe. Due to this, the keystroke order is swapped unexpectedly: 1) start non-cygwin app 2) press 'a' ('a' goes to nat pipe) 3) non-cygwin app exits 4) press 'b' ('b' goes to cyg pipe) 5) the stub process for non-cygwin app transfers input in nat pipe to cyg pipe ('a' goes to cyg pipe) 6) the result in the cyg pipe is "ba" Fix this by dropping the `nat_fg()` check from `to_be_read_from_nat_pipe()`. The function now returns true when `!pcon_start && switch_to_nat_pipe && !masked`. Each component has a specific purpose: - `!pcon_start`: keystrokes go through the CSI6n response handler during pseudo console initialization rather than the fast path. - `switch_to_nat_pipe`: this session-level flag stays true from `setup_for_non_cygwin_app()` through `cleanup_for_non_cygwin_app()`, spanning the entire native process lifetime including the post-exit cleanup window. - `!masked` (`TTY_SLAVE_READING` event does not exist): keystrokes go to the Cygwin pipe when a Cygwin process is actively reading from the slave, since that process expects POSIX-processed input. Removing `nat_fg()` is safe because conhost's input buffer accumulates keystrokes as INPUT_RECORDs during the post-exit window, and `transfer_input(to_cyg)` in `cleanup_for_non_cygwin_app()` reads them back via `ReadConsoleInputA()` and writes them to the cyg pipe. Those transferred bytes then go through `line_edit()` in the master's forward thread (via `input_transferred_to_cyg` from an earlier patch in this series), ensuring proper POSIX line discipline processing. Additionally, add a `nat_fg()` check to the disable_pcon transfer path in `master::write()`. That transfer moves cyg pipe data to the nat pipe when a Cygwin child exits and a native process regains the foreground with pcon disabled. Without pcon, there is no conhost buffer to accumulate keystrokes (the nat pipe is a raw pipe), so keystrokes must only go there when a native process is genuinely in the foreground and ready to read them. The `nat_fg()` guard prevents the transfer from stealing readline's data during the post-exit window. Fixes: f20641789427 ("Cygwin: pty: Reduce unecessary input transfer.") Signed-off-by: Takashi Yano Reviewed-by: Johannes Schindelin Signed-off-by: Johannes Schindelin --- winsup/cygwin/fhandler/pty.cc | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/winsup/cygwin/fhandler/pty.cc b/winsup/cygwin/fhandler/pty.cc index 4a628ea0a5..07904d9303 100644 --- a/winsup/cygwin/fhandler/pty.cc +++ b/winsup/cygwin/fhandler/pty.cc @@ -1308,14 +1308,8 @@ fhandler_pty_common::to_be_read_from_nat_pipe (void) goto out; } - if (!pinfo (get_ttyp ()->getpgid ())) - /* GDB may set invalid process group for non-cygwin process. */ - { - ret = true; - goto out; - } + ret = true; /* !pcon_start && switch_to_nat_pipe && !masked */ - ret = get_ttyp ()->nat_fg (get_ttyp ()->getpgid ()); out: ReleaseMutex (pipe_sw_mutex); return ret; @@ -2354,6 +2348,7 @@ fhandler_pty_master::write (const void *ptr, size_t len) /* This input transfer is needed when cygwin-app which is started from non-cygwin app is terminated if pseudo console is disabled. */ if (to_be_read_from_nat_pipe () && !get_ttyp ()->pcon_activated + && get_ttyp ()->nat_fg (get_ttyp ()->getpgid ()) && get_ttyp ()->pty_input_state == tty::to_cyg) { acquire_attach_mutex (mutex_timeout); From 59ea95a64f71d1ca36840a9babef852070d93165 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 20 Feb 2026 23:51:07 +0100 Subject: [PATCH 21/28] ui-tests: add mintty launch and capture helpers to the library The existing UI test infrastructure only supports Windows Terminal, but the keystroke reordering bug reported in https://github.com/git-for-windows/git/issues/5632 manifests most reliably in mintty, which uses a different PTY code path. To write a reproducer for that bug, we need library functions that can launch mintty and read back what it displayed. An initial attempt used mintty's `-l` flag to write a terminal log file, then read back that log with ANSI escape sequences stripped. This approach turned out to be unreliable: mintty buffers its log output, so content that is already visible on screen (such as the `$ ` prompt) may not have been flushed to the log file yet. Polling for a prompt that is already displayed but not yet logged leads to an indefinite wait. Instead, LaunchMintty() configures mintty's Ctrl+F5 keybinding to trigger the `export-html` action, which writes an HTML snapshot of the current screen to a file. This is instantaneous and always reflects exactly what is on screen. The function uses window-class enumeration to identify the newly-created mintty window among any pre-existing instances and returns its handle. CaptureBufferFromMintty() sends Ctrl+F5 to trigger the export, reads the resulting HTML file, extracts the `` content, strips HTML tags, and decodes common entities to return plain text suitable for substring matching. It accepts an optional window title to activate the correct mintty instance before sending the keystroke. Note that AHK's ControlSend cannot be used here because mintty passes the raw keycodes through to the terminal session rather than interpreting them as window-level shortcuts, so WinActivate followed by Send is the only way to trigger the export action. Assisted-by: Claude Opus 4.6 Signed-off-by: Johannes Schindelin --- ui-tests/ui-test-library.ahk | 66 ++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/ui-tests/ui-test-library.ahk b/ui-tests/ui-test-library.ahk index e6d261ed45..645bcd0d82 100644 --- a/ui-tests/ui-test-library.ahk +++ b/ui-tests/ui-test-library.ahk @@ -116,4 +116,70 @@ WaitForRegExInWindowsTerminal(regex, errorMessage, successMessage, timeout := 50 ExitWithError errorMessage } } +} + +; Launch mintty with HTML export support. Returns the window handle. +; Ctrl+F5 is bound to export-html; the file is written to /mintty-export.html. +LaunchMintty(extraArgs := '') { + exportFile := A_ScriptDir . '\mintty-export.html' + savePattern := StrReplace(A_ScriptDir, '\', '/') '/mintty-export' + minttyClass := 'ahk_class mintty' + existing := Map() + for h in WinGetList(minttyClass) + existing[h] := true + + cmd := 'mintty.exe -o "KeyFunctions=C+F5:export-html" -o "SaveFilename=' savePattern '"' + if extraArgs != '' + cmd .= ' ' extraArgs + cmd .= ' -' + Run cmd, , , &childPid + Info 'Launched mintty, PID: ' childPid + + hwnd := 0 + deadline := A_TickCount + 10000 + while A_TickCount < deadline + { + for h in WinGetList(minttyClass) + { + if !existing.Has(h) + { + hwnd := h + break 2 + } + } + Sleep 100 + } + if !hwnd + ExitWithError 'New mintty window did not appear' + WinActivate('ahk_id ' hwnd) + Info 'Found new mintty: ' hwnd + return hwnd +} + +; Trigger Ctrl+F5 to export mintty's screen as HTML, read it, strip tags, +; and return the plain text. +CaptureBufferFromMintty(winTitle := '') { + static exportFile := A_ScriptDir . '\mintty-export.html' + if FileExist(exportFile) + FileDelete exportFile + if winTitle != '' + WinActivate winTitle + Send '^{F5}' + deadline := A_TickCount + 3000 + while !FileExist(exportFile) && A_TickCount < deadline + Sleep 50 + if !FileExist(exportFile) + return '' + Sleep 100 + html := FileRead(exportFile) + ; Extract body content only (skip CSS in