From 67911ee13d6e9f1db0895e1a12ffa663c4897fdf Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Sat, 3 Oct 2026 16:53:09 -0700 Subject: [PATCH 1/7] Refractored.TinyClips version 1.8.2.0 --- .../Refractored.TinyClips.installer.yaml | 22 +++++++++++++++ .../Refractored.TinyClips.locale.en-US.yaml | 28 +++++++++++++++++++ .../1.8.2.0/Refractored.TinyClips.yaml | 8 ++++++ 3 files changed, 58 insertions(+) create mode 100644 manifests/r/Refractored/TinyClips/1.8.2.0/Refractored.TinyClips.installer.yaml create mode 100644 manifests/r/Refractored/TinyClips/1.8.2.0/Refractored.TinyClips.locale.en-US.yaml create mode 100644 manifests/r/Refractored/TinyClips/1.8.2.0/Refractored.TinyClips.yaml diff --git a/manifests/r/Refractored/TinyClips/1.8.2.0/Refractored.TinyClips.installer.yaml b/manifests/r/Refractored/TinyClips/1.8.2.0/Refractored.TinyClips.installer.yaml new file mode 100644 index 0000000000000..150ce21f776a9 --- /dev/null +++ b/manifests/r/Refractored/TinyClips/1.8.2.0/Refractored.TinyClips.installer.yaml @@ -0,0 +1,22 @@ +# Created using wingetcreate 1.12.13.0 +# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.12.0.schema.json + +PackageIdentifier: Refractored.TinyClips +PackageVersion: 1.8.2.0 +MinimumOSVersion: 10.0.22000.0 +InstallerType: msix +InstallModes: +- silent +- silentWithProgress +PackageFamilyName: Refractored.TinyClips_vmshqmcyy894t +Installers: +- Architecture: x64 + InstallerUrl: https://github.com/jamesmontemagno/tiny-clips/releases/download/v1.8.2-windows/TinyClips-1.8.2-x64.msix + InstallerSha256: bbec5fd75360b0f729a391f1172189c3f90ebab8d33198e1a01149b1a7d7734b + SignatureSha256: 9c121f71d4a6a3703cb94c576073fc1ee32d5b1d1a39206cef2e576e9ec03b46 +- Architecture: arm64 + InstallerUrl: https://github.com/jamesmontemagno/tiny-clips/releases/download/v1.8.2-windows/TinyClips-1.8.2-arm64.msix + InstallerSha256: 8a1f9818988415457c94c286273b53448dc79ef732fd2b7753b991a2cec4be1d + SignatureSha256: 58419077f732dee590db402897a375aaadc97ca3c49e2e093d554ecba26a9dc5 +ManifestType: installer +ManifestVersion: 1.12.0 diff --git a/manifests/r/Refractored/TinyClips/1.8.2.0/Refractored.TinyClips.locale.en-US.yaml b/manifests/r/Refractored/TinyClips/1.8.2.0/Refractored.TinyClips.locale.en-US.yaml new file mode 100644 index 0000000000000..21b615628aeb6 --- /dev/null +++ b/manifests/r/Refractored/TinyClips/1.8.2.0/Refractored.TinyClips.locale.en-US.yaml @@ -0,0 +1,28 @@ +# Created using wingetcreate 1.12.13.0 +# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.12.0.schema.json + +PackageIdentifier: Refractored.TinyClips +PackageVersion: 1.8.2.0 +PackageLocale: en-US +Publisher: Refractored LLC +PublisherUrl: https://github.com/jamesmontemagno +PublisherSupportUrl: https://github.com/jamesmontemagno/tiny-clips/issues +PrivacyUrl: https://tinyclips.app/privacy.html +Author: James Montemagno +PackageName: Tiny Clips +PackageUrl: https://github.com/jamesmontemagno/tiny-clips +License: MIT +LicenseUrl: https://github.com/jamesmontemagno/tiny-clips/blob/main/LICENSE +ShortDescription: Capture screenshots, video, and GIFs of any screen region from the system tray. +Description: |- + Tiny Clips is a lightweight system-tray app for quickly capturing screenshots, + screen recordings (MP4), and animated GIFs. Pick a region or the full screen, + use global hotkeys, and save straight to your Pictures/Videos folders. +Tags: +- screenshot +- screen-recorder +- gif +- capture +- productivity +ManifestType: defaultLocale +ManifestVersion: 1.12.0 diff --git a/manifests/r/Refractored/TinyClips/1.8.2.0/Refractored.TinyClips.yaml b/manifests/r/Refractored/TinyClips/1.8.2.0/Refractored.TinyClips.yaml new file mode 100644 index 0000000000000..d4a40c72c5957 --- /dev/null +++ b/manifests/r/Refractored/TinyClips/1.8.2.0/Refractored.TinyClips.yaml @@ -0,0 +1,8 @@ +# Created using wingetcreate 1.12.13.0 +# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.12.0.schema.json + +PackageIdentifier: Refractored.TinyClips +PackageVersion: 1.8.2.0 +DefaultLocale: en-US +ManifestType: version +ManifestVersion: 1.12.0 From bda64066aeb8649ba9890267f0c1695626ffdb0a Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Mon, 12 Oct 2026 08:50:02 -0700 Subject: [PATCH 2/7] Add fork-only Tiny Clips ARM64 validation reproduction Exercise the exact submitted signed MSIX under a temporary standard user on ARM64 and x64, with repeated launches and unrelated working directories. Upload text diagnostics only; leave the submission branch untouched. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../workflows/tinyclips-arm64-validation.yml | 68 ++++++ Tools/TinyClipsValidation.ps1 | 228 ++++++++++++++++++ 2 files changed, 296 insertions(+) create mode 100644 .github/workflows/tinyclips-arm64-validation.yml create mode 100644 Tools/TinyClipsValidation.ps1 diff --git a/.github/workflows/tinyclips-arm64-validation.yml b/.github/workflows/tinyclips-arm64-validation.yml new file mode 100644 index 0000000000000..38a1b7aee27c4 --- /dev/null +++ b/.github/workflows/tinyclips-arm64-validation.yml @@ -0,0 +1,68 @@ +name: Tiny Clips winget reproduction + +on: + push: + branches: + - diagnostics/tinyclips-arm64-validation + paths: + - .github/workflows/tinyclips-arm64-validation.yml + - Tools/TinyClipsValidation.ps1 + +permissions: + contents: read + +jobs: + reproduce: + if: github.repository == 'jamesmontemagno/winget-pkgs' + name: Standard-user launch (${{ matrix.arch }}) + runs-on: ${{ matrix.runner }} + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + include: + - arch: arm64 + runner: windows-11-arm + - arch: x64 + runner: windows-latest + env: + ARCH: ${{ matrix.arch }} + steps: + - uses: actions/checkout@v6 + with: + sparse-checkout: | + Tools/TinyClipsValidation.ps1 + manifests/r/Refractored/TinyClips/1.8.2.0 + sparse-checkout-cone-mode: false + persist-credentials: false + + - name: Download and verify the submitted signed package + shell: pwsh + run: | + $manifest = Get-Content 'manifests\r\Refractored\TinyClips\1.8.2.0\Refractored.TinyClips.installer.yaml' -Raw + $block = [regex]::Match($manifest, "(?ms)^- Architecture: $env:ARCH\r?\n(?.*?)(?=^- Architecture:|\z)") + if (-not $block.Success) { throw "No $env:ARCH installer in the submitted manifest." } + $url = [regex]::Match($block.Groups['installer'].Value, '(?m)^\s+InstallerUrl:\s*(\S+)').Groups[1].Value + $expected = [regex]::Match($block.Groups['installer'].Value, '(?m)^\s+InstallerSha256:\s*([a-fA-F0-9]{64})').Groups[1].Value + if (-not $url -or -not $expected) { throw 'Installer URL or hash is missing.' } + Invoke-WebRequest -Uri $url -OutFile package.msix + if ((Get-FileHash package.msix -Algorithm SHA256).Hash -ine $expected) { throw 'Package SHA256 does not match the submitted manifest.' } + "Testing signed 1.8.2.0 $env:ARCH package; SHA256 verified." + + - name: Install and exercise launches as a standard user + shell: pwsh + run: | + & .\Tools\TinyClipsValidation.ps1 -MsixPath (Resolve-Path package.msix).Path ` + -OutputDirectory (Join-Path $env:GITHUB_WORKSPACE 'diagnostics') -WaitSeconds 60 + + - name: Upload text diagnostics only + if: always() + uses: actions/upload-artifact@v7 + with: + name: tinyclips-validation-${{ matrix.arch }} + path: | + diagnostics/*.json + diagnostics/*.log + diagnostics/*.txt + if-no-files-found: warn + retention-days: 7 diff --git a/Tools/TinyClipsValidation.ps1 b/Tools/TinyClipsValidation.ps1 new file mode 100644 index 0000000000000..5885af697e0c8 --- /dev/null +++ b/Tools/TinyClipsValidation.ps1 @@ -0,0 +1,228 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string] $MsixPath, + + [Parameter(Mandatory)] + [string] $OutputDirectory, + + [ValidateRange(20, 120)] + [int] $WaitSeconds = 60, + + [switch] $StandardUser +) + +$ErrorActionPreference = 'Stop' +if ($env:GITHUB_ACTIONS -ne 'true') { + throw 'Run this account-creating diagnostic only on a disposable GitHub-hosted runner.' +} +New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null + +if (-not $StandardUser) { + $root = 'C:\TinyClipsValidation' + $userName = 'TinyClipsValidation' + $process = $null + $userCreated = $false + $rootCreated = $false + try { + if (Test-Path $root) { throw "Refusing to reuse existing directory $root." } + if (Get-LocalUser -Name $userName -ErrorAction SilentlyContinue) { throw "Refusing to reuse existing user $userName." } + New-Item -ItemType Directory -Path $root | Out-Null + $rootCreated = $true + $workerOutput = Join-Path $root 'diagnostics' + New-Item -ItemType Directory -Path $workerOutput | Out-Null + Copy-Item $MsixPath (Join-Path $root 'package.msix') + Copy-Item $PSCommandPath (Join-Path $root 'worker.ps1') + & icacls.exe $root /grant '*S-1-5-32-545:(OI)(CI)M' | Out-Null + if ($LASTEXITCODE -ne 0) { throw 'Unable to grant the validation user access to test files.' } + + $password = "Tc!$([Guid]::NewGuid().ToString('N'))a9" + "::add-mask::$password" + $securePassword = [Security.SecureString]::new() + foreach ($character in $password.ToCharArray()) { $securePassword.AppendChar($character) } + $securePassword.MakeReadOnly() + New-LocalUser -Name $userName -Password $securePassword -AccountNeverExpires | Out-Null + $userCreated = $true + $users = Get-LocalGroup -SID 'S-1-5-32-545' + $user = Get-LocalUser -Name $userName + if (-not (Get-LocalGroupMember -Group $users | Where-Object { $_.SID -eq $user.SID })) { + Add-LocalGroupMember -Group $users -Member $userName + } + $credential = [PSCredential]::new("$env:COMPUTERNAME\$userName", $securePassword) + $pwsh = (Get-Process -Id $PID).Path + $arguments = "-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File `"$root\worker.ps1`" -StandardUser -MsixPath `"$root\package.msix`" -OutputDirectory `"$workerOutput`" -WaitSeconds $WaitSeconds" + $process = Start-Process -FilePath $pwsh -ArgumentList $arguments -Credential $credential ` + -LoadUserProfile -WorkingDirectory $root -PassThru ` + -RedirectStandardOutput (Join-Path $workerOutput 'stdout.log') ` + -RedirectStandardError (Join-Path $workerOutput 'stderr.log') + $null = $process.Handle + if (-not $process.WaitForExit(720000)) { throw 'Standard-user validation exceeded 12 minutes.' } + if (-not (Test-Path (Join-Path $workerOutput 'results.json'))) { + throw "Standard-user validation produced no results (exit code $($process.ExitCode))." + } + Get-Content (Join-Path $workerOutput 'results.json') + if ($process.ExitCode -ne 0) { throw "Standard-user validation failed (exit code $($process.ExitCode))." } + } + catch { + $_ | Out-String | Set-Content (Join-Path $OutputDirectory 'controller-error.txt') + throw + } + finally { + if ($process -and -not $process.HasExited) { Stop-Process -Id $process.Id -Force } + if ($rootCreated -and (Test-Path "$root\diagnostics")) { + Copy-Item "$root\diagnostics\*" $OutputDirectory -Force + } + if ($userCreated) { Remove-LocalUser -Name $userName } + } + return +} + +$identity = [Security.Principal.WindowsIdentity]::GetCurrent() +$principal = [Security.Principal.WindowsPrincipal]::new($identity) +$isAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +if ($isAdmin) { throw 'Worker must run as a non-elevated standard user.' } + +$start = Get-Date +$results = [Collections.Generic.List[object]]::new() +$failure = $false +$mappedDrive = $false +$packageInstalled = $false +$ownedProcesses = [Collections.Generic.List[Diagnostics.Process]]::new() +$log = Join-Path $OutputDirectory 'worker.log' +Start-Transcript -Path $log | Out-Null + +try { + "Non-elevated user confirmed. Process architecture: $([Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)" + "Session: $((Get-Process -Id $PID).SessionId); interactive: $([Environment]::UserInteractive)" + "This is a reproduction approximation, not Microsoft's private validation harness." + $localAppData = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData) + $workingDirectory = 'C:\TinyClipsValidation\cwd' + New-Item -ItemType Directory -Path $workingDirectory -Force | Out-Null + if (-not (Test-Path 'E:\')) { + & subst.exe E: $workingDirectory + if ($LASTEXITCODE -ne 0 -or -not (Test-Path 'E:\')) { throw 'Unable to provide E: for the working-directory scenarios.' } + $mappedDrive = $true + } + "E: drive: $(if ($mappedDrive) { 'user-scoped SUBST mapping' } else { 'existing runner volume' })" + + Add-Type -TypeDefinition @' +using System; +using System.Runtime.InteropServices; +public static class TinyClipsValidationWindow +{ + [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr window, int command); + [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr window); +} +'@ + + foreach ($scenario in @( + @{ Name = 'unrelated-directory'; Directory = $workingDirectory }, + @{ Name = 'e-root'; Directory = 'E:\' }, + @{ Name = 'e-drive-relative'; Directory = 'E:' } + )) { + $ownedProcesses.Clear() + $scenarioStart = Get-Date + try { + if (Get-AppxPackage -Name Refractored.TinyClips) { throw 'Expected a fresh validation account without Tiny Clips installed.' } + Add-AppxPackage -Path $MsixPath + $packageInstalled = $true + $package = Get-AppxPackage -Name Refractored.TinyClips + if (-not $package -or $package.Version -ne '1.8.2.0') { throw 'The submitted Tiny Clips version did not register.' } + "Scenario $($scenario.Name): $($package.PackageFullName); cwd=$($scenario.Directory)" + + $exe = Join-Path $package.InstallLocation 'TinyClips.App.exe' + foreach ($launch in @('first', 'second')) { + $info = [Diagnostics.ProcessStartInfo]::new() + $info.FileName = $exe + $info.WorkingDirectory = $scenario.Directory + $info.UseShellExecute = $false + $process = [Diagnostics.Process]::Start($info) + $null = $process.Handle + $ownedProcesses.Add($process) + "Launched $launch instance; PID $($process.Id)" + Start-Sleep -Seconds 15 + $process.Refresh() + if (-not $process.HasExited -and $process.MainWindowHandle -ne [IntPtr]::Zero) { + [void][TinyClipsValidationWindow]::ShowWindow($process.MainWindowHandle, 5) + $foreground = [TinyClipsValidationWindow]::SetForegroundWindow($process.MainWindowHandle) + "Foreground request for $launch instance: $foreground" + } + } + $deadline = (Get-Date).AddSeconds($WaitSeconds) + while ((Get-Date) -lt $deadline) { + Start-Sleep -Seconds 2 + if (@($ownedProcesses | Where-Object { -not $_.HasExited }).Count -eq 0) { break } + } + for ($index = 0; $index -lt $ownedProcesses.Count; $index++) { + $process = $ownedProcesses[$index] + $process.Refresh() + $exited = $process.HasExited + $code = if ($exited) { $process.ExitCode } else { $null } + $status = if (-not $exited) { 'alive' } elseif ($index -eq 1 -and $code -eq 0) { 'clean-secondary-exit' } else { 'unexpected-exit' } + if ($status -eq 'unexpected-exit') { $failure = $true } + $results.Add([pscustomobject]@{ + Scenario = $scenario.Name + WorkingDirectory = $scenario.Directory + Launch = if ($index -eq 0) { 'first' } else { 'second' } + Status = $status + ExitCode = $code + ExitCodeHex = if ($exited) { '0x{0:X8}' -f $code } else { $null } + ObservedSeconds = [int]((Get-Date) - $scenarioStart).TotalSeconds + MappedEDrive = $mappedDrive + }) + } + } + catch { + $failure = $true + $results.Add([pscustomobject]@{ Scenario = $scenario.Name; Status = 'harness-or-install-error'; Error = $_.ToString() }) + Write-Warning $_ + } + finally { + foreach ($process in $ownedProcesses) { + if (-not $process.HasExited) { Stop-Process -Id $process.Id -Force } + $process.Dispose() + } + if ($packageInstalled) { + $package = Get-AppxPackage -Name Refractored.TinyClips + if ($package) { + foreach ($crashLog in @( + "$localAppData\TinyClips\Logs\crash.log", + "$localAppData\Packages\$($package.PackageFamilyName)\LocalCache\Local\TinyClips\Logs\crash.log" + )) { + if (Test-Path $crashLog) { + Get-Content $crashLog | Add-Content (Join-Path $OutputDirectory "$($scenario.Name)-crash.log") + } + } + Remove-AppxPackage -Package $package.PackageFullName + } + $packageInstalled = $false + } + } + } +} +catch { + $failure = $true + $results.Add([pscustomobject]@{ Status = 'harness-error'; Error = $_.ToString() }) + Write-Warning $_ +} +finally { + $eventErrors = @() + $events = Get-WinEvent -FilterHashtable @{ LogName = 'Application'; StartTime = $start } ` + -ErrorAction SilentlyContinue -ErrorVariable eventErrors + if ($eventErrors) { + $eventErrors | Out-String | Set-Content (Join-Path $OutputDirectory 'event-read-status.txt') + } + $events | Where-Object { + $_.ProviderName -in @('.NET Runtime', 'Application Error', 'Windows Error Reporting') -and + $_.Message -match 'TinyClips|Tiny Clips' + } | Select-Object TimeCreated, ProviderName, Id, Message | + Format-List | Out-String -Width 240 | Set-Content (Join-Path $OutputDirectory 'application-events.txt') + ConvertTo-Json -InputObject $results.ToArray() -Depth 5 | Set-Content (Join-Path $OutputDirectory 'results.json') + if ($mappedDrive) { + & subst.exe E: /D + if ($LASTEXITCODE -ne 0) { Write-Warning 'Unable to remove the test E: mapping.' } + } + Stop-Transcript | Out-Null +} + +if ($failure) { exit 1 } From c7d2a5f6719c982ee99450611c7750c66ad8f122 Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Mon, 12 Oct 2026 08:53:02 -0700 Subject: [PATCH 3/7] Use shell activation for standard-user packaged executable launches Record the launch method explicitly. The direct CreateProcess experiment was denied before the app started on both architectures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Tools/TinyClipsValidation.ps1 | 457 +++++++++++++++++----------------- 1 file changed, 229 insertions(+), 228 deletions(-) diff --git a/Tools/TinyClipsValidation.ps1 b/Tools/TinyClipsValidation.ps1 index 5885af697e0c8..ade4d2257f11f 100644 --- a/Tools/TinyClipsValidation.ps1 +++ b/Tools/TinyClipsValidation.ps1 @@ -1,228 +1,229 @@ -[CmdletBinding()] -param( - [Parameter(Mandatory)] - [string] $MsixPath, - - [Parameter(Mandatory)] - [string] $OutputDirectory, - - [ValidateRange(20, 120)] - [int] $WaitSeconds = 60, - - [switch] $StandardUser -) - -$ErrorActionPreference = 'Stop' -if ($env:GITHUB_ACTIONS -ne 'true') { - throw 'Run this account-creating diagnostic only on a disposable GitHub-hosted runner.' -} -New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null - -if (-not $StandardUser) { - $root = 'C:\TinyClipsValidation' - $userName = 'TinyClipsValidation' - $process = $null - $userCreated = $false - $rootCreated = $false - try { - if (Test-Path $root) { throw "Refusing to reuse existing directory $root." } - if (Get-LocalUser -Name $userName -ErrorAction SilentlyContinue) { throw "Refusing to reuse existing user $userName." } - New-Item -ItemType Directory -Path $root | Out-Null - $rootCreated = $true - $workerOutput = Join-Path $root 'diagnostics' - New-Item -ItemType Directory -Path $workerOutput | Out-Null - Copy-Item $MsixPath (Join-Path $root 'package.msix') - Copy-Item $PSCommandPath (Join-Path $root 'worker.ps1') - & icacls.exe $root /grant '*S-1-5-32-545:(OI)(CI)M' | Out-Null - if ($LASTEXITCODE -ne 0) { throw 'Unable to grant the validation user access to test files.' } - - $password = "Tc!$([Guid]::NewGuid().ToString('N'))a9" - "::add-mask::$password" - $securePassword = [Security.SecureString]::new() - foreach ($character in $password.ToCharArray()) { $securePassword.AppendChar($character) } - $securePassword.MakeReadOnly() - New-LocalUser -Name $userName -Password $securePassword -AccountNeverExpires | Out-Null - $userCreated = $true - $users = Get-LocalGroup -SID 'S-1-5-32-545' - $user = Get-LocalUser -Name $userName - if (-not (Get-LocalGroupMember -Group $users | Where-Object { $_.SID -eq $user.SID })) { - Add-LocalGroupMember -Group $users -Member $userName - } - $credential = [PSCredential]::new("$env:COMPUTERNAME\$userName", $securePassword) - $pwsh = (Get-Process -Id $PID).Path - $arguments = "-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File `"$root\worker.ps1`" -StandardUser -MsixPath `"$root\package.msix`" -OutputDirectory `"$workerOutput`" -WaitSeconds $WaitSeconds" - $process = Start-Process -FilePath $pwsh -ArgumentList $arguments -Credential $credential ` - -LoadUserProfile -WorkingDirectory $root -PassThru ` - -RedirectStandardOutput (Join-Path $workerOutput 'stdout.log') ` - -RedirectStandardError (Join-Path $workerOutput 'stderr.log') - $null = $process.Handle - if (-not $process.WaitForExit(720000)) { throw 'Standard-user validation exceeded 12 minutes.' } - if (-not (Test-Path (Join-Path $workerOutput 'results.json'))) { - throw "Standard-user validation produced no results (exit code $($process.ExitCode))." - } - Get-Content (Join-Path $workerOutput 'results.json') - if ($process.ExitCode -ne 0) { throw "Standard-user validation failed (exit code $($process.ExitCode))." } - } - catch { - $_ | Out-String | Set-Content (Join-Path $OutputDirectory 'controller-error.txt') - throw - } - finally { - if ($process -and -not $process.HasExited) { Stop-Process -Id $process.Id -Force } - if ($rootCreated -and (Test-Path "$root\diagnostics")) { - Copy-Item "$root\diagnostics\*" $OutputDirectory -Force - } - if ($userCreated) { Remove-LocalUser -Name $userName } - } - return -} - -$identity = [Security.Principal.WindowsIdentity]::GetCurrent() -$principal = [Security.Principal.WindowsPrincipal]::new($identity) -$isAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) -if ($isAdmin) { throw 'Worker must run as a non-elevated standard user.' } - -$start = Get-Date -$results = [Collections.Generic.List[object]]::new() -$failure = $false -$mappedDrive = $false -$packageInstalled = $false -$ownedProcesses = [Collections.Generic.List[Diagnostics.Process]]::new() -$log = Join-Path $OutputDirectory 'worker.log' -Start-Transcript -Path $log | Out-Null - -try { - "Non-elevated user confirmed. Process architecture: $([Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)" - "Session: $((Get-Process -Id $PID).SessionId); interactive: $([Environment]::UserInteractive)" - "This is a reproduction approximation, not Microsoft's private validation harness." - $localAppData = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData) - $workingDirectory = 'C:\TinyClipsValidation\cwd' - New-Item -ItemType Directory -Path $workingDirectory -Force | Out-Null - if (-not (Test-Path 'E:\')) { - & subst.exe E: $workingDirectory - if ($LASTEXITCODE -ne 0 -or -not (Test-Path 'E:\')) { throw 'Unable to provide E: for the working-directory scenarios.' } - $mappedDrive = $true - } - "E: drive: $(if ($mappedDrive) { 'user-scoped SUBST mapping' } else { 'existing runner volume' })" - - Add-Type -TypeDefinition @' -using System; -using System.Runtime.InteropServices; -public static class TinyClipsValidationWindow -{ - [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr window, int command); - [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr window); -} -'@ - - foreach ($scenario in @( - @{ Name = 'unrelated-directory'; Directory = $workingDirectory }, - @{ Name = 'e-root'; Directory = 'E:\' }, - @{ Name = 'e-drive-relative'; Directory = 'E:' } - )) { - $ownedProcesses.Clear() - $scenarioStart = Get-Date - try { - if (Get-AppxPackage -Name Refractored.TinyClips) { throw 'Expected a fresh validation account without Tiny Clips installed.' } - Add-AppxPackage -Path $MsixPath - $packageInstalled = $true - $package = Get-AppxPackage -Name Refractored.TinyClips - if (-not $package -or $package.Version -ne '1.8.2.0') { throw 'The submitted Tiny Clips version did not register.' } - "Scenario $($scenario.Name): $($package.PackageFullName); cwd=$($scenario.Directory)" - - $exe = Join-Path $package.InstallLocation 'TinyClips.App.exe' - foreach ($launch in @('first', 'second')) { - $info = [Diagnostics.ProcessStartInfo]::new() - $info.FileName = $exe - $info.WorkingDirectory = $scenario.Directory - $info.UseShellExecute = $false - $process = [Diagnostics.Process]::Start($info) - $null = $process.Handle - $ownedProcesses.Add($process) - "Launched $launch instance; PID $($process.Id)" - Start-Sleep -Seconds 15 - $process.Refresh() - if (-not $process.HasExited -and $process.MainWindowHandle -ne [IntPtr]::Zero) { - [void][TinyClipsValidationWindow]::ShowWindow($process.MainWindowHandle, 5) - $foreground = [TinyClipsValidationWindow]::SetForegroundWindow($process.MainWindowHandle) - "Foreground request for $launch instance: $foreground" - } - } - $deadline = (Get-Date).AddSeconds($WaitSeconds) - while ((Get-Date) -lt $deadline) { - Start-Sleep -Seconds 2 - if (@($ownedProcesses | Where-Object { -not $_.HasExited }).Count -eq 0) { break } - } - for ($index = 0; $index -lt $ownedProcesses.Count; $index++) { - $process = $ownedProcesses[$index] - $process.Refresh() - $exited = $process.HasExited - $code = if ($exited) { $process.ExitCode } else { $null } - $status = if (-not $exited) { 'alive' } elseif ($index -eq 1 -and $code -eq 0) { 'clean-secondary-exit' } else { 'unexpected-exit' } - if ($status -eq 'unexpected-exit') { $failure = $true } - $results.Add([pscustomobject]@{ - Scenario = $scenario.Name - WorkingDirectory = $scenario.Directory - Launch = if ($index -eq 0) { 'first' } else { 'second' } - Status = $status - ExitCode = $code - ExitCodeHex = if ($exited) { '0x{0:X8}' -f $code } else { $null } - ObservedSeconds = [int]((Get-Date) - $scenarioStart).TotalSeconds - MappedEDrive = $mappedDrive - }) - } - } - catch { - $failure = $true - $results.Add([pscustomobject]@{ Scenario = $scenario.Name; Status = 'harness-or-install-error'; Error = $_.ToString() }) - Write-Warning $_ - } - finally { - foreach ($process in $ownedProcesses) { - if (-not $process.HasExited) { Stop-Process -Id $process.Id -Force } - $process.Dispose() - } - if ($packageInstalled) { - $package = Get-AppxPackage -Name Refractored.TinyClips - if ($package) { - foreach ($crashLog in @( - "$localAppData\TinyClips\Logs\crash.log", - "$localAppData\Packages\$($package.PackageFamilyName)\LocalCache\Local\TinyClips\Logs\crash.log" - )) { - if (Test-Path $crashLog) { - Get-Content $crashLog | Add-Content (Join-Path $OutputDirectory "$($scenario.Name)-crash.log") - } - } - Remove-AppxPackage -Package $package.PackageFullName - } - $packageInstalled = $false - } - } - } -} -catch { - $failure = $true - $results.Add([pscustomobject]@{ Status = 'harness-error'; Error = $_.ToString() }) - Write-Warning $_ -} -finally { - $eventErrors = @() - $events = Get-WinEvent -FilterHashtable @{ LogName = 'Application'; StartTime = $start } ` - -ErrorAction SilentlyContinue -ErrorVariable eventErrors - if ($eventErrors) { - $eventErrors | Out-String | Set-Content (Join-Path $OutputDirectory 'event-read-status.txt') - } - $events | Where-Object { - $_.ProviderName -in @('.NET Runtime', 'Application Error', 'Windows Error Reporting') -and - $_.Message -match 'TinyClips|Tiny Clips' - } | Select-Object TimeCreated, ProviderName, Id, Message | - Format-List | Out-String -Width 240 | Set-Content (Join-Path $OutputDirectory 'application-events.txt') - ConvertTo-Json -InputObject $results.ToArray() -Depth 5 | Set-Content (Join-Path $OutputDirectory 'results.json') - if ($mappedDrive) { - & subst.exe E: /D - if ($LASTEXITCODE -ne 0) { Write-Warning 'Unable to remove the test E: mapping.' } - } - Stop-Transcript | Out-Null -} - -if ($failure) { exit 1 } +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string] $MsixPath, + + [Parameter(Mandatory)] + [string] $OutputDirectory, + + [ValidateRange(20, 120)] + [int] $WaitSeconds = 60, + + [switch] $StandardUser +) + +$ErrorActionPreference = 'Stop' +if ($env:GITHUB_ACTIONS -ne 'true') { + throw 'Run this account-creating diagnostic only on a disposable GitHub-hosted runner.' +} +New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null + +if (-not $StandardUser) { + $root = 'C:\TinyClipsValidation' + $userName = 'TinyClipsValidation' + $process = $null + $userCreated = $false + $rootCreated = $false + try { + if (Test-Path $root) { throw "Refusing to reuse existing directory $root." } + if (Get-LocalUser -Name $userName -ErrorAction SilentlyContinue) { throw "Refusing to reuse existing user $userName." } + New-Item -ItemType Directory -Path $root | Out-Null + $rootCreated = $true + $workerOutput = Join-Path $root 'diagnostics' + New-Item -ItemType Directory -Path $workerOutput | Out-Null + Copy-Item $MsixPath (Join-Path $root 'package.msix') + Copy-Item $PSCommandPath (Join-Path $root 'worker.ps1') + & icacls.exe $root /grant '*S-1-5-32-545:(OI)(CI)M' | Out-Null + if ($LASTEXITCODE -ne 0) { throw 'Unable to grant the validation user access to test files.' } + + $password = "Tc!$([Guid]::NewGuid().ToString('N'))a9" + "::add-mask::$password" + $securePassword = [Security.SecureString]::new() + foreach ($character in $password.ToCharArray()) { $securePassword.AppendChar($character) } + $securePassword.MakeReadOnly() + New-LocalUser -Name $userName -Password $securePassword -AccountNeverExpires | Out-Null + $userCreated = $true + $users = Get-LocalGroup -SID 'S-1-5-32-545' + $user = Get-LocalUser -Name $userName + if (-not (Get-LocalGroupMember -Group $users | Where-Object { $_.SID -eq $user.SID })) { + Add-LocalGroupMember -Group $users -Member $userName + } + $credential = [PSCredential]::new("$env:COMPUTERNAME\$userName", $securePassword) + $pwsh = (Get-Process -Id $PID).Path + $arguments = "-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File `"$root\worker.ps1`" -StandardUser -MsixPath `"$root\package.msix`" -OutputDirectory `"$workerOutput`" -WaitSeconds $WaitSeconds" + $process = Start-Process -FilePath $pwsh -ArgumentList $arguments -Credential $credential ` + -LoadUserProfile -WorkingDirectory $root -PassThru ` + -RedirectStandardOutput (Join-Path $workerOutput 'stdout.log') ` + -RedirectStandardError (Join-Path $workerOutput 'stderr.log') + $null = $process.Handle + if (-not $process.WaitForExit(720000)) { throw 'Standard-user validation exceeded 12 minutes.' } + if (-not (Test-Path (Join-Path $workerOutput 'results.json'))) { + throw "Standard-user validation produced no results (exit code $($process.ExitCode))." + } + Get-Content (Join-Path $workerOutput 'results.json') + if ($process.ExitCode -ne 0) { throw "Standard-user validation failed (exit code $($process.ExitCode))." } + } + catch { + $_ | Out-String | Set-Content (Join-Path $OutputDirectory 'controller-error.txt') + throw + } + finally { + if ($process -and -not $process.HasExited) { Stop-Process -Id $process.Id -Force } + if ($rootCreated -and (Test-Path "$root\diagnostics")) { + Copy-Item "$root\diagnostics\*" $OutputDirectory -Force + } + if ($userCreated) { Remove-LocalUser -Name $userName } + } + return +} + +$identity = [Security.Principal.WindowsIdentity]::GetCurrent() +$principal = [Security.Principal.WindowsPrincipal]::new($identity) +$isAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +if ($isAdmin) { throw 'Worker must run as a non-elevated standard user.' } + +$start = Get-Date +$results = [Collections.Generic.List[object]]::new() +$failure = $false +$mappedDrive = $false +$packageInstalled = $false +$ownedProcesses = [Collections.Generic.List[Diagnostics.Process]]::new() +$log = Join-Path $OutputDirectory 'worker.log' +Start-Transcript -Path $log | Out-Null + +try { + "Non-elevated user confirmed. Process architecture: $([Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)" + "Session: $((Get-Process -Id $PID).SessionId); interactive: $([Environment]::UserInteractive)" + "This is a reproduction approximation, not Microsoft's private validation harness." + $localAppData = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData) + $workingDirectory = 'C:\TinyClipsValidation\cwd' + New-Item -ItemType Directory -Path $workingDirectory -Force | Out-Null + if (-not (Test-Path 'E:\')) { + & subst.exe E: $workingDirectory + if ($LASTEXITCODE -ne 0 -or -not (Test-Path 'E:\')) { throw 'Unable to provide E: for the working-directory scenarios.' } + $mappedDrive = $true + } + "E: drive: $(if ($mappedDrive) { 'user-scoped SUBST mapping' } else { 'existing runner volume' })" + + Add-Type -TypeDefinition @' +using System; +using System.Runtime.InteropServices; +public static class TinyClipsValidationWindow +{ + [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr window, int command); + [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr window); +} +'@ + + foreach ($scenario in @( + @{ Name = 'unrelated-directory'; Directory = $workingDirectory }, + @{ Name = 'e-root'; Directory = 'E:\' }, + @{ Name = 'e-drive-relative'; Directory = 'E:' } + )) { + $ownedProcesses.Clear() + $scenarioStart = Get-Date + try { + if (Get-AppxPackage -Name Refractored.TinyClips) { throw 'Expected a fresh validation account without Tiny Clips installed.' } + Add-AppxPackage -Path $MsixPath + $packageInstalled = $true + $package = Get-AppxPackage -Name Refractored.TinyClips + if (-not $package -or $package.Version -ne '1.8.2.0') { throw 'The submitted Tiny Clips version did not register.' } + "Scenario $($scenario.Name): $($package.PackageFullName); cwd=$($scenario.Directory)" + + $exe = Join-Path $package.InstallLocation 'TinyClips.App.exe' + foreach ($launch in @('first', 'second')) { + $info = [Diagnostics.ProcessStartInfo]::new() + $info.FileName = $exe + $info.WorkingDirectory = $scenario.Directory + $info.UseShellExecute = $true + $process = [Diagnostics.Process]::Start($info) + $null = $process.Handle + $ownedProcesses.Add($process) + "Launched $launch instance; PID $($process.Id)" + Start-Sleep -Seconds 15 + $process.Refresh() + if (-not $process.HasExited -and $process.MainWindowHandle -ne [IntPtr]::Zero) { + [void][TinyClipsValidationWindow]::ShowWindow($process.MainWindowHandle, 5) + $foreground = [TinyClipsValidationWindow]::SetForegroundWindow($process.MainWindowHandle) + "Foreground request for $launch instance: $foreground" + } + } + $deadline = (Get-Date).AddSeconds($WaitSeconds) + while ((Get-Date) -lt $deadline) { + Start-Sleep -Seconds 2 + if (@($ownedProcesses | Where-Object { -not $_.HasExited }).Count -eq 0) { break } + } + for ($index = 0; $index -lt $ownedProcesses.Count; $index++) { + $process = $ownedProcesses[$index] + $process.Refresh() + $exited = $process.HasExited + $code = if ($exited) { $process.ExitCode } else { $null } + $status = if (-not $exited) { 'alive' } elseif ($index -eq 1 -and $code -eq 0) { 'clean-secondary-exit' } else { 'unexpected-exit' } + if ($status -eq 'unexpected-exit') { $failure = $true } + $results.Add([pscustomobject]@{ + Scenario = $scenario.Name + WorkingDirectory = $scenario.Directory + LaunchMethod = 'ShellExecute' + Launch = if ($index -eq 0) { 'first' } else { 'second' } + Status = $status + ExitCode = $code + ExitCodeHex = if ($exited) { '0x{0:X8}' -f $code } else { $null } + ObservedSeconds = [int]((Get-Date) - $scenarioStart).TotalSeconds + MappedEDrive = $mappedDrive + }) + } + } + catch { + $failure = $true + $results.Add([pscustomobject]@{ Scenario = $scenario.Name; Status = 'harness-or-install-error'; Error = $_.ToString() }) + Write-Warning $_ + } + finally { + foreach ($process in $ownedProcesses) { + if (-not $process.HasExited) { Stop-Process -Id $process.Id -Force } + $process.Dispose() + } + if ($packageInstalled) { + $package = Get-AppxPackage -Name Refractored.TinyClips + if ($package) { + foreach ($crashLog in @( + "$localAppData\TinyClips\Logs\crash.log", + "$localAppData\Packages\$($package.PackageFamilyName)\LocalCache\Local\TinyClips\Logs\crash.log" + )) { + if (Test-Path $crashLog) { + Get-Content $crashLog | Add-Content (Join-Path $OutputDirectory "$($scenario.Name)-crash.log") + } + } + Remove-AppxPackage -Package $package.PackageFullName + } + $packageInstalled = $false + } + } + } +} +catch { + $failure = $true + $results.Add([pscustomobject]@{ Status = 'harness-error'; Error = $_.ToString() }) + Write-Warning $_ +} +finally { + $eventErrors = @() + $events = Get-WinEvent -FilterHashtable @{ LogName = 'Application'; StartTime = $start } ` + -ErrorAction SilentlyContinue -ErrorVariable eventErrors + if ($eventErrors) { + $eventErrors | Out-String | Set-Content (Join-Path $OutputDirectory 'event-read-status.txt') + } + $events | Where-Object { + $_.ProviderName -in @('.NET Runtime', 'Application Error', 'Windows Error Reporting') -and + $_.Message -match 'TinyClips|Tiny Clips' + } | Select-Object TimeCreated, ProviderName, Id, Message | + Format-List | Out-String -Width 240 | Set-Content (Join-Path $OutputDirectory 'application-events.txt') + ConvertTo-Json -InputObject $results.ToArray() -Depth 5 | Set-Content (Join-Path $OutputDirectory 'results.json') + if ($mappedDrive) { + & subst.exe E: /D + if ($LASTEXITCODE -ne 0) { Write-Warning 'Unable to remove the test E: mapping.' } + } + Stop-Transcript | Out-Null +} + +if ($failure) { exit 1 } From 04a2944b4d10c803fd70eb4ff71206692c2f549e Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Mon, 12 Oct 2026 08:57:39 -0700 Subject: [PATCH 4/7] Separate registered-app activation and runner-account controls Keep standard-user launch-denied results visible and independent of the elevated runner control. Do not alter WindowsApps permissions or claim an AUMID launch tests a specified working directory. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../workflows/tinyclips-arm64-validation.yml | 145 ++++++++++-------- Tools/TinyClipsValidation.ps1 | 65 ++++++-- 2 files changed, 130 insertions(+), 80 deletions(-) diff --git a/.github/workflows/tinyclips-arm64-validation.yml b/.github/workflows/tinyclips-arm64-validation.yml index 38a1b7aee27c4..370e757864bef 100644 --- a/.github/workflows/tinyclips-arm64-validation.yml +++ b/.github/workflows/tinyclips-arm64-validation.yml @@ -1,68 +1,77 @@ -name: Tiny Clips winget reproduction - -on: - push: - branches: - - diagnostics/tinyclips-arm64-validation - paths: - - .github/workflows/tinyclips-arm64-validation.yml - - Tools/TinyClipsValidation.ps1 - -permissions: - contents: read - -jobs: - reproduce: - if: github.repository == 'jamesmontemagno/winget-pkgs' - name: Standard-user launch (${{ matrix.arch }}) - runs-on: ${{ matrix.runner }} - timeout-minutes: 20 - strategy: - fail-fast: false - matrix: - include: - - arch: arm64 - runner: windows-11-arm - - arch: x64 - runner: windows-latest - env: - ARCH: ${{ matrix.arch }} - steps: - - uses: actions/checkout@v6 - with: - sparse-checkout: | - Tools/TinyClipsValidation.ps1 - manifests/r/Refractored/TinyClips/1.8.2.0 - sparse-checkout-cone-mode: false - persist-credentials: false - - - name: Download and verify the submitted signed package - shell: pwsh - run: | - $manifest = Get-Content 'manifests\r\Refractored\TinyClips\1.8.2.0\Refractored.TinyClips.installer.yaml' -Raw - $block = [regex]::Match($manifest, "(?ms)^- Architecture: $env:ARCH\r?\n(?.*?)(?=^- Architecture:|\z)") - if (-not $block.Success) { throw "No $env:ARCH installer in the submitted manifest." } - $url = [regex]::Match($block.Groups['installer'].Value, '(?m)^\s+InstallerUrl:\s*(\S+)').Groups[1].Value - $expected = [regex]::Match($block.Groups['installer'].Value, '(?m)^\s+InstallerSha256:\s*([a-fA-F0-9]{64})').Groups[1].Value - if (-not $url -or -not $expected) { throw 'Installer URL or hash is missing.' } - Invoke-WebRequest -Uri $url -OutFile package.msix - if ((Get-FileHash package.msix -Algorithm SHA256).Hash -ine $expected) { throw 'Package SHA256 does not match the submitted manifest.' } - "Testing signed 1.8.2.0 $env:ARCH package; SHA256 verified." - - - name: Install and exercise launches as a standard user - shell: pwsh - run: | - & .\Tools\TinyClipsValidation.ps1 -MsixPath (Resolve-Path package.msix).Path ` - -OutputDirectory (Join-Path $env:GITHUB_WORKSPACE 'diagnostics') -WaitSeconds 60 - - - name: Upload text diagnostics only - if: always() - uses: actions/upload-artifact@v7 - with: - name: tinyclips-validation-${{ matrix.arch }} - path: | - diagnostics/*.json - diagnostics/*.log - diagnostics/*.txt - if-no-files-found: warn - retention-days: 7 +name: Tiny Clips winget reproduction + +on: + push: + branches: + - diagnostics/tinyclips-arm64-validation + paths: + - .github/workflows/tinyclips-arm64-validation.yml + - Tools/TinyClipsValidation.ps1 + +permissions: + contents: read + +jobs: + reproduce: + if: github.repository == 'jamesmontemagno/winget-pkgs' + name: Standard-user launch (${{ matrix.arch }}) + runs-on: ${{ matrix.runner }} + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + include: + - arch: arm64 + runner: windows-11-arm + - arch: x64 + runner: windows-latest + env: + ARCH: ${{ matrix.arch }} + steps: + - uses: actions/checkout@v6 + with: + sparse-checkout: | + Tools/TinyClipsValidation.ps1 + manifests/r/Refractored/TinyClips/1.8.2.0 + sparse-checkout-cone-mode: false + persist-credentials: false + + - name: Download and verify the submitted signed package + id: package + shell: pwsh + run: | + $manifest = Get-Content 'manifests\r\Refractored\TinyClips\1.8.2.0\Refractored.TinyClips.installer.yaml' -Raw + $block = [regex]::Match($manifest, "(?ms)^- Architecture: $env:ARCH\r?\n(?.*?)(?=^- Architecture:|\z)") + if (-not $block.Success) { throw "No $env:ARCH installer in the submitted manifest." } + $url = [regex]::Match($block.Groups['installer'].Value, '(?m)^\s+InstallerUrl:\s*(\S+)').Groups[1].Value + $expected = [regex]::Match($block.Groups['installer'].Value, '(?m)^\s+InstallerSha256:\s*([a-fA-F0-9]{64})').Groups[1].Value + if (-not $url -or -not $expected) { throw 'Installer URL or hash is missing.' } + Invoke-WebRequest -Uri $url -OutFile package.msix + if ((Get-FileHash package.msix -Algorithm SHA256).Hash -ine $expected) { throw 'Package SHA256 does not match the submitted manifest.' } + "Testing signed 1.8.2.0 $env:ARCH package; SHA256 verified." + + - name: Install and exercise launches as a standard user + shell: pwsh + run: | + & .\Tools\TinyClipsValidation.ps1 -MsixPath (Resolve-Path package.msix).Path ` + -OutputDirectory (Join-Path $env:GITHUB_WORKSPACE 'diagnostics\standard-user') -WaitSeconds 60 + + - name: Runner-account control (not a standard-user result) + if: always() && steps.package.outcome == 'success' + shell: pwsh + run: | + & .\Tools\TinyClipsValidation.ps1 -StandardUser -RunnerControl ` + -MsixPath (Resolve-Path package.msix).Path ` + -OutputDirectory (Join-Path $env:GITHUB_WORKSPACE 'diagnostics\runner-control') -WaitSeconds 60 + + - name: Upload text diagnostics only + if: always() + uses: actions/upload-artifact@v7 + with: + name: tinyclips-validation-${{ matrix.arch }} + path: | + diagnostics/**/*.json + diagnostics/**/*.log + diagnostics/**/*.txt + if-no-files-found: warn + retention-days: 7 diff --git a/Tools/TinyClipsValidation.ps1 b/Tools/TinyClipsValidation.ps1 index ade4d2257f11f..dbb2e682303d8 100644 --- a/Tools/TinyClipsValidation.ps1 +++ b/Tools/TinyClipsValidation.ps1 @@ -9,13 +9,16 @@ param( [ValidateRange(20, 120)] [int] $WaitSeconds = 60, - [switch] $StandardUser + [switch] $StandardUser, + + [switch] $RunnerControl ) $ErrorActionPreference = 'Stop' if ($env:GITHUB_ACTIONS -ne 'true') { throw 'Run this account-creating diagnostic only on a disposable GitHub-hosted runner.' } +if ($RunnerControl -and -not $StandardUser) { throw 'RunnerControl is only valid for the worker mode.' } New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null if (-not $StandardUser) { @@ -50,7 +53,7 @@ if (-not $StandardUser) { } $credential = [PSCredential]::new("$env:COMPUTERNAME\$userName", $securePassword) $pwsh = (Get-Process -Id $PID).Path - $arguments = "-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File `"$root\worker.ps1`" -StandardUser -MsixPath `"$root\package.msix`" -OutputDirectory `"$workerOutput`" -WaitSeconds $WaitSeconds" + $arguments = "-NoLogo -NoProfile -NonInteractive -STA -ExecutionPolicy Bypass -File `"$root\worker.ps1`" -StandardUser -MsixPath `"$root\package.msix`" -OutputDirectory `"$workerOutput`" -WaitSeconds $WaitSeconds" $process = Start-Process -FilePath $pwsh -ArgumentList $arguments -Credential $credential ` -LoadUserProfile -WorkingDirectory $root -PassThru ` -RedirectStandardOutput (Join-Path $workerOutput 'stdout.log') ` @@ -80,7 +83,7 @@ if (-not $StandardUser) { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = [Security.Principal.WindowsPrincipal]::new($identity) $isAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) -if ($isAdmin) { throw 'Worker must run as a non-elevated standard user.' } +if ($isAdmin -and -not $RunnerControl) { throw 'Worker must run as a non-elevated standard user.' } $start = Get-Date $results = [Collections.Generic.List[object]]::new() @@ -92,7 +95,8 @@ $log = Join-Path $OutputDirectory 'worker.log' Start-Transcript -Path $log | Out-Null try { - "Non-elevated user confirmed. Process architecture: $([Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)" + "Account mode: $(if ($RunnerControl) { 'runner-control' } else { 'standard-user' }); elevated: $isAdmin" + "Process architecture: $([Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)" "Session: $((Get-Process -Id $PID).SessionId); interactive: $([Environment]::UserInteractive)" "This is a reproduction approximation, not Microsoft's private validation harness." $localAppData = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData) @@ -112,13 +116,36 @@ public static class TinyClipsValidationWindow { [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr window, int command); [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr window); + + [ComImport, Guid("2E941141-7F97-4756-BA1D-9DECDE894A3D"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + private interface IActivationManager + { + [PreserveSig] int ActivateApplication( + [MarshalAs(UnmanagedType.LPWStr)] string appId, + [MarshalAs(UnmanagedType.LPWStr)] string arguments, + uint options, out uint processId); + } + + public static uint Activate(string appId) + { + var type = Type.GetTypeFromCLSID(new Guid("45BA127D-10A8-46EA-8AB7-56EA9078943C"), true); + var manager = (IActivationManager)Activator.CreateInstance(type); + try + { + uint processId; + Marshal.ThrowExceptionForHR(manager.ActivateApplication(appId, null, 2, out processId)); + return processId; + } + finally { Marshal.ReleaseComObject(manager); } + } } '@ foreach ($scenario in @( @{ Name = 'unrelated-directory'; Directory = $workingDirectory }, @{ Name = 'e-root'; Directory = 'E:\' }, - @{ Name = 'e-drive-relative'; Directory = 'E:' } + @{ Name = 'e-drive-relative'; Directory = 'E:' }, + @{ Name = 'registered-activation-control'; Directory = $null } )) { $ownedProcesses.Clear() $scenarioStart = Get-Date @@ -131,12 +158,20 @@ public static class TinyClipsValidationWindow "Scenario $($scenario.Name): $($package.PackageFullName); cwd=$($scenario.Directory)" $exe = Join-Path $package.InstallLocation 'TinyClips.App.exe' + $launchMethod = if ($scenario.Directory) { 'ShellExecute' } else { 'registered-AUMID' } foreach ($launch in @('first', 'second')) { - $info = [Diagnostics.ProcessStartInfo]::new() - $info.FileName = $exe - $info.WorkingDirectory = $scenario.Directory - $info.UseShellExecute = $true - $process = [Diagnostics.Process]::Start($info) + if ($scenario.Directory) { + $info = [Diagnostics.ProcessStartInfo]::new() + $info.FileName = $exe + $info.WorkingDirectory = $scenario.Directory + $info.UseShellExecute = $true + $process = [Diagnostics.Process]::Start($info) + } + else { + $appId = "$($package.PackageFamilyName)!App" + $activatedPid = [TinyClipsValidationWindow]::Activate($appId) + $process = Get-Process -Id $activatedPid + } $null = $process.Handle $ownedProcesses.Add($process) "Launched $launch instance; PID $($process.Id)" @@ -163,7 +198,8 @@ public static class TinyClipsValidationWindow $results.Add([pscustomobject]@{ Scenario = $scenario.Name WorkingDirectory = $scenario.Directory - LaunchMethod = 'ShellExecute' + AccountMode = if ($RunnerControl) { 'runner-control' } else { 'standard-user' } + LaunchMethod = $launchMethod Launch = if ($index -eq 0) { 'first' } else { 'second' } Status = $status ExitCode = $code @@ -175,7 +211,12 @@ public static class TinyClipsValidationWindow } catch { $failure = $true - $results.Add([pscustomobject]@{ Scenario = $scenario.Name; Status = 'harness-or-install-error'; Error = $_.ToString() }) + $results.Add([pscustomobject]@{ + Scenario = $scenario.Name + AccountMode = if ($RunnerControl) { 'runner-control' } else { 'standard-user' } + Status = 'harness-or-install-error' + Error = $_.ToString() + }) Write-Warning $_ } finally { From 0d62a5e6d2df2b16b15b43ffae40d598503b5c43 Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Mon, 12 Oct 2026 09:13:30 -0700 Subject: [PATCH 5/7] Run non-admin validation in the existing interactive logon session Use a restricted medium-integrity token rather than an alternate account with no interactive logon. Prepare the runner desktop, retain elevated controls, and leave package permissions unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../workflows/tinyclips-arm64-validation.yml | 12 ++ Tools/TinyClipsValidation.ps1 | 166 +++++++++++++++--- 2 files changed, 154 insertions(+), 24 deletions(-) diff --git a/.github/workflows/tinyclips-arm64-validation.yml b/.github/workflows/tinyclips-arm64-validation.yml index 370e757864bef..6e8b5c4ba7e4e 100644 --- a/.github/workflows/tinyclips-arm64-validation.yml +++ b/.github/workflows/tinyclips-arm64-validation.yml @@ -50,6 +50,18 @@ jobs: if ((Get-FileHash package.msix -Algorithm SHA256).Hash -ine $expected) { throw 'Package SHA256 does not match the submitted manifest.' } "Testing signed 1.8.2.0 $env:ARCH package; SHA256 verified." + - name: Prepare the interactive runner desktop + shell: pwsh + run: | + $session = (Get-Process -Id $PID).SessionId + Get-Process | Where-Object { + $_.SessionId -eq $session -and $_.ProcessName -in @('msoobe', 'CloudExperienceHost', 'WWAHost') + } | ForEach-Object { Stop-Process -Id $_.Id -Force } + if (-not (Get-Process explorer -ErrorAction SilentlyContinue | Where-Object { $_.SessionId -eq $session })) { + Start-Process "$env:WINDIR\explorer.exe" + } + Start-Sleep -Seconds 15 + - name: Install and exercise launches as a standard user shell: pwsh run: | diff --git a/Tools/TinyClipsValidation.ps1 b/Tools/TinyClipsValidation.ps1 index dbb2e682303d8..07e3e9b854c65 100644 --- a/Tools/TinyClipsValidation.ps1 +++ b/Tools/TinyClipsValidation.ps1 @@ -15,49 +15,165 @@ param( ) $ErrorActionPreference = 'Stop' -if ($env:GITHUB_ACTIONS -ne 'true') { - throw 'Run this account-creating diagnostic only on a disposable GitHub-hosted runner.' +if ($env:GITHUB_ACTIONS -ne 'true' -or $env:RUNNER_ENVIRONMENT -ne 'github-hosted') { + throw 'Run this diagnostic only on a disposable GitHub-hosted runner.' } if ($RunnerControl -and -not $StandardUser) { throw 'RunnerControl is only valid for the worker mode.' } New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null +Add-Type -TypeDefinition @' +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; + +public static class TinyClipsLimitedProcess +{ + [StructLayout(LayoutKind.Sequential)] + private struct SidAndAttributes { public IntPtr Sid; public uint Attributes; } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct StartupInfo + { + public uint Size; + public string Reserved, Desktop, Title; + public uint X, Y, XSize, YSize, XCountChars, YCountChars, FillAttribute, Flags; + public ushort ShowWindow, ReservedSize; + public IntPtr ReservedPointer, Input, Output, Error; + } + + [StructLayout(LayoutKind.Sequential)] + private struct ProcessInformation + { + public IntPtr Process, Thread; + public uint ProcessId, ThreadId; + } + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern bool OpenProcessToken(IntPtr process, uint access, out IntPtr token); + [DllImport("advapi32.dll", SetLastError = true)] + private static extern bool CreateRestrictedToken(IntPtr original, uint flags, uint disableCount, + SidAndAttributes[] disabled, uint deleteCount, IntPtr deleted, uint restrictCount, + IntPtr restricted, out IntPtr token); + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern bool ConvertStringSidToSid(string sid, out IntPtr result); + [DllImport("advapi32.dll", SetLastError = true)] + private static extern uint GetLengthSid(IntPtr sid); + [DllImport("advapi32.dll", SetLastError = true)] + private static extern bool CopySid(uint length, IntPtr destination, IntPtr source); + [DllImport("advapi32.dll", SetLastError = true)] + private static extern bool SetTokenInformation(IntPtr token, int kind, IntPtr data, uint size); + [DllImport("advapi32.dll", SetLastError = true)] + private static extern bool GetTokenInformation(IntPtr token, int kind, IntPtr data, uint size, out uint needed); + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern bool CreateProcessWithTokenW(IntPtr token, uint logonFlags, string application, + StringBuilder commandLine, uint flags, IntPtr environment, string directory, + ref StartupInfo startup, out ProcessInformation process); + [DllImport("kernel32.dll")] + private static extern IntPtr GetCurrentProcess(); + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool CloseHandle(IntPtr handle); + [DllImport("kernel32.dll")] + private static extern IntPtr LocalFree(IntPtr memory); + + private static void Check(bool success) + { + if (!success) { throw new Win32Exception(Marshal.GetLastWin32Error()); } + } + + public static uint IntegrityLevel() + { + IntPtr token = IntPtr.Zero, buffer = IntPtr.Zero; + try + { + Check(OpenProcessToken(GetCurrentProcess(), 0x8, out token)); + uint size; + GetTokenInformation(token, 25, IntPtr.Zero, 0, out size); + if (size == 0) { throw new Win32Exception(Marshal.GetLastWin32Error()); } + buffer = Marshal.AllocHGlobal(checked((int)size)); + Check(GetTokenInformation(token, 25, buffer, size, out size)); + var sid = Marshal.PtrToStructure(buffer).Sid; + int count = Marshal.ReadByte(sid, 1); + return unchecked((uint)Marshal.ReadInt32(sid, 8 + (count - 1) * 4)); + } + finally + { + if (buffer != IntPtr.Zero) { Marshal.FreeHGlobal(buffer); } + if (token != IntPtr.Zero) { CloseHandle(token); } + } + } + + public static Process Start(string executable, string arguments, string directory) + { + IntPtr original = IntPtr.Zero, limited = IntPtr.Zero, admins = IntPtr.Zero; + IntPtr medium = IntPtr.Zero, label = IntPtr.Zero, environment = IntPtr.Zero; + ProcessInformation process = default(ProcessInformation); + try + { + Check(OpenProcessToken(GetCurrentProcess(), 0xF01FF, out original)); + Check(ConvertStringSidToSid("S-1-5-32-544", out admins)); + var disabled = new[] { new SidAndAttributes { Sid = admins } }; + Check(CreateRestrictedToken(original, 5, 1, disabled, 0, IntPtr.Zero, 0, IntPtr.Zero, out limited)); + Check(ConvertStringSidToSid("S-1-16-8192", out medium)); + int structureSize = Marshal.SizeOf(); + uint sidSize = GetLengthSid(medium); + int labelSize = checked(structureSize + (int)sidSize); + label = Marshal.AllocHGlobal(labelSize); + var copiedSid = IntPtr.Add(label, structureSize); + Check(CopySid(sidSize, copiedSid, medium)); + Marshal.StructureToPtr(new SidAndAttributes { Sid = copiedSid, Attributes = 0x20 }, label, false); + Check(SetTokenInformation(limited, 25, label, checked((uint)labelSize))); + var variables = new List(); + foreach (DictionaryEntry variable in Environment.GetEnvironmentVariables()) + { + variables.Add(variable.Key + "=" + variable.Value); + } + variables.Sort(StringComparer.OrdinalIgnoreCase); + environment = Marshal.StringToHGlobalUni(string.Join("\0", variables) + "\0\0"); + var startup = new StartupInfo { Size = checked((uint)Marshal.SizeOf()) }; + var commandLine = new StringBuilder("\"" + executable + "\" " + arguments); + Check(CreateProcessWithTokenW(limited, 0, executable, commandLine, 0x08000400, + environment, directory, ref startup, out process)); + return Process.GetProcessById(checked((int)process.ProcessId)); + } + finally + { + if (process.Thread != IntPtr.Zero) { CloseHandle(process.Thread); } + if (process.Process != IntPtr.Zero) { CloseHandle(process.Process); } + if (environment != IntPtr.Zero) { Marshal.FreeHGlobal(environment); } + if (label != IntPtr.Zero) { Marshal.FreeHGlobal(label); } + if (medium != IntPtr.Zero) { LocalFree(medium); } + if (admins != IntPtr.Zero) { LocalFree(admins); } + if (limited != IntPtr.Zero) { CloseHandle(limited); } + if (original != IntPtr.Zero) { CloseHandle(original); } + } + } +} +'@ + if (-not $StandardUser) { $root = 'C:\TinyClipsValidation' - $userName = 'TinyClipsValidation' $process = $null - $userCreated = $false $rootCreated = $false try { if (Test-Path $root) { throw "Refusing to reuse existing directory $root." } - if (Get-LocalUser -Name $userName -ErrorAction SilentlyContinue) { throw "Refusing to reuse existing user $userName." } New-Item -ItemType Directory -Path $root | Out-Null $rootCreated = $true $workerOutput = Join-Path $root 'diagnostics' New-Item -ItemType Directory -Path $workerOutput | Out-Null Copy-Item $MsixPath (Join-Path $root 'package.msix') Copy-Item $PSCommandPath (Join-Path $root 'worker.ps1') - & icacls.exe $root /grant '*S-1-5-32-545:(OI)(CI)M' | Out-Null + $userSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value + & icacls.exe $root /grant "*${userSid}:(OI)(CI)M" | Out-Null if ($LASTEXITCODE -ne 0) { throw 'Unable to grant the validation user access to test files.' } - $password = "Tc!$([Guid]::NewGuid().ToString('N'))a9" - "::add-mask::$password" - $securePassword = [Security.SecureString]::new() - foreach ($character in $password.ToCharArray()) { $securePassword.AppendChar($character) } - $securePassword.MakeReadOnly() - New-LocalUser -Name $userName -Password $securePassword -AccountNeverExpires | Out-Null - $userCreated = $true - $users = Get-LocalGroup -SID 'S-1-5-32-545' - $user = Get-LocalUser -Name $userName - if (-not (Get-LocalGroupMember -Group $users | Where-Object { $_.SID -eq $user.SID })) { - Add-LocalGroupMember -Group $users -Member $userName - } - $credential = [PSCredential]::new("$env:COMPUTERNAME\$userName", $securePassword) $pwsh = (Get-Process -Id $PID).Path $arguments = "-NoLogo -NoProfile -NonInteractive -STA -ExecutionPolicy Bypass -File `"$root\worker.ps1`" -StandardUser -MsixPath `"$root\package.msix`" -OutputDirectory `"$workerOutput`" -WaitSeconds $WaitSeconds" - $process = Start-Process -FilePath $pwsh -ArgumentList $arguments -Credential $credential ` - -LoadUserProfile -WorkingDirectory $root -PassThru ` - -RedirectStandardOutput (Join-Path $workerOutput 'stdout.log') ` - -RedirectStandardError (Join-Path $workerOutput 'stderr.log') + # Preserve the real desktop logon while removing admin membership and privileges. + $process = [TinyClipsLimitedProcess]::Start($pwsh, $arguments, $root) $null = $process.Handle if (-not $process.WaitForExit(720000)) { throw 'Standard-user validation exceeded 12 minutes.' } if (-not (Test-Path (Join-Path $workerOutput 'results.json'))) { @@ -75,7 +191,6 @@ if (-not $StandardUser) { if ($rootCreated -and (Test-Path "$root\diagnostics")) { Copy-Item "$root\diagnostics\*" $OutputDirectory -Force } - if ($userCreated) { Remove-LocalUser -Name $userName } } return } @@ -84,6 +199,8 @@ $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = [Security.Principal.WindowsPrincipal]::new($identity) $isAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if ($isAdmin -and -not $RunnerControl) { throw 'Worker must run as a non-elevated standard user.' } +$integrity = [TinyClipsLimitedProcess]::IntegrityLevel() +if (-not $RunnerControl -and $integrity -ne 8192) { throw "Expected medium integrity, observed $integrity." } $start = Get-Date $results = [Collections.Generic.List[object]]::new() @@ -96,6 +213,7 @@ Start-Transcript -Path $log | Out-Null try { "Account mode: $(if ($RunnerControl) { 'runner-control' } else { 'standard-user' }); elevated: $isAdmin" + "Integrity level: $integrity; standard-user mode uses the existing desktop account with a restricted token." "Process architecture: $([Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)" "Session: $((Get-Process -Id $PID).SessionId); interactive: $([Environment]::UserInteractive)" "This is a reproduction approximation, not Microsoft's private validation harness." From ea5784214f5f8ef19a1b5be1f1d2a2a53462b27d Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Mon, 12 Oct 2026 09:38:05 -0700 Subject: [PATCH 6/7] Use the Windows UAC-linked token and bound worker readiness Replace synthetic restricted-token construction with the real unelevated token. Load the existing profile, retain the interactive session, and record worker bootstrap progress before interop initialization. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../workflows/tinyclips-arm64-validation.yml | 2 +- Tools/TinyClipsValidation.ps1 | 61 ++++++++----------- 2 files changed, 25 insertions(+), 38 deletions(-) diff --git a/.github/workflows/tinyclips-arm64-validation.yml b/.github/workflows/tinyclips-arm64-validation.yml index 6e8b5c4ba7e4e..015870f9122e5 100644 --- a/.github/workflows/tinyclips-arm64-validation.yml +++ b/.github/workflows/tinyclips-arm64-validation.yml @@ -16,7 +16,7 @@ jobs: if: github.repository == 'jamesmontemagno/winget-pkgs' name: Standard-user launch (${{ matrix.arch }}) runs-on: ${{ matrix.runner }} - timeout-minutes: 20 + timeout-minutes: 25 strategy: fail-fast: false matrix: diff --git a/Tools/TinyClipsValidation.ps1 b/Tools/TinyClipsValidation.ps1 index 07e3e9b854c65..4417a1e90f6cf 100644 --- a/Tools/TinyClipsValidation.ps1 +++ b/Tools/TinyClipsValidation.ps1 @@ -20,6 +20,9 @@ if ($env:GITHUB_ACTIONS -ne 'true' -or $env:RUNNER_ENVIRONMENT -ne 'github-hoste } if ($RunnerControl -and -not $StandardUser) { throw 'RunnerControl is only valid for the worker mode.' } New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null +if ($StandardUser) { + "Worker entered the script (PID $PID)." | Set-Content (Join-Path $OutputDirectory 'bootstrap.log') +} Add-Type -TypeDefinition @' using System; @@ -55,18 +58,6 @@ public static class TinyClipsLimitedProcess [DllImport("advapi32.dll", SetLastError = true)] private static extern bool OpenProcessToken(IntPtr process, uint access, out IntPtr token); [DllImport("advapi32.dll", SetLastError = true)] - private static extern bool CreateRestrictedToken(IntPtr original, uint flags, uint disableCount, - SidAndAttributes[] disabled, uint deleteCount, IntPtr deleted, uint restrictCount, - IntPtr restricted, out IntPtr token); - [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - private static extern bool ConvertStringSidToSid(string sid, out IntPtr result); - [DllImport("advapi32.dll", SetLastError = true)] - private static extern uint GetLengthSid(IntPtr sid); - [DllImport("advapi32.dll", SetLastError = true)] - private static extern bool CopySid(uint length, IntPtr destination, IntPtr source); - [DllImport("advapi32.dll", SetLastError = true)] - private static extern bool SetTokenInformation(IntPtr token, int kind, IntPtr data, uint size); - [DllImport("advapi32.dll", SetLastError = true)] private static extern bool GetTokenInformation(IntPtr token, int kind, IntPtr data, uint size, out uint needed); [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern bool CreateProcessWithTokenW(IntPtr token, uint logonFlags, string application, @@ -76,9 +67,6 @@ public static class TinyClipsLimitedProcess private static extern IntPtr GetCurrentProcess(); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool CloseHandle(IntPtr handle); - [DllImport("kernel32.dll")] - private static extern IntPtr LocalFree(IntPtr memory); - private static void Check(bool success) { if (!success) { throw new Win32Exception(Marshal.GetLastWin32Error()); } @@ -108,24 +96,15 @@ public static class TinyClipsLimitedProcess public static Process Start(string executable, string arguments, string directory) { - IntPtr original = IntPtr.Zero, limited = IntPtr.Zero, admins = IntPtr.Zero; - IntPtr medium = IntPtr.Zero, label = IntPtr.Zero, environment = IntPtr.Zero; + IntPtr original = IntPtr.Zero, limited = IntPtr.Zero, linked = IntPtr.Zero, environment = IntPtr.Zero; ProcessInformation process = default(ProcessInformation); try { - Check(OpenProcessToken(GetCurrentProcess(), 0xF01FF, out original)); - Check(ConvertStringSidToSid("S-1-5-32-544", out admins)); - var disabled = new[] { new SidAndAttributes { Sid = admins } }; - Check(CreateRestrictedToken(original, 5, 1, disabled, 0, IntPtr.Zero, 0, IntPtr.Zero, out limited)); - Check(ConvertStringSidToSid("S-1-16-8192", out medium)); - int structureSize = Marshal.SizeOf(); - uint sidSize = GetLengthSid(medium); - int labelSize = checked(structureSize + (int)sidSize); - label = Marshal.AllocHGlobal(labelSize); - var copiedSid = IntPtr.Add(label, structureSize); - Check(CopySid(sidSize, copiedSid, medium)); - Marshal.StructureToPtr(new SidAndAttributes { Sid = copiedSid, Attributes = 0x20 }, label, false); - Check(SetTokenInformation(limited, 25, label, checked((uint)labelSize))); + Check(OpenProcessToken(GetCurrentProcess(), 0x8, out original)); + linked = Marshal.AllocHGlobal(IntPtr.Size); + uint needed; + Check(GetTokenInformation(original, 19, linked, checked((uint)IntPtr.Size), out needed)); + limited = Marshal.ReadIntPtr(linked); var variables = new List(); foreach (DictionaryEntry variable in Environment.GetEnvironmentVariables()) { @@ -133,9 +112,9 @@ public static class TinyClipsLimitedProcess } variables.Sort(StringComparer.OrdinalIgnoreCase); environment = Marshal.StringToHGlobalUni(string.Join("\0", variables) + "\0\0"); - var startup = new StartupInfo { Size = checked((uint)Marshal.SizeOf()) }; + var startup = new StartupInfo { Size = checked((uint)Marshal.SizeOf()), Flags = 1, ShowWindow = 0 }; var commandLine = new StringBuilder("\"" + executable + "\" " + arguments); - Check(CreateProcessWithTokenW(limited, 0, executable, commandLine, 0x08000400, + Check(CreateProcessWithTokenW(limited, 1, executable, commandLine, 0x00000410, environment, directory, ref startup, out process)); return Process.GetProcessById(checked((int)process.ProcessId)); } @@ -144,15 +123,16 @@ public static class TinyClipsLimitedProcess if (process.Thread != IntPtr.Zero) { CloseHandle(process.Thread); } if (process.Process != IntPtr.Zero) { CloseHandle(process.Process); } if (environment != IntPtr.Zero) { Marshal.FreeHGlobal(environment); } - if (label != IntPtr.Zero) { Marshal.FreeHGlobal(label); } - if (medium != IntPtr.Zero) { LocalFree(medium); } - if (admins != IntPtr.Zero) { LocalFree(admins); } + if (linked != IntPtr.Zero) { Marshal.FreeHGlobal(linked); } if (limited != IntPtr.Zero) { CloseHandle(limited); } if (original != IntPtr.Zero) { CloseHandle(original); } } } } '@ +if ($StandardUser) { + 'Native interop compiled.' | Add-Content (Join-Path $OutputDirectory 'bootstrap.log') +} if (-not $StandardUser) { $root = 'C:\TinyClipsValidation' @@ -172,9 +152,16 @@ if (-not $StandardUser) { $pwsh = (Get-Process -Id $PID).Path $arguments = "-NoLogo -NoProfile -NonInteractive -STA -ExecutionPolicy Bypass -File `"$root\worker.ps1`" -StandardUser -MsixPath `"$root\package.msix`" -OutputDirectory `"$workerOutput`" -WaitSeconds $WaitSeconds" - # Preserve the real desktop logon while removing admin membership and privileges. + # Use Windows' existing unelevated token rather than constructing a synthetic logon. $process = [TinyClipsLimitedProcess]::Start($pwsh, $arguments, $root) $null = $process.Handle + $readyDeadline = (Get-Date).AddSeconds(60) + while (-not (Test-Path (Join-Path $workerOutput 'bootstrap.log')) -and -not $process.HasExited -and (Get-Date) -lt $readyDeadline) { + Start-Sleep -Seconds 1 + } + if (-not (Test-Path (Join-Path $workerOutput 'bootstrap.log'))) { + throw 'The non-admin worker did not enter the script within 60 seconds.' + } if (-not $process.WaitForExit(720000)) { throw 'Standard-user validation exceeded 12 minutes.' } if (-not (Test-Path (Join-Path $workerOutput 'results.json'))) { throw "Standard-user validation produced no results (exit code $($process.ExitCode))." @@ -213,7 +200,7 @@ Start-Transcript -Path $log | Out-Null try { "Account mode: $(if ($RunnerControl) { 'runner-control' } else { 'standard-user' }); elevated: $isAdmin" - "Integrity level: $integrity; standard-user mode uses the existing desktop account with a restricted token." + "Integrity level: $integrity; standard-user mode uses the existing desktop account with its Windows UAC-linked token." "Process architecture: $([Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)" "Session: $((Get-Process -Id $PID).SessionId); interactive: $([Environment]::UserInteractive)" "This is a reproduction approximation, not Microsoft's private validation harness." From 1fbb3be75ad61a3475ab555d5b738d411d1ee282 Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Mon, 12 Oct 2026 09:49:01 -0700 Subject: [PATCH 7/7] Report unavailable non-admin coverage separately from application crashes Name the failing Windows API and write structured worker-unavailable results when no app launch was executed. Keep the diagnostic gate failing rather than presenting missing coverage as a pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Tools/TinyClipsValidation.ps1 | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/Tools/TinyClipsValidation.ps1 b/Tools/TinyClipsValidation.ps1 index 4417a1e90f6cf..b4e670a3f7e7f 100644 --- a/Tools/TinyClipsValidation.ps1 +++ b/Tools/TinyClipsValidation.ps1 @@ -67,9 +67,13 @@ public static class TinyClipsLimitedProcess private static extern IntPtr GetCurrentProcess(); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool CloseHandle(IntPtr handle); - private static void Check(bool success) + private static void Check(bool success, string operation) { - if (!success) { throw new Win32Exception(Marshal.GetLastWin32Error()); } + if (!success) + { + int error = Marshal.GetLastWin32Error(); + throw new Win32Exception(error, operation + ": " + new Win32Exception(error).Message); + } } public static uint IntegrityLevel() @@ -77,12 +81,12 @@ public static class TinyClipsLimitedProcess IntPtr token = IntPtr.Zero, buffer = IntPtr.Zero; try { - Check(OpenProcessToken(GetCurrentProcess(), 0x8, out token)); + Check(OpenProcessToken(GetCurrentProcess(), 0x8, out token), "OpenProcessToken(current worker)"); uint size; GetTokenInformation(token, 25, IntPtr.Zero, 0, out size); if (size == 0) { throw new Win32Exception(Marshal.GetLastWin32Error()); } buffer = Marshal.AllocHGlobal(checked((int)size)); - Check(GetTokenInformation(token, 25, buffer, size, out size)); + Check(GetTokenInformation(token, 25, buffer, size, out size), "GetTokenInformation(TokenIntegrityLevel)"); var sid = Marshal.PtrToStructure(buffer).Sid; int count = Marshal.ReadByte(sid, 1); return unchecked((uint)Marshal.ReadInt32(sid, 8 + (count - 1) * 4)); @@ -100,10 +104,10 @@ public static class TinyClipsLimitedProcess ProcessInformation process = default(ProcessInformation); try { - Check(OpenProcessToken(GetCurrentProcess(), 0x8, out original)); + Check(OpenProcessToken(GetCurrentProcess(), 0x8, out original), "OpenProcessToken(controller)"); linked = Marshal.AllocHGlobal(IntPtr.Size); uint needed; - Check(GetTokenInformation(original, 19, linked, checked((uint)IntPtr.Size), out needed)); + Check(GetTokenInformation(original, 19, linked, checked((uint)IntPtr.Size), out needed), "GetTokenInformation(TokenLinkedToken)"); limited = Marshal.ReadIntPtr(linked); var variables = new List(); foreach (DictionaryEntry variable in Environment.GetEnvironmentVariables()) @@ -115,7 +119,7 @@ public static class TinyClipsLimitedProcess var startup = new StartupInfo { Size = checked((uint)Marshal.SizeOf()), Flags = 1, ShowWindow = 0 }; var commandLine = new StringBuilder("\"" + executable + "\" " + arguments); Check(CreateProcessWithTokenW(limited, 1, executable, commandLine, 0x00000410, - environment, directory, ref startup, out process)); + environment, directory, ref startup, out process), "CreateProcessWithTokenW(non-admin worker)"); return Process.GetProcessById(checked((int)process.ProcessId)); } finally @@ -171,6 +175,15 @@ if (-not $StandardUser) { } catch { $_ | Out-String | Set-Content (Join-Path $OutputDirectory 'controller-error.txt') + $workerResultsExist = $rootCreated -and (Test-Path "$root\diagnostics\results.json") + if (-not $workerResultsExist) { + ConvertTo-Json -InputObject @([pscustomobject]@{ + AccountMode = 'standard-user' + Status = 'worker-unavailable' + AppLaunchTestExecuted = $false + Error = $_.ToString() + }) -Depth 5 | Set-Content (Join-Path $OutputDirectory 'results.json') + } throw } finally {