diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-07-25 02:40:14 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-25 02:40:14 +0300 |
| commit | 8afd52ae57f9237f4f46138ad224dfc621d7ba59 (patch) | |
| tree | 349bbbd86c57863539ebc4c2eacf34550e076fa2 /vendor | |
| parent | 5f1957c27bd23f73b339265d3acd32f4a21878a3 (diff) | |
fix: bundle modern ConPTY on Windows (#1828)
refs #1533
refs #1644
Diffstat (limited to 'vendor')
| -rw-r--r-- | vendor/patches/portable-pty/0001-control-conpty-loading.patch | 357 | ||||
| -rw-r--r-- | vendor/patches/portable-pty/0001-force-system-conpty.patch | 37 | ||||
| -rw-r--r-- | vendor/portable-pty.patches.md | 37 | ||||
| -rw-r--r-- | vendor/portable-pty/Cargo.toml | 5 | ||||
| -rw-r--r-- | vendor/portable-pty/Cargo.toml.orig | 3 | ||||
| -rw-r--r-- | vendor/portable-pty/src/win/psuedocon.rs | 272 |
6 files changed, 637 insertions, 74 deletions
diff --git a/vendor/patches/portable-pty/0001-control-conpty-loading.patch b/vendor/patches/portable-pty/0001-control-conpty-loading.patch new file mode 100644 index 00000000..4c49300e --- /dev/null +++ b/vendor/patches/portable-pty/0001-control-conpty-loading.patch @@ -0,0 +1,357 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: herdr maintainers <maintainers@herdr.dev> +Date: Tue, 23 Jun 2026 00:00:00 +0000 +Subject: [PATCH] control ConPTY loading + +Load Herdr's hash-verified app-local ConPTY with a constrained DLL search, +otherwise use the exports from the already loaded kernel32 module. Never +probe for a bare conpty.dll through the DLL search path. + +Herdr issues: https://github.com/ogulcancelik/herdr/issues/761 + https://github.com/ogulcancelik/herdr/issues/1533 +Vendored base: portable-pty 0.9.0 +--- +diff --git a/vendor/portable-pty/Cargo.toml b/vendor/portable-pty/Cargo.toml +index b8d11230..8a33abf9 100644 +--- a/vendor/portable-pty/Cargo.toml ++++ b/vendor/portable-pty/Cargo.toml +@@ -102,8 +102,8 @@ version = "1.3" + [target."cfg(windows)".dependencies.lazy_static] + version = "1.4" + +-[target."cfg(windows)".dependencies.shared_library] +-version = "0.1" ++[target."cfg(windows)".dependencies.sha2] ++version = "0.10" + + [target."cfg(windows)".dependencies.winapi] + version = "0.3" +@@ -112,6 +112,7 @@ features = [ + "consoleapi", + "handleapi", + "fileapi", ++ "libloaderapi", + "namedpipeapi", + "synchapi", + ] +diff --git a/vendor/portable-pty/Cargo.toml.orig b/vendor/portable-pty/Cargo.toml.orig +index 0c3d6b8d..21abe3b6 100644 +--- a/vendor/portable-pty/Cargo.toml.orig ++++ b/vendor/portable-pty/Cargo.toml.orig +@@ -27,12 +27,13 @@ serde_support = ["serde", "serde_derive"] + [target."cfg(windows)".dependencies] + bitflags = "1.3" + lazy_static = "1.4" +-shared_library = "0.1" ++sha2 = "0.10" + winapi = { version = "0.3", features = [ + "winuser", + "consoleapi", + "handleapi", + "fileapi", ++ "libloaderapi", + "namedpipeapi", + "synchapi", + ]} +diff --git a/vendor/portable-pty/src/win/psuedocon.rs b/vendor/portable-pty/src/win/psuedocon.rs +index 133eab52..d4f111bb 100644 +--- a/vendor/portable-pty/src/win/psuedocon.rs ++++ b/vendor/portable-pty/src/win/psuedocon.rs +@@ -4,17 +4,24 @@ use crate::win::procthreadattr::ProcThreadAttributeList; + use anyhow::{bail, ensure, Error}; + use filedescriptor::{FileDescriptor, OwnedHandle}; + use lazy_static::lazy_static; +-use shared_library::shared_library; +-use std::ffi::OsString; +-use std::io::Error as IoError; +-use std::os::windows::ffi::OsStringExt; ++use sha2::{Digest, Sha256}; ++use std::collections::BTreeSet; ++use std::ffi::{OsStr, OsString}; ++use std::fs::File; ++use std::io::{Error as IoError, Read}; ++use std::os::windows::ffi::{OsStrExt, OsStringExt}; ++use std::os::windows::fs::MetadataExt; + use std::os::windows::io::{AsRawHandle, FromRawHandle}; +-use std::path::Path; ++use std::path::{Path, PathBuf}; + use std::sync::Mutex; + use std::{mem, ptr}; +-use winapi::shared::minwindef::DWORD; ++use winapi::shared::minwindef::{DWORD, HMODULE}; + use winapi::shared::winerror::{HRESULT, S_OK}; + use winapi::um::handleapi::*; ++use winapi::um::libloaderapi::{ ++ FreeLibrary, GetModuleHandleW, GetProcAddress, LoadLibraryExW, ++ LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR, LOAD_LIBRARY_SEARCH_SYSTEM32, ++}; + use winapi::um::processthreadsapi::*; + use winapi::um::winbase::{ + CREATE_UNICODE_ENVIRONMENT, EXTENDED_STARTUPINFO_PRESENT, STARTF_USESTDHANDLES, STARTUPINFOEXW, +@@ -30,26 +37,249 @@ pub const PSEUDOCONSOLE_WIN32_INPUT_MODE: DWORD = 0x4; + #[allow(dead_code)] + pub const PSEUDOCONSOLE_PASSTHROUGH_MODE: DWORD = 0x8; + +-shared_library!(ConPtyFuncs, +- pub fn CreatePseudoConsole( +- size: COORD, +- hInput: HANDLE, +- hOutput: HANDLE, +- flags: DWORD, +- hpc: *mut HPCON +- ) -> HRESULT, +- pub fn ResizePseudoConsole(hpc: HPCON, size: COORD) -> HRESULT, +- pub fn ClosePseudoConsole(hpc: HPCON), +-); ++type CreatePseudoConsoleFn = unsafe extern "system" fn( ++ size: COORD, ++ h_input: HANDLE, ++ h_output: HANDLE, ++ flags: DWORD, ++ hpc: *mut HPCON, ++) -> HRESULT; ++type ResizePseudoConsoleFn = unsafe extern "system" fn(hpc: HPCON, size: COORD) -> HRESULT; ++type ClosePseudoConsoleFn = unsafe extern "system" fn(hpc: HPCON); ++ ++// These fields intentionally mirror the exported Win32 symbol names. ++#[allow(non_snake_case)] ++struct ConPtyFuncs { ++ module: HMODULE, ++ owned: bool, ++ CreatePseudoConsole: CreatePseudoConsoleFn, ++ ResizePseudoConsole: ResizePseudoConsoleFn, ++ ClosePseudoConsole: ClosePseudoConsoleFn, ++} ++ ++unsafe impl Send for ConPtyFuncs {} ++unsafe impl Sync for ConPtyFuncs {} ++ ++impl Drop for ConPtyFuncs { ++ fn drop(&mut self) { ++ if self.owned { ++ unsafe { FreeLibrary(self.module) }; ++ } ++ } ++} ++ ++impl ConPtyFuncs { ++ unsafe fn from_module(module: HMODULE, owned: bool) -> Result<Self, String> { ++ if module.is_null() { ++ return Err(IoError::last_os_error().to_string()); ++ } ++ Ok(Self { ++ module, ++ owned, ++ CreatePseudoConsole: load_symbol(module, b"CreatePseudoConsole\0")?, ++ ResizePseudoConsole: load_symbol(module, b"ResizePseudoConsole\0")?, ++ ClosePseudoConsole: load_symbol(module, b"ClosePseudoConsole\0")?, ++ }) ++ } ++} ++ ++unsafe fn load_symbol<T: Copy>(module: HMODULE, name: &'static [u8]) -> Result<T, String> { ++ let symbol = GetProcAddress(module, name.as_ptr().cast()); ++ if symbol.is_null() { ++ return Err(format!( ++ "missing symbol {}", ++ String::from_utf8_lossy(&name[..name.len() - 1]) ++ )); ++ } ++ Ok(mem::transmute_copy(&symbol)) ++} ++ ++fn load_system_conpty() -> Result<ConPtyFuncs, String> { ++ let kernel_name = wide_string(OsStr::new("kernel32.dll")); ++ let module = unsafe { GetModuleHandleW(kernel_name.as_ptr()) }; ++ unsafe { ConPtyFuncs::from_module(module, false) } ++} ++ ++fn load_app_local_conpty(path: &Path) -> Result<ConPtyFuncs, String> { ++ let wide_path = wide_string(path.as_os_str()); ++ let module = unsafe { ++ LoadLibraryExW( ++ wide_path.as_ptr(), ++ ptr::null_mut(), ++ LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32, ++ ) ++ }; ++ match unsafe { ConPtyFuncs::from_module(module, true) } { ++ Ok(functions) => Ok(functions), ++ Err(error) => { ++ if !module.is_null() { ++ unsafe { FreeLibrary(module) }; ++ } ++ Err(error) ++ } ++ } ++} ++ ++fn wide_string(value: &OsStr) -> Vec<u16> { ++ value.encode_wide().chain(Some(0)).collect() ++} + + fn load_conpty() -> ConPtyFuncs { + // If the kernel doesn't export these functions then their system is + // too old and we cannot run. +- let kernel = ConPtyFuncs::open(Path::new("kernel32.dll")).expect( +- "this system does not support conpty. Windows 10 October 2018 or newer is required", +- ); ++ let system = load_system_conpty().unwrap_or_else(|error| { ++ panic!( ++ "this system does not support conpty. Windows 10 October 2018 or newer is required: {}", ++ error ++ ) ++ }); ++ ++ if std::env::var("HERDR_WINDOWS_CONPTY").as_deref() == Ok("system") { ++ log::warn!("HERDR_WINDOWS_CONPTY=system disables Herdr's bundled ConPTY"); ++ return system; ++ } ++ ++ match app_local_conpty_path() { ++ Ok(Some(path)) => load_app_local_conpty(&path).unwrap_or_else(|error| { ++ panic!( ++ "failed to load verified app-local ConPTY from {}: {error}; set HERDR_WINDOWS_CONPTY=system to use the Windows system ConPTY", ++ path.display() ++ ) ++ }), ++ Ok(None) => system, ++ Err(error) => panic!( ++ "Herdr's app-local ConPTY bundle is invalid: {}; reinstall Herdr or set HERDR_WINDOWS_CONPTY=system to use the Windows system ConPTY", ++ error ++ ), ++ } ++} ++ ++const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400; ++const CONPTY_FILES: &[(&str, &str)] = &[ ++ ( ++ "conpty.dll", ++ "39fba2713e2495117b1591ae8c32a3b904bea7aa66069cf7815e2844c76d75d8", ++ ), ++ ( ++ "x64/OpenConsole.exe", ++ "b7fd936c2668b87b9ecf7b3366dc6568afc1c6f981874cba3e955a1c35cf8160", ++ ), ++ ( ++ "arm64/OpenConsole.exe", ++ "ed7622fd0d3bedc9ab9f122f5e58edf0def9e7999224f52dd395ba9f54edbe09", ++ ), ++]; ++ ++fn app_local_conpty_path() -> Result<Option<PathBuf>, String> { ++ if std::env::consts::ARCH != "x86_64" { ++ return Ok(None); ++ } ++ let executable = std::env::current_exe() ++ .map_err(|error| format!("cannot locate the Herdr executable: {error}"))?; ++ let executable_dir = executable ++ .parent() ++ .ok_or_else(|| "Herdr executable has no parent directory".to_string())?; ++ let bundle = executable_dir.join("conpty"); ++ if !bundle.exists() { ++ return Ok(None); ++ } ++ reject_reparse_point(&bundle)?; ++ ++ let mut expected = BTreeSet::from(["herdr-conpty.json".to_string()]); ++ for (relative, expected_hash) in CONPTY_FILES { ++ expected.insert((*relative).to_string()); ++ verify_bundle_file(&bundle, relative, expected_hash)?; ++ } ++ let marker = bundle.join("herdr-conpty.json"); ++ verify_regular_file(&marker)?; ++ ++ let actual = bundle_files(&bundle)?; ++ if actual != expected { ++ return Err(format!( ++ "unexpected bundle layout; expected {expected:?}, found {actual:?}" ++ )); ++ } ++ Ok(Some(bundle.join("conpty.dll"))) ++} ++ ++fn verify_bundle_file(root: &Path, relative: &str, expected_hash: &str) -> Result<(), String> { ++ let path = root.join(relative); ++ verify_regular_file(&path)?; ++ let mut file = File::open(&path).map_err(|error| format!("cannot open {}: {error}", path.display()))?; ++ let mut digest = Sha256::new(); ++ let mut buffer = [0; 64 * 1024]; ++ loop { ++ let read = file ++ .read(&mut buffer) ++ .map_err(|error| format!("cannot read {}: {error}", path.display()))?; ++ if read == 0 { ++ break; ++ } ++ digest.update(&buffer[..read]); ++ } ++ let actual_hash = format!("{:x}", digest.finalize()); ++ if actual_hash != expected_hash { ++ return Err(format!( ++ "{} has hash {actual_hash}, expected {expected_hash}", ++ path.display() ++ )); ++ } ++ Ok(()) ++} ++ ++fn verify_regular_file(path: &Path) -> Result<(), String> { ++ let metadata = path ++ .symlink_metadata() ++ .map_err(|error| format!("cannot inspect {}: {error}", path.display()))?; ++ if !metadata.is_file() || metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 { ++ return Err(format!("{} is not a regular file", path.display())); ++ } ++ Ok(()) ++} ++ ++fn reject_reparse_point(path: &Path) -> Result<(), String> { ++ let metadata = path ++ .symlink_metadata() ++ .map_err(|error| format!("cannot inspect {}: {error}", path.display()))?; ++ if !metadata.is_dir() || metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 { ++ return Err(format!("{} is not a regular directory", path.display())); ++ } ++ Ok(()) ++} ++ ++fn bundle_files(root: &Path) -> Result<BTreeSet<String>, String> { ++ fn visit(root: &Path, directory: &Path, files: &mut BTreeSet<String>) -> Result<(), String> { ++ reject_reparse_point(directory)?; ++ for entry in std::fs::read_dir(directory) ++ .map_err(|error| format!("cannot read {}: {error}", directory.display()))? ++ { ++ let entry = entry.map_err(|error| error.to_string())?; ++ let path = entry.path(); ++ let metadata = path ++ .symlink_metadata() ++ .map_err(|error| format!("cannot inspect {}: {error}", path.display()))?; ++ if metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 { ++ return Err(format!("{} is a reparse point", path.display())); ++ } ++ if metadata.is_dir() { ++ visit(root, &path, files)?; ++ } else if metadata.is_file() { ++ let relative = path ++ .strip_prefix(root) ++ .map_err(|error| error.to_string())? ++ .to_string_lossy() ++ .replace('\\', "/"); ++ files.insert(relative); ++ } else { ++ return Err(format!("{} is not a regular file", path.display())); ++ } ++ } ++ Ok(()) ++ } + +- kernel ++ let mut files = BTreeSet::new(); ++ visit(root, root, &mut files)?; ++ Ok(files) + } + + lazy_static! { +-- +2.50.1 diff --git a/vendor/patches/portable-pty/0001-force-system-conpty.patch b/vendor/patches/portable-pty/0001-force-system-conpty.patch deleted file mode 100644 index 6074df57..00000000 --- a/vendor/patches/portable-pty/0001-force-system-conpty.patch +++ /dev/null @@ -1,37 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: herdr maintainers <maintainers@herdr.dev> -Date: Tue, 23 Jun 2026 00:00:00 +0000 -Subject: [PATCH] force system ConPTY - -Herdr should use the ConPTY functions exported by kernel32.dll and must not -load a bare conpty.dll from PATH. portable-pty's sideload probe is intentional -for WezTerm's bundled OpenConsole.exe/conpty.dll pair, but Herdr does not ship -that pair. - -Herdr issue: https://github.com/ogulcancelik/herdr/issues/761 -Vendored base: portable-pty 0.9.0 ---- - vendor/portable-pty/src/win/psuedocon.rs | 9 +-------- - 1 file changed, 1 insertion(+), 8 deletions(-) - -diff --git a/vendor/portable-pty/src/win/psuedocon.rs b/vendor/portable-pty/src/win/psuedocon.rs -index 0000000..0000000 100644 ---- a/vendor/portable-pty/src/win/psuedocon.rs -+++ b/vendor/portable-pty/src/win/psuedocon.rs -@@ -49,14 +49,7 @@ fn load_conpty() -> ConPtyFuncs { - "this system does not support conpty. Windows 10 October 2018 or newer is required", - ); -- -- // We prefer to use a sideloaded conpty.dll and openconsole.exe host deployed -- // alongside the application. We check for this after checking for kernel -- // support so that we don't try to proceed and do something crazy. -- if let Ok(sideloaded) = ConPtyFuncs::open(Path::new("conpty.dll")) { -- sideloaded -- } else { -- kernel -- } -+ -+ kernel - } - - lazy_static! { diff --git a/vendor/portable-pty.patches.md b/vendor/portable-pty.patches.md index 7e6d2743..83976ac5 100644 --- a/vendor/portable-pty.patches.md +++ b/vendor/portable-pty.patches.md @@ -4,15 +4,18 @@ This file tracks intentional local changes applied on top of the vendored `portable-pty` source. Remove a patch only when the upstream crate contains an equivalent fix or exposes an option that lets Herdr keep the same behavior. -## 0001 force system ConPTY +## 0001 control ConPTY loading status: active -patch: `vendor/patches/portable-pty/0001-force-system-conpty.patch` +patch: `vendor/patches/portable-pty/0001-control-conpty-loading.patch` -herdr issue: https://github.com/ogulcancelik/herdr/issues/761 +herdr issues: -upstream discussion: none found +- https://github.com/ogulcancelik/herdr/issues/761 +- https://github.com/ogulcancelik/herdr/issues/1533 + +upstream discussion: https://github.com/microsoft/terminal/issues/17452 upstream pr: none @@ -20,16 +23,23 @@ vendored base: `portable-pty 0.9.0` local files: +- `vendor/portable-pty/Cargo.toml` +- `vendor/portable-pty/Cargo.toml.orig` - `vendor/portable-pty/src/win/psuedocon.rs` -reason: `portable-pty` intentionally probes a bare `conpty.dll` after verifying -that `kernel32.dll` exports the ConPTY API. That is useful for WezTerm's bundled -`OpenConsole.exe` and `conpty.dll` pair, but Herdr does not ship that pair and -must not load another application's `conpty.dll` from `PATH`. +reason: `portable-pty` intentionally probes a bare `conpty.dll` through the DLL +search path. Herdr must never load another application's DLL from `PATH`. The +Windows package ships a pinned Microsoft ConPTY runtime under `conpty/`; Herdr +verifies the exact DLL and x64/ARM64 host hashes, rejects reparse points and +unexpected files, then loads the DLL by absolute path with its dependency search +limited to that directory and System32. Installations without a bundle continue +using the ConPTY exports from the already loaded `kernel32.dll`. Set +`HERDR_WINDOWS_CONPTY=system` to bypass the bundle during compatibility +recovery. -remove when: upstream `portable-pty` no longer loads bare `conpty.dll` from the -DLL search path, upstream exposes a way for consumers to force system ConPTY, or -Herdr replaces the Windows PTY backend. +remove when: upstream `portable-pty` exposes hash-verified app-local and system +ConPTY selection with constrained DLL loading and no bare DLL search, or Herdr +replaces the Windows PTY backend. verification: @@ -37,8 +47,9 @@ verification: python3 -m unittest scripts.test_vendor_portable_pty ``` -On Windows, also verify that pane creation succeeds when `PATH` contains a -directory with `conpty.dll`. +On Windows, run the enhanced-input CI probe against the packaged `conpty/` +bundle. It verifies the exact OpenConsole process path on Server 2022 and 2025, +rejects a tampered bundle, and verifies the explicit system fallback. ## 0002 expose Windows raw command tails diff --git a/vendor/portable-pty/Cargo.toml b/vendor/portable-pty/Cargo.toml index b8d11230..8a33abf9 100644 --- a/vendor/portable-pty/Cargo.toml +++ b/vendor/portable-pty/Cargo.toml @@ -102,8 +102,8 @@ version = "1.3" [target."cfg(windows)".dependencies.lazy_static] version = "1.4" -[target."cfg(windows)".dependencies.shared_library] -version = "0.1" +[target."cfg(windows)".dependencies.sha2] +version = "0.10" [target."cfg(windows)".dependencies.winapi] version = "0.3" @@ -112,6 +112,7 @@ features = [ "consoleapi", "handleapi", "fileapi", + "libloaderapi", "namedpipeapi", "synchapi", ] diff --git a/vendor/portable-pty/Cargo.toml.orig b/vendor/portable-pty/Cargo.toml.orig index 0c3d6b8d..21abe3b6 100644 --- a/vendor/portable-pty/Cargo.toml.orig +++ b/vendor/portable-pty/Cargo.toml.orig @@ -27,12 +27,13 @@ serde_support = ["serde", "serde_derive"] [target."cfg(windows)".dependencies] bitflags = "1.3" lazy_static = "1.4" -shared_library = "0.1" +sha2 = "0.10" winapi = { version = "0.3", features = [ "winuser", "consoleapi", "handleapi", "fileapi", + "libloaderapi", "namedpipeapi", "synchapi", ]} diff --git a/vendor/portable-pty/src/win/psuedocon.rs b/vendor/portable-pty/src/win/psuedocon.rs index 133eab52..21700f70 100644 --- a/vendor/portable-pty/src/win/psuedocon.rs +++ b/vendor/portable-pty/src/win/psuedocon.rs @@ -4,17 +4,24 @@ use crate::win::procthreadattr::ProcThreadAttributeList; use anyhow::{bail, ensure, Error}; use filedescriptor::{FileDescriptor, OwnedHandle}; use lazy_static::lazy_static; -use shared_library::shared_library; -use std::ffi::OsString; -use std::io::Error as IoError; -use std::os::windows::ffi::OsStringExt; +use sha2::{Digest, Sha256}; +use std::collections::BTreeSet; +use std::ffi::{OsStr, OsString}; +use std::fs::File; +use std::io::{Error as IoError, Read}; +use std::os::windows::ffi::{OsStrExt, OsStringExt}; +use std::os::windows::fs::MetadataExt; use std::os::windows::io::{AsRawHandle, FromRawHandle}; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::sync::Mutex; use std::{mem, ptr}; -use winapi::shared::minwindef::DWORD; +use winapi::shared::minwindef::{DWORD, HMODULE}; use winapi::shared::winerror::{HRESULT, S_OK}; use winapi::um::handleapi::*; +use winapi::um::libloaderapi::{ + FreeLibrary, GetModuleHandleW, GetProcAddress, LoadLibraryExW, + LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR, LOAD_LIBRARY_SEARCH_SYSTEM32, +}; use winapi::um::processthreadsapi::*; use winapi::um::winbase::{ CREATE_UNICODE_ENVIRONMENT, EXTENDED_STARTUPINFO_PRESENT, STARTF_USESTDHANDLES, STARTUPINFOEXW, @@ -30,26 +37,249 @@ pub const PSEUDOCONSOLE_WIN32_INPUT_MODE: DWORD = 0x4; #[allow(dead_code)] pub const PSEUDOCONSOLE_PASSTHROUGH_MODE: DWORD = 0x8; -shared_library!(ConPtyFuncs, - pub fn CreatePseudoConsole( - size: COORD, - hInput: HANDLE, - hOutput: HANDLE, - flags: DWORD, - hpc: *mut HPCON - ) -> HRESULT, - pub fn ResizePseudoConsole(hpc: HPCON, size: COORD) -> HRESULT, - pub fn ClosePseudoConsole(hpc: HPCON), -); +type CreatePseudoConsoleFn = unsafe extern "system" fn( + size: COORD, + h_input: HANDLE, + h_output: HANDLE, + flags: DWORD, + hpc: *mut HPCON, +) -> HRESULT; +type ResizePseudoConsoleFn = unsafe extern "system" fn(hpc: HPCON, size: COORD) -> HRESULT; +type ClosePseudoConsoleFn = unsafe extern "system" fn(hpc: HPCON); + +// These fields intentionally mirror the exported Win32 symbol names. +#[allow(non_snake_case)] +struct ConPtyFuncs { + module: HMODULE, + owned: bool, + CreatePseudoConsole: CreatePseudoConsoleFn, + ResizePseudoConsole: ResizePseudoConsoleFn, + ClosePseudoConsole: ClosePseudoConsoleFn, +} + +unsafe impl Send for ConPtyFuncs {} +unsafe impl Sync for ConPtyFuncs {} + +impl Drop for ConPtyFuncs { + fn drop(&mut self) { + if self.owned { + unsafe { FreeLibrary(self.module) }; + } + } +} + +impl ConPtyFuncs { + unsafe fn from_module(module: HMODULE, owned: bool) -> Result<Self, String> { + if module.is_null() { + return Err(IoError::last_os_error().to_string()); + } + Ok(Self { + module, + owned, + CreatePseudoConsole: load_symbol(module, b"CreatePseudoConsole\0")?, + ResizePseudoConsole: load_symbol(module, b"ResizePseudoConsole\0")?, + ClosePseudoConsole: load_symbol(module, b"ClosePseudoConsole\0")?, + }) + } +} + +unsafe fn load_symbol<T: Copy>(module: HMODULE, name: &'static [u8]) -> Result<T, String> { + let symbol = GetProcAddress(module, name.as_ptr().cast()); + if symbol.is_null() { + return Err(format!( + "missing symbol {}", + String::from_utf8_lossy(&name[..name.len() - 1]) + )); + } + Ok(mem::transmute_copy(&symbol)) +} + +fn load_system_conpty() -> Result<ConPtyFuncs, String> { + let kernel_name = wide_string(OsStr::new("kernel32.dll")); + let module = unsafe { GetModuleHandleW(kernel_name.as_ptr()) }; + unsafe { ConPtyFuncs::from_module(module, false) } +} + +fn load_app_local_conpty(path: &Path) -> Result<ConPtyFuncs, String> { + let wide_path = wide_string(path.as_os_str()); + let module = unsafe { + LoadLibraryExW( + wide_path.as_ptr(), + ptr::null_mut(), + LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32, + ) + }; + match unsafe { ConPtyFuncs::from_module(module, true) } { + Ok(functions) => Ok(functions), + Err(error) => { + if !module.is_null() { + unsafe { FreeLibrary(module) }; + } + Err(error) + } + } +} + +fn wide_string(value: &OsStr) -> Vec<u16> { + value.encode_wide().chain(Some(0)).collect() +} fn load_conpty() -> ConPtyFuncs { // If the kernel doesn't export these functions then their system is // too old and we cannot run. - let kernel = ConPtyFuncs::open(Path::new("kernel32.dll")).expect( - "this system does not support conpty. Windows 10 October 2018 or newer is required", - ); + let system = load_system_conpty().unwrap_or_else(|error| { + panic!( + "this system does not support conpty. Windows 10 October 2018 or newer is required: {}", + error + ) + }); + + if std::env::var("HERDR_WINDOWS_CONPTY").as_deref() == Ok("system") { + log::warn!("HERDR_WINDOWS_CONPTY=system disables Herdr's bundled ConPTY"); + return system; + } + + match app_local_conpty_path() { + Ok(Some(path)) => load_app_local_conpty(&path).unwrap_or_else(|error| { + panic!( + "failed to load verified app-local ConPTY from {}: {error}; set HERDR_WINDOWS_CONPTY=system to use the Windows system ConPTY", + path.display() + ) + }), + Ok(None) => system, + Err(error) => panic!( + "Herdr's app-local ConPTY bundle is invalid: {}; reinstall Herdr or set HERDR_WINDOWS_CONPTY=system to use the Windows system ConPTY", + error + ), + } +} + +const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400; +const CONPTY_FILES: &[(&str, &str)] = &[ + ( + "conpty.dll", + "39fba2713e2495117b1591ae8c32a3b904bea7aa66069cf7815e2844c76d75d8", + ), + ( + "x64/OpenConsole.exe", + "b7fd936c2668b87b9ecf7b3366dc6568afc1c6f981874cba3e955a1c35cf8160", + ), + ( + "arm64/OpenConsole.exe", + "ed7622fd0d3bedc9ab9f122f5e58edf0def9e7999224f52dd395ba9f54edbe09", + ), +]; + +fn app_local_conpty_path() -> Result<Option<PathBuf>, String> { + if std::env::consts::ARCH != "x86_64" { + return Ok(None); + } + let executable = std::env::current_exe() + .map_err(|error| format!("cannot locate the Herdr executable: {error}"))?; + let executable_dir = executable + .parent() + .ok_or_else(|| "Herdr executable has no parent directory".to_string())?; + let bundle = executable_dir.join("conpty"); + if !bundle.exists() { + return Ok(None); + } + reject_reparse_point(&bundle)?; + + let mut expected = BTreeSet::from(["herdr-conpty.json".to_string()]); + for (relative, expected_hash) in CONPTY_FILES { + expected.insert((*relative).to_string()); + verify_bundle_file(&bundle, relative, expected_hash)?; + } + let marker = bundle.join("herdr-conpty.json"); + verify_regular_file(&marker)?; + + let actual = bundle_files(&bundle)?; + if actual != expected { + return Err(format!( + "unexpected bundle layout; expected {expected:?}, found {actual:?}" + )); + } + Ok(Some(bundle.join("conpty.dll"))) +} + +fn verify_bundle_file(root: &Path, relative: &str, expected_hash: &str) -> Result<(), String> { + let path = root.join(relative); + verify_regular_file(&path)?; + let mut file = File::open(&path).map_err(|error| format!("cannot open {}: {error}", path.display()))?; + let mut digest = Sha256::new(); + let mut buffer = [0; 64 * 1024]; + loop { + let read = file + .read(&mut buffer) + .map_err(|error| format!("cannot read {}: {error}", path.display()))?; + if read == 0 { + break; + } + digest.update(&buffer[..read]); + } + let actual_hash = format!("{:x}", digest.finalize()); + if actual_hash != expected_hash { + return Err(format!( + "{} has hash {actual_hash}, expected {expected_hash}", + path.display() + )); + } + Ok(()) +} + +fn verify_regular_file(path: &Path) -> Result<(), String> { + let metadata = path + .symlink_metadata() + .map_err(|error| format!("cannot inspect {}: {error}", path.display()))?; + if !metadata.is_file() || metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 { + return Err(format!("{} is not a regular file", path.display())); + } + Ok(()) +} + +fn reject_reparse_point(path: &Path) -> Result<(), String> { + let metadata = path + .symlink_metadata() + .map_err(|error| format!("cannot inspect {}: {error}", path.display()))?; + if !metadata.is_dir() || metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 { + return Err(format!("{} is not a regular directory", path.display())); + } + Ok(()) +} + +fn bundle_files(root: &Path) -> Result<BTreeSet<String>, String> { + fn visit(root: &Path, directory: &Path, files: &mut BTreeSet<String>) -> Result<(), String> { + reject_reparse_point(directory)?; + for entry in std::fs::read_dir(directory) + .map_err(|error| format!("cannot read {}: {error}", directory.display()))? + { + let entry = entry.map_err(|error| error.to_string())?; + let path = entry.path(); + let metadata = path + .symlink_metadata() + .map_err(|error| format!("cannot inspect {}: {error}", path.display()))?; + if metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 { + return Err(format!("{} is a reparse point", path.display())); + } + if metadata.is_dir() { + visit(root, &path, files)?; + } else if metadata.is_file() { + let relative = path + .strip_prefix(root) + .map_err(|error| error.to_string())? + .to_string_lossy() + .replace('\\', "/"); + files.insert(relative); + } else { + return Err(format!("{} is not a regular file", path.display())); + } + } + Ok(()) + } - kernel + let mut files = BTreeSet::new(); + visit(root, root, &mut files)?; + Ok(files) } lazy_static! { |
