Fall back to mklink when Windows daemon junction updates are denied (#50802)

## Why

Some Windows policies deny in-process reparse-point mutation while allowing the system junction creator, preventing managed daemon release selection.

## What changed

Fall back to `cmd.exe` with `mklink /J` when native junction creation or retargeting returns `PermissionDenied`. Pass quoted paths through environment variables and disable delayed expansion to handle shell metacharacters.

Create the replacement junction before moving the existing `current` junction aside, and attempt to restore the previous junction if installation fails. Preserve it for recovery if restoration also fails.

## Testing

Extend the Windows junction test to cover fallback creation, native retargeting, and fallback replacement under a path containing spaces, `&`, `%`, and `!`, while verifying that no standalone CLI installation is created.

GitOrigin-RevId: 039ee850f8c788f44f08116614ca3927e3e9952f
This commit is contained in:
Eric Traut
2026-10-04 06:27:57 +00:00
committed by copyberry
parent b8dceb0d4f
commit b989f795b1
2 changed files with 111 additions and 16 deletions
@@ -3,7 +3,9 @@
use std::os::windows::ffi::OsStrExt;
use std::os::windows::fs::OpenOptionsExt;
use std::os::windows::io::AsRawHandle;
use std::os::windows::process::CommandExt;
use std::path::Path;
use std::process::Command;
use std::time::Duration;
use anyhow::Context;
@@ -15,6 +17,7 @@ use windows_sys::Win32::Storage::FileSystem::FILE_SHARE_DELETE;
use windows_sys::Win32::Storage::FileSystem::FILE_SHARE_READ;
use windows_sys::Win32::Storage::FileSystem::FILE_SHARE_WRITE;
use windows_sys::Win32::System::IO::DeviceIoControl;
use windows_sys::Win32::System::Threading::CREATE_NO_WINDOW;
const PUBLISH_RELEASE_RETRY_INTERVAL: Duration = Duration::from_millis(50);
const PUBLISH_RELEASE_RETRY_LIMIT: usize = 100;
@@ -47,30 +50,97 @@ pub(super) async fn publish_release(stage: &Path, release: &Path) -> Result<()>
}
pub(super) fn select_release(root: &Path, release: &Path) -> Result<()> {
select_release_with(root, release, retarget_junction)
}
fn select_release_with(
root: &Path,
release: &Path,
retarget: impl FnOnce(&Path, &Path) -> Result<()>,
) -> Result<()> {
let release = release.canonicalize()?;
let current = root.join("current");
if current.symlink_metadata().is_err() {
let temporary = tempfile::TempDir::new_in(root)?;
let junction = temporary.path().join("current");
std::fs::create_dir(&junction)?;
retarget_junction(&junction, &release)?;
std::fs::rename(junction, &current)?;
return Ok(());
return match retarget(&junction, &release) {
Ok(()) => Ok(std::fs::rename(junction, current)?),
Err(error) if is_permission_denied(&error) => install_junction(root, &release)
.with_context(|| format!("failed to create managed daemon junction after native creation was denied: {error:#}")),
Err(error) => Err(error),
};
}
validate_selection(root)?;
retarget_junction(&current, &release)
match retarget(&current, &release) {
Ok(()) => Ok(()),
Err(error) if is_permission_denied(&error) => {
install_junction(root, &release).with_context(|| format!("failed to replace managed daemon junction after retargeting was denied: {error:#}"))
}
Err(error) => Err(error),
}
}
pub(super) fn validate_selection(root: &Path) -> Result<()> {
fn is_permission_denied(error: &anyhow::Error) -> bool {
error
.downcast_ref::<std::io::Error>()
.is_some_and(|error| error.kind() == std::io::ErrorKind::PermissionDenied)
}
fn install_junction(root: &Path, release: &Path) -> Result<()> {
let temporary = tempfile::TempDir::new_in(root)?;
let junction = temporary.path().join("current");
let current = root.join("current");
if matches!(current.symlink_metadata(), Err(error) if error.kind() == std::io::ErrorKind::NotFound)
{
let replacing = current.symlink_metadata().is_ok();
if replacing {
validate_selection(root)?;
}
// Some Windows policies deny in-process reparse-point mutation while allowing the system
// junction creator. Pass paths through the environment so cmd metacharacters stay quoted.
let system_root = std::env::var_os("SystemRoot").context("SystemRoot is not set")?;
let command_shell = Path::new(&system_root).join("System32").join("cmd.exe");
anyhow::ensure!(
command_shell.is_absolute(),
"SystemRoot must be an absolute path"
);
let output = Command::new(command_shell)
.env("CODEX_DAEMON_JUNCTION_LINK", &junction)
.env("CODEX_DAEMON_JUNCTION_TARGET", release)
.args(["/d", "/s", "/e:on", "/v:off", "/c"])
.raw_arg(r#""mklink /J "%CODEX_DAEMON_JUNCTION_LINK%" "%CODEX_DAEMON_JUNCTION_TARGET%"""#)
.creation_flags(CREATE_NO_WINDOW)
.output()
.context("failed to run cmd.exe to create managed daemon junction")?;
anyhow::ensure!(
output.status.success(),
"cmd.exe could not create managed daemon junction (status {}): stdout: {}; stderr: {}",
output.status,
String::from_utf8_lossy(&output.stdout).trim(),
String::from_utf8_lossy(&output.stderr).trim()
);
if !replacing {
std::fs::rename(junction, current)?;
return Ok(());
}
anyhow::ensure!(
current.canonicalize()?.parent() == Some(root.join("releases").canonicalize()?.as_path()),
"refusing to replace a daemon selection outside its releases directory"
);
let previous = temporary.path().join("previous");
std::fs::rename(&current, &previous)?;
if let Err(error) = std::fs::rename(&junction, &current) {
if let Err(restore_error) = std::fs::rename(&previous, &current) {
let preserved = temporary.keep();
anyhow::bail!(
"failed to replace managed daemon junction: {error}; also failed to restore the previous junction from {}: {restore_error}",
preserved.display()
);
}
return Err(error).context("failed to replace managed daemon junction");
}
if let Err(error) = std::fs::remove_dir(previous) {
tracing::warn!(
%error,
"failed to remove previous managed daemon junction"
);
}
Ok(())
}
@@ -124,6 +194,19 @@ fn retarget_junction(current: &Path, release: &Path) -> Result<()> {
Ok(())
}
pub(super) fn validate_selection(root: &Path) -> Result<()> {
let current = root.join("current");
if matches!(current.symlink_metadata(), Err(error) if error.kind() == std::io::ErrorKind::NotFound)
{
return Ok(());
}
anyhow::ensure!(
current.canonicalize()?.parent() == Some(root.join("releases").canonicalize()?.as_path()),
"refusing to replace a daemon selection outside its releases directory"
);
Ok(())
}
#[cfg(test)]
#[path = "prepare_install_windows_tests.rs"]
mod tests;
@@ -1,4 +1,6 @@
//! Verifies Windows package publication and selection without requiring an elevated daemon.
use anyhow::Context;
use pretty_assertions::assert_eq;
use std::os::windows::fs::OpenOptionsExt;
use std::time::Duration;
@@ -32,17 +34,27 @@ async fn publishing_release_retries_transient_access_denied() {
#[cfg(windows)]
#[test]
fn daemon_junction_can_be_created_and_retargeted_without_cli_links() {
fn daemon_junction_can_be_created_retargeted_and_replaced_without_cli_links() {
let home = tempfile::TempDir::new().unwrap();
let root = home.path().join("packages/app-server-daemon");
for version in ["first", "second"] {
let root = home
.path()
.join("home & 100% ready!/packages/app-server-daemon");
for (version, native_denied) in [("first", true), ("second", false), ("third", true)] {
let release = root.join("releases").join(version);
std::fs::create_dir_all(&release).unwrap();
super::select_release(&root, &release).unwrap();
if native_denied {
super::select_release_with(&root, &release, |_, _| {
Err(std::io::Error::from(std::io::ErrorKind::PermissionDenied))
.context("failed to retarget managed daemon junction")
})
.unwrap();
} else {
super::select_release(&root, &release).unwrap();
}
assert_eq!(
root.join("current").canonicalize().unwrap(),
release.canonicalize().unwrap()
);
}
assert!(!home.path().join("packages/standalone").exists());
assert!(!root.with_file_name("standalone").exists());
}