feat: probe uipi, dpi awareness, and chromium activation

UIPI is measured across a boundary that actually exists. This box runs the
built-in Administrator at High with Admin Approval Mode off, so RunAs yields
High-vs-High and runas /trustlevel leaves the mandatory label untouched; the
probe manufactures a real Medium token instead and refuses to record a verdict
unless the worker reads back S-1-16-8192. Both arms run identical code against
the same High Notepad, so the High arm is a control proving the injection
works. UIA reads succeed from Medium; SendInput does not land, and the two arms
return the identical success code -- only re-reading the target separates them.
PostMessage is denied with error 5 while SendMessage WM_GETTEXT succeeds, so
reads cross the boundary and writes do not.

The 125 percent DPI measurement is not achievable here and the reason is
measured rather than assumed: the VMware display carries no EDID and offers a
single scale step. DisplayConfigSetDeviceInfo returns success and persists the
registry value while effective DPI stays at 96, which is itself worth
recording -- that API's success return is not evidence the scale changed. The
non-zero delta defers to 2.4, which owns per-monitor scale. What does land for
2.1 is that PER_MONITOR_AWARE_V2 succeeds on 17763 with both arms verified at
the awareness levels claimed.

Chromium activation is graded, not guessed. The bundled version was read off
this installation by scanning the shipped binary's user-agent string: Chromium
142, Electron 39.8.3. Both stacks reach the same settled tree with no flag, so
the exposure claim holds and the plan's candidate contradiction is superseded.
The flag is still not redundant: without it first contact is deterministically
the pre-activation shell, and with it first contact is a race. Neither arm
removes the need to settle before trusting a first snapshot.
This commit is contained in:
Lahfir 2026-07-26 19:38:46 -06:00
parent 297cc38dcc
commit 90fd24b4ed
9 changed files with 2359 additions and 0 deletions

View file

@ -0,0 +1,465 @@
<#
.SYNOPSIS
Probe 09 (sub-phase 2.0, unit U8): UIPI measured across a real integrity boundary.
.DESCRIPTION
Engineering Invariant #6 is about a Medium-integrity automation process driving a
High-integrity target. This box cannot produce that pair by ordinary means: the
built-in Administrator (RID 500) runs a full token at S-1-16-12288 and
FilterAdministratorToken is unset, so Admin Approval Mode is off and
"Start-Process -Verb RunAs" yields High-vs-High. runas.exe /trustlevel:0x20000 was
also ruled out - it restricts the token but leaves the mandatory label at High.
The boundary is therefore manufactured with common.ps1's
Start-MediumIntegrityProcess, which lowers a duplicated token to S-1-16-8192 and
asserts the label on read-back.
Two arms run the SAME worker code against the SAME probe-launched High Notepad;
only the integrity level of the worker differs:
arm "high" control - worker at S-1-16-12288, injection expected to land
arm "medium" test - worker at S-1-16-8192, injection expected to be dropped
Effect is never read from SendInput's return value (which is documented to report
success when UIPI drops the input). The parent re-reads the target's edit text over
WM_GETTEXT before and after each arm, High-to-High, as independent observation.
#>
[CmdletBinding()]
param(
[switch]$Worker,
[string]$WorkerOut = '',
[string]$HelperDir = '',
[int]$TargetProcessId = 0,
[string]$TargetHwnd = '0',
[string]$EditHwnd = '0',
[string]$Marker = 'ZZZ',
[string]$Arm = 'unknown'
)
$ErrorActionPreference = 'Stop'
. "$PSScriptRoot\common.ps1"
$Probe = '09-elevation-uipi'
$NativeShimSource = @'
using System;
using System.Runtime.InteropServices;
namespace AgentDesktopProbe {
public static class Native {
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd);
[DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr OpenProcess(uint access, bool inherit, int pid);
[DllImport("advapi32.dll", SetLastError = true)] public static extern bool OpenProcessToken(IntPtr proc, uint access, out IntPtr tok);
[DllImport("advapi32.dll", SetLastError = true)] public static extern bool GetTokenInformation(IntPtr tok, int cls, IntPtr buf, int len, out int ret);
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] public static extern bool ConvertSidToStringSid(IntPtr sid, out IntPtr str);
[DllImport("kernel32.dll", SetLastError = true)] public static extern bool CloseHandle(IntPtr h);
[DllImport("kernel32.dll")] public static extern IntPtr LocalFree(IntPtr h);
public static int GetForegroundProcessId() {
IntPtr h = GetForegroundWindow();
if (h == IntPtr.Zero) { return 0; }
uint p = 0;
GetWindowThreadProcessId(h, out p);
return (int)p;
}
public static string GetIntegritySid(int processId) {
IntPtr proc = OpenProcess(0x1000, false, processId);
if (proc == IntPtr.Zero) { throw new InvalidOperationException("OpenProcess failed for pid " + processId + ", error " + Marshal.GetLastWin32Error()); }
IntPtr tok = IntPtr.Zero;
try {
if (!OpenProcessToken(proc, 0x0008, out tok)) { throw new InvalidOperationException("OpenProcessToken failed, error " + Marshal.GetLastWin32Error()); }
int len = 0;
GetTokenInformation(tok, 25, IntPtr.Zero, 0, out len);
if (len <= 0) { throw new InvalidOperationException("GetTokenInformation sizing failed, error " + Marshal.GetLastWin32Error()); }
IntPtr buf = Marshal.AllocHGlobal(len);
try {
if (!GetTokenInformation(tok, 25, buf, len, out len)) { throw new InvalidOperationException("GetTokenInformation failed, error " + Marshal.GetLastWin32Error()); }
IntPtr str = IntPtr.Zero;
if (!ConvertSidToStringSid(Marshal.ReadIntPtr(buf), out str)) { throw new InvalidOperationException("ConvertSidToStringSid failed, error " + Marshal.GetLastWin32Error()); }
string s = Marshal.PtrToStringUni(str);
LocalFree(str);
return s;
} finally { Marshal.FreeHGlobal(buf); }
} finally {
if (tok != IntPtr.Zero) { CloseHandle(tok); }
CloseHandle(proc);
}
}
}
}
'@
$HelperSource = @'
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
namespace AgentDesktopProbe {
[StructLayout(LayoutKind.Sequential)]
public struct KeybdInput { public ushort wVk; public ushort wScan; public uint dwFlags; public uint time; public IntPtr dwExtraInfo; }
[StructLayout(LayoutKind.Sequential)]
public struct Input { public uint type; public KeybdInput ki; public int padA; public int padB; }
public static class U8 {
private delegate bool EnumProc(IntPtr h, IntPtr l);
[DllImport("user32.dll", SetLastError = true)] private static extern uint SendInput(uint n, Input[] inputs, int size);
[DllImport("user32.dll")] private static extern bool EnumChildWindows(IntPtr parent, EnumProc cb, IntPtr l);
[DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern int GetClassNameW(IntPtr h, StringBuilder b, int max);
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "SendMessageTimeoutW")]
private static extern IntPtr SendMessageTimeoutText(IntPtr h, uint msg, IntPtr w, StringBuilder l, uint flags, uint ms, out IntPtr res);
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "SendMessageTimeoutW")]
private static extern IntPtr SendMessageTimeoutStr(IntPtr h, uint msg, IntPtr w, string l, uint flags, uint ms, out IntPtr res);
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern bool PostMessageW(IntPtr h, uint msg, IntPtr w, IntPtr l);
[DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow();
[DllImport("kernel32.dll")] private static extern void SetLastError(uint code);
public static int InputStructSize() { return Marshal.SizeOf(typeof(Input)); }
public static IntPtr FindChildByClass(IntPtr parent, string wanted) {
IntPtr hit = IntPtr.Zero;
EnumChildWindows(parent, delegate(IntPtr h, IntPtr l) {
StringBuilder b = new StringBuilder(256);
GetClassNameW(h, b, 256);
if (String.Equals(b.ToString(), wanted, StringComparison.OrdinalIgnoreCase)) { hit = h; return false; }
return true;
}, IntPtr.Zero);
return hit;
}
public static string ReadText(IntPtr h, out long sendResult, out int lastError) {
StringBuilder b = new StringBuilder(8192);
IntPtr res;
IntPtr ok = SendMessageTimeoutText(h, 0x000D, (IntPtr)8192, b, 0x0002, 4000, out res);
lastError = Marshal.GetLastWin32Error();
sendResult = res.ToInt64();
if (ok == IntPtr.Zero) { return null; }
return b.ToString();
}
public static bool WriteText(IntPtr h, string text, out int lastError) {
IntPtr res;
IntPtr ok = SendMessageTimeoutStr(h, 0x000C, IntPtr.Zero, text, 0x0002, 4000, out res);
lastError = Marshal.GetLastWin32Error();
return ok != IntPtr.Zero;
}
public static bool PostChar(IntPtr h, char c, out int lastError) {
bool ok = PostMessageW(h, 0x0102, (IntPtr)c, IntPtr.Zero);
lastError = Marshal.GetLastWin32Error();
return ok;
}
public static uint SendUnicode(string text, out int lastError) {
List<Input> batch = new List<Input>();
for (int i = 0; i < text.Length; i++) {
Input down = new Input();
down.type = 1;
down.ki.wScan = (ushort)text[i];
down.ki.dwFlags = 0x0004;
batch.Add(down);
Input up = new Input();
up.type = 1;
up.ki.wScan = (ushort)text[i];
up.ki.dwFlags = 0x0004 | 0x0002;
batch.Add(up);
}
Input[] arr = batch.ToArray();
SetLastError(0);
uint sent = SendInput((uint)arr.Length, arr, Marshal.SizeOf(typeof(Input)));
lastError = Marshal.GetLastWin32Error();
return sent;
}
}
}
'@
function Get-UiaSnapshot {
param([IntPtr]$WindowHandle)
$row = [ordered]@{}
try {
Add-Type -AssemblyName UIAutomationClient -ErrorAction Stop
Add-Type -AssemblyName UIAutomationTypes -ErrorAction Stop
$root = [System.Windows.Automation.AutomationElement]::FromHandle($WindowHandle)
$row['fromHandle'] = 'ok'
$row['name'] = $root.Current.Name
$row['className'] = $root.Current.ClassName
$row['controlType'] = ($root.Current.ControlType.ProgrammaticName -replace '^ControlType\.', '')
$row['ownerProcessId'] = $root.Current.ProcessId
$r = $root.Current.BoundingRectangle
if ($r.IsEmpty -or [double]::IsInfinity($r.Left)) {
$row['boundingRectangle'] = 'empty (window minimized or off-screen)'
} else {
$row['boundingRectangle'] = [ordered]@{ Left = [int]$r.Left; Top = [int]$r.Top; Width = [int]$r.Width; Height = [int]$r.Height }
}
$walker = [System.Windows.Automation.TreeWalker]::RawViewWalker
$stack = New-Object System.Collections.Stack
$stack.Push($root)
$count = 0
$editValue = $null
$editPatterns = @()
while ($stack.Count -gt 0 -and $count -lt 200) {
$node = $stack.Pop()
$count++
if ($node.Current.ClassName -eq 'Edit' -and $null -eq $editValue) {
$editPatterns = @($node.GetSupportedPatterns() | ForEach-Object { $_.ProgrammaticName -replace '^(\w+)PatternIdentifiers\.Pattern$', '$1' })
try {
$vp = $node.GetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern)
$editValue = $vp.Current.Value
} catch {
try {
$tp = $node.GetCurrentPattern([System.Windows.Automation.TextPattern]::Pattern)
$editValue = $tp.DocumentRange.GetText(-1)
} catch { $editValue = '<no value/text pattern: ' + $_.Exception.Message + '>' }
}
}
$child = $walker.GetFirstChild($node)
while ($null -ne $child) {
$stack.Push($child)
$child = $walker.GetNextSibling($child)
}
}
$row['rawViewNodes'] = $count
$row['editPatterns'] = $editPatterns
$row['editValueSeenByThisProcess'] = $editValue
} catch {
$row['fromHandle'] = 'failed'
$row['error'] = $_.Exception.Message
$row['hresult'] = ('0x' + ('{0:x8}' -f $_.Exception.HResult))
}
return $row
}
function Invoke-WorkerArm {
$result = [ordered]@{ arm = $Arm; processId = $PID }
Add-Type -Path (Join-Path $HelperDir 'u8native.dll')
Add-Type -Path (Join-Path $HelperDir 'u8helper.dll')
$console = [AgentDesktopProbe.U8]::GetConsoleWindow()
[void][AgentDesktopProbe.Native]::ShowWindow($console, 6)
[void][AgentDesktopProbe.Native]::ShowWindow($console, 0)
$result['addTypeCompilationAvailable'] = $null
try {
Add-Type -TypeDefinition 'public static class U8Compile { public static int V() { return 1; } }' -Language CSharp -ErrorAction Stop
$result['addTypeCompilationAvailable'] = $true
} catch {
$result['addTypeCompilationAvailable'] = $false
$result['addTypeCompilationError'] = $_.Exception.Message
}
$target = [IntPtr][int64]$TargetHwnd
$edit = [IntPtr][int64]$EditHwnd
$result['selfIntegritySid'] = [AgentDesktopProbe.Native]::GetIntegritySid($PID)
try { $result['targetIntegritySid'] = [AgentDesktopProbe.Native]::GetIntegritySid($TargetProcessId) }
catch { $result['targetIntegritySid'] = 'ERROR ' + $_.Exception.Message }
$result['uiaRead'] = Get-UiaSnapshot -WindowHandle $target
$err = 0
$raw = [int64]0
$textBefore = [AgentDesktopProbe.U8]::ReadText($edit, [ref]$raw, [ref]$err)
$result['wmGetTextFromWorker'] = [ordered]@{ returned = $textBefore; sendMessageResult = $raw; lastError = $err }
[IO.File]::WriteAllText(($WorkerOut + '.ready'), 'ready')
$deadline = (Get-Date).AddSeconds(120)
while ((Get-Date) -lt $deadline -and -not (Test-Path -LiteralPath ($WorkerOut + '.go'))) { Start-Sleep -Milliseconds 200 }
$deadline = (Get-Date).AddSeconds(20)
while ((Get-Date) -lt $deadline -and [AgentDesktopProbe.Native]::GetForegroundProcessId() -ne $TargetProcessId) {
Start-Sleep -Milliseconds 200
}
$result['foregroundOwnerAtAssert'] = [ordered]@{ processId = [AgentDesktopProbe.Native]::GetForegroundProcessId() }
try {
Assert-Foreground -ExpectedProcessId $TargetProcessId -Stage ('sendinput-pre-' + $Arm)
$result['foregroundAssertPre'] = 'ok'
$sent = [AgentDesktopProbe.U8]::SendUnicode($Marker, [ref]$err)
$result['sendInputEventsAccepted'] = [int]$sent
$result['sendInputLastError'] = $err
$result['sendInputStructSize'] = [AgentDesktopProbe.U8]::InputStructSize()
Start-Sleep -Milliseconds 700
Assert-Foreground -ExpectedProcessId $TargetProcessId -Stage ('sendinput-post-' + $Arm)
$result['foregroundAssertPost'] = 'ok'
} catch {
$result['foregroundAssertPre'] = 'interference'
$result['interference'] = $_.Exception.Message
}
$postOk = [AgentDesktopProbe.U8]::PostChar($edit, 'P', [ref]$err)
$result['postMessageWmChar'] = [ordered]@{ returned = $postOk; lastError = $err }
Start-Sleep -Milliseconds 400
$textAfter = [AgentDesktopProbe.U8]::ReadText($edit, [ref]$raw, [ref]$err)
$result['wmGetTextAfterFromWorker'] = [ordered]@{ returned = $textAfter; sendMessageResult = $raw; lastError = $err }
$result['uiaReadAfter'] = Get-UiaSnapshot -WindowHandle $target
[IO.File]::WriteAllText($WorkerOut, (ConvertTo-Json -InputObject $result -Depth 12), (New-Object System.Text.UTF8Encoding $false))
}
if ($Worker) {
try { Invoke-WorkerArm } catch {
[IO.File]::WriteAllText($WorkerOut, (ConvertTo-Json -InputObject ([ordered]@{ arm = $Arm; fatal = $_.Exception.Message }) -Depth 6), (New-Object System.Text.UTF8Encoding $false))
}
exit 0
}
$status = 'ok'
$message = ''
$summary = [ordered]@{}
$script:Spawned = New-Object System.Collections.ArrayList
try {
Initialize-ProbeNative
$work = Join-Path $env:TEMP 'agent-desktop-u8'
if (-not (Test-Path -LiteralPath $work)) { New-Item -ItemType Directory -Path $work -Force | Out-Null }
Add-Type -TypeDefinition $NativeShimSource -Language CSharp -OutputAssembly (Join-Path $work 'u8native.dll') -OutputType Library
Add-Type -TypeDefinition $HelperSource -Language CSharp -OutputAssembly (Join-Path $work 'u8helper.dll') -OutputType Library
Add-Type -Path (Join-Path $work 'u8helper.dll')
Write-ProbeLog -Message 'compiled u8native.dll + u8helper.dll at High integrity for the Medium worker to load'
$policy = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
$uac = [ordered]@{}
foreach ($n in @('EnableLUA', 'FilterAdministratorToken', 'ConsentPromptBehaviorAdmin', 'EnableInstallerDetection')) {
$v = (Get-ItemProperty -Path $policy -Name $n -ErrorAction SilentlyContinue).$n
if ($null -eq $v) { $uac[$n] = '<value not present>' } else { $uac[$n] = [int]$v }
}
$uac['sessionIntegritySid'] = [AgentDesktopProbe.Native]::GetIntegritySid($PID)
$uac['adminApprovalModeActive'] = ($uac['EnableLUA'] -eq 1 -and $uac['FilterAdministratorToken'] -eq 1)
$notepad = Start-ScratchProcess -FilePath (Join-Path $env:WINDIR 'System32\notepad.exe') -TimeoutSec 20
[void]$script:Spawned.Add($notepad.ProcessId)
if ($notepad.MainWindowHandle -eq [IntPtr]::Zero) { throw 'target notepad window never appeared' }
$targetSid = [AgentDesktopProbe.Native]::GetIntegritySid($notepad.ProcessId)
if ($targetSid -ne 'S-1-16-12288') { throw ('target notepad is not High integrity: ' + $targetSid) }
$editHandle = [AgentDesktopProbe.U8]::FindChildByClass($notepad.MainWindowHandle, 'Edit')
if ($editHandle -eq [IntPtr]::Zero) { throw 'notepad Edit child window not found' }
$arms = @()
$lastErr = 0
$rawRes = [int64]0
foreach ($armSpec in @(
[pscustomobject]@{ Name = 'high'; Marker = 'HHH'; Medium = $false },
[pscustomobject]@{ Name = 'medium'; Marker = 'MMM'; Medium = $true })) {
[void][AgentDesktopProbe.U8]::WriteText($editHandle, 'U8-BASE', [ref]$lastErr)
Start-Sleep -Milliseconds 300
$before = [AgentDesktopProbe.U8]::ReadText($editHandle, [ref]$rawRes, [ref]$lastErr)
$outFile = Join-Path $work ('arm-' + $armSpec.Name + '.json')
foreach ($stale in @($outFile, ($outFile + '.ready'), ($outFile + '.go'))) {
if (Test-Path -LiteralPath $stale) { Remove-Item -LiteralPath $stale -Force }
}
$argv = @('-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
'-File', $PSCommandPath, '-Worker', '-WorkerOut', $outFile, '-HelperDir', $work,
'-TargetProcessId', ([string]$notepad.ProcessId),
'-TargetHwnd', ([string]$notepad.MainWindowHandle.ToInt64()),
'-EditHwnd', ([string]$editHandle.ToInt64()),
'-Marker', $armSpec.Marker, '-Arm', $armSpec.Name)
$shell = Join-Path $env:WINDIR 'System32\WindowsPowerShell\v1.0\powershell.exe'
$launchError = ''
$workerPid = 0
$workerSid = ''
try {
if ($armSpec.Medium) {
$w = Start-MediumIntegrityProcess -FilePath $shell -ArgumentList $argv
$workerPid = $w.ProcessId
$workerSid = $w.IntegritySid
} else {
$w = Start-Process -FilePath $shell -ArgumentList $argv -PassThru
$workerPid = $w.Id
Register-ScratchProcessId -ProcessId $workerPid
$workerSid = [AgentDesktopProbe.Native]::GetIntegritySid($workerPid)
}
[void]$script:Spawned.Add($workerPid)
} catch {
$launchError = $_.Exception.Message
}
if ($armSpec.Medium -and $workerSid -ne 'S-1-16-8192') {
throw ('PROBE-HARNESS: refusing to record a UIPI verdict without a real boundary - medium arm token label is ' + $workerSid + ' ' + $launchError)
}
$handoff = 'worker never signalled ready'
$deadline = (Get-Date).AddSeconds(120)
while ((Get-Date) -lt $deadline -and -not (Test-Path -LiteralPath ($outFile + '.ready'))) { Start-Sleep -Milliseconds 300 }
if (Test-Path -LiteralPath ($outFile + '.ready')) {
[void][AgentDesktopProbe.Native]::ShowWindow($notepad.MainWindowHandle, 6)
Start-Sleep -Milliseconds 500
[void][AgentDesktopProbe.Native]::ShowWindow($notepad.MainWindowHandle, 9)
$fgDeadline = (Get-Date).AddSeconds(15)
while ((Get-Date) -lt $fgDeadline -and [AgentDesktopProbe.Native]::GetForegroundProcessId() -ne $notepad.ProcessId) { Start-Sleep -Milliseconds 250 }
$observedFg = [AgentDesktopProbe.Native]::GetForegroundProcessId()
$handoff = if ($observedFg -eq $notepad.ProcessId) { 'target foreground restored by parent ShowWindow(SW_MINIMIZE->SW_RESTORE) on the probe-launched window; SetForegroundWindow never called' }
else { 'FAILED - foreground pid ' + $observedFg + ' is not the probe-launched target' }
[IO.File]::WriteAllText(($outFile + '.go'), 'go')
}
$deadline = (Get-Date).AddSeconds(180)
while ((Get-Date) -lt $deadline -and -not (Test-Path -LiteralPath $outFile)) { Start-Sleep -Milliseconds 500 }
$workerData = $null
if (Test-Path -LiteralPath $outFile) { $workerData = (Get-Content -LiteralPath $outFile -Raw | ConvertFrom-Json) }
Start-Sleep -Milliseconds 500
$after = [AgentDesktopProbe.U8]::ReadText($editHandle, [ref]$rawRes, [ref]$lastErr)
$arms += [ordered]@{
arm = $armSpec.Name
workerIntegritySid = $workerSid
workerLaunchError = $launchError
marker = $armSpec.Marker
parentForegroundHandoff = $handoff
targetStateBeforeInjection = $before
targetStateAfterInjection = $after
targetStateChanged = ($before -ne $after)
markerPresentInTargetAfterwards = ($null -ne $after -and $after.Contains($armSpec.Marker))
observedBy = 'parent High-integrity WM_GETTEXT on the notepad Edit child, independent of SendInput return'
worker = $workerData
}
try { Stop-ScratchProcess -ProcessId $workerPid } catch { Write-ProbeLog -Message ('worker teardown: ' + $_.Exception.Message) -Level 'warn' }
}
$high = $arms | Where-Object { $_.arm -eq 'high' } | Select-Object -First 1
$medium = $arms | Where-Object { $_.arm -eq 'medium' } | Select-Object -First 1
$capture = [ordered]@{
question = 'does UIPI block SendInput and window messages from a Medium-integrity process into a High-integrity target, and are UIA reads still allowed'
invariant = 'Engineering Invariant #6 (UIPI)'
stack = 'managed (UIA reads) + raw Win32 SendInput/SendMessage/PostMessage'
scope = 'api-contract'
boundaryOrigin = 'manufactured by common.ps1 Start-MediumIntegrityProcess (DuplicateTokenEx + SetTokenInformation(TokenIntegrityLevel, S-1-16-8192) + CreateProcessAsUser), label asserted on read-back'
boundaryRuledOut = @(
'Start-Process -Verb RunAs: AAM off on this box (FilterAdministratorToken unset, RID 500 full token) so it yields High-vs-High',
'runas.exe /trustlevel:0x20000: restricts the token but leaves the mandatory label at S-1-16-12288'
)
uacPolicy = $uac
target = [ordered]@{
process = 'notepad.exe'
processId = $notepad.ProcessId
integritySid = $targetSid
windowHandle = $notepad.MainWindowHandle.ToInt64()
editChild = [ordered]@{ className = 'Edit'; windowHandle = $editHandle.ToInt64() }
launchedByThisProbe = $true
}
arms = $arms
findings = [ordered]@{
uiaReadFromMediumAgainstHigh = if ($medium -and $medium.worker) { $medium.worker.uiaRead.fromHandle } else { 'unavailable' }
sendInputFromHighLanded = if ($high) { $high.targetStateChanged } else { $null }
sendInputFromMediumLanded = if ($medium) { $medium.targetStateChanged } else { $null }
sendInputReturnIsNotEvidence = 'SendInput reports the event count it accepted in both arms; only the re-read of the target distinguishes them'
wmGetTextFromMediumBlocked = if ($medium -and $medium.worker) { ($null -eq $medium.worker.wmGetTextFromWorker.returned -or $medium.worker.wmGetTextFromWorker.returned -eq '') } else { $null }
}
}
Write-ProbeJson -Probe $Probe -Name 'uipi.json' -InputObject $capture | Out-Null
$summary['sessionIntegritySid'] = $uac['sessionIntegritySid']
$summary['targetIntegritySid'] = $targetSid
$summary['mediumArmIntegritySid'] = if ($medium) { $medium.workerIntegritySid } else { '' }
$summary['highArmInjectionLanded'] = $capture.findings.sendInputFromHighLanded
$summary['mediumArmInjectionLanded'] = $capture.findings.sendInputFromMediumLanded
$summary['mediumArmUiaRead'] = $capture.findings.uiaReadFromMediumAgainstHigh
$message = 'uipi measured across a manufactured Medium-vs-High boundary'
} catch {
$status = 'fail'
$message = ($_.Exception.Message -replace '[\r\n]+', ' ')
Write-ProbeLog -Message ('probe failed: ' + $message) -Level 'error'
} finally {
foreach ($id in @($script:Spawned)) {
try { Stop-ScratchProcess -ProcessId $id } catch { Write-ProbeLog -Message ('teardown: ' + $_.Exception.Message) -Level 'warn' }
}
foreach ($f in @(Get-ChildItem -LiteralPath (Get-CaptureDir -Probe $Probe) -File -ErrorAction SilentlyContinue)) {
if ($f.Name -like '*.normalized') { continue }
if (-not (Test-CaptureRedaction -Path $f.FullName)) { $status = 'fail'; $message = ('redaction residue in ' + $f.Name) }
}
}
Write-ProbeResult -Probe $Probe -Status $status -Message $message -Data $summary
if ($status -eq 'fail') { exit 1 }
exit 0

View file

@ -0,0 +1,393 @@
<#
.SYNOPSIS
Probe 10 (sub-phase 2.0, unit U8): session facts and the DPI-awareness bounds delta.
.DESCRIPTION
Reads the same probe-launched element's BoundingRectangle from a DPI-aware child
process and a DPI-unaware child process, and reports the per-edge numeric delta.
The measurement is taken twice: at the display's recommended scale, and again after
the probe asks for the next scale step up (125%). The original scale is restored in
teardown and the restoration is proved by re-reading it back.
Delta fields are named deltaLeft/deltaTop/deltaWidth/deltaHeight on purpose. The
KTD9 normalizer buckets any key named exactly x/y/left/top/right/bottom/width/height
to an 8-pixel bucket to absorb layout jitter; a measured delta must not be bucketed
away, and "deltaLeft" has no word boundary before "Left", so it survives verbatim
while the raw rectangles it is derived from are still bucketed.
Multi-monitor and mixed-DPI behavior is a DEFERRED row: this VM has exactly one
display. It closes at sub-phase 2.4, which owns list_displays and per-monitor
scale_factor.
#>
[CmdletBinding()]
param(
[switch]$Worker,
[string]$WorkerOut = '',
[string]$Mode = 'unaware',
[string]$TargetHwnd = '0',
[string]$EditHwnd = '0'
)
$ErrorActionPreference = 'Stop'
. "$PSScriptRoot\common.ps1"
$Probe = '10-session-dpi'
$DpiSource = @'
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
namespace AgentDesktopProbe {
[StructLayout(LayoutKind.Sequential)]
public struct DcLuid { public uint LowPart; public int HighPart; }
[StructLayout(LayoutKind.Sequential)]
public struct DcHeader { public int type; public uint size; public DcLuid adapterId; public uint id; }
[StructLayout(LayoutKind.Sequential)]
public struct DcScaleGet { public DcHeader header; public int minScaleRel; public int curScaleRel; public int maxScaleRel; }
[StructLayout(LayoutKind.Sequential)]
public struct DcScaleSet { public DcHeader header; public int scaleRel; }
[StructLayout(LayoutKind.Sequential, Size = 72)]
public struct DcPathInfo { public DcLuid srcAdapterId; public uint srcId; }
[StructLayout(LayoutKind.Sequential, Size = 64)]
public struct DcModeInfo { public uint infoType; }
[StructLayout(LayoutKind.Sequential)]
public struct Rect { public int Left; public int Top; public int Right; public int Bottom; }
public static class Dpi {
private delegate bool EnumProc(IntPtr h, IntPtr l);
[DllImport("user32.dll")] public static extern int GetDisplayConfigBufferSizes(uint flags, out uint numPath, out uint numMode);
[DllImport("user32.dll")] public static extern int QueryDisplayConfig(uint flags, ref uint numPath, [Out] DcPathInfo[] paths, ref uint numMode, [Out] DcModeInfo[] modes, IntPtr topology);
[DllImport("user32.dll")] public static extern int DisplayConfigGetDeviceInfo(ref DcScaleGet req);
[DllImport("user32.dll")] public static extern int DisplayConfigSetDeviceInfo(ref DcScaleSet req);
[DllImport("user32.dll", SetLastError = true)] public static extern IntPtr SetProcessDpiAwarenessContext(IntPtr context);
[DllImport("user32.dll")] public static extern int GetSystemMetrics(int index);
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out Rect r);
[DllImport("user32.dll")] public static extern IntPtr MonitorFromPoint(long pt, uint flags);
[DllImport("user32.dll")] public static extern IntPtr GetDC(IntPtr h);
[DllImport("user32.dll")] public static extern int ReleaseDC(IntPtr h, IntPtr dc);
[DllImport("user32.dll")] private static extern bool EnumChildWindows(IntPtr parent, EnumProc cb, IntPtr l);
[DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetClassNameW(IntPtr h, StringBuilder b, int max);
[DllImport("gdi32.dll")] public static extern int GetDeviceCaps(IntPtr dc, int index);
[DllImport("shcore.dll")] public static extern int GetProcessDpiAwareness(IntPtr proc, out int value);
[DllImport("shcore.dll")] public static extern int GetDpiForMonitor(IntPtr mon, int type, out uint dx, out uint dy);
[DllImport("kernel32.dll")] public static extern uint WTSGetActiveConsoleSessionId();
[DllImport("kernel32.dll")] public static extern uint GetCurrentProcessId();
[DllImport("kernel32.dll", SetLastError = true)] public static extern bool ProcessIdToSessionId(uint pid, out uint sid);
public static int LogPixels() {
IntPtr dc = GetDC(IntPtr.Zero);
int v = GetDeviceCaps(dc, 88);
ReleaseDC(IntPtr.Zero, dc);
return v;
}
public static IntPtr FindChildByClass(IntPtr parent, string wanted) {
IntPtr hit = IntPtr.Zero;
EnumChildWindows(parent, delegate(IntPtr h, IntPtr l) {
StringBuilder b = new StringBuilder(256);
GetClassNameW(h, b, 256);
if (String.Equals(b.ToString(), wanted, StringComparison.OrdinalIgnoreCase)) { hit = h; return false; }
return true;
}, IntPtr.Zero);
return hit;
}
public static bool GetSourceIds(out DcLuid adapterId, out uint sourceId) {
adapterId = new DcLuid();
sourceId = 0;
uint numPath = 0;
uint numMode = 0;
if (GetDisplayConfigBufferSizes(2, out numPath, out numMode) != 0) { return false; }
DcPathInfo[] paths = new DcPathInfo[numPath];
DcModeInfo[] modes = new DcModeInfo[numMode];
if (QueryDisplayConfig(2, ref numPath, paths, ref numMode, modes, IntPtr.Zero) != 0) { return false; }
if (numPath < 1) { return false; }
adapterId = paths[0].srcAdapterId;
sourceId = paths[0].srcId;
return true;
}
public static int[] GetScaleRange() {
DcLuid adapter;
uint source;
if (!GetSourceIds(out adapter, out source)) { return new int[] { -999, -999, -999, -999 }; }
DcScaleGet g = new DcScaleGet();
g.header.type = -3;
g.header.size = (uint)Marshal.SizeOf(typeof(DcScaleGet));
g.header.adapterId = adapter;
g.header.id = source;
int rc = DisplayConfigGetDeviceInfo(ref g);
return new int[] { rc, g.minScaleRel, g.curScaleRel, g.maxScaleRel };
}
public static int SetScaleRelative(int rel) {
DcLuid adapter;
uint source;
if (!GetSourceIds(out adapter, out source)) { return -999; }
DcScaleSet s = new DcScaleSet();
s.header.type = -4;
s.header.size = (uint)Marshal.SizeOf(typeof(DcScaleSet));
s.header.adapterId = adapter;
s.header.id = source;
s.scaleRel = rel;
return DisplayConfigSetDeviceInfo(ref s);
}
}
}
'@
function Get-AwarenessName {
param([int]$Value)
switch ($Value) {
0 { return 'PROCESS_DPI_UNAWARE' }
1 { return 'PROCESS_SYSTEM_DPI_AWARE' }
2 { return 'PROCESS_PER_MONITOR_DPI_AWARE' }
default { return ('unknown(' + $Value + ')') }
}
}
function Invoke-DpiWorker {
Add-Type -TypeDefinition $DpiSource -Language CSharp
$row = [ordered]@{ mode = $Mode; processId = [int][AgentDesktopProbe.Dpi]::GetCurrentProcessId() }
$a = 0
[void][AgentDesktopProbe.Dpi]::GetProcessDpiAwareness([IntPtr]::Zero, [ref]$a)
$row['awarenessAtStartup'] = Get-AwarenessName -Value $a
if ($Mode -eq 'aware') {
$rc = [AgentDesktopProbe.Dpi]::SetProcessDpiAwarenessContext([IntPtr](-4))
$row['setProcessDpiAwarenessContextPerMonitorV2'] = if ($rc -ne [IntPtr]::Zero) { 'succeeded' } else { 'failed, error ' + [Runtime.InteropServices.Marshal]::GetLastWin32Error() }
} else {
$row['setProcessDpiAwarenessContextPerMonitorV2'] = 'not attempted (unaware arm, forced with __COMPAT_LAYER=DPIUNAWARE)'
}
[void][AgentDesktopProbe.Dpi]::GetProcessDpiAwareness([IntPtr]::Zero, [ref]$a)
$row['awarenessEffective'] = Get-AwarenessName -Value $a
$dx = 0
$dy = 0
$mon = [AgentDesktopProbe.Dpi]::MonitorFromPoint(0, 2)
[void][AgentDesktopProbe.Dpi]::GetDpiForMonitor($mon, 0, [ref]$dx, [ref]$dy)
$row['monitorEffectiveDpiX'] = [int]$dx
$row['monitorEffectiveDpiY'] = [int]$dy
$row['logPixels'] = [AgentDesktopProbe.Dpi]::LogPixels()
$row['screenMetricsCx'] = [AgentDesktopProbe.Dpi]::GetSystemMetrics(0)
$row['screenMetricsCy'] = [AgentDesktopProbe.Dpi]::GetSystemMetrics(1)
$target = [IntPtr][int64]$TargetHwnd
$edit = [IntPtr][int64]$EditHwnd
$r = New-Object AgentDesktopProbe.Rect
[void][AgentDesktopProbe.Dpi]::GetWindowRect($target, [ref]$r)
$row['getWindowRect'] = [ordered]@{ Left = $r.Left; Top = $r.Top; Right = $r.Right; Bottom = $r.Bottom }
Add-Type -AssemblyName UIAutomationClient
Add-Type -AssemblyName UIAutomationTypes
foreach ($pair in @(@('window', $target), @('editChild', $edit))) {
$cell = [ordered]@{}
try {
$el = [System.Windows.Automation.AutomationElement]::FromHandle([IntPtr]$pair[1])
$b = $el.Current.BoundingRectangle
$cell['className'] = $el.Current.ClassName
$cell['Left'] = [int]$b.Left
$cell['Top'] = [int]$b.Top
$cell['Width'] = [int]$b.Width
$cell['Height'] = [int]$b.Height
} catch { $cell['error'] = $_.Exception.Message }
$row[('uiaBounds_' + $pair[0])] = $cell
}
[IO.File]::WriteAllText($WorkerOut, (ConvertTo-Json -InputObject $row -Depth 10), (New-Object System.Text.UTF8Encoding $false))
}
if ($Worker) {
try { Invoke-DpiWorker } catch {
[IO.File]::WriteAllText($WorkerOut, (ConvertTo-Json -InputObject ([ordered]@{ mode = $Mode; fatal = $_.Exception.Message }) -Depth 6), (New-Object System.Text.UTF8Encoding $false))
}
exit 0
}
$status = 'ok'
$message = ''
$summary = [ordered]@{}
$script:Spawned = New-Object System.Collections.ArrayList
$script:OriginalScaleRel = $null
$script:WorkDir = Join-Path $env:TEMP 'agent-desktop-u8'
function Invoke-DpiArm {
param([string]$ArmMode, [string]$Tag, [IntPtr]$Target, [IntPtr]$Edit)
$out = Join-Path $script:WorkDir ('dpi-' + $Tag + '-' + $ArmMode + '.json')
if (Test-Path -LiteralPath $out) { Remove-Item -LiteralPath $out -Force }
$argv = @('-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-ExecutionPolicy', 'Bypass',
'-File', $PSCommandPath, '-Worker', '-WorkerOut', $out, '-Mode', $ArmMode,
'-TargetHwnd', ([string]$Target.ToInt64()), '-EditHwnd', ([string]$Edit.ToInt64()))
if ($ArmMode -eq 'unaware') { $env:__COMPAT_LAYER = 'DPIUNAWARE' } else { $env:__COMPAT_LAYER = $null }
$proc = Start-Process -FilePath (Join-Path $env:WINDIR 'System32\WindowsPowerShell\v1.0\powershell.exe') -ArgumentList $argv -WindowStyle Hidden -PassThru
Register-ScratchProcessId -ProcessId $proc.Id
[void]$script:Spawned.Add($proc.Id)
$env:__COMPAT_LAYER = $null
$deadline = (Get-Date).AddSeconds(120)
while ((Get-Date) -lt $deadline -and -not (Test-Path -LiteralPath $out)) { Start-Sleep -Milliseconds 400 }
try { Stop-ScratchProcess -ProcessId $proc.Id } catch { }
if (-not (Test-Path -LiteralPath $out)) { return [ordered]@{ mode = $ArmMode; fatal = 'worker produced no output' } }
return (Get-Content -LiteralPath $out -Raw | ConvertFrom-Json)
}
function Get-BoundsDelta {
param($Aware, $Unaware, [string]$Field)
$a = $Aware.$Field
$u = $Unaware.$Field
if ($null -eq $a -or $null -eq $u -or $a.error -or $u.error) { return [ordered]@{ measurable = $false } }
return [ordered]@{
measurable = $true
deltaLeft = ([int]$a.Left - [int]$u.Left)
deltaTop = ([int]$a.Top - [int]$u.Top)
deltaWidth = ([int]$a.Width - [int]$u.Width)
deltaHeight = ([int]$a.Height - [int]$u.Height)
}
}
try {
Initialize-ProbeNative
Add-Type -TypeDefinition $DpiSource -Language CSharp
if (-not (Test-Path -LiteralPath $script:WorkDir)) { New-Item -ItemType Directory -Path $script:WorkDir -Force | Out-Null }
$sid = 0
[void][AgentDesktopProbe.Dpi]::ProcessIdToSessionId([AgentDesktopProbe.Dpi]::GetCurrentProcessId(), [ref]$sid)
$session = [ordered]@{
sessionId = [int]$sid
activeConsoleSessionId = [int][AgentDesktopProbe.Dpi]::WTSGetActiveConsoleSessionId()
isRemoteSession = ([AgentDesktopProbe.Dpi]::GetSystemMetrics(0x1000) -ne 0)
windowStation = ([System.Environment]::GetEnvironmentVariable('SESSIONNAME'))
note = 'RDP session-transition behavior is not measurable here (physical console session); it closes at sub-phase 2.1 runner registration'
}
Add-Type -AssemblyName System.Windows.Forms
$displays = @()
foreach ($s in [System.Windows.Forms.Screen]::AllScreens) {
$displays += [ordered]@{
primary = $s.Primary
Left = $s.Bounds.Left
Top = $s.Bounds.Top
Width = $s.Bounds.Width
Height = $s.Bounds.Height
}
}
$monitorKeys = @(Get-ChildItem -LiteralPath 'HKCU:\Control Panel\Desktop\PerMonitorSettings' -ErrorAction SilentlyContinue | ForEach-Object { $_.PSChildName })
$range = [AgentDesktopProbe.Dpi]::GetScaleRange()
$script:OriginalScaleRel = $range[2]
$scale = [ordered]@{
displayConfigGetResult = $range[0]
scaleRelMin = $range[1]
scaleRelCurrent = $range[2]
scaleRelMax = $range[3]
monitorRegistryKeys = $monitorKeys
stepsAvailableAboveRecommended = ($range[3] - $range[1])
}
$notepad = Start-ScratchProcess -FilePath (Join-Path $env:WINDIR 'System32\notepad.exe') -TimeoutSec 20
[void]$script:Spawned.Add($notepad.ProcessId)
if ($notepad.MainWindowHandle -eq [IntPtr]::Zero) { throw 'dpi target notepad window never appeared' }
$editHandle = [AgentDesktopProbe.Dpi]::FindChildByClass($notepad.MainWindowHandle, 'Edit')
Start-Sleep -Seconds 1
$passes = @()
foreach ($step in @(
[pscustomobject]@{ Tag = 'recommended'; Rel = $script:OriginalScaleRel; Wanted = 'display recommended scale (100%)' },
[pscustomobject]@{ Tag = 'plus-one-step'; Rel = ($script:OriginalScaleRel + 1); Wanted = 'one scale step above recommended (125%)' })) {
$setResult = $null
if ($step.Rel -ne $script:OriginalScaleRel) {
$setResult = [AgentDesktopProbe.Dpi]::SetScaleRelative($step.Rel)
Start-Sleep -Milliseconds 1500
}
$after = [AgentDesktopProbe.Dpi]::GetScaleRange()
$regValue = $null
if ($monitorKeys.Count -gt 0) {
$regValue = (Get-ItemProperty -Path ('HKCU:\Control Panel\Desktop\PerMonitorSettings\' + $monitorKeys[0]) -Name 'DpiValue' -ErrorAction SilentlyContinue).DpiValue
}
$aware = Invoke-DpiArm -ArmMode 'aware' -Tag $step.Tag -Target $notepad.MainWindowHandle -Edit $editHandle
$unaware = Invoke-DpiArm -ArmMode 'unaware' -Tag $step.Tag -Target $notepad.MainWindowHandle -Edit $editHandle
$passes += [ordered]@{
pass = $step.Tag
requested = $step.Wanted
requestedScaleRel = $step.Rel
displayConfigSetResult = $setResult
scaleRelReportedAfterwards = $after[2]
registryDpiValueAfterwards = $regValue
effectiveDpiSeenByAwareArm = $aware.monitorEffectiveDpiX
scaleActuallyApplied = ($aware.monitorEffectiveDpiX -ne 96)
awareArm = $aware
unawareArm = $unaware
windowBoundsDelta = (Get-BoundsDelta -Aware $aware -Unaware $unaware -Field 'uiaBounds_window')
editChildBoundsDelta = (Get-BoundsDelta -Aware $aware -Unaware $unaware -Field 'uiaBounds_editChild')
}
}
$restoreResult = [AgentDesktopProbe.Dpi]::SetScaleRelative($script:OriginalScaleRel)
Start-Sleep -Milliseconds 1500
$restored = [AgentDesktopProbe.Dpi]::GetScaleRange()
$restoredReg = $null
if ($monitorKeys.Count -gt 0) {
$restoredReg = (Get-ItemProperty -Path ('HKCU:\Control Panel\Desktop\PerMonitorSettings\' + $monitorKeys[0]) -Name 'DpiValue' -ErrorAction SilentlyContinue).DpiValue
}
$teardown = [ordered]@{
originalScaleRel = $script:OriginalScaleRel
displayConfigSetResult = $restoreResult
scaleRelReadBackAfterwards = $restored[2]
registryDpiValueAfterwards = $restoredReg
restored = ($restored[2] -eq $script:OriginalScaleRel)
}
$capture = [ordered]@{
question = 'what bounds delta does DPI awareness produce between two processes reading the same element, and what scale can this environment actually apply'
stack = 'managed UIA for BoundingRectangle; raw Win32 for awareness, DisplayConfig and metrics'
scope = 'api-contract for the awareness APIs; app/provider and environment-specific for the achievable scale'
session = $session
displays = $displays
displayScaleCapability = $scale
method = 'two sibling powershell children read the same probe-launched notepad window and its Edit child; the unaware arm is forced with __COMPAT_LAYER=DPIUNAWARE, the aware arm calls SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)'
normalizationNote = 'raw rectangles are bucketed to 8px by the KTD9 normalizer; delta fields are named deltaLeft/deltaTop/deltaWidth/deltaHeight, which the bucketing regex does not match, so the measured delta survives the normalized twin verbatim'
passes = $passes
teardown = $teardown
deferred = [ordered]@{
row = 'multi-monitor and mixed-DPI bounds behavior, and any non-zero aware-vs-unaware delta'
reason = 'single display, and that display reports no EDID so Windows offers exactly one scale step (min=cur=max); the 125% request is accepted by DisplayConfigSetDeviceInfo and persisted to the registry but never takes effect'
closesAt = 'sub-phase 2.4 (owns list_displays and per-monitor scale_factor) on a runner with a scalable display'
neverLeaves = 'Phase 2'
}
}
Write-ProbeJson -Probe $Probe -Name 'session-dpi.json' -InputObject $capture | Out-Null
$summary['sessionId'] = $session.sessionId
$summary['isRemoteSession'] = $session.isRemoteSession
$summary['displayCount'] = $displays.Count
$summary['scaleRelMinCurMax'] = ('' + $scale.scaleRelMin + '/' + $scale.scaleRelCurrent + '/' + $scale.scaleRelMax)
$summary['awarenessPerMonitorV2'] = $passes[0].awareArm.setProcessDpiAwarenessContextPerMonitorV2
$summary['unawareArmAwareness'] = $passes[0].unawareArm.awarenessEffective
$summary['awareArmAwareness'] = $passes[0].awareArm.awarenessEffective
$summary['windowDeltaAtRecommended'] = ('' + $passes[0].windowBoundsDelta.deltaLeft + ',' + $passes[0].windowBoundsDelta.deltaTop + ',' + $passes[0].windowBoundsDelta.deltaWidth + ',' + $passes[0].windowBoundsDelta.deltaHeight)
$summary['windowDeltaAt125Request'] = ('' + $passes[1].windowBoundsDelta.deltaLeft + ',' + $passes[1].windowBoundsDelta.deltaTop + ',' + $passes[1].windowBoundsDelta.deltaWidth + ',' + $passes[1].windowBoundsDelta.deltaHeight)
$summary['scaleActuallyAppliedAt125Request'] = $passes[1].scaleActuallyApplied
$summary['scaleRestored'] = $teardown.restored
$message = 'session and dpi measured; scale request and restoration both verified by read-back'
} catch {
$status = 'fail'
$message = ($_.Exception.Message -replace '[\r\n]+', ' ')
Write-ProbeLog -Message ('probe failed: ' + $message) -Level 'error'
} finally {
if ($null -ne $script:OriginalScaleRel) {
try { [void][AgentDesktopProbe.Dpi]::SetScaleRelative($script:OriginalScaleRel) } catch { }
}
$env:__COMPAT_LAYER = $null
foreach ($id in @($script:Spawned)) {
try { Stop-ScratchProcess -ProcessId $id } catch { Write-ProbeLog -Message ('teardown: ' + $_.Exception.Message) -Level 'warn' }
}
foreach ($f in @(Get-ChildItem -LiteralPath (Get-CaptureDir -Probe $Probe) -File -ErrorAction SilentlyContinue)) {
if ($f.Name -like '*.normalized') { continue }
if (-not (Test-CaptureRedaction -Path $f.FullName)) { $status = 'fail'; $message = ('redaction residue in ' + $f.Name) }
}
}
Write-ProbeResult -Probe $Probe -Status $status -Message $message -Data $summary
if ($status -eq 'fail') { exit 1 }
exit 0

View file

@ -0,0 +1,337 @@
<#
.SYNOPSIS
Probe 11 (sub-phase 2.0, unit U8): Chromium/Electron accessibility activation.
.DESCRIPTION
Grades the committed phases.md claim that Chromium 138+ "exposes a UIA tree to any
UIA client with no flag" against the installed Obsidian build, on both client stacks
(KTD1): managed System.Windows.Automation and the U7 UIA3 COM shim.
Four Obsidian instances are launched, one per (stack x flag) cell, because the first
UIA client to touch a renderer activates accessibility for that process - reusing one
instance would make the second stack's "first contact" a settled reading.
The bundled Chromium and Electron versions are read off THIS installation by scanning
Obsidian.exe for the embedded user-agent string. ELECTRON_RUN_AS_NODE=1 with
-p process.versions produces no output on this VM and the framework DLLs carry
component versions, so the UA scan is the determination of record. The activation
verdict is only graded once that version is established.
Two hazards carried from earlier units, both recorded as probe-placement hazards and
explicitly NOT product claims:
- U3: leaving other windows on top of Obsidian held it at its first-contact node
count through a full settle. Mechanism not isolated. This probe places Obsidian
itself and records the foreground owner at read time.
- U4: matching a bare Chrome_WidgetWin_1 window found an unrelated window and
recorded 9 nodes / 0% coverage as if it were the Electron answer. This probe
matches only windows whose owning pid is one of the launched Obsidian pids.
#>
[CmdletBinding()]
param()
$ErrorActionPreference = 'Stop'
. "$PSScriptRoot\common.ps1"
$Probe = '11-electron-activation'
$script:Spawned = New-Object System.Collections.ArrayList
function Initialize-WindowSupport {
if ('AgentDesktopProbe.Win11' -as [type]) { return }
Add-Type -Language CSharp -TypeDefinition @'
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
namespace AgentDesktopProbe {
public static class Win11 {
private delegate bool EnumProc(IntPtr h, IntPtr l);
[DllImport("user32.dll")] private static extern bool EnumWindows(EnumProc cb, IntPtr l);
[DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern int GetClassNameW(IntPtr h, StringBuilder b, int max);
[DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern int GetWindowTextW(IntPtr h, StringBuilder b, int max);
[DllImport("user32.dll")] private static extern bool IsWindowVisible(IntPtr h);
[DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
public static string ClassOf(IntPtr h) { StringBuilder b = new StringBuilder(256); GetClassNameW(h, b, 256); return b.ToString(); }
public static string TitleOf(IntPtr h) { StringBuilder b = new StringBuilder(512); GetWindowTextW(h, b, 512); return b.ToString(); }
public static int PidOf(IntPtr h) { uint p; GetWindowThreadProcessId(h, out p); return (int)p; }
public static IntPtr[] VisibleTopLevel() {
List<IntPtr> found = new List<IntPtr>();
EnumWindows(delegate(IntPtr h, IntPtr l) { if (IsWindowVisible(h)) { found.Add(h); } return true; }, IntPtr.Zero);
return found.ToArray();
}
}
}
'@ | Out-Null
}
function Get-BundledVersions {
param([string]$ExePath)
$rx = New-Object Text.RegularExpressions.Regex 'Chrome/(\d+\.\d+\.\d+\.\d+) Electron/(\d+\.\d+\.\d+)'
$fs = [IO.File]::OpenRead($ExePath)
$buf = New-Object byte[] (8MB)
$prev = ''
$chrome = ''
$electron = ''
try {
while (($n = $fs.Read($buf, 0, 8MB)) -gt 0) {
$text = $prev + [Text.Encoding]::GetEncoding(28591).GetString($buf, 0, $n)
$m = $rx.Match($text)
if ($m.Success) { $chrome = $m.Groups[1].Value; $electron = $m.Groups[2].Value; break }
if ($text.Length -ge 256) { $prev = $text.Substring($text.Length - 256) } else { $prev = $text }
}
} finally { $fs.Close() }
return [ordered]@{
method = 'user-agent string scanned out of the shipped Obsidian.exe on this installation'
obsidianFileVersion = (Get-Item -LiteralPath $ExePath).VersionInfo.ProductVersion
chromiumVersion = $chrome
electronVersion = $electron
established = ($chrome -ne '' -and $electron -ne '')
rejectedMethods = @(
'ELECTRON_RUN_AS_NODE=1 Obsidian.exe -p process.versions produced no output on this VM',
'framework DLL file versions carry component versions, not the Chromium version'
)
}
}
function Start-ObsidianInstance {
param([string]$ExePath, [string[]]$ExtraArgs = @())
Initialize-WindowSupport
if ($ExtraArgs.Count -gt 0) { Start-Process -FilePath $ExePath -ArgumentList $ExtraArgs | Out-Null }
else { Start-Process -FilePath $ExePath | Out-Null }
$deadline = (Get-Date).AddSeconds(75)
while ((Get-Date) -lt $deadline) {
$pids = @(Get-Process -Name 'Obsidian' -ErrorAction SilentlyContinue | ForEach-Object { $_.Id })
foreach ($id in $pids) {
if (-not $script:Spawned.Contains($id)) {
Register-ScratchProcessId -ProcessId $id
[void]$script:Spawned.Add($id)
}
}
foreach ($h in [AgentDesktopProbe.Win11]::VisibleTopLevel()) {
if ([AgentDesktopProbe.Win11]::ClassOf($h) -ne 'Chrome_WidgetWin_1') { continue }
$owner = [AgentDesktopProbe.Win11]::PidOf($h)
if ($pids -notcontains $owner) { continue }
if (-not [AgentDesktopProbe.Win11]::TitleOf($h)) { continue }
Show-WindowNoActivate -WindowHandle $h -X 0 -Y 0 -Width 1280 -Height 720
return [pscustomobject]@{ WindowHandle = $h; ProcessId = $owner; AllPids = $pids }
}
Start-Sleep -Milliseconds 700
}
return $null
}
function Stop-AllObsidian {
foreach ($p in @(Get-Process -Name 'Obsidian' -ErrorAction SilentlyContinue)) {
try { Stop-Process -Id $p.Id -Force -ErrorAction Stop } catch { }
}
$deadline = (Get-Date).AddSeconds(20)
while ((Get-Date) -lt $deadline) {
if (-not (Get-Process -Name 'Obsidian' -ErrorAction SilentlyContinue)) { return $true }
Start-Sleep -Milliseconds 400
}
return (-not (Get-Process -Name 'Obsidian' -ErrorAction SilentlyContinue))
}
function Measure-ManagedTree {
param([IntPtr]$WindowHandle, [string]$Prefix = '')
Add-Type -AssemblyName UIAutomationClient
Add-Type -AssemblyName UIAutomationTypes
$root = [System.Windows.Automation.AutomationElement]::FromHandle($WindowHandle)
$out = [ordered]@{}
foreach ($view in @('RawView', 'ControlView')) {
$walker = if ($view -eq 'RawView') { [System.Windows.Automation.TreeWalker]::RawViewWalker } else { [System.Windows.Automation.TreeWalker]::ControlViewWalker }
$stack = New-Object System.Collections.Stack
$stack.Push($root)
$count = 0
$refable = 0
while ($stack.Count -gt 0 -and $count -lt 8000) {
$node = $stack.Pop()
$count++
if (@($node.GetSupportedPatterns()).Count -gt 0) { $refable++ }
$child = $walker.GetFirstChild($node)
while ($null -ne $child) {
$stack.Push($child)
$child = $walker.GetNextSibling($child)
}
}
$out[($Prefix + $view + 'Nodes')] = $count
$out[($Prefix + $view + 'RefableNodes')] = $refable
}
return $out
}
function Invoke-ComShim {
param([string]$ExePath, [string[]]$ShimArgs, [int]$TimeoutSec = 300)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $ExePath
$psi.Arguments = ($ShimArgs -join ' ')
$psi.UseShellExecute = $false
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
$psi.CreateNoWindow = $true
$proc = [System.Diagnostics.Process]::Start($psi)
Register-ScratchProcessId -ProcessId $proc.Id
[void]$script:Spawned.Add($proc.Id)
$stdout = $proc.StandardOutput.ReadToEnd()
$stderr = $proc.StandardError.ReadToEnd()
if (-not $proc.WaitForExit($TimeoutSec * 1000)) { try { $proc.Kill() } catch { }; throw ('shim ' + $ShimArgs[0] + ' timed out') }
if ($proc.ExitCode -ne 0) { throw ('shim ' + $ShimArgs[0] + ' exited ' + $proc.ExitCode + ': ' + $stderr) }
return ($stdout | ConvertFrom-Json)
}
$status = 'ok'
$message = ''
$summary = [ordered]@{}
try {
Initialize-ProbeNative
Initialize-WindowSupport
$obsidianExe = Join-Path $env:LOCALAPPDATA 'Programs\Obsidian\Obsidian.exe'
if (-not (Test-Path -LiteralPath $obsidianExe)) { throw ('Obsidian not installed at ' + $obsidianExe) }
$preexisting = @(Get-Process -Name 'Obsidian' -ErrorAction SilentlyContinue).Count -gt 0
$versions = Get-BundledVersions -ExePath $obsidianExe
if (-not $versions.established) { throw 'bundled Chromium version could not be read off this installation' }
Write-ProbeLog -Message ('obsidian ' + $versions.obsidianFileVersion + ' bundles chromium ' + $versions.chromiumVersion + ' / electron ' + $versions.electronVersion)
$csc = Join-Path $env:WINDIR 'Microsoft.NET\Framework64\v4.0.30319\csc.exe'
$buildDir = Join-Path $env:TEMP 'agent-desktop-uia3'
if (-not (Test-Path -LiteralPath $buildDir)) { New-Item -ItemType Directory -Path $buildDir -Force | Out-Null }
$shim = Join-Path $buildDir '08-uia3-com.exe'
$shimOut = (& $csc @('/nologo', '/target:exe', '/langversion:5', '/platform:anycpu', ('/out:' + $shim),
'/reference:System.dll', '/reference:System.Core.dll', (Join-Path $PSScriptRoot '08-uia3-com.cs')) 2>&1 | Out-String).Trim()
if ($LASTEXITCODE -ne 0) { throw ('csc.exe failed building the U7 shim: ' + $shimOut) }
$cells = @()
foreach ($flagArm in @(
[pscustomobject]@{ Name = 'no-flag'; Args = @() },
[pscustomobject]@{ Name = 'force-renderer-accessibility'; Args = @('--force-renderer-accessibility') })) {
foreach ($stack in @('managed', 'uia3-com')) {
if (-not (Stop-AllObsidian)) { throw 'could not bring Obsidian to a clean state before an arm' }
$inst = Start-ObsidianInstance -ExePath $obsidianExe -ExtraArgs $flagArm.Args
if ($null -eq $inst) { throw ('Obsidian window never appeared for arm ' + $flagArm.Name + '/' + $stack) }
$cell = [ordered]@{
flagArm = $flagArm.Name
stack = $stack
authoritative = ($stack -eq 'uia3-com')
windowClassName = [AgentDesktopProbe.Win11]::ClassOf($inst.WindowHandle)
windowHandle = $inst.WindowHandle.ToInt64()
windowHost = [ordered]@{ processId = $inst.ProcessId }
latencyObsidianProcessCountAtWindowDiscovery = $inst.AllPids.Count
foregroundOwner = [ordered]@{ processId = [AgentDesktopProbe.Native]::GetForegroundProcessId() }
settleMs = 8000
}
if ($stack -eq 'managed') {
$cell['firstContact'] = Measure-ManagedTree -WindowHandle $inst.WindowHandle -Prefix 'latencyFirstContact'
Start-Sleep -Milliseconds 8000
$cell['afterSettle'] = Measure-ManagedTree -WindowHandle $inst.WindowHandle -Prefix 'settled'
} else {
$walker = Invoke-ComShim -ExePath $shim -ShimArgs @('walker', '--hwnd', ('0x' + $inst.WindowHandle.ToInt64().ToString('x')),
'--label', 'obsidian', '--max-nodes', '20000', '--max-depth', '60', '--settle-ms', '8000')
$fc = [ordered]@{}
foreach ($v in $walker.result.viewsFirstContact) { $fc[('latencyFirstContact' + $v.view + 'Nodes')] = $v.nodeCount }
$st = [ordered]@{}
foreach ($v in $walker.result.viewsAfterSettle) { $st[('settled' + $v.view + 'Nodes')] = $v.nodeCount }
$census = Invoke-ComShim -ExePath $shim -ShimArgs @('census', '--target', ('obsidian=0x' + $inst.WindowHandle.ToInt64().ToString('x')),
'--max-nodes', '20000', '--max-depth', '60')
$t = $census.result.targets[0]
$st['settledRefableNodesWithAnyPattern'] = $t.elementsWithAnyPattern
$st['settledRefableNodesExcludingLegacy'] = $t.elementsWithAnyPatternExcludingLegacy
$cell['firstContact'] = $fc
$cell['afterSettle'] = $st
$cell['censusNote'] = 'census ran in a second shim process against the already-activated instance, so its counts are settled counts; per-element rows are deliberately not captured (R11: Obsidian note titles are content)'
}
$rawFc = $cell.firstContact['latencyFirstContactRawViewNodes']
$rawSt = $cell.afterSettle['settledRawViewNodes']
$cell['latencyRawViewGrowthRatio'] = if ($rawFc -gt 0) { [math]::Round(($rawSt / [double]$rawFc), 2) } else { $null }
$cell['activatedWithoutFlag'] = ($flagArm.Name -eq 'no-flag' -and $rawSt -gt $rawFc)
$cells += $cell
Write-ProbeLog -Message ('arm ' + $flagArm.Name + '/' + $stack + ': raw ' + $rawFc + ' -> ' + $rawSt)
}
}
$allGone = Stop-AllObsidian
$noFlagCom = $cells | Where-Object { $_.flagArm -eq 'no-flag' -and $_.stack -eq 'uia3-com' } | Select-Object -First 1
$flagCom = $cells | Where-Object { $_.flagArm -ne 'no-flag' -and $_.stack -eq 'uia3-com' } | Select-Object -First 1
$noFlagManaged = $cells | Where-Object { $_.flagArm -eq 'no-flag' -and $_.stack -eq 'managed' } | Select-Object -First 1
$flagManaged = $cells | Where-Object { $_.flagArm -ne 'no-flag' -and $_.stack -eq 'managed' } | Select-Object -First 1
$bothActivate = ($noFlagCom.activatedWithoutFlag -and $noFlagManaged.activatedWithoutFlag)
$flagVerdict = 'ungraded'
if ($versions.established) {
$major = [int]($versions.chromiumVersion -split '\.')[0]
if ($major -lt 138) {
$flagVerdict = 'flag still required - bundled Chromium predates the 138 auto-UIA default'
} elseif ($bothActivate -and $noFlagCom.afterSettle['settledRawViewNodes'] -eq $flagCom.afterSettle['settledRawViewNodes']) {
$flagVerdict = 'partially useful - redundant for eventual tree exposure: on Chromium ' + $versions.chromiumVersion +
' both client stacks reach the same settled RawView count with no flag, so the phases.md claim holds for exposure. It is not redundant for first-contact readiness: without the flag first contact is deterministically the small pre-activation shell on every run and both stacks, while with the flag first contact is a race against the async tree build and was observed anywhere from a fraction of the settled count to nearly all of it. Neither arm removes the need to settle before trusting a first snapshot.'
} elseif ($bothActivate) {
$flagVerdict = 'redundant for tree exposure - both client stacks get a renderer tree with no flag on Chromium ' + $versions.chromiumVersion
} else {
$flagVerdict = 'still required - a 138+ Chromium did not expose a renderer tree without the flag on at least one stack'
}
}
$capture = [ordered]@{
question = 'does Chromium 138+ expose a UIA tree to any UIA client with no flag, and does --force-renderer-accessibility still change anything'
gradedAgainst = 'the committed phases.md line that Chromium 138+ exposes a UIA tree to any UIA client with no flag (P2-O15 / 2.4)'
stack = 'both: managed System.Windows.Automation and the U7 UIA3 COM shim (KTD1 - the COM rows are the authoritative ones)'
scope = 'app/provider - specific to this Obsidian/Electron/Chromium build; the activation mechanism is Chromium behavior, not a Windows API contract'
versions = $versions
obsidianPreexistingAtProbeStart = $preexisting
method = 'one fresh Obsidian instance per (stack x flag) cell, because the first UIA client to touch a renderer activates accessibility for that process'
placementHazards = @(
'U3: an earlier revision that left other windows on top of Obsidian held it at its first-contact node count through a full 8s settle and a 16s instrumented hold, three runs running. Minimizing the covering windows fixed it. The mechanism was NOT isolated (Chromium native-window occlusion tracking is the obvious candidate, unproven). Carried here as a probe-placement hazard, explicitly not a product claim.',
'U4: matching a bare Chrome_WidgetWin_1 window found an unrelated window and recorded 9 nodes / 0% coverage as if it were the Electron answer. This probe only accepts a window whose owning pid is one of the launched Obsidian pids, and reads the hosting pid back off the window.'
)
countStabilityNote = 'settled node counts are content-dependent (U7 measured RawView 172 on a bare walker run, U3 measured 119 through its probe). The stable fact is the growth ratio across the settle, not the absolute count.'
cells = $cells
findings = [ordered]@{
managedActivatesWithoutFlag = $noFlagManaged.activatedWithoutFlag
comActivatesWithoutFlag = $noFlagCom.activatedWithoutFlag
clientStackDivergence = ($noFlagManaged.activatedWithoutFlag -ne $noFlagCom.activatedWithoutFlag)
latencyComRawViewNoFlagFirstContact = $noFlagCom.firstContact['latencyFirstContactRawViewNodes']
latencyComRawViewWithFlagFirstContact = $flagCom.firstContact['latencyFirstContactRawViewNodes']
latencyManagedRawViewNoFlagFirstContact = $noFlagManaged.firstContact['latencyFirstContactRawViewNodes']
latencyManagedRawViewWithFlagFirstContact = $flagManaged.firstContact['latencyFirstContactRawViewNodes']
settledComRawViewNoFlag = $noFlagCom.afterSettle['settledRawViewNodes']
settledComRawViewWithFlag = $flagCom.afterSettle['settledRawViewNodes']
settledManagedRawViewNoFlag = $noFlagManaged.afterSettle['settledRawViewNodes']
settledManagedRawViewWithFlag = $flagManaged.afterSettle['settledRawViewNodes']
settledCountsMatchAcrossFlagArms = ($noFlagCom.afterSettle['settledRawViewNodes'] -eq $flagCom.afterSettle['settledRawViewNodes'] -and $noFlagManaged.afterSettle['settledRawViewNodes'] -eq $flagManaged.afterSettle['settledRawViewNodes'])
firstContactCountsAreRunVarying = 'first-contact counts are a race against Chromium async tree construction and are named latency* so the KTD9 normalizer canonicalizes them; the raw samples stay in this capture, the settled counts are the stable evidence'
flagVerdict = $flagVerdict
supersededPreFinding = 'the plan pre-finding (8 managed descendants, stable at 2/5/10s, candidate CONTRADICTS) is superseded: it was measured through an occluded window with a managed client, and neither the occlusion nor the settle window was controlled'
}
teardown = [ordered]@{ allObsidianProcessesTerminated = $allGone }
}
Write-ProbeJson -Probe $Probe -Name 'electron-activation.json' -InputObject $capture | Out-Null
$summary['chromiumVersion'] = $versions.chromiumVersion
$summary['electronVersion'] = $versions.electronVersion
$summary['obsidianVersion'] = $versions.obsidianFileVersion
$summary['comRawViewNoFlag'] = ('' + $capture.findings.latencyComRawViewNoFlagFirstContact + ' -> ' + $capture.findings.settledComRawViewNoFlag)
$summary['comRawViewWithFlag'] = ('' + $capture.findings.latencyComRawViewWithFlagFirstContact + ' -> ' + $capture.findings.settledComRawViewWithFlag)
$summary['managedRawViewNoFlag'] = ('' + $capture.findings.latencyManagedRawViewNoFlagFirstContact + ' -> ' + $capture.findings.settledManagedRawViewNoFlag)
$summary['managedRawViewWithFlag'] = ('' + $capture.findings.latencyManagedRawViewWithFlagFirstContact + ' -> ' + $capture.findings.settledManagedRawViewWithFlag)
$summary['clientStackDivergence'] = $capture.findings.clientStackDivergence
$summary['flagVerdict'] = $flagVerdict
$message = 'electron activation measured on both stacks with and without the flag'
} catch {
$status = 'fail'
$message = ($_.Exception.Message -replace '[\r\n]+', ' ')
Write-ProbeLog -Message ('probe failed: ' + $message) -Level 'error'
} finally {
[void](Stop-AllObsidian)
foreach ($id in @($script:Spawned)) {
try { Stop-ScratchProcess -ProcessId $id } catch { Write-ProbeLog -Message ('teardown: ' + $_.Exception.Message) -Level 'warn' }
}
foreach ($f in @(Get-ChildItem -LiteralPath (Get-CaptureDir -Probe $Probe) -File -ErrorAction SilentlyContinue)) {
if ($f.Name -like '*.normalized') { continue }
if (-not (Test-CaptureRedaction -Path $f.FullName)) { $status = 'fail'; $message = ('redaction residue in ' + $f.Name) }
}
}
Write-ProbeResult -Probe $Probe -Status $status -Message $message -Data $summary
if ($status -eq 'fail') { exit 1 }
exit 0

View file

@ -0,0 +1,194 @@
{
"question": "does UIPI block SendInput and window messages from a Medium-integrity process into a High-integrity target, and are UIA reads still allowed",
"invariant": "Engineering Invariant #6 (UIPI)",
"stack": "managed (UIA reads) + raw Win32 SendInput/SendMessage/PostMessage",
"scope": "api-contract",
"boundaryOrigin": "manufactured by common.ps1 Start-MediumIntegrityProcess (DuplicateTokenEx + SetTokenInformation(TokenIntegrityLevel, S-1-16-8192) + CreateProcessAsUser), label asserted on read-back",
"boundaryRuledOut": [
"Start-Process -Verb RunAs: AAM off on this box (FilterAdministratorToken unset, RID 500 full token) so it yields High-vs-High",
"runas.exe /trustlevel:0x20000: restricts the token but leaves the mandatory label at S-1-16-12288"
],
"uacPolicy": {
"EnableLUA": 1,
"FilterAdministratorToken": "\u003cvalue not present\u003e",
"ConsentPromptBehaviorAdmin": 5,
"EnableInstallerDetection": 1,
"sessionIntegritySid": "S-1-16-12288",
"adminApprovalModeActive": false
},
"target": {
"process": "notepad.exe",
"processId": 7156,
"integritySid": "S-1-16-12288",
"windowHandle": 6948082,
"editChild": {
"className": "Edit",
"windowHandle": 5243138
},
"launchedByThisProbe": true
},
"arms": [
{
"arm": "high",
"workerIntegritySid": "S-1-16-12288",
"workerLaunchError": "",
"marker": "HHH",
"parentForegroundHandoff": "target foreground restored by parent ShowWindow(SW_MINIMIZE-\u003eSW_RESTORE) on the probe-launched window; SetForegroundWindow never called",
"targetStateBeforeInjection": "U8-BASE",
"targetStateAfterInjection": "HHHPU8-BASE",
"targetStateChanged": true,
"markerPresentInTargetAfterwards": true,
"observedBy": "parent High-integrity WM_GETTEXT on the notepad Edit child, independent of SendInput return",
"worker": {
"arm": "high",
"processId": 7916,
"addTypeCompilationAvailable": true,
"selfIntegritySid": "S-1-16-12288",
"targetIntegritySid": "S-1-16-12288",
"uiaRead": {
"fromHandle": "ok",
"name": "Untitled - Notepad",
"className": "Notepad",
"controlType": "Window",
"ownerProcessId": 7156,
"boundingRectangle": {
"Left": 476,
"Top": 100,
"Width": 768,
"Height": 525
},
"rawViewNodes": 3,
"editPatterns": [
],
"editValueSeenByThisProcess": "\u003cno value/text pattern: Exception calling \"GetCurrentPattern\" with \"1\" argument(s): \"Unsupported Pattern.\"\u003e"
},
"wmGetTextFromWorker": {
"returned": "U8-BASE",
"sendMessageResult": 7,
"lastError": 0
},
"foregroundOwnerAtAssert": {
"processId": 7156
},
"foregroundAssertPre": "ok",
"sendInputEventsAccepted": 6,
"sendInputLastError": 0,
"sendInputStructSize": 40,
"foregroundAssertPost": "ok",
"postMessageWmChar": {
"returned": true,
"lastError": 203
},
"wmGetTextAfterFromWorker": {
"returned": "HHHPU8-BASE",
"sendMessageResult": 11,
"lastError": 0
},
"uiaReadAfter": {
"fromHandle": "ok",
"name": "Untitled - Notepad",
"className": "Notepad",
"controlType": "Window",
"ownerProcessId": 7156,
"boundingRectangle": {
"Left": 476,
"Top": 100,
"Width": 768,
"Height": 525
},
"rawViewNodes": 3,
"editPatterns": [
],
"editValueSeenByThisProcess": "\u003cno value/text pattern: Exception calling \"GetCurrentPattern\" with \"1\" argument(s): \"Unsupported Pattern.\"\u003e"
}
}
},
{
"arm": "medium",
"workerIntegritySid": "S-1-16-8192",
"workerLaunchError": "",
"marker": "MMM",
"parentForegroundHandoff": "target foreground restored by parent ShowWindow(SW_MINIMIZE-\u003eSW_RESTORE) on the probe-launched window; SetForegroundWindow never called",
"targetStateBeforeInjection": "U8-BASE",
"targetStateAfterInjection": "U8-BASE",
"targetStateChanged": false,
"markerPresentInTargetAfterwards": false,
"observedBy": "parent High-integrity WM_GETTEXT on the notepad Edit child, independent of SendInput return",
"worker": {
"arm": "medium",
"processId": 7508,
"addTypeCompilationAvailable": false,
"addTypeCompilationError": "A required privilege is not held by the client",
"selfIntegritySid": "S-1-16-8192",
"targetIntegritySid": "S-1-16-12288",
"uiaRead": {
"fromHandle": "ok",
"name": "Untitled - Notepad",
"className": "Notepad",
"controlType": "Window",
"ownerProcessId": 7156,
"boundingRectangle": {
"Left": 476,
"Top": 100,
"Width": 768,
"Height": 525
},
"rawViewNodes": 3,
"editPatterns": [
],
"editValueSeenByThisProcess": "\u003cno value/text pattern: Exception calling \"GetCurrentPattern\" with \"1\" argument(s): \"Unsupported Pattern.\"\u003e"
},
"wmGetTextFromWorker": {
"returned": "U8-BASE",
"sendMessageResult": 7,
"lastError": 0
},
"foregroundOwnerAtAssert": {
"processId": 7156
},
"foregroundAssertPre": "ok",
"sendInputEventsAccepted": 6,
"sendInputLastError": 0,
"sendInputStructSize": 40,
"foregroundAssertPost": "ok",
"postMessageWmChar": {
"returned": false,
"lastError": 5
},
"wmGetTextAfterFromWorker": {
"returned": "U8-BASE",
"sendMessageResult": 7,
"lastError": 0
},
"uiaReadAfter": {
"fromHandle": "ok",
"name": "Untitled - Notepad",
"className": "Notepad",
"controlType": "Window",
"ownerProcessId": 7156,
"boundingRectangle": {
"Left": 476,
"Top": 100,
"Width": 768,
"Height": 525
},
"rawViewNodes": 3,
"editPatterns": [
],
"editValueSeenByThisProcess": "\u003cno value/text pattern: Exception calling \"GetCurrentPattern\" with \"1\" argument(s): \"Unsupported Pattern.\"\u003e"
}
}
}
],
"findings": {
"uiaReadFromMediumAgainstHigh": "ok",
"sendInputFromHighLanded": true,
"sendInputFromMediumLanded": false,
"sendInputReturnIsNotEvidence": "SendInput reports the event count it accepted in both arms; only the re-read of the target distinguishes them",
"wmGetTextFromMediumBlocked": false
}
}

View file

@ -0,0 +1,194 @@
{
"question": "does UIPI block SendInput and window messages from a Medium-integrity process into a High-integrity target, and are UIA reads still allowed",
"invariant": "Engineering Invariant #6 (UIPI)",
"stack": "managed (UIA reads) + raw Win32 SendInput/SendMessage/PostMessage",
"scope": "api-contract",
"boundaryOrigin": "manufactured by common.ps1 Start-MediumIntegrityProcess (DuplicateTokenEx + SetTokenInformation(TokenIntegrityLevel, S-1-16-8192) + CreateProcessAsUser), label asserted on read-back",
"boundaryRuledOut": [
"Start-Process -Verb RunAs: AAM off on this box (FilterAdministratorToken unset, RID 500 full token) so it yields High-vs-High",
"runas.exe /trustlevel:0x20000: restricts the token but leaves the mandatory label at S-1-16-12288"
],
"uacPolicy": {
"EnableLUA": 1,
"FilterAdministratorToken": "\u003cvalue not present\u003e",
"ConsentPromptBehaviorAdmin": 5,
"EnableInstallerDetection": 1,
"sessionIntegritySid": "S-1-16-12288",
"adminApprovalModeActive": false
},
"target": {
"process": "notepad.exe",
"processId": <pid>,
"integritySid": "S-1-16-12288",
"windowHandle": <hwnd>,
"editChild": {
"className": "Edit",
"windowHandle": <hwnd>
},
"launchedByThisProbe": true
},
"arms": [
{
"arm": "high",
"workerIntegritySid": "S-1-16-12288",
"workerLaunchError": "",
"marker": "HHH",
"parentForegroundHandoff": "target foreground restored by parent ShowWindow(SW_MINIMIZE-\u003eSW_RESTORE) on the probe-launched window; SetForegroundWindow never called",
"targetStateBeforeInjection": "U8-BASE",
"targetStateAfterInjection": "HHHPU8-BASE",
"targetStateChanged": true,
"markerPresentInTargetAfterwards": true,
"observedBy": "parent High-integrity WM_GETTEXT on the notepad Edit child, independent of SendInput return",
"worker": {
"arm": "high",
"processId": <pid>,
"addTypeCompilationAvailable": true,
"selfIntegritySid": "S-1-16-12288",
"targetIntegritySid": "S-1-16-12288",
"uiaRead": {
"fromHandle": "ok",
"name": "Untitled - Notepad",
"className": "Notepad",
"controlType": "Window",
"ownerProcessId": <pid>,
"boundingRectangle": {
"Left": 480,
"Top": 96,
"Width": 768,
"Height": 528
},
"rawViewNodes": 3,
"editPatterns": [
],
"editValueSeenByThisProcess": "\u003cno value/text pattern: Exception calling \"GetCurrentPattern\" with \"1\" argument(s): \"Unsupported Pattern.\"\u003e"
},
"wmGetTextFromWorker": {
"returned": "U8-BASE",
"sendMessageResult": 7,
"lastError": 0
},
"foregroundOwnerAtAssert": {
"processId": <pid>
},
"foregroundAssertPre": "ok",
"sendInputEventsAccepted": 6,
"sendInputLastError": 0,
"sendInputStructSize": 40,
"foregroundAssertPost": "ok",
"postMessageWmChar": {
"returned": true,
"lastError": 203
},
"wmGetTextAfterFromWorker": {
"returned": "HHHPU8-BASE",
"sendMessageResult": 11,
"lastError": 0
},
"uiaReadAfter": {
"fromHandle": "ok",
"name": "Untitled - Notepad",
"className": "Notepad",
"controlType": "Window",
"ownerProcessId": <pid>,
"boundingRectangle": {
"Left": 480,
"Top": 96,
"Width": 768,
"Height": 528
},
"rawViewNodes": 3,
"editPatterns": [
],
"editValueSeenByThisProcess": "\u003cno value/text pattern: Exception calling \"GetCurrentPattern\" with \"1\" argument(s): \"Unsupported Pattern.\"\u003e"
}
}
},
{
"arm": "medium",
"workerIntegritySid": "S-1-16-8192",
"workerLaunchError": "",
"marker": "MMM",
"parentForegroundHandoff": "target foreground restored by parent ShowWindow(SW_MINIMIZE-\u003eSW_RESTORE) on the probe-launched window; SetForegroundWindow never called",
"targetStateBeforeInjection": "U8-BASE",
"targetStateAfterInjection": "U8-BASE",
"targetStateChanged": false,
"markerPresentInTargetAfterwards": false,
"observedBy": "parent High-integrity WM_GETTEXT on the notepad Edit child, independent of SendInput return",
"worker": {
"arm": "medium",
"processId": <pid>,
"addTypeCompilationAvailable": false,
"addTypeCompilationError": "A required privilege is not held by the client",
"selfIntegritySid": "S-1-16-8192",
"targetIntegritySid": "S-1-16-12288",
"uiaRead": {
"fromHandle": "ok",
"name": "Untitled - Notepad",
"className": "Notepad",
"controlType": "Window",
"ownerProcessId": <pid>,
"boundingRectangle": {
"Left": 480,
"Top": 96,
"Width": 768,
"Height": 528
},
"rawViewNodes": 3,
"editPatterns": [
],
"editValueSeenByThisProcess": "\u003cno value/text pattern: Exception calling \"GetCurrentPattern\" with \"1\" argument(s): \"Unsupported Pattern.\"\u003e"
},
"wmGetTextFromWorker": {
"returned": "U8-BASE",
"sendMessageResult": 7,
"lastError": 0
},
"foregroundOwnerAtAssert": {
"processId": <pid>
},
"foregroundAssertPre": "ok",
"sendInputEventsAccepted": 6,
"sendInputLastError": 0,
"sendInputStructSize": 40,
"foregroundAssertPost": "ok",
"postMessageWmChar": {
"returned": false,
"lastError": 5
},
"wmGetTextAfterFromWorker": {
"returned": "U8-BASE",
"sendMessageResult": 7,
"lastError": 0
},
"uiaReadAfter": {
"fromHandle": "ok",
"name": "Untitled - Notepad",
"className": "Notepad",
"controlType": "Window",
"ownerProcessId": <pid>,
"boundingRectangle": {
"Left": 480,
"Top": 96,
"Width": 768,
"Height": 528
},
"rawViewNodes": 3,
"editPatterns": [
],
"editValueSeenByThisProcess": "\u003cno value/text pattern: Exception calling \"GetCurrentPattern\" with \"1\" argument(s): \"Unsupported Pattern.\"\u003e"
}
}
}
],
"findings": {
"uiaReadFromMediumAgainstHigh": "ok",
"sendInputFromHighLanded": true,
"sendInputFromMediumLanded": false,
"sendInputReturnIsNotEvidence": "SendInput reports the event count it accepted in both arms; only the re-read of the target distinguishes them",
"wmGetTextFromMediumBlocked": false
}
}

View file

@ -0,0 +1,224 @@
{
"question": "what bounds delta does DPI awareness produce between two processes reading the same element, and what scale can this environment actually apply",
"stack": "managed UIA for BoundingRectangle; raw Win32 for awareness, DisplayConfig and metrics",
"scope": "api-contract for the awareness APIs; app/provider and environment-specific for the achievable scale",
"session": {
"sessionId": 1,
"activeConsoleSessionId": 1,
"isRemoteSession": false,
"windowStation": "Console",
"note": "RDP session-transition behavior is not measurable here (physical console session); it closes at sub-phase 2.1 runner registration"
},
"displays": [
{
"primary": true,
"Left": 0,
"Top": 0,
"Width": 1639,
"Height": 732
}
],
"displayScaleCapability": {
"displayConfigGetResult": 0,
"scaleRelMin": 0,
"scaleRelCurrent": 0,
"scaleRelMax": 0,
"monitorRegistryKeys": [
"NOEDID_15AD_0405_00000000_000F0000_0^20ED182961F2CFDB3A2D28C95A99744F"
],
"stepsAvailableAboveRecommended": 0
},
"method": "two sibling powershell children read the same probe-launched notepad window and its Edit child; the unaware arm is forced with __COMPAT_LAYER=DPIUNAWARE, the aware arm calls SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)",
"normalizationNote": "raw rectangles are bucketed to 8px by the KTD9 normalizer; delta fields are named deltaLeft/deltaTop/deltaWidth/deltaHeight, which the bucketing regex does not match, so the measured delta survives the normalized twin verbatim",
"passes": [
{
"pass": "recommended",
"requested": "display recommended scale (100%)",
"requestedScaleRel": 0,
"displayConfigSetResult": null,
"scaleRelReportedAfterwards": 0,
"registryDpiValueAfterwards": 0,
"effectiveDpiSeenByAwareArm": 96,
"scaleActuallyApplied": false,
"awareArm": {
"mode": "aware",
"processId": 8764,
"awarenessAtStartup": "PROCESS_DPI_UNAWARE",
"setProcessDpiAwarenessContextPerMonitorV2": "succeeded",
"awarenessEffective": "PROCESS_PER_MONITOR_DPI_AWARE",
"monitorEffectiveDpiX": 96,
"monitorEffectiveDpiY": 96,
"logPixels": 96,
"screenMetricsCx": 1639,
"screenMetricsCy": 732,
"getWindowRect": {
"Left": 476,
"Top": 100,
"Right": 1244,
"Bottom": 625
},
"uiaBounds_window": {
"className": "Notepad",
"Left": 476,
"Top": 100,
"Width": 768,
"Height": 525
},
"uiaBounds_editChild": {
"className": "Edit",
"Left": 484,
"Top": 151,
"Width": 752,
"Height": 443
}
},
"unawareArm": {
"mode": "unaware",
"processId": 304,
"awarenessAtStartup": "PROCESS_DPI_UNAWARE",
"setProcessDpiAwarenessContextPerMonitorV2": "not attempted (unaware arm, forced with __COMPAT_LAYER=DPIUNAWARE)",
"awarenessEffective": "PROCESS_DPI_UNAWARE",
"monitorEffectiveDpiX": 96,
"monitorEffectiveDpiY": 96,
"logPixels": 96,
"screenMetricsCx": 1639,
"screenMetricsCy": 732,
"getWindowRect": {
"Left": 476,
"Top": 100,
"Right": 1244,
"Bottom": 625
},
"uiaBounds_window": {
"className": "Notepad",
"Left": 476,
"Top": 100,
"Width": 768,
"Height": 525
},
"uiaBounds_editChild": {
"className": "Edit",
"Left": 484,
"Top": 151,
"Width": 752,
"Height": 443
}
},
"windowBoundsDelta": {
"measurable": true,
"deltaLeft": 0,
"deltaTop": 0,
"deltaWidth": 0,
"deltaHeight": 0
},
"editChildBoundsDelta": {
"measurable": true,
"deltaLeft": 0,
"deltaTop": 0,
"deltaWidth": 0,
"deltaHeight": 0
}
},
{
"pass": "plus-one-step",
"requested": "one scale step above recommended (125%)",
"requestedScaleRel": 1,
"displayConfigSetResult": 0,
"scaleRelReportedAfterwards": 1,
"registryDpiValueAfterwards": 1,
"effectiveDpiSeenByAwareArm": 96,
"scaleActuallyApplied": false,
"awareArm": {
"mode": "aware",
"processId": 8412,
"awarenessAtStartup": "PROCESS_DPI_UNAWARE",
"setProcessDpiAwarenessContextPerMonitorV2": "succeeded",
"awarenessEffective": "PROCESS_PER_MONITOR_DPI_AWARE",
"monitorEffectiveDpiX": 96,
"monitorEffectiveDpiY": 96,
"logPixels": 96,
"screenMetricsCx": 1639,
"screenMetricsCy": 732,
"getWindowRect": {
"Left": 476,
"Top": 100,
"Right": 1244,
"Bottom": 625
},
"uiaBounds_window": {
"className": "Notepad",
"Left": 476,
"Top": 100,
"Width": 768,
"Height": 525
},
"uiaBounds_editChild": {
"className": "Edit",
"Left": 484,
"Top": 151,
"Width": 752,
"Height": 443
}
},
"unawareArm": {
"mode": "unaware",
"processId": 5060,
"awarenessAtStartup": "PROCESS_DPI_UNAWARE",
"setProcessDpiAwarenessContextPerMonitorV2": "not attempted (unaware arm, forced with __COMPAT_LAYER=DPIUNAWARE)",
"awarenessEffective": "PROCESS_DPI_UNAWARE",
"monitorEffectiveDpiX": 96,
"monitorEffectiveDpiY": 96,
"logPixels": 96,
"screenMetricsCx": 1639,
"screenMetricsCy": 732,
"getWindowRect": {
"Left": 476,
"Top": 100,
"Right": 1244,
"Bottom": 625
},
"uiaBounds_window": {
"className": "Notepad",
"Left": 476,
"Top": 100,
"Width": 768,
"Height": 525
},
"uiaBounds_editChild": {
"className": "Edit",
"Left": 484,
"Top": 151,
"Width": 752,
"Height": 443
}
},
"windowBoundsDelta": {
"measurable": true,
"deltaLeft": 0,
"deltaTop": 0,
"deltaWidth": 0,
"deltaHeight": 0
},
"editChildBoundsDelta": {
"measurable": true,
"deltaLeft": 0,
"deltaTop": 0,
"deltaWidth": 0,
"deltaHeight": 0
}
}
],
"teardown": {
"originalScaleRel": 0,
"displayConfigSetResult": 0,
"scaleRelReadBackAfterwards": 0,
"registryDpiValueAfterwards": 0,
"restored": true
},
"deferred": {
"row": "multi-monitor and mixed-DPI bounds behavior, and any non-zero aware-vs-unaware delta",
"reason": "single display, and that display reports no EDID so Windows offers exactly one scale step (min=cur=max); the 125% request is accepted by DisplayConfigSetDeviceInfo and persisted to the registry but never takes effect",
"closesAt": "sub-phase 2.4 (owns list_displays and per-monitor scale_factor) on a runner with a scalable display",
"neverLeaves": "Phase 2"
}
}

View file

@ -0,0 +1,224 @@
{
"question": "what bounds delta does DPI awareness produce between two processes reading the same element, and what scale can this environment actually apply",
"stack": "managed UIA for BoundingRectangle; raw Win32 for awareness, DisplayConfig and metrics",
"scope": "api-contract for the awareness APIs; app/provider and environment-specific for the achievable scale",
"session": {
"sessionId": 1,
"activeConsoleSessionId": 1,
"isRemoteSession": false,
"windowStation": "Console",
"note": "RDP session-transition behavior is not measurable here (physical console session); it closes at sub-phase 2.1 runner registration"
},
"displays": [
{
"primary": true,
"Left": 0,
"Top": 0,
"Width": 1640,
"Height": 736
}
],
"displayScaleCapability": {
"displayConfigGetResult": 0,
"scaleRelMin": 0,
"scaleRelCurrent": 0,
"scaleRelMax": 0,
"monitorRegistryKeys": [
"NOEDID_15AD_0405_00000000_000F0000_0^20ED182961F2CFDB3A2D28C95A99744F"
],
"stepsAvailableAboveRecommended": 0
},
"method": "two sibling powershell children read the same probe-launched notepad window and its Edit child; the unaware arm is forced with __COMPAT_LAYER=DPIUNAWARE, the aware arm calls SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)",
"normalizationNote": "raw rectangles are bucketed to 8px by the KTD9 normalizer; delta fields are named deltaLeft/deltaTop/deltaWidth/deltaHeight, which the bucketing regex does not match, so the measured delta survives the normalized twin verbatim",
"passes": [
{
"pass": "recommended",
"requested": "display recommended scale (100%)",
"requestedScaleRel": 0,
"displayConfigSetResult": null,
"scaleRelReportedAfterwards": 0,
"registryDpiValueAfterwards": 0,
"effectiveDpiSeenByAwareArm": 96,
"scaleActuallyApplied": false,
"awareArm": {
"mode": "aware",
"processId": <pid>,
"awarenessAtStartup": "PROCESS_DPI_UNAWARE",
"setProcessDpiAwarenessContextPerMonitorV2": "succeeded",
"awarenessEffective": "PROCESS_PER_MONITOR_DPI_AWARE",
"monitorEffectiveDpiX": 96,
"monitorEffectiveDpiY": 96,
"logPixels": 96,
"screenMetricsCx": 1639,
"screenMetricsCy": 732,
"getWindowRect": {
"Left": 480,
"Top": 96,
"Right": 1248,
"Bottom": 624
},
"uiaBounds_window": {
"className": "Notepad",
"Left": 480,
"Top": 96,
"Width": 768,
"Height": 528
},
"uiaBounds_editChild": {
"className": "Edit",
"Left": 480,
"Top": 152,
"Width": 752,
"Height": 440
}
},
"unawareArm": {
"mode": "unaware",
"processId": <pid>,
"awarenessAtStartup": "PROCESS_DPI_UNAWARE",
"setProcessDpiAwarenessContextPerMonitorV2": "not attempted (unaware arm, forced with __COMPAT_LAYER=DPIUNAWARE)",
"awarenessEffective": "PROCESS_DPI_UNAWARE",
"monitorEffectiveDpiX": 96,
"monitorEffectiveDpiY": 96,
"logPixels": 96,
"screenMetricsCx": 1639,
"screenMetricsCy": 732,
"getWindowRect": {
"Left": 480,
"Top": 96,
"Right": 1248,
"Bottom": 624
},
"uiaBounds_window": {
"className": "Notepad",
"Left": 480,
"Top": 96,
"Width": 768,
"Height": 528
},
"uiaBounds_editChild": {
"className": "Edit",
"Left": 480,
"Top": 152,
"Width": 752,
"Height": 440
}
},
"windowBoundsDelta": {
"measurable": true,
"deltaLeft": 0,
"deltaTop": 0,
"deltaWidth": 0,
"deltaHeight": 0
},
"editChildBoundsDelta": {
"measurable": true,
"deltaLeft": 0,
"deltaTop": 0,
"deltaWidth": 0,
"deltaHeight": 0
}
},
{
"pass": "plus-one-step",
"requested": "one scale step above recommended (125%)",
"requestedScaleRel": 1,
"displayConfigSetResult": 0,
"scaleRelReportedAfterwards": 1,
"registryDpiValueAfterwards": 1,
"effectiveDpiSeenByAwareArm": 96,
"scaleActuallyApplied": false,
"awareArm": {
"mode": "aware",
"processId": <pid>,
"awarenessAtStartup": "PROCESS_DPI_UNAWARE",
"setProcessDpiAwarenessContextPerMonitorV2": "succeeded",
"awarenessEffective": "PROCESS_PER_MONITOR_DPI_AWARE",
"monitorEffectiveDpiX": 96,
"monitorEffectiveDpiY": 96,
"logPixels": 96,
"screenMetricsCx": 1639,
"screenMetricsCy": 732,
"getWindowRect": {
"Left": 480,
"Top": 96,
"Right": 1248,
"Bottom": 624
},
"uiaBounds_window": {
"className": "Notepad",
"Left": 480,
"Top": 96,
"Width": 768,
"Height": 528
},
"uiaBounds_editChild": {
"className": "Edit",
"Left": 480,
"Top": 152,
"Width": 752,
"Height": 440
}
},
"unawareArm": {
"mode": "unaware",
"processId": <pid>,
"awarenessAtStartup": "PROCESS_DPI_UNAWARE",
"setProcessDpiAwarenessContextPerMonitorV2": "not attempted (unaware arm, forced with __COMPAT_LAYER=DPIUNAWARE)",
"awarenessEffective": "PROCESS_DPI_UNAWARE",
"monitorEffectiveDpiX": 96,
"monitorEffectiveDpiY": 96,
"logPixels": 96,
"screenMetricsCx": 1639,
"screenMetricsCy": 732,
"getWindowRect": {
"Left": 480,
"Top": 96,
"Right": 1248,
"Bottom": 624
},
"uiaBounds_window": {
"className": "Notepad",
"Left": 480,
"Top": 96,
"Width": 768,
"Height": 528
},
"uiaBounds_editChild": {
"className": "Edit",
"Left": 480,
"Top": 152,
"Width": 752,
"Height": 440
}
},
"windowBoundsDelta": {
"measurable": true,
"deltaLeft": 0,
"deltaTop": 0,
"deltaWidth": 0,
"deltaHeight": 0
},
"editChildBoundsDelta": {
"measurable": true,
"deltaLeft": 0,
"deltaTop": 0,
"deltaWidth": 0,
"deltaHeight": 0
}
}
],
"teardown": {
"originalScaleRel": 0,
"displayConfigSetResult": 0,
"scaleRelReadBackAfterwards": 0,
"registryDpiValueAfterwards": 0,
"restored": true
},
"deferred": {
"row": "multi-monitor and mixed-DPI bounds behavior, and any non-zero aware-vs-unaware delta",
"reason": "single display, and that display reports no EDID so Windows offers exactly one scale step (min=cur=max); the 125% request is accepted by DisplayConfigSetDeviceInfo and persisted to the registry but never takes effect",
"closesAt": "sub-phase 2.4 (owns list_displays and per-monitor scale_factor) on a runner with a scalable display",
"neverLeaves": "Phase 2"
}
}

View file

@ -0,0 +1,164 @@
{
"question": "does Chromium 138+ expose a UIA tree to any UIA client with no flag, and does --force-renderer-accessibility still change anything",
"gradedAgainst": "the committed phases.md line that Chromium 138+ exposes a UIA tree to any UIA client with no flag (P2-O15 / 2.4)",
"stack": "both: managed System.Windows.Automation and the U7 UIA3 COM shim (KTD1 - the COM rows are the authoritative ones)",
"scope": "app/provider - specific to this Obsidian/Electron/Chromium build; the activation mechanism is Chromium behavior, not a Windows API contract",
"versions": {
"method": "user-agent string scanned out of the shipped Obsidian.exe on this installation",
"obsidianFileVersion": "1.12.7.0",
"chromiumVersion": "142.0.7444.265",
"electronVersion": "39.8.3",
"established": true,
"rejectedMethods": [
"ELECTRON_RUN_AS_NODE=1 Obsidian.exe -p process.versions produced no output on this VM",
"framework DLL file versions carry component versions, not the Chromium version"
]
},
"obsidianPreexistingAtProbeStart": false,
"method": "one fresh Obsidian instance per (stack x flag) cell, because the first UIA client to touch a renderer activates accessibility for that process",
"placementHazards": [
"U3: an earlier revision that left other windows on top of Obsidian held it at its first-contact node count through a full 8s settle and a 16s instrumented hold, three runs running. Minimizing the covering windows fixed it. The mechanism was NOT isolated (Chromium native-window occlusion tracking is the obvious candidate, unproven). Carried here as a probe-placement hazard, explicitly not a product claim.",
"U4: matching a bare Chrome_WidgetWin_1 window found an unrelated window and recorded 9 nodes / 0% coverage as if it were the Electron answer. This probe only accepts a window whose owning pid is one of the launched Obsidian pids, and reads the hosting pid back off the window."
],
"countStabilityNote": "settled node counts are content-dependent (U7 measured RawView 172 on a bare walker run, U3 measured 119 through its probe). The stable fact is the growth ratio across the settle, not the absolute count.",
"cells": [
{
"flagArm": "no-flag",
"stack": "managed",
"authoritative": false,
"windowClassName": "Chrome_WidgetWin_1",
"windowHandle": 5309714,
"windowHost": {
"processId": 9204
},
"latencyObsidianProcessCountAtWindowDiscovery": 4,
"foregroundOwner": {
"processId": 980
},
"settleMs": 8000,
"firstContact": {
"latencyFirstContactRawViewNodes": 13,
"latencyFirstContactRawViewRefableNodes": 13,
"latencyFirstContactControlViewNodes": 130,
"latencyFirstContactControlViewRefableNodes": 130
},
"afterSettle": {
"settledRawViewNodes": 172,
"settledRawViewRefableNodes": 172,
"settledControlViewNodes": 133,
"settledControlViewRefableNodes": 133
},
"latencyRawViewGrowthRatio": 13.23,
"activatedWithoutFlag": true
},
{
"flagArm": "no-flag",
"stack": "uia3-com",
"authoritative": true,
"windowClassName": "Chrome_WidgetWin_1",
"windowHandle": 6422788,
"windowHost": {
"processId": 9000
},
"latencyObsidianProcessCountAtWindowDiscovery": 4,
"foregroundOwner": {
"processId": 980
},
"settleMs": 8000,
"firstContact": {
"latencyFirstContactRawViewNodes": 13,
"latencyFirstContactControlViewNodes": 9,
"latencyFirstContactContentViewNodes": 9
},
"afterSettle": {
"settledRawViewNodes": 172,
"settledControlViewNodes": 133,
"settledContentViewNodes": 133,
"settledRefableNodesWithAnyPattern": 171,
"settledRefableNodesExcludingLegacy": 171
},
"censusNote": "census ran in a second shim process against the already-activated instance, so its counts are settled counts; per-element rows are deliberately not captured (R11: Obsidian note titles are content)",
"latencyRawViewGrowthRatio": 13.23,
"activatedWithoutFlag": true
},
{
"flagArm": "force-renderer-accessibility",
"stack": "managed",
"authoritative": false,
"windowClassName": "Chrome_WidgetWin_1",
"windowHandle": 11993994,
"windowHost": {
"processId": 7840
},
"latencyObsidianProcessCountAtWindowDiscovery": 4,
"foregroundOwner": {
"processId": 980
},
"settleMs": 8000,
"firstContact": {
"latencyFirstContactRawViewNodes": 164,
"latencyFirstContactRawViewRefableNodes": 163,
"latencyFirstContactControlViewNodes": 133,
"latencyFirstContactControlViewRefableNodes": 133
},
"afterSettle": {
"settledRawViewNodes": 172,
"settledRawViewRefableNodes": 172,
"settledControlViewNodes": 133,
"settledControlViewRefableNodes": 133
},
"latencyRawViewGrowthRatio": 1.05,
"activatedWithoutFlag": false
},
{
"flagArm": "force-renderer-accessibility",
"stack": "uia3-com",
"authoritative": true,
"windowClassName": "Chrome_WidgetWin_1",
"windowHandle": 5570818,
"windowHost": {
"processId": 6108
},
"latencyObsidianProcessCountAtWindowDiscovery": 4,
"foregroundOwner": {
"processId": 980
},
"settleMs": 8000,
"firstContact": {
"latencyFirstContactRawViewNodes": 141,
"latencyFirstContactControlViewNodes": 127,
"latencyFirstContactContentViewNodes": 131
},
"afterSettle": {
"settledRawViewNodes": 172,
"settledControlViewNodes": 133,
"settledContentViewNodes": 133,
"settledRefableNodesWithAnyPattern": 172,
"settledRefableNodesExcludingLegacy": 172
},
"censusNote": "census ran in a second shim process against the already-activated instance, so its counts are settled counts; per-element rows are deliberately not captured (R11: Obsidian note titles are content)",
"latencyRawViewGrowthRatio": 1.22,
"activatedWithoutFlag": false
}
],
"findings": {
"managedActivatesWithoutFlag": true,
"comActivatesWithoutFlag": true,
"clientStackDivergence": false,
"latencyComRawViewNoFlagFirstContact": 13,
"latencyComRawViewWithFlagFirstContact": 141,
"latencyManagedRawViewNoFlagFirstContact": 13,
"latencyManagedRawViewWithFlagFirstContact": 164,
"settledComRawViewNoFlag": 172,
"settledComRawViewWithFlag": 172,
"settledManagedRawViewNoFlag": 172,
"settledManagedRawViewWithFlag": 172,
"settledCountsMatchAcrossFlagArms": true,
"firstContactCountsAreRunVarying": "first-contact counts are a race against Chromium async tree construction and are named latency* so the KTD9 normalizer canonicalizes them; the raw samples stay in this capture, the settled counts are the stable evidence",
"flagVerdict": "partially useful - redundant for eventual tree exposure: on Chromium 142.0.7444.265 both client stacks reach the same settled RawView count with no flag, so the phases.md claim holds for exposure. It is not redundant for first-contact readiness: without the flag first contact is deterministically the small pre-activation shell on every run and both stacks, while with the flag first contact is a race against the async tree build and was observed anywhere from a fraction of the settled count to nearly all of it. Neither arm removes the need to settle before trusting a first snapshot.",
"supersededPreFinding": "the plan pre-finding (8 managed descendants, stable at 2/5/10s, candidate CONTRADICTS) is superseded: it was measured through an occluded window with a managed client, and neither the occlusion nor the settle window was controlled"
},
"teardown": {
"allObsidianProcessesTerminated": true
}
}

View file

@ -0,0 +1,164 @@
{
"question": "does Chromium 138+ expose a UIA tree to any UIA client with no flag, and does --force-renderer-accessibility still change anything",
"gradedAgainst": "the committed phases.md line that Chromium 138+ exposes a UIA tree to any UIA client with no flag (P2-O15 / 2.4)",
"stack": "both: managed System.Windows.Automation and the U7 UIA3 COM shim (KTD1 - the COM rows are the authoritative ones)",
"scope": "app/provider - specific to this Obsidian/Electron/Chromium build; the activation mechanism is Chromium behavior, not a Windows API contract",
"versions": {
"method": "user-agent string scanned out of the shipped Obsidian.exe on this installation",
"obsidianFileVersion": "1.12.7.0",
"chromiumVersion": "142.0.7444.265",
"electronVersion": "39.8.3",
"established": true,
"rejectedMethods": [
"ELECTRON_RUN_AS_NODE=1 Obsidian.exe -p process.versions produced no output on this VM",
"framework DLL file versions carry component versions, not the Chromium version"
]
},
"obsidianPreexistingAtProbeStart": false,
"method": "one fresh Obsidian instance per (stack x flag) cell, because the first UIA client to touch a renderer activates accessibility for that process",
"placementHazards": [
"U3: an earlier revision that left other windows on top of Obsidian held it at its first-contact node count through a full 8s settle and a 16s instrumented hold, three runs running. Minimizing the covering windows fixed it. The mechanism was NOT isolated (Chromium native-window occlusion tracking is the obvious candidate, unproven). Carried here as a probe-placement hazard, explicitly not a product claim.",
"U4: matching a bare Chrome_WidgetWin_1 window found an unrelated window and recorded 9 nodes / 0% coverage as if it were the Electron answer. This probe only accepts a window whose owning pid is one of the launched Obsidian pids, and reads the hosting pid back off the window."
],
"countStabilityNote": "settled node counts are content-dependent (U7 measured RawView 172 on a bare walker run, U3 measured 119 through its probe). The stable fact is the growth ratio across the settle, not the absolute count.",
"cells": [
{
"flagArm": "no-flag",
"stack": "managed",
"authoritative": false,
"windowClassName": "Chrome_WidgetWin_1",
"windowHandle": <hwnd>,
"windowHost": {
"processId": <pid>
},
"latencyObsidianProcessCountAtWindowDiscovery": <duration>,
"foregroundOwner": {
"processId": <pid>
},
"settleMs": 8000,
"firstContact": {
"latencyFirstContactRawViewNodes": <duration>,
"latencyFirstContactRawViewRefableNodes": <duration>,
"latencyFirstContactControlViewNodes": <duration>,
"latencyFirstContactControlViewRefableNodes": <duration>
},
"afterSettle": {
"settledRawViewNodes": 172,
"settledRawViewRefableNodes": 172,
"settledControlViewNodes": 133,
"settledControlViewRefableNodes": 133
},
"latencyRawViewGrowthRatio": <duration>,
"activatedWithoutFlag": true
},
{
"flagArm": "no-flag",
"stack": "uia3-com",
"authoritative": true,
"windowClassName": "Chrome_WidgetWin_1",
"windowHandle": <hwnd>,
"windowHost": {
"processId": <pid>
},
"latencyObsidianProcessCountAtWindowDiscovery": <duration>,
"foregroundOwner": {
"processId": <pid>
},
"settleMs": 8000,
"firstContact": {
"latencyFirstContactRawViewNodes": <duration>,
"latencyFirstContactControlViewNodes": <duration>,
"latencyFirstContactContentViewNodes": <duration>
},
"afterSettle": {
"settledRawViewNodes": 172,
"settledControlViewNodes": 133,
"settledContentViewNodes": 133,
"settledRefableNodesWithAnyPattern": 171,
"settledRefableNodesExcludingLegacy": 171
},
"censusNote": "census ran in a second shim process against the already-activated instance, so its counts are settled counts; per-element rows are deliberately not captured (R11: Obsidian note titles are content)",
"latencyRawViewGrowthRatio": <duration>,
"activatedWithoutFlag": true
},
{
"flagArm": "force-renderer-accessibility",
"stack": "managed",
"authoritative": false,
"windowClassName": "Chrome_WidgetWin_1",
"windowHandle": <hwnd>,
"windowHost": {
"processId": <pid>
},
"latencyObsidianProcessCountAtWindowDiscovery": <duration>,
"foregroundOwner": {
"processId": <pid>
},
"settleMs": 8000,
"firstContact": {
"latencyFirstContactRawViewNodes": <duration>,
"latencyFirstContactRawViewRefableNodes": <duration>,
"latencyFirstContactControlViewNodes": <duration>,
"latencyFirstContactControlViewRefableNodes": <duration>
},
"afterSettle": {
"settledRawViewNodes": 172,
"settledRawViewRefableNodes": 172,
"settledControlViewNodes": 133,
"settledControlViewRefableNodes": 133
},
"latencyRawViewGrowthRatio": <duration>,
"activatedWithoutFlag": false
},
{
"flagArm": "force-renderer-accessibility",
"stack": "uia3-com",
"authoritative": true,
"windowClassName": "Chrome_WidgetWin_1",
"windowHandle": <hwnd>,
"windowHost": {
"processId": <pid>
},
"latencyObsidianProcessCountAtWindowDiscovery": <duration>,
"foregroundOwner": {
"processId": <pid>
},
"settleMs": 8000,
"firstContact": {
"latencyFirstContactRawViewNodes": <duration>,
"latencyFirstContactControlViewNodes": <duration>,
"latencyFirstContactContentViewNodes": <duration>
},
"afterSettle": {
"settledRawViewNodes": 172,
"settledControlViewNodes": 133,
"settledContentViewNodes": 133,
"settledRefableNodesWithAnyPattern": 172,
"settledRefableNodesExcludingLegacy": 172
},
"censusNote": "census ran in a second shim process against the already-activated instance, so its counts are settled counts; per-element rows are deliberately not captured (R11: Obsidian note titles are content)",
"latencyRawViewGrowthRatio": <duration>,
"activatedWithoutFlag": false
}
],
"findings": {
"managedActivatesWithoutFlag": true,
"comActivatesWithoutFlag": true,
"clientStackDivergence": false,
"latencyComRawViewNoFlagFirstContact": <duration>,
"latencyComRawViewWithFlagFirstContact": <duration>,
"latencyManagedRawViewNoFlagFirstContact": <duration>,
"latencyManagedRawViewWithFlagFirstContact": <duration>,
"settledComRawViewNoFlag": 172,
"settledComRawViewWithFlag": 172,
"settledManagedRawViewNoFlag": 172,
"settledManagedRawViewWithFlag": 172,
"settledCountsMatchAcrossFlagArms": true,
"firstContactCountsAreRunVarying": "first-contact counts are a race against Chromium async tree construction and are named latency* so the KTD9 normalizer canonicalizes them; the raw samples stay in this capture, the settled counts are the stable evidence",
"flagVerdict": "partially useful - redundant for eventual tree exposure: on Chromium 142.0.7444.265 both client stacks reach the same settled RawView count with no flag, so the phases.md claim holds for exposure. It is not redundant for first-contact readiness: without the flag first contact is deterministically the small pre-activation shell on every run and both stacks, while with the flag first contact is a race against the async tree build and was observed anywhere from a fraction of the settled count to nearly all of it. Neither arm removes the need to settle before trusting a first snapshot.",
"supersededPreFinding": "the plan pre-finding (8 managed descendants, stable at 2/5/10s, candidate CONTRADICTS) is superseded: it was measured through an occluded window with a managed client, and neither the occlusion nor the settle window was controlled"
},
"teardown": {
"allObsidianProcessesTerminated": true
}
}