|
| 1 | +// Copyright (c) Microsoft Corporation. |
| 2 | +// Licensed under the MIT License. |
| 3 | + |
| 4 | +//! App-id resolution for the isolation session backend. |
| 5 | +//! |
| 6 | +//! The OS Preview `AddUserAsync2` takes an app-scoped registration id |
| 7 | +//! ("appId"). When the caller supplies one it is used verbatim; otherwise we |
| 8 | +//! detect the Package Family Name of wxc-exec's *immediate parent* process — |
| 9 | +//! the app that invoked MXC — and pass "PFN:<pfn>". This mirrors the OS-side |
| 10 | +//! `ResolveRegistrationId` format so the resulting registration is identical |
| 11 | +//! whether formed here or in the OS client DLL. An unpackaged parent (or any |
| 12 | +//! detection failure) yields an empty appId, which lets the OS fall back to |
| 13 | +//! its default registration. |
| 14 | +//! |
| 15 | +//! Note: the OS `ResolveRegistrationId` runs in-proc inside wxc-exec, so an |
| 16 | +//! empty appId there would resolve to *wxc-exec's* own PFN — not the invoking |
| 17 | +//! app's. Detecting the parent here, and passing a non-empty "PFN:<pfn>" that |
| 18 | +//! the OS then uses verbatim, is what scopes the registration to the caller. |
| 19 | +
|
| 20 | +use wxc_common::process_util::OwnedHandle; |
| 21 | + |
| 22 | +use windows::Win32::Foundation::{ERROR_SUCCESS, HANDLE}; |
| 23 | +use windows::Win32::Storage::Packaging::Appx::GetPackageFamilyName; |
| 24 | +use windows::Win32::System::Diagnostics::ToolHelp::{ |
| 25 | + CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W, TH32CS_SNAPPROCESS, |
| 26 | +}; |
| 27 | +use windows::Win32::System::Threading::{ |
| 28 | + GetCurrentProcessId, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION, |
| 29 | +}; |
| 30 | +use windows_core::PWSTR; |
| 31 | + |
| 32 | +/// The prefix the OS uses for package-family-scoped registration ids. |
| 33 | +const PFN_PREFIX: &str = "PFN:"; |
| 34 | + |
| 35 | +/// Resolves the appId to pass to `AddUserAsync2`. |
| 36 | +/// |
| 37 | +/// - Non-empty `explicit` -> returned unchanged (caller opted in). |
| 38 | +/// - Empty `explicit` -> detect the immediate parent process's Package Family |
| 39 | +/// Name and return "PFN:<pfn>". |
| 40 | +/// - Detection failure (unpackaged parent, PID lookup miss, OS error) -> |
| 41 | +/// empty string (OS default registration). |
| 42 | +pub(super) fn resolve_app_id(explicit: &str) -> String { |
| 43 | + if !explicit.is_empty() { |
| 44 | + return explicit.to_string(); |
| 45 | + } |
| 46 | + match detect_parent_pfn() { |
| 47 | + Some(pfn) => format!("{PFN_PREFIX}{pfn}"), |
| 48 | + None => String::new(), |
| 49 | + } |
| 50 | +} |
| 51 | + |
| 52 | +/// Returns the Package Family Name of wxc-exec's immediate parent process, or |
| 53 | +/// `None` if the parent is unpackaged or cannot be inspected. |
| 54 | +fn detect_parent_pfn() -> Option<String> { |
| 55 | + let parent_pid = parent_process_id()?; |
| 56 | + // PROCESS_QUERY_LIMITED_INFORMATION is sufficient for GetPackageFamilyName |
| 57 | + // and is grantable across integrity levels. |
| 58 | + let handle = |
| 59 | + unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, parent_pid) }.ok()?; |
| 60 | + let owned = OwnedHandle::new(handle); |
| 61 | + package_family_name(owned.get()) |
| 62 | +} |
| 63 | + |
| 64 | +/// Walks a ToolHelp process snapshot to find the current process's |
| 65 | +/// `th32ParentProcessID`. Best-effort: PID reuse can race, but this is only an |
| 66 | +/// identity hint for registration scoping. |
| 67 | +fn parent_process_id() -> Option<u32> { |
| 68 | + let current = unsafe { GetCurrentProcessId() }; |
| 69 | + let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }.ok()?; |
| 70 | + let owned = OwnedHandle::new(snapshot); |
| 71 | + |
| 72 | + let mut entry = PROCESSENTRY32W { |
| 73 | + dwSize: std::mem::size_of::<PROCESSENTRY32W>() as u32, |
| 74 | + ..Default::default() |
| 75 | + }; |
| 76 | + |
| 77 | + if unsafe { Process32FirstW(owned.get(), &mut entry) }.is_err() { |
| 78 | + return None; |
| 79 | + } |
| 80 | + loop { |
| 81 | + if entry.th32ProcessID == current { |
| 82 | + return Some(entry.th32ParentProcessID); |
| 83 | + } |
| 84 | + if unsafe { Process32NextW(owned.get(), &mut entry) }.is_err() { |
| 85 | + return None; |
| 86 | + } |
| 87 | + } |
| 88 | +} |
| 89 | + |
| 90 | +/// Queries the Package Family Name for a process handle. `None` when the |
| 91 | +/// process is unpackaged (`APPMODEL_ERROR_NO_PACKAGE`) or on any error. |
| 92 | +fn package_family_name(process: HANDLE) -> Option<String> { |
| 93 | + // First call with a null buffer to obtain the required length. A packaged |
| 94 | + // process returns ERROR_INSUFFICIENT_BUFFER with `length` set; an |
| 95 | + // unpackaged one returns APPMODEL_ERROR_NO_PACKAGE and leaves length 0. |
| 96 | + let mut length: u32 = 0; |
| 97 | + let _ = unsafe { GetPackageFamilyName(process, &mut length, None) }; |
| 98 | + if length == 0 { |
| 99 | + return None; |
| 100 | + } |
| 101 | + |
| 102 | + let mut buffer = vec![0u16; length as usize]; |
| 103 | + let rc = |
| 104 | + unsafe { GetPackageFamilyName(process, &mut length, Some(PWSTR(buffer.as_mut_ptr()))) }; |
| 105 | + if rc != ERROR_SUCCESS { |
| 106 | + return None; |
| 107 | + } |
| 108 | + |
| 109 | + // `length` includes the terminating null; trim at the first null. |
| 110 | + let end = buffer.iter().position(|&c| c == 0).unwrap_or(buffer.len()); |
| 111 | + Some(String::from_utf16_lossy(&buffer[..end])) |
| 112 | +} |
| 113 | + |
| 114 | +#[cfg(test)] |
| 115 | +mod tests { |
| 116 | + use super::*; |
| 117 | + |
| 118 | + #[test] |
| 119 | + fn explicit_app_id_is_passed_through_unchanged() { |
| 120 | + assert_eq!(resolve_app_id("PFN:Contoso.App_8wekyb3d8bbwe"), "PFN:Contoso.App_8wekyb3d8bbwe"); |
| 121 | + assert_eq!(resolve_app_id("my-literal-id"), "my-literal-id"); |
| 122 | + } |
| 123 | + |
| 124 | + #[test] |
| 125 | + fn empty_explicit_resolves_via_parent_detection() { |
| 126 | + // Environment-dependent: the test runner's parent is typically an |
| 127 | + // unpackaged shell/cargo process, so this yields "". When the parent |
| 128 | + // *is* packaged, the value must carry the "PFN:" prefix. Either way it |
| 129 | + // must never panic and must be a valid (possibly empty) appId. |
| 130 | + let resolved = resolve_app_id(""); |
| 131 | + if !resolved.is_empty() { |
| 132 | + assert!( |
| 133 | + resolved.starts_with(PFN_PREFIX), |
| 134 | + "detected appId must be PFN-prefixed, got {resolved:?}" |
| 135 | + ); |
| 136 | + } |
| 137 | + } |
| 138 | +} |
0 commit comments