From b42d9b6467c30d6f226bea0d00f676f0a08457af Mon Sep 17 00:00:00 2001 From: Francesco Bonacci Date: Thu, 21 May 2026 18:59:20 +0200 Subject: [PATCH] fix(cua-driver-rs)(windows): skip cua-driver-uia spawn when main daemon is at High IL (#1602) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since PR #1630 the autostart task runs the main daemon at RunLevel=Highest, which puts cua-driver.exe at High IL with full UWP / AppContainer UIA access. The sibling cua-driver-uia.exe worker is therefore redundant for the common case — and worse, attempting to ShellExecute the (currently unsigned) uiAccess'd worker from a High-IL parent pops a Windows AIS error dialog ("A referral was returned from the server" = AIS refusing to elevate an unsigned uiAccess binary). The dialog blocks daemon startup and visibly confuses users. Repro: run `cua-driver autostart enable` from a standard admin user (non-RID-500), accept the UAC prompt, then `cua-driver autostart kick`. The main daemon starts at High IL, tries to spawn the uia worker via ShellExecute, AIS refuses, error dialog pops up over the desktop. ## Fix maybe_spawn_uia_worker() now gates on three conditions: 1. Main daemon is NOT at High IL (checked via PowerShell WindowsPrincipal.IsInRole(Administrator)). At High IL the worker is redundant. 2. CUA_DRIVER_RS_SPAWN_UIA_WORKER=1 env var is set. Default-off until the worker is actually EV-signed (#1602) and the spawn doesn't trip AIS. 3. The worker binary actually exists on disk next to cua-driver.exe. All three must be true for the spawn to fire. Common case (RunLevel=Highest install via the canonical install.ps1 + cua-driver autostart enable flow): condition (1) trips first — no spawn, no dialog, no UWP regression because the High-IL main daemon already does what the worker would have done. ## Future EV-cert path When we ship a signed cua-driver-uia.exe (the long-term #1602 answer), users running the daemon at Medium IL can opt into the worker by setting CUA_DRIVER_RS_SPAWN_UIA_WORKER=1. Default-off gives a no-surprises upgrade path: existing users on Medium IL stay on whatever path they're already on, and the explicit env var keeps us honest about which path is being exercised during the signed-binary rollout. Co-Authored-By: Claude Opus 4.7 --- .../crates/cua-driver/src/serve.rs | 86 ++++++++++++++++--- 1 file changed, 72 insertions(+), 14 deletions(-) diff --git a/libs/cua-driver-rs/crates/cua-driver/src/serve.rs b/libs/cua-driver-rs/crates/cua-driver/src/serve.rs index 43df32da24..07d60e55f5 100644 --- a/libs/cua-driver-rs/crates/cua-driver/src/serve.rs +++ b/libs/cua-driver-rs/crates/cua-driver/src/serve.rs @@ -398,21 +398,54 @@ pub async fn run_serve( Ok(()) } -/// On Windows, spawn the sibling uiAccess'd worker (`cua-driver-uia.exe`) via -/// ShellExecute (through a PowerShell one-liner — no new deps) if it lives -/// next to the main binary. uiAccess PEs can only be launched via -/// ShellExecute (CreateProcess returns ERROR_ELEVATION_REQUIRED), and Task -/// Scheduler's PowerShell-wrapper Action path can't establish a logon -/// session for the call (ERROR_NOT_LOGGED_ON). But spawning it as a child -/// of `cua-driver serve` works because the call originates from an already- -/// Session-2 process with an interactive desktop attached. +/// On Windows, optionally spawn the sibling uiAccess'd worker +/// (`cua-driver-uia.exe`) via ShellExecute if it lives next to the main binary +/// AND we're at Medium IL AND the binary is opt-in via env var. /// -/// Best-effort: if the worker isn't installed, the spawn fails, or -/// ShellExecute returns an error, the main daemon still serves requests -/// via the regular `\\.\pipe\cua-driver` path — just without UIPI bypass -/// for UWP apps. See #1602 / the `cua-driver-uia` crate. +/// History: the uia worker was the original answer to "drive UWP / AppContainer +/// apps from a Medium-IL daemon" — it carries `uiAccess="true"` in its manifest +/// and was meant to be Authenticode-signed (EV cert per #1602) so Windows AIS +/// would elevate it to UIAccess integrity at launch. With #1630 the canonical +/// answer became "register the autostart task at RunLevel=Highest so the main +/// daemon is already at High IL", which obviates the worker entirely for the +/// vast majority of users. +/// +/// Current behavior: +/// +/// 1. If the main daemon is already at High IL (the RunLevel=Highest path), +/// skip the worker — it's redundant and, more importantly, attempting to +/// ShellExecute an unsigned uiAccess'd PE pops a Windows error dialog +/// ("A referral was returned from the server" = AIS refusing to elevate +/// an unsigned uiAccess binary). That dialog blocks the daemon's startup +/// and confuses users. +/// +/// 2. If the main daemon is at Medium IL (older installs without the +/// Highest task), AND `CUA_DRIVER_RS_SPAWN_UIA_WORKER=1` is set (opt-in), +/// AND a uiAccess'd worker is installed, spawn it. This path is kept for +/// the future EV-cert flow where the worker IS properly signed. +/// +/// 3. Otherwise: skip silently. The main daemon still serves requests; UWP +/// automation will require either re-running with the Highest autostart +/// task or (when shipped) the signed uia worker. See #1602. #[cfg(target_os = "windows")] fn maybe_spawn_uia_worker() { + // Skip when at High IL — main daemon already has the privileges the + // worker was supposed to provide. + if is_self_at_high_il() { + tracing::debug!("uia spawn skipped: main daemon already at High IL"); + return; + } + + // Opt-in for the future EV-cert flow. Default-off until the worker is + // actually signed and tested. + if !crate::bundle::is_env_truthy("CUA_DRIVER_RS_SPAWN_UIA_WORKER") { + tracing::debug!( + "uia spawn skipped: CUA_DRIVER_RS_SPAWN_UIA_WORKER not set (opt-in only \ + until the worker is EV-signed; see #1602)" + ); + return; + } + let current = match std::env::current_exe() { Ok(p) => p, Err(e) => { @@ -429,8 +462,6 @@ fn maybe_spawn_uia_worker() { return; } let uia_str = uia.display().to_string(); - // PowerShell ShellExecute call — same shape we proved works from Session 2. - // `0` = SW_HIDE so the worker doesn't flash a window. let cmd = format!( "(New-Object -ComObject Shell.Application).ShellExecute('{uia_str}','','','',0)" ); @@ -447,6 +478,33 @@ fn maybe_spawn_uia_worker() { } } +/// Returns true when the current process is at High IL (admin token). Checked +/// via a one-shot PowerShell call to `WindowsPrincipal.IsInRole(Administrator)` +/// — the standard managed equivalent of OpenProcessToken + GetTokenInformation. +/// +/// Done via PowerShell instead of the windows-crate Win32 API because cua-driver +/// doesn't depend on the `windows` crate directly (only platform-windows does), +/// and `serve.rs` runs only once at daemon start so the ~50ms PowerShell-spawn +/// cost is acceptable. +#[cfg(target_os = "windows")] +fn is_self_at_high_il() -> bool { + let out = std::process::Command::new("powershell.exe") + .args([ + "-NoProfile", + "-NonInteractive", + "-Command", + "([System.Security.Principal.WindowsPrincipal][System.Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)", + ]) + .output(); + match out { + Ok(o) => { + let s = String::from_utf8_lossy(&o.stdout); + s.trim().eq_ignore_ascii_case("True") + } + Err(_) => false, + } +} + #[cfg(target_os = "windows")] pub async fn run_serve( registry: std::sync::Arc,