Fix Windows PTY teardown by preserving ConPTY ownership (#20685)

## Why

On Windows, background terminals could stay visible after their shell
process had already exited. The elevated runner waits for the PTY output
reader to reach EOF before it sends the final exit message, but the
ConPTY helper was reducing ownership down to raw handles too early. That
left the pseudoconsole's borrowed pipe handles alive past teardown, so
EOF never propagated and the session stayed `running`.

## What changed

- change `utils/pty/src/win/conpty.rs` to hand off owned ConPTY
resources instead of leaking only raw handles
- make `windows-sandbox-rs/src/conpty/mod.rs` keep the pseudoconsole
owner and the backing pipe handles together until teardown
- update the elevated runner and the legacy unified-exec backend to keep
that `ConptyInstance` alive, take only the specific pipe handles they
need, and drop the owner at teardown instead of trying to close a
detached pseudoconsole handle later

## Testing

- desktop app in `Auto-review`: 11 x `cmd /c "ping -n 3 google.com"` all
exited cleanly and did not accumulate in the UI
- desktop app in `Auto-review`: 5 x `cmd /c "ping -n 30 google.com"`
appeared in the UI and drained back out on their own
This commit is contained in:
iceweasel-oai authored and GitHub committed 2026-05-04 18:40:00 +00:00
1 parent 905987c08f
commit 5d5500650b
7 files changed
+70 -57

No files matched your search

@@ -1,6 +1,7 @@
use super::windows_common::finish_driver_spawn;
use super::windows_common::normalize_windows_tty_input;
use crate::acl::revoke_ace;
use crate::conpty::ConptyInstance;
use crate::conpty::spawn_conpty_process_as_user;
use crate::desktop::LaunchDesktop;
use crate::logging::log_failure;
@@ -33,7 +34,6 @@ use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
use windows_sys::Win32::Storage::FileSystem::WriteFile;
use windows_sys::Win32::System::Console::COORD;
use windows_sys::Win32::System::Console::ClosePseudoConsole;
use windows_sys::Win32::System::Console::ResizePseudoConsole;
use windows_sys::Win32::System::Threading::GetExitCodeProcess;
use windows_sys::Win32::System::Threading::INFINITE;
@@ -48,6 +48,7 @@ struct LegacyProcessHandles {
output_join: std::thread::JoinHandle<()>,
writer_handle: tokio::task::JoinHandle<()>,
hpc: Option<HANDLE>,
conpty_owner: Option<ConptyInstance>,
token_handle: HANDLE,
desktop: Option<LaunchDesktop>,
}
@@ -66,8 +67,8 @@ fn spawn_legacy_process(
writer_rx: mpsc::Receiver<Vec<u8>>,
logs_base_dir: Option<&Path>,
) -> Result<LegacyProcessHandles> {
let (pi, output_join, writer_handle, hpc, desktop) = if tty {
let (pi, conpty) = spawn_conpty_process_as_user(
let (pi, output_join, writer_handle, hpc, conpty_owner, desktop) = if tty {
let (pi, mut conpty) = spawn_conpty_process_as_user(
h_token,
command,
cwd,
@@ -75,14 +76,14 @@ fn spawn_legacy_process(
use_private_desktop,
logs_base_dir,
)?;
let (hpc, input_write, output_read, desktop) = conpty.into_raw();
let output_join = spawn_output_reader(output_read, stdout_tx);
let hpc = conpty.raw_handle();
let output_join = spawn_output_reader(conpty.take_output_read(), stdout_tx);
let writer_handle = spawn_input_writer(
Some(input_write),
Some(conpty.take_input_write()),
writer_rx,
/*normalize_newlines*/ true,
);
(pi, output_join, writer_handle, Some(hpc), desktop)
(pi, output_join, writer_handle, hpc, Some(conpty), None)
} else {
let pipe_handles = spawn_process_with_pipes(
h_token,
@@ -120,6 +121,7 @@ fn spawn_legacy_process(
output_join,
writer_handle,
None,
None,
Some(pipe_handles.desktop),
)
};
@@ -128,6 +130,7 @@ fn spawn_legacy_process(
output_join,
writer_handle,
hpc,
conpty_owner,
token_handle: h_token,
desktop,
})
@@ -328,6 +331,7 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy(
output_join,
writer_handle,
hpc,
mut conpty_owner,
token_handle,
desktop,
} = match spawn_legacy_process(
@@ -386,12 +390,10 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy(
}
if let Some(hpc) = hpc_for_wait
&& let Ok(mut guard) = hpc.lock()
&& let Some(hpc) = guard.take()
{
unsafe {
ClosePseudoConsole(hpc);
}
let _ = guard.take();
}
drop(conpty_owner.take());
unsafe {
if token_handle != 0 && token_handle != INVALID_HANDLE_VALUE {
CloseHandle(token_handle);