Skip to content

Commit 439f1be

Browse files
SteveL-MSFTCopilot
andcommitted
Close safe COM boundaries and preserve symlinked assemblies
Make Automation storage and arbitrary COM queries private, add compile-fail API regressions, follow DLL symlinks, and exercise payload-independent launcher smoke checks. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 701c6a4 commit 439f1be

5 files changed

Lines changed: 71 additions & 17 deletions

File tree

‎rust/tests/WindowsLauncher.Tests.ps1‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,11 @@ Describe 'Packaged Rust Windows launcher' {
102102
Join-Path (Split-Path $ExecutablePath) 'pwrshplugin.dll' | Should -Not -Exist
103103
}
104104

105+
It 'passes the documented native smoke script without requiring a managed payload' {
106+
& "$PSScriptRoot/../windows/tests/launcher-smoke.ps1" -ExecutablePath $ExecutablePath -Configuration $Configuration |
107+
Should -Be 'Launcher resources and native smoke checks passed.'
108+
}
109+
105110
if ($Configuration -eq 'coreclr') {
106111
It 'prints native help for <Argument> without requiring CoreCLR' -ForEach @(
107112
@{ Argument = '-?' }
@@ -159,6 +164,24 @@ Describe 'Packaged Rust Windows launcher' {
159164
$result.Output | Should -Not -Match '\x00'
160165
}
161166

167+
It 'starts CoreCLR when required managed assemblies are symbolic links' {
168+
$linkedHome = Join-Path $TestDrive 'symlinked-coreclr'
169+
New-Item -ItemType Directory $linkedHome > $null
170+
Copy-Item "$coreClrHome/*" $linkedHome -Recurse
171+
$targets = Join-Path $TestDrive 'assembly-targets'
172+
New-Item -ItemType Directory $targets > $null
173+
foreach ($name in @('System.Private.CoreLib.dll', 'Microsoft.PowerShell.ConsoleHost.dll')) {
174+
$link = Join-Path $linkedHome $name
175+
$target = Join-Path $targets $name
176+
Move-Item -LiteralPath $link -Destination $target
177+
New-Item -ItemType SymbolicLink -Path $link -Target $target > $null
178+
}
179+
$result = Invoke-Launcher -Path (Join-Path $linkedHome 'powershell.exe') -Arguments @(
180+
'-NoLogo', '-NoProfile', '-NonInteractive', '-Command', 'exit 37'
181+
)
182+
$result.ExitCode | Should -Be 37 -Because "$($result.Output) $($result.Error)"
183+
}
184+
162185
It 'loads an external payload from BOM-prefixed configuration starting with <FirstSetting>' -ForEach @(
163186
@{ FirstSetting = 'PSHOMEDIR' }
164187
@{ FirstSetting = 'CORECLRDIR' }
@@ -183,8 +206,8 @@ Describe 'Packaged Rust Windows launcher' {
183206
} else {
184207
It 'rejects malformed Framework options without requiring a managed payload' {
185208
$result = Invoke-Launcher -Path $ExecutablePath -Arguments @('-Version', 'invalid')
186-
$result.ExitCode | Should -Not -Be 0
187-
$result.Error | Should -Match 'PowerShell native host error'
209+
$result.ExitCode | Should -Be -196608
210+
$result.Error | Should -Match 'PowerShell native host error 0xfffd0000'
188211
}
189212
}
190213
}

‎rust/windows/README.md‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,18 @@ cargo clippy -p powershell-native-windows --all-targets --no-default-features --
102102
Portable tests cover version formats/overflow, registry-family mappings,
103103
parameter abbreviations/Unicode dashes, native TPA exclusions, and configuration
104104
fallback. Windows-only tests cover Automation layout, null dispatch/unknown
105-
variants, and launcher behavior.
105+
variants, and launcher behavior. Compile-fail documentation tests enforce private
106+
variant storage and prevent arbitrary COM interface queries through the safe API.
106107
The Windows CI jobs build/link both launcher configurations and architectures
107108
with SDK resources. Pester v5 checks the packaged executable's architecture,
108109
version resources, native CoreCLR help and missing-runtime errors, successful
109110
CoreCLR command execution, Unicode/empty/quoted/spaced arguments, positive and
110111
negative managed exit codes, and native Framework argument rejection.
111112
Regression coverage also checks BOM-prefixed external-payload configuration
112113
in both setting orders and byte-oriented redirected help/debug/verbose output.
114+
CoreCLR startup is also tested with symlinked core-library and console-host
115+
assemblies; creating Windows symbolic links requires Developer Mode or the
116+
symbolic-link privilege.
113117
Framework integration tests compile a COM-visible C# fixture with the installed
114118
.NET Framework compiler and exercise actual CLR startup, AppDomain activation,
115119
object unwrapping, argument marshaling, and signed managed exit codes on x86/x64.
@@ -128,7 +132,8 @@ Install-Module Pester -RequiredVersion 5.9.0 -Scope CurrentUser -Repository PSGa
128132
CI packages these launchers under `artifacts/rust/win-<arch>-<configuration>/`,
129133
separate from the existing C++ CoreCLR binaries and WSMan plugin under
130134
`artifacts/cpp/win-<arch>-coreclr/`. Pester NUnit XML is retained on failures.
131-
The lightweight manual smoke script is also available:
135+
The lightweight manual smoke script checks CoreCLR help or Framework native
136+
argument rejection, without requiring an installed managed PowerShell payload:
132137

133138
```powershell
134139
./windows/tests/launcher-smoke.ps1 -ExecutablePath ./target/x86_64-pc-windows-msvc/release/powershell.exe -Configuration coreclr

‎rust/windows/src/common.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ impl HostEnvironment {
247247
let entry = entry.ok()?;
248248
let path = entry.path();
249249
let name = entry.file_name().to_string_lossy().into_owned();
250-
if entry.file_type().ok()?.is_file()
250+
if path.is_file()
251251
&& name.to_ascii_lowercase().ends_with(".dll")
252252
&& !exclude_tpa(&name)
253253
{

‎rust/windows/src/hosting.rs‎

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -162,18 +162,31 @@ struct ArrayBound {
162162
}
163163

164164
#[repr(C)]
165-
pub union VariantValue {
166-
pub pointer: Ptr,
167-
pub signed: i64,
168-
pub int: i32,
169-
pub uint: u32,
165+
union VariantValue {
166+
pointer: Ptr,
167+
// Preserve native VARIANT alignment on 32-bit Windows.
168+
_signed: i64,
169+
uint: u32,
170170
layout: [usize; 2],
171171
}
172+
/// An owning Automation value whose tag and payload can only be set by this module.
173+
///
174+
/// ```compile_fail,E0616
175+
/// use powershell_native_host::hosting::AutomationVariant;
176+
/// let mut value = AutomationVariant::default();
177+
/// value.kind = 8;
178+
/// ```
179+
///
180+
/// ```compile_fail,E0616
181+
/// use powershell_native_host::hosting::AutomationVariant;
182+
/// let value = AutomationVariant::default();
183+
/// let _ = &value.value;
184+
/// ```
172185
#[repr(C)]
173186
pub struct AutomationVariant {
174-
pub kind: u16,
187+
kind: u16,
175188
reserved: [u16; 3],
176-
pub value: VariantValue,
189+
value: VariantValue,
177190
}
178191
impl Default for AutomationVariant {
179192
fn default() -> Self {
@@ -328,14 +341,27 @@ const CLSID_COR_RUNTIME: GUID = GUID::from_u128(0xcb2f6723_ab3a_11d2_9c40_00c04f
328341
const IID_COR_RUNTIME: GUID = GUID::from_u128(0xcb2f6722_ab3a_11d2_9c40_00c04fa30a3e);
329342
const IID_NULL: GUID = GUID::zeroed();
330343

344+
/// An owning COM reference. Public APIs only return dispatch interfaces;
345+
/// arbitrary interface queries remain internal.
346+
///
347+
/// ```compile_fail,E0624
348+
/// use powershell_native_host::hosting::ComObject;
349+
/// use windows::core::GUID;
350+
/// fn cannot_invoke_unknown(object: &ComObject) {
351+
/// let unknown = object.query(
352+
/// &GUID::from_u128(0x00000000_0000_0000_c000_000000000046)
353+
/// ).unwrap();
354+
/// let _ = unknown.invoke("Start", &mut []);
355+
/// }
356+
/// ```
331357
pub struct ComObject(Ptr);
332358
impl ComObject {
333359
unsafe fn slot(&self, index: usize) -> Ptr {
334360
*(*(self.0 as *const *const Ptr)).add(index)
335361
}
336362
/// # Errors
337363
/// Returns the HRESULT if the requested COM interface is unavailable.
338-
pub fn query(&self, iid: &GUID) -> Result<Self> {
364+
fn query(&self, iid: &GUID) -> Result<Self> {
339365
let query: unsafe extern "system" fn(Ptr, *const GUID, *mut Ptr) -> i32 =
340366
unsafe { mem::transmute(self.slot(0)) };
341367
let mut result = ptr::null_mut();

‎rust/windows/tests/launcher-smoke.ps1‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ try {
3535
$arguments = if ($Configuration -eq 'coreclr') {
3636
@('-?')
3737
} else {
38-
@('-NoLogo', '-NoProfile', '-NonInteractive', '-Command', 'exit 37')
38+
@('-Version', 'invalid')
3939
}
4040
foreach ($argument in $arguments) {
4141
$process.StartInfo.ArgumentList.Add($argument)
@@ -55,8 +55,8 @@ try {
5555
if ($process.ExitCode -ne -1 -or $output -notmatch 'USAGE: powershell') {
5656
throw "CoreCLR native help failed ($($process.ExitCode)): $output $errorText"
5757
}
58-
} elseif ($process.ExitCode -ne 37) {
59-
throw "Framework host did not execute managed PowerShell ($($process.ExitCode)): $output $errorText"
58+
} elseif ($process.ExitCode -ne -196608 -or $errorText -notmatch 'PowerShell native host error 0xfffd0000') {
59+
throw "Framework native argument rejection failed ($($process.ExitCode)): $output $errorText"
6060
}
6161
} finally {
6262
if ($started -and -not $process.HasExited) {
@@ -65,4 +65,4 @@ try {
6565
}
6666
$process.Dispose()
6767
}
68-
'Launcher resources and startup smoke checks passed.'
68+
'Launcher resources and native smoke checks passed.'

0 commit comments

Comments
 (0)