fix(wpf): de-elevate when spawned by elevated explorer (NDI mDNS isolation)
Some checks failed
CI / build-and-test (push) Failing after 27s
Some checks failed
CI / build-and-test (push) Failing after 27s
Observed behavior: on admin-user boxes with UAC effectively disabled, double-clicking the Start Menu / Desktop shortcut spawns TeamsISO with elevated File Explorer as parent. NDI Find then returns zero sources even when Teams is broadcasting — same exe spawned from any other parent (PowerShell, cmd, runas, etc.) discovers sources fine. Suspected window-station / desktop-handle inheritance quirk in NDI's mDNS layer; can't fix from inside the runtime. Workaround: in OnStartup, if parent IS explorer.exe AND we're elevated AND we haven't already re-launched (--relaunched guard), re-spawn ourselves via 'runas /trustlevel:0x20000' to drop to medium integrity. Original process Shutdowns; only the medium child remains. Verified by reproducing the failure case in an elevated PowerShell, then watching the same runas command produce a working child (REST returns participants, log writes work). Add PackageReference for System.Management (Win32_Process via ManagementObjectSearcher) so the parent-PID lookup compiles.
This commit is contained in:
parent
e01fa364e8
commit
191b2c5f52
2 changed files with 137 additions and 0 deletions
|
|
@ -83,6 +83,23 @@ public partial class App : Application
|
|||
{
|
||||
base.OnStartup(e);
|
||||
|
||||
// Re-launch detection: when explorer.exe is the parent AND we're elevated,
|
||||
// NDI Find returns zero sources — reproducible on this user's box and
|
||||
// suspected to be a window-station / desktop-handle inheritance quirk
|
||||
// that NDI's mDNS layer is sensitive to. The exact same exe spawned
|
||||
// from any other parent (PowerShell, cmd, another non-explorer process)
|
||||
// discovers sources fine. Re-spawn through runas /trustlevel:0x20000
|
||||
// to drop to medium integrity and detach from explorer's process tree.
|
||||
//
|
||||
// We pass --relaunched on the re-spawn so we don't loop if the trustlevel
|
||||
// demotion didn't take. CLI args that the operator passed (e.g.
|
||||
// --apply-preset NAME) are forwarded verbatim to the relaunched child.
|
||||
if (ShouldDeElevate(e.Args, out var relaunchArgs))
|
||||
{
|
||||
TryDeElevateAndExit(relaunchArgs);
|
||||
return; // Shutdown happens inside TryDeElevateAndExit if the spawn succeeds.
|
||||
}
|
||||
|
||||
// Crash diagnostics — wire the three exception channels WPF leaves open by
|
||||
// default to a single handler that logs Fatal to Serilog (which has the
|
||||
// rolling-daily file sink at %LOCALAPPDATA%\TeamsISO\Logs) and then shows
|
||||
|
|
@ -160,6 +177,118 @@ public partial class App : Application
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true when we need to re-spawn ourselves with a non-elevated
|
||||
/// medium-integrity token. This is the case when:
|
||||
/// <list type="number">
|
||||
/// <item>We haven't already been relaunched (<c>--relaunched</c> guard
|
||||
/// prevents infinite loops if the demotion didn't take).</item>
|
||||
/// <item>The current process token has the Administrators group
|
||||
/// elevated (UAC "split-token" — admin SID is present and active).</item>
|
||||
/// <item>Our parent process is <c>explorer.exe</c> — that's the spawn
|
||||
/// path that triggers the NDI mDNS-isolation bug. Launches from
|
||||
/// PowerShell, cmd, or any other parent work fine even when
|
||||
/// elevated, so we don't need to fight them.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
private static bool ShouldDeElevate(string[] args, out string[] forwardArgs)
|
||||
{
|
||||
forwardArgs = args;
|
||||
// Already relaunched once — don't loop.
|
||||
if (Array.IndexOf(args, "--relaunched") >= 0)
|
||||
{
|
||||
// Strip the marker so it doesn't propagate further.
|
||||
forwardArgs = args.Where(a => a != "--relaunched").ToArray();
|
||||
return false;
|
||||
}
|
||||
// Not elevated — nothing to demote from.
|
||||
try
|
||||
{
|
||||
using var identity = System.Security.Principal.WindowsIdentity.GetCurrent();
|
||||
var principal = new System.Security.Principal.WindowsPrincipal(identity);
|
||||
if (!principal.IsInRole(System.Security.Principal.WindowsBuiltInRole.Administrator))
|
||||
return false;
|
||||
}
|
||||
catch { return false; }
|
||||
// Check parent process; if anything but explorer.exe we leave well alone.
|
||||
try
|
||||
{
|
||||
var parentName = TryGetParentProcessName();
|
||||
if (!string.Equals(parentName, "explorer", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
}
|
||||
catch { return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Look up our parent process's image name (without extension). Returns
|
||||
/// null if it can't be determined (PID gone, denied, etc.).
|
||||
/// </summary>
|
||||
private static string? TryGetParentProcessName()
|
||||
{
|
||||
try
|
||||
{
|
||||
var pid = Environment.ProcessId;
|
||||
using var search = new System.Management.ManagementObjectSearcher(
|
||||
$"SELECT ParentProcessId FROM Win32_Process WHERE ProcessId={pid}");
|
||||
foreach (var m in search.Get())
|
||||
{
|
||||
var ppid = Convert.ToInt32(m["ParentProcessId"]);
|
||||
using var parent = System.Diagnostics.Process.GetProcessById(ppid);
|
||||
return parent.ProcessName;
|
||||
}
|
||||
}
|
||||
catch { /* fall through */ }
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-launch TeamsISO via <c>runas.exe /trustlevel:0x20000</c>. The
|
||||
/// trustlevel argument requests a medium-integrity restricted token —
|
||||
/// even when the caller (us) is elevated, the spawned child runs at
|
||||
/// medium. This detaches us from explorer's spawn quirks AND from the
|
||||
/// elevation that was tripping NDI Find. We then <see cref="Application.Shutdown(int)"/>
|
||||
/// the current process so only the medium-integrity child remains.
|
||||
///
|
||||
/// If the spawn fails for any reason (runas missing, permission denied,
|
||||
/// etc.) we silently continue startup — the operator may still see the
|
||||
/// "no ndi sources visible" state, but at least the app launches.
|
||||
/// </summary>
|
||||
private void TryDeElevateAndExit(string[] forwardArgs)
|
||||
{
|
||||
try
|
||||
{
|
||||
var exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName;
|
||||
if (string.IsNullOrEmpty(exePath)) return; // can't relaunch what we can't find
|
||||
|
||||
var quotedExe = "\"" + exePath + "\"";
|
||||
var forwarded = string.Join(" ", forwardArgs.Select(a => "\"" + a + "\""));
|
||||
var trustArg = string.IsNullOrEmpty(forwarded)
|
||||
? quotedExe + " --relaunched"
|
||||
: quotedExe + " --relaunched " + forwarded;
|
||||
|
||||
var psi = new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = "runas.exe",
|
||||
Arguments = "/trustlevel:0x20000 " + trustArg,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden,
|
||||
};
|
||||
System.Diagnostics.Process.Start(psi);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Relaunch failed — let normal startup proceed. Worst case the operator
|
||||
// sees the empty-state and has to launch differently.
|
||||
return;
|
||||
}
|
||||
// Shutdown WITHOUT a value so OnExit handlers don't run a teardown for an
|
||||
// engine that was never wired up.
|
||||
Shutdown(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse the supported CLI flags. Currently:
|
||||
/// <c>--apply-preset NAME</c> — apply the named preset once participants
|
||||
|
|
|
|||
|
|
@ -25,6 +25,14 @@
|
|||
<ItemGroup>
|
||||
<ProjectReference Include="..\TeamsISO.Engine\TeamsISO.Engine.csproj" />
|
||||
<ProjectReference Include="..\TeamsISO.Engine.NdiInterop\TeamsISO.Engine.NdiInterop.csproj" />
|
||||
<!--
|
||||
System.Management gives us Win32_Process via ManagementObjectSearcher,
|
||||
used in App.xaml.cs's ShouldDeElevate() to look up the parent process
|
||||
name (we re-spawn ourselves via runas /trustlevel:0x20000 when the
|
||||
parent is explorer.exe AND we're elevated — that combo triggers an
|
||||
NDI mDNS-isolation bug that returns zero discovered sources).
|
||||
-->
|
||||
<PackageReference Include="System.Management" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
|
|
|
|||
Loading…
Reference in a new issue