aboutsummaryrefslogtreecommitdiff
path: root/vendor
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-07-25 02:40:14 +0300
committerGitHub <noreply@github.com>2026-07-25 02:40:14 +0300
commit8afd52ae57f9237f4f46138ad224dfc621d7ba59 (patch)
tree349bbbd86c57863539ebc4c2eacf34550e076fa2 /vendor
parent5f1957c27bd23f73b339265d3acd32f4a21878a3 (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.patch357
-rw-r--r--vendor/patches/portable-pty/0001-force-system-conpty.patch37
-rw-r--r--vendor/portable-pty.patches.md37
-rw-r--r--vendor/portable-pty/Cargo.toml5
-rw-r--r--vendor/portable-pty/Cargo.toml.orig3
-rw-r--r--vendor/portable-pty/src/win/psuedocon.rs272
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! {