Harden Windows control socket rendezvous (#42326)

## Why

Windows control sockets need a rendezvous directory that cannot be accessed or
replaced by another user while the listener is active.

## What changed

- Create socket directories with a protected, inheritable, current-user-only
  DACL, and reject existing directories with broader permissions.
- Validate socket paths without following a junction at the rendezvous
  directory, and pin the directory through listener cleanup to prevent
  replacement after validation.
- Add a Windows peer check that verifies the kernel-reported peer process uses
  the current user's non-elevated token.
- Document the Windows requirements for custom control socket paths.

## Testing

Add Windows coverage for ACL inheritance and rejection, junction and volume-root
rejection, extended-length paths, directory pinning, and elevated peer rejection.

GitOrigin-RevId: cc8dd76e5732c288f6346cfd6138051188533bfa
This commit is contained in:
Eric Traut authored and copyberry committed 2026-09-02 18:42:06 +00:00
1 parent e6ff749506
commit 73e94ee7a6
11 files changed
+642 -5

No files matched your search

+1
View File
@@ -4622,6 +4622,7 @@ dependencies = [
"tokio", "tokio",
"tokio-util", "tokio-util",
"uds_windows", "uds_windows",
"windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -1,3 +1,5 @@
//! Control socket startup, guarded rendezvous paths, and WebSocket acceptance.
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::io::ErrorKind; use std::io::ErrorKind;
use std::io::Result as IoResult; use std::io::Result as IoResult;
@@ -26,9 +28,21 @@ pub async fn start_control_socket_acceptor(
transport_event_tx: mpsc::Sender<TransportEvent>, transport_event_tx: mpsc::Sender<TransportEvent>,
shutdown_token: CancellationToken, shutdown_token: CancellationToken,
) -> IoResult<JoinHandle<()>> { ) -> IoResult<JoinHandle<()>> {
#[cfg(windows)]
let (socket_path, directory_guard) = {
if let Some(parent) = socket_path.as_path().parent() {
codex_uds::prepare_private_socket_directory(parent).await?;
}
let (path, guard) = codex_uds::validate_private_socket_path(socket_path.as_path())?;
(AbsolutePathBuf::from_absolute_path_checked(path)?, guard)
};
prepare_control_socket_path(socket_path.as_path()).await?; prepare_control_socket_path(socket_path.as_path()).await?;
let listener = UnixListener::bind(socket_path.as_path()).await?; let listener = UnixListener::bind(socket_path.as_path()).await?;
let socket_guard = ControlSocketFileGuard { socket_path }; let socket_guard = ControlSocketFileGuard {
socket_path,
#[cfg(windows)]
_directory_guard: directory_guard,
};
set_control_socket_permissions(socket_guard.socket_path.as_path()).await?; set_control_socket_permissions(socket_guard.socket_path.as_path()).await?;
info!( info!(
socket_path = %socket_guard.socket_path.display(), socket_path = %socket_guard.socket_path.display(),
@@ -95,6 +109,13 @@ pub async fn prepare_control_socket_path(socket_path: &Path) -> IoResult<()> {
codex_uds::prepare_private_socket_directory(parent).await?; codex_uds::prepare_private_socket_directory(parent).await?;
} }
#[cfg(windows)]
let (socket_path, _directory_guard) = codex_uds::validate_private_socket_path(socket_path)?;
#[cfg(windows)]
let socket_path = AbsolutePathBuf::from_absolute_path_checked(socket_path)?;
#[cfg(windows)]
let socket_path = socket_path.as_path();
match UnixStream::connect(socket_path).await { match UnixStream::connect(socket_path).await {
Ok(_stream) => { Ok(_stream) => {
return Err(std::io::Error::new( return Err(std::io::Error::new(
@@ -173,6 +194,9 @@ async fn set_control_socket_permissions(_socket_path: &Path) -> IoResult<()> {
struct ControlSocketFileGuard { struct ControlSocketFileGuard {
socket_path: AbsolutePathBuf, socket_path: AbsolutePathBuf,
// Keep the directory pinned until after the socket file is removed in Drop.
#[cfg(windows)]
_directory_guard: std::os::windows::io::OwnedHandle,
} }
impl Drop for ControlSocketFileGuard { impl Drop for ControlSocketFileGuard {
@@ -190,6 +190,24 @@ async fn control_socket_file_is_private_after_bind() {
accept_handle.await.expect("acceptor should join"); accept_handle.await.expect("acceptor should join");
} }
#[cfg(windows)]
#[tokio::test]
async fn control_socket_pins_directory_until_shutdown() {
let temp_dir = tempfile::TempDir::new().expect("temp dir");
let socket_path = test_socket_path(temp_dir.path());
let directory = socket_path.as_path().parent().unwrap();
let moved = temp_dir.path().join("moved");
let (tx, _rx) = mpsc::channel::<TransportEvent>(CHANNEL_CAPACITY);
let shutdown = CancellationToken::new();
let acceptor = start_control_socket_acceptor(socket_path.clone(), tx, shutdown.clone())
.await
.expect("acceptor");
assert!(std::fs::rename(directory, &moved).is_err());
shutdown.cancel();
acceptor.await.expect("shutdown");
std::fs::rename(directory, moved).expect("directory unpinned after cleanup");
}
fn absolute_path(path: &str) -> AbsolutePathBuf { fn absolute_path(path: &str) -> AbsolutePathBuf {
AbsolutePathBuf::from_absolute_path(path).expect("absolute path") AbsolutePathBuf::from_absolute_path(path).expect("absolute path")
} }
+5
View File
@@ -43,6 +43,11 @@ opens exactly one raw stream connection to `$CODEX_HOME/app-server-control/app-s
by default, or to `--sock PATH` when provided, and proxies bytes between that socket and stdin/stdout. by default, or to `--sock PATH` when provided, and proxies bytes between that socket and stdin/stdout.
The proxied stream carries the websocket HTTP Upgrade handshake followed by websocket frames. The proxied stream carries the websocket HTTP Upgrade handshake followed by websocket frames.
On Windows, the socket directory is created with a protected current-user-only DACL. Existing
directories must already have that owner and DACL; startup rejects broader permissions rather
than attempting to repair previously exposed state. Custom sockets should use a new dedicated
subdirectory. The listener pins the validated directory until socket cleanup completes.
Tracing/log output: Tracing/log output:
- `RUST_LOG` controls log filtering/verbosity. - `RUST_LOG` controls log filtering/verbosity.
@@ -213,7 +213,10 @@ async fn explicit_remote_control_startup_fails_when_disabled_by_requirements() -
"allow_remote_control = false\n", "allow_remote_control = false\n",
)?; )?;
let managed_config_path = codex_home.path().join("managed_config.toml"); let managed_config_path = codex_home.path().join("managed_config.toml");
let socket_path = codex_home.path().join("app-server.sock"); let socket_path = codex_home
.path()
.join("app-server-control")
.join("app-server.sock");
let transport = let transport =
AppServerTransport::from_listen_url(&format!("unix://{}", socket_path.display()))?; AppServerTransport::from_listen_url(&format!("unix://{}", socket_path.display()))?;
let _codex_home_guard = EnvVarGuard::set("CODEX_HOME", codex_home.path().as_os_str()); let _codex_home_guard = EnvVarGuard::set("CODEX_HOME", codex_home.path().as_os_str());
+1
View File
@@ -19,6 +19,7 @@ tokio = { workspace = true, features = ["fs", "net", "rt"] }
async-io = { workspace = true } async-io = { workspace = true }
tokio-util = { workspace = true, features = ["compat"] } tokio-util = { workspace = true, features = ["compat"] }
uds_windows = { workspace = true } uds_windows = { workspace = true }
windows-sys = { version = "0.52", features = ["Win32_Foundation", "Win32_Security", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Threading", "Win32_System_SystemServices", "Win32_Networking_WinSock", "Win32_System_IO"] }
[dev-dependencies] [dev-dependencies]
pretty_assertions = { workspace = true } pretty_assertions = { workspace = true }
+33 -3
View File
@@ -10,8 +10,10 @@ use tokio::io::AsyncRead;
use tokio::io::AsyncWrite; use tokio::io::AsyncWrite;
use tokio::io::ReadBuf; use tokio::io::ReadBuf;
/// Creates `socket_dir` if needed and restricts it to the current user where /// Creates `socket_dir` if needed and restricts it to the current user as
/// the platform exposes Unix permissions. /// supported by the platform (0700 on Unix, a protected user-only DACL on Windows).
/// Windows rejects existing directories unless they already have that owner and DACL;
/// changing permissions cannot revoke access held through preexisting handles.
pub async fn prepare_private_socket_directory(socket_dir: impl AsRef<Path>) -> IoResult<()> { pub async fn prepare_private_socket_directory(socket_dir: impl AsRef<Path>) -> IoResult<()> {
platform::prepare_private_socket_directory(socket_dir.as_ref()).await platform::prepare_private_socket_directory(socket_dir.as_ref()).await
} }
@@ -50,6 +52,13 @@ pub struct UnixStream {
} }
impl UnixStream { impl UnixStream {
/// Requires the Windows peer to belong to the current user, with neither
/// process elevated. Call before sending any application data.
#[cfg(windows)]
pub fn ensure_non_elevated_peer(&self) -> IoResult<()> {
self.inner.ensure_non_elevated_peer()
}
/// Connects to `socket_path`. /// Connects to `socket_path`.
pub async fn connect(socket_path: impl AsRef<Path>) -> IoResult<Self> { pub async fn connect(socket_path: impl AsRef<Path>) -> IoResult<Self> {
platform::connect_stream(socket_path.as_ref()) platform::connect_stream(socket_path.as_ref())
@@ -184,8 +193,18 @@ mod platform {
pub(super) struct Stream(Compat<Async<WindowsUnixStream>>); pub(super) struct Stream(Compat<Async<WindowsUnixStream>>);
impl Stream {
pub(super) fn ensure_non_elevated_peer(&self) -> IoResult<()> {
crate::windows_peer::ensure_non_elevated_peer(
self.0.get_ref().get_ref().as_raw_socket(),
)
}
}
pub(super) async fn prepare_private_socket_directory(socket_dir: &Path) -> IoResult<()> { pub(super) async fn prepare_private_socket_directory(socket_dir: &Path) -> IoResult<()> {
tokio::fs::create_dir_all(socket_dir).await let socket_dir = socket_dir.to_path_buf();
spawn_blocking_io(move || crate::windows_security::prepare_private_directory(&socket_dir))
.await
} }
pub(super) struct Listener(Async<WindowsUnixListener>); pub(super) struct Listener(Async<WindowsUnixListener>);
@@ -329,3 +348,14 @@ mod platform {
#[cfg(test)] #[cfg(test)]
mod lib_tests; mod lib_tests;
#[cfg(windows)]
mod windows_security;
#[cfg(windows)]
mod windows_socket_validation;
#[cfg(windows)]
pub use windows_socket_validation::validate_private_socket_path;
#[cfg(windows)]
mod windows_peer;
+186
View File
@@ -6,6 +6,168 @@ use tokio::io::AsyncWriteExt;
use super::*; use super::*;
#[cfg(windows)]
#[tokio::test]
async fn private_directory_rejects_volume_roots() {
// A nonexistent volume keeps this regression safe even if root validation breaks.
let root = Path::new(r"\\?\Volume{00000000-0000-0000-0000-000000000000}\");
assert_eq!(
prepare_private_socket_directory(root)
.await
.unwrap_err()
.kind(),
ErrorKind::InvalidInput,
);
}
#[cfg(windows)]
#[tokio::test]
async fn private_directory_rejects_junctions() {
let temp = tempfile::TempDir::new().expect("temp directory");
let target = temp.path().join("target");
let junction = temp.path().join("junction");
std::fs::create_dir(&target).expect("target");
let output = std::process::Command::new("powershell.exe")
.args(["-NoProfile", "-NonInteractive", "-Command", "$ErrorActionPreference = 'Stop'; New-Item -ItemType Junction -Path $env:CODEX_TEST_LINK -Target $env:CODEX_TEST_TARGET | Out-Null"])
.env("CODEX_TEST_LINK", &junction)
.env("CODEX_TEST_TARGET", &target)
.output().expect("create junction");
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
prepare_private_socket_directory(&junction)
.await
.unwrap_err()
.kind(),
ErrorKind::PermissionDenied,
);
assert_eq!(
validate_private_socket_path(&junction.join("socket"))
.unwrap_err()
.kind(),
ErrorKind::PermissionDenied,
);
}
#[cfg(windows)]
#[tokio::test]
async fn socket_validation_pins_private_directory_without_creating_missing_paths() {
let temp = tempfile::TempDir::new().expect("temp directory");
let directory = temp.path().join("private");
let socket_path = directory.join("socket");
assert!(validate_private_socket_path(&socket_path).is_err());
assert!(!directory.exists());
prepare_private_socket_directory(&directory).await.unwrap();
let (_validated_path, _guard) = validate_private_socket_path(&socket_path).unwrap();
assert!(std::fs::rename(&directory, temp.path().join("moved")).is_err());
}
#[cfg(windows)]
#[tokio::test]
async fn socket_validation_rejects_broad_acl_without_repairing_it() {
let temp = tempfile::TempDir::new().expect("temp directory");
let directory = temp.path().join("private");
prepare_private_socket_directory(&directory).await.unwrap();
let inspect = |script: &str| {
let output = std::process::Command::new("powershell.exe")
.args(["-NoProfile", "-NonInteractive", "-Command", script])
.env("CODEX_TEST_DIRECTORY", &directory)
.output()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
output.stdout
};
let before = inspect(
r#"
$ErrorActionPreference = 'Stop'
$acl = Get-Acl -LiteralPath $env:CODEX_TEST_DIRECTORY
$everyone = [System.Security.Principal.SecurityIdentifier]::new('S-1-1-0')
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new($everyone, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')
$acl.AddAccessRule($rule)
Set-Acl -LiteralPath $env:CODEX_TEST_DIRECTORY -AclObject $acl
(Get-Acl -LiteralPath $env:CODEX_TEST_DIRECTORY).Sddl
"#,
);
assert_eq!(
validate_private_socket_path(&directory.join("socket"))
.unwrap_err()
.kind(),
ErrorKind::PermissionDenied,
);
assert_eq!(
prepare_private_socket_directory(&directory)
.await
.unwrap_err()
.kind(),
ErrorKind::PermissionDenied,
);
assert_eq!(
before,
inspect(
"$ErrorActionPreference = 'Stop'; (Get-Acl -LiteralPath $env:CODEX_TEST_DIRECTORY).Sddl"
)
);
}
#[cfg(windows)]
#[tokio::test]
async fn private_directory_supports_extended_length_paths() {
let temp = tempfile::TempDir::new().expect("temp directory");
let directory = temp.path().join("a".repeat(150)).join("b".repeat(150));
prepare_private_socket_directory(&directory)
.await
.expect("create long private directory");
prepare_private_socket_directory(&directory)
.await
.expect("validate long private directory");
assert!(directory.is_dir());
}
#[cfg(windows)]
#[tokio::test]
async fn private_directory_acl_is_user_only_and_inherited_by_files() {
let temp = tempfile::TempDir::new().expect("temp directory");
let directory = temp.path().join("private");
// Cover reusing an existing private directory as well as inheriting its DACL.
prepare_private_socket_directory(&directory)
.await
.expect("create user-owned directory");
prepare_private_socket_directory(&directory)
.await
.expect("private directory");
std::fs::write(directory.join("child"), b"").expect("child file");
let _listener = UnixListener::bind(directory.join("socket"))
.await
.expect("private socket");
let output = std::process::Command::new("powershell.exe")
.args(["-NoProfile", "-NonInteractive", "-Command", r#"
$ErrorActionPreference = 'Stop'
$sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value
$directory = Get-Acl -LiteralPath $env:CODEX_TEST_PRIVATE_DIRECTORY
if (-not $directory.AreAccessRulesProtected) { throw 'directory inherits broad access' }
foreach ($acl in @($directory, (Get-Acl -LiteralPath (Join-Path $env:CODEX_TEST_PRIVATE_DIRECTORY 'child')), (Get-Acl -LiteralPath (Join-Path $env:CODEX_TEST_PRIVATE_DIRECTORY 'socket')))) {
$rules = @($acl.Access)
if ($rules.Count -ne 1) { throw 'unexpected access rules' }
if ($rules[0].IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value -ne $sid) { throw 'wrong user' }
if ($rules[0].AccessControlType -ne 'Allow') { throw 'user is denied access' }
}
"#])
.env("CODEX_TEST_PRIVATE_DIRECTORY", &directory)
.output().expect("inspect ACLs");
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
}
#[tokio::test] #[tokio::test]
async fn prepare_private_socket_directory_creates_directory() { async fn prepare_private_socket_directory_creates_directory() {
let temp_dir = tempfile::TempDir::new().expect("temp dir"); let temp_dir = tempfile::TempDir::new().expect("temp dir");
@@ -119,3 +281,27 @@ async fn stream_round_trips_data_between_listener_and_client() {
server_task.await.expect("server task should join"); server_task.await.expect("server task should join");
} }
#[cfg(windows)]
#[tokio::test]
async fn implicit_peer_validation_rejects_elevated_listener() {
let temp = tempfile::TempDir::new().unwrap();
let path = temp.path().join("socket");
let mut listener = UnixListener::bind(&path).await.unwrap();
let client = UnixStream::connect(&path).await.unwrap();
let _server = listener.accept().await.unwrap();
// PowerShell inherits our token and supplies an independent elevation oracle.
let output = std::process::Command::new("powershell.exe")
.args(["-NoProfile", "-NonInteractive", "-Command", "([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)"])
.output().unwrap();
assert!(output.status.success());
let expected = match String::from_utf8(output.stdout).unwrap().trim() {
"True" => Err(ErrorKind::PermissionDenied),
"False" => Ok(()),
other => panic!("unexpected elevation result: {other}"),
};
assert_eq!(
client.ensure_non_elevated_peer().map_err(|err| err.kind()),
expected
);
}
+103
View File
@@ -0,0 +1,103 @@
//! Authenticate implicitly discovered Windows socket peers before sending data.
//! The socket's kernel-reported PID, rather than a PID file, identifies the token.
use std::io;
use std::os::windows::io::FromRawHandle;
use std::os::windows::io::OwnedHandle;
use std::os::windows::io::RawSocket;
use std::ptr;
use windows_sys::Win32::Networking::WinSock::SIO_AF_UNIX_GETPEERPID;
use windows_sys::Win32::Networking::WinSock::SOCKET_ERROR;
use windows_sys::Win32::Networking::WinSock::WSAGetLastError;
use windows_sys::Win32::Networking::WinSock::WSAIoctl;
use windows_sys::Win32::Security::EqualSid;
use windows_sys::Win32::Security::GetTokenInformation;
use windows_sys::Win32::Security::TOKEN_ELEVATION;
use windows_sys::Win32::Security::TOKEN_QUERY;
use windows_sys::Win32::Security::TOKEN_USER;
use windows_sys::Win32::Security::TokenElevation;
use windows_sys::Win32::System::Threading::GetCurrentProcess;
use windows_sys::Win32::System::Threading::OpenProcess;
use windows_sys::Win32::System::Threading::OpenProcessToken;
use windows_sys::Win32::System::Threading::PROCESS_QUERY_LIMITED_INFORMATION;
pub(super) fn ensure_non_elevated_peer(socket: RawSocket) -> io::Result<()> {
let mut pid = 0u32;
let mut returned = 0;
if unsafe {
WSAIoctl(
socket as _,
SIO_AF_UNIX_GETPEERPID,
ptr::null(),
/*cbinbuffer*/ 0,
ptr::addr_of_mut!(pid).cast(),
std::mem::size_of_val(&pid) as u32,
&mut returned,
ptr::null_mut(),
/*lpcompletionroutine*/ None,
)
} == SOCKET_ERROR
{
return Err(io::Error::from_raw_os_error(unsafe { WSAGetLastError() }));
}
// Older Windows versions can return a valid PID with zero bytes reported.
if pid == 0 {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"socket peer PID unavailable",
));
}
let process = unsafe {
OpenProcess(
PROCESS_QUERY_LIMITED_INFORMATION,
/*binherithandle*/ 0,
pid,
)
};
if process == 0 {
return Err(io::Error::last_os_error());
}
let _process = unsafe { OwnedHandle::from_raw_handle(process as _) };
let mut token = 0;
if unsafe { OpenProcessToken(process, TOKEN_QUERY, &mut token) } == 0 {
return Err(io::Error::last_os_error());
}
let _token = unsafe { OwnedHandle::from_raw_handle(token as _) };
let mut current_token = 0;
if unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut current_token) } == 0 {
return Err(io::Error::last_os_error());
}
let _current_token = unsafe { OwnedHandle::from_raw_handle(current_token as _) };
let current_user = crate::windows_security::token_user(current_token)?;
let peer_user = crate::windows_security::token_user(token)?;
let same_user = unsafe {
EqualSid(
(*(current_user.as_ptr().cast::<TOKEN_USER>())).User.Sid,
(*(peer_user.as_ptr().cast::<TOKEN_USER>())).User.Sid,
) != 0
};
// Implicit reuse must not change either side's elevation context.
for token in [current_token, token] {
let mut elevation: TOKEN_ELEVATION = unsafe { std::mem::zeroed() };
if unsafe {
GetTokenInformation(
token,
TokenElevation,
ptr::addr_of_mut!(elevation).cast(),
std::mem::size_of_val(&elevation) as u32,
&mut returned,
)
} == 0
{
return Err(io::Error::last_os_error());
}
if !same_user || elevation.TokenIsElevated != 0 {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"implicit daemon connection requires non-elevated current-user tokens",
));
}
}
Ok(())
}
+128
View File
@@ -0,0 +1,128 @@
//! Protect Windows socket rendezvous directories with an inheritable user-only
//! DACL at creation time. Reject unsafe existing directories instead of repairing them.
use std::io;
use std::os::windows::ffi::OsStrExt;
use std::os::windows::io::FromRawHandle;
use std::os::windows::io::OwnedHandle;
use std::path::Path;
use std::ptr;
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Foundation::LocalFree;
use windows_sys::Win32::Security::Authorization::ConvertSidToStringSidW;
use windows_sys::Win32::Security::Authorization::ConvertStringSecurityDescriptorToSecurityDescriptorW;
use windows_sys::Win32::Security::GetTokenInformation;
use windows_sys::Win32::Security::SECURITY_ATTRIBUTES;
use windows_sys::Win32::Security::TOKEN_QUERY;
use windows_sys::Win32::Security::TOKEN_USER;
use windows_sys::Win32::Security::TokenUser;
use windows_sys::Win32::Storage::FileSystem::CreateDirectoryW;
use windows_sys::Win32::System::Threading::GetCurrentProcess;
use windows_sys::Win32::System::Threading::OpenProcessToken;
pub(super) fn prepare_private_directory(path: &Path) -> io::Result<()> {
let user = current_user()?;
let user_sid = unsafe { (*(user.as_ptr().cast::<TOKEN_USER>())).User.Sid };
let mut sid_string = ptr::null_mut();
if unsafe { ConvertSidToStringSidW(user_sid, &mut sid_string) } == 0 {
return Err(io::Error::last_os_error());
}
let mut sid_length = 0;
while unsafe { *sid_string.add(sid_length) } != 0 {
sid_length += 1;
}
let sid =
String::from_utf16_lossy(unsafe { std::slice::from_raw_parts(sid_string, sid_length) });
unsafe { LocalFree(sid_string as _) };
let sddl: Vec<u16> = format!("O:{sid}D:P(A;OICI;FA;;;{sid})")
.encode_utf16()
.chain(Some(0))
.collect();
let mut descriptor = ptr::null_mut();
if unsafe {
ConvertStringSecurityDescriptorToSecurityDescriptorW(
sddl.as_ptr(),
/*stringsdrevision*/ 1,
&mut descriptor,
ptr::null_mut(),
)
} == 0
{
return Err(io::Error::last_os_error());
}
let result = (|| {
// Canonicalize only the parent: this supplies Win32's extended-length
// prefix without following a reparse point at the directory being secured.
let absolute = std::path::absolute(path)?;
let path = match (absolute.parent(), absolute.file_name()) {
(Some(parent), Some(name)) => {
std::fs::create_dir_all(parent)?;
std::fs::canonicalize(parent)?.join(name)
}
_ => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"private directory must not be a filesystem root",
));
}
};
let path_wide: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
let attributes = SECURITY_ATTRIBUTES {
nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
lpSecurityDescriptor: descriptor,
bInheritHandle: 0,
};
if unsafe { CreateDirectoryW(path_wide.as_ptr(), &attributes) } != 0 {
return Ok(());
}
let err = io::Error::last_os_error();
if err.kind() != io::ErrorKind::AlreadyExists {
return Err(err);
}
// Changing a DACL cannot revoke handles already held by another user.
// Existing directories must already meet our private-directory contract.
let _guard = crate::windows_socket_validation::validate_private_directory(&path)?;
Ok(())
})();
unsafe { LocalFree(descriptor as _) };
result
}
// Keep the pointer-aligned buffer alive while using its TOKEN_USER SID.
pub(super) fn current_user() -> io::Result<Vec<usize>> {
// Token information contains pointers, so allocate pointer-aligned storage.
let mut token = 0;
if unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) } == 0 {
return Err(io::Error::last_os_error());
}
let _token = unsafe { OwnedHandle::from_raw_handle(token as _) };
token_user(token)
}
pub(super) fn token_user(token: HANDLE) -> io::Result<Vec<usize>> {
let mut length = 0;
unsafe {
GetTokenInformation(
token,
TokenUser,
ptr::null_mut(),
/*tokeninformationlength*/ 0,
&mut length,
)
};
let mut user = vec![0usize; (length as usize).div_ceil(std::mem::size_of::<usize>())];
if unsafe {
GetTokenInformation(
token,
TokenUser,
user.as_mut_ptr().cast(),
length,
&mut length,
)
} == 0
{
return Err(io::Error::last_os_error());
}
Ok(user)
}
@@ -0,0 +1,138 @@
//! Read-only validation of Windows daemon rendezvous directories. Pin the
//! validated directory while connecting; never repair an untrusted listener's ACL.
use std::io;
use std::os::windows::ffi::OsStrExt;
use std::os::windows::io::FromRawHandle;
use std::os::windows::io::OwnedHandle;
use std::path::Path;
use std::path::PathBuf;
use std::ptr;
use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
use windows_sys::Win32::Foundation::LocalFree;
use windows_sys::Win32::Security::ACCESS_ALLOWED_ACE;
use windows_sys::Win32::Security::ACE_HEADER;
use windows_sys::Win32::Security::Authorization::GetSecurityInfo;
use windows_sys::Win32::Security::Authorization::SE_FILE_OBJECT;
use windows_sys::Win32::Security::CONTAINER_INHERIT_ACE;
use windows_sys::Win32::Security::DACL_SECURITY_INFORMATION;
use windows_sys::Win32::Security::EqualSid;
use windows_sys::Win32::Security::GetAce;
use windows_sys::Win32::Security::GetSecurityDescriptorControl;
use windows_sys::Win32::Security::OBJECT_INHERIT_ACE;
use windows_sys::Win32::Security::OWNER_SECURITY_INFORMATION;
use windows_sys::Win32::Security::SE_DACL_PROTECTED;
use windows_sys::Win32::Security::TOKEN_USER;
use windows_sys::Win32::Storage::FileSystem::BY_HANDLE_FILE_INFORMATION;
use windows_sys::Win32::Storage::FileSystem::CreateFileW;
use windows_sys::Win32::Storage::FileSystem::FILE_ALL_ACCESS;
use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_DIRECTORY;
use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT;
use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS;
use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_OPEN_REPARSE_POINT;
use windows_sys::Win32::Storage::FileSystem::FILE_LIST_DIRECTORY;
use windows_sys::Win32::Storage::FileSystem::FILE_READ_ATTRIBUTES;
use windows_sys::Win32::Storage::FileSystem::FILE_SHARE_READ;
use windows_sys::Win32::Storage::FileSystem::FILE_SHARE_WRITE;
use windows_sys::Win32::Storage::FileSystem::GetFileInformationByHandle;
use windows_sys::Win32::Storage::FileSystem::OPEN_EXISTING;
use windows_sys::Win32::Storage::FileSystem::READ_CONTROL;
use windows_sys::Win32::System::SystemServices::ACCESS_ALLOWED_ACE_TYPE;
/// Validates an existing daemon socket directory without creating or changing it.
/// Connect using the returned path and retain the handle through connection setup
/// to prevent replacement of the validated directory. Ancestor junctions are
/// resolved before validation; the socket directory itself must not be a reparse point.
pub fn validate_private_socket_path(socket_path: &Path) -> io::Result<(PathBuf, OwnedHandle)> {
let absolute = std::path::absolute(socket_path)?;
let directory = absolute.parent().ok_or(io::ErrorKind::InvalidInput)?;
let socket_name = absolute.file_name().ok_or(io::ErrorKind::InvalidInput)?;
let (directory, guard) = validate_private_directory(directory)?;
Ok((directory.join(socket_name), guard))
}
pub(super) fn validate_private_directory(directory: &Path) -> io::Result<(PathBuf, OwnedHandle)> {
let parent = directory.parent().ok_or(io::ErrorKind::InvalidInput)?;
let directory_name = directory.file_name().ok_or(io::ErrorKind::InvalidInput)?;
let directory = std::fs::canonicalize(parent)?.join(directory_name);
let path_wide: Vec<u16> = directory.as_os_str().encode_wide().chain(Some(0)).collect();
// Metadata-only access does not enforce sharing restrictions on Windows.
// Directory listing access makes omission of FILE_SHARE_DELETE pin the path.
let handle = unsafe {
CreateFileW(
path_wide.as_ptr(),
READ_CONTROL | FILE_READ_ATTRIBUTES | FILE_LIST_DIRECTORY,
FILE_SHARE_READ | FILE_SHARE_WRITE,
ptr::null(),
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
/*htemplatefile*/ 0,
)
};
if handle == INVALID_HANDLE_VALUE {
return Err(io::Error::last_os_error());
}
let guard = unsafe { OwnedHandle::from_raw_handle(handle as _) };
let mut info: BY_HANDLE_FILE_INFORMATION = unsafe { std::mem::zeroed() };
if unsafe { GetFileInformationByHandle(handle, &mut info) } == 0 {
return Err(io::Error::last_os_error());
}
if info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY == 0
|| info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT != 0
{
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"untrusted socket directory",
));
}
let user = crate::windows_security::current_user()?;
let user_sid = unsafe { (*(user.as_ptr().cast::<TOKEN_USER>())).User.Sid };
let mut owner = ptr::null_mut();
let mut dacl = ptr::null_mut();
let mut descriptor = ptr::null_mut();
let status = unsafe {
GetSecurityInfo(
handle,
SE_FILE_OBJECT,
OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
&mut owner,
ptr::null_mut(),
&mut dacl,
ptr::null_mut(),
&mut descriptor,
)
};
if status != 0 {
return Err(io::Error::from_raw_os_error(status as i32));
}
// Accept exactly the protected, inheritable user-only ACL set at daemon bind.
let trusted = unsafe {
let mut control = 0;
let mut revision = 0;
let mut ace = ptr::null_mut();
!owner.is_null()
&& EqualSid(owner, user_sid) != 0
&& GetSecurityDescriptorControl(descriptor, &mut control, &mut revision) != 0
&& control & SE_DACL_PROTECTED != 0
&& !dacl.is_null()
&& (*dacl).AceCount == 1
&& GetAce(dacl, /*dwaceindex*/ 0, &mut ace) != 0
&& (*ace.cast::<ACE_HEADER>()).AceType == ACCESS_ALLOWED_ACE_TYPE as u8
&& (*ace.cast::<ACE_HEADER>()).AceFlags
== (CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE) as u8
&& (*ace.cast::<ACCESS_ALLOWED_ACE>()).Mask == FILE_ALL_ACCESS
&& EqualSid(
ptr::addr_of_mut!((*ace.cast::<ACCESS_ALLOWED_ACE>()).SidStart).cast(),
user_sid,
) != 0
};
unsafe { LocalFree(descriptor as _) };
if !trusted {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"socket directory is not private to the current user",
));
}
Ok((directory, guard))
}