Skip to content

Commit d4b55bd

Browse files
committed
fix(stage1): own-check.ps1 asked the platform to OPEN the candidate, never to run it
Two platforms answered the same question wrong, and both times the platform was not at fault. On Linux, `& $rustCore` against a file the loader cannot start returned 0 and printed `xdg-open: no method available for opening ...`. I read that as a platform limit and declared the spawn-seam control not-applicable off Windows. Windows CI then returned 0 with both streams empty, and the job's own cleanup line terminated an orphaned NOTEPAD. Same defect, two desktop handlers. PowerShell's call operator does not spawn a program; it asks the platform to OPEN it, and a file that is not a runnable image goes to whatever is registered for it. So D3.1's seam could not be reached anywhere: the try/catch added for review defect 3 was unreachable, and `own-check.ps1 -Engine rust` with an existing but unrunnable OWEN_RUST_CORE exited 0 with no findings — a clean bill of health for an analysis that never happened, which is worse than any wrong exit code. `Invoke-CandidateProcess` starts the image with UseShellExecute = $false and nothing redirected, so the child inherits this process's streams and its output still arrives live. The start either succeeds or throws, and the throw is what the existing catch was written for. Measured on Linux: exit 2 with the "could not be started" diagnostic, where the same input previously exited 0. The compare path was already correct — it goes through Invoke-CapturedProcess, which has always set UseShellExecute = $false. Only the direct Rust path opened its candidate. Consequences for the evidence, both of which cut against my earlier reading: The control is required on BOTH platforms and the not-applicable is gone. The seam is answerable anywhere now, because it is a real spawn. The N/A I added one commit ago was describing a bug in this script, not a property of Linux — and it was reasoned from a measurement I trusted too quickly. P07 is the mutation, and it is the shipped code: back to the call operator. It belongs in the Windows campaign with the rest of this surface, where a PowerShell catcher observes it natively. Also kept: the near-miss. This control PASSED on a developer container with no xdg-open installed, because the invocation failed there and looked exactly like a refusal. A verdict that turns on which desktop helper happens to be installed is not measuring the contract. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CYLNQy6tLXqsV1CbuNqsSb
1 parent a565c00 commit d4b55bd

3 files changed

Lines changed: 70 additions & 24 deletions

File tree

‎docs/evidence/p022-stage1-windows.json‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"schema": 1,
33
"comment": "GENERATED-BY-HAND definition; the RESULT beside it is recorded by scripts/mutate_campaign.py --run on a WINDOWS runner and the counts are derived from it, never typed.",
44
"campaign": "p022-stage1-windows",
5-
"description": "#262 Stage 1 — the surfaces whose contract only Windows can be asked about. `scripts/own-check.ps1` was driven by a CI smoke step but was absent from the adversarial control set, which is why three of its engine-contract defects survived a 16/16 campaign on the other surfaces: a campaign can only prove what some control observes. Beside it sits the Windows half of `scripts/own-check.sh`'s locator classifier, which the Linux campaign cannot reach at all — a drive-rooted path is absolute only where Windows resolves it, and a mutant of that arm is invisible on Linux whatever the runner reports. Every mutation here is a plausible MISREADING rather than a syntactic accident.",
5+
"description": "#262 Stage 1 — the surfaces whose contract only Windows can be asked about. `scripts/own-check.ps1` was driven by a CI smoke step but was absent from the adversarial control set, which is why three of its engine-contract defects survived a 16/16 campaign on the other surfaces: a campaign can only prove what some control observes. Beside it sits the Windows half of `scripts/own-check.sh`'s locator classifier, which the Linux campaign cannot reach at all — a drive-rooted path is absolute only where Windows resolves it, and a mutant of that arm is invisible on Linux whatever the runner reports. It also carries the spawn seam itself: a candidate must be STARTED, never handed to the platform to open, and the two desktop handlers that proved this — notepad and xdg-open — are why the seam was unreachable on either platform until it was fixed. Every mutation here is a plausible MISREADING rather than a syntactic accident.",
66
"layers": [
77
{
88
"id": "ps1",
@@ -97,6 +97,17 @@
9797
"expected_catchers": [
9898
"ps1::ps1-absolute-locator"
9999
]
100+
},
101+
{
102+
"id": "P07",
103+
"rule": "the-candidate-is-spawned-not-opened",
104+
"description": "own-check.ps1 goes back to invoking the candidate with the call operator — 'PowerShell runs it either way, why the ceremony?'. It does not run it: it asks the platform to OPEN it, so a file the loader cannot start is handed to a desktop handler (notepad on Windows, xdg-open on Linux), the run exits 0 with empty streams, and Owen reports a clean finding-free analysis of nothing",
105+
"target": "scripts/own-check.ps1",
106+
"pattern": " \\$psi\\.UseShellExecute = \\$false\n \\$proc = \\[System\\.Diagnostics\\.Process\\]::Start\\(\\$psi\\)\n \\$proc\\.WaitForExit\\(\\)\n return \\$proc\\.ExitCode",
107+
"replacement": " & $FilePath @ArgumentList\n return $LASTEXITCODE",
108+
"expected_catchers": [
109+
"ps1::ps1-not-started-is-2"
110+
]
100111
}
101112
]
102113
}

‎scripts/own-check.ps1‎

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,43 @@ param(
8080
Set-StrictMode -Version Latest
8181
$ErrorActionPreference = "Stop"
8282

83+
function Invoke-CandidateProcess {
84+
<#
85+
.SYNOPSIS
86+
Run the candidate with the parent's streams, as a REAL process spawn.
87+
88+
.DESCRIPTION
89+
PowerShell's call operator does not spawn a candidate; it asks the
90+
PLATFORM to "open" it, and a file the loader cannot run is then handed to
91+
whatever is registered for it. Measured on both platforms: on the Windows
92+
CI runner a non-image candidate opened in NOTEPAD (the job's own cleanup
93+
terminated it) and `& $rustCore` returned 0 with both streams empty; on
94+
Linux the same file went to xdg-open. Either way own-check.ps1 reported a
95+
clean, finding-free run having analysed nothing — a false "no findings",
96+
which is worse than any exit code.
97+
98+
UseShellExecute = $false is what makes this a spawn: the image is started
99+
or the start FAILS, with no file association anywhere in the path. The
100+
failure is deliberately allowed to propagate so the caller can map it to
101+
D3.1's configuration exit (2), which is the seam this whole path exists
102+
to honour.
103+
104+
Nothing is redirected, so the child inherits this process's stdout and
105+
stderr and its output streams live, exactly as the call operator's did.
106+
#>
107+
param(
108+
[Parameter(Mandatory = $true)][string]$FilePath,
109+
[Parameter(Mandatory = $true)][string[]]$ArgumentList
110+
)
111+
$psi = New-Object System.Diagnostics.ProcessStartInfo
112+
$psi.FileName = $FilePath
113+
foreach ($a in $ArgumentList) { $psi.ArgumentList.Add($a) }
114+
$psi.UseShellExecute = $false
115+
$proc = [System.Diagnostics.Process]::Start($psi)
116+
$proc.WaitForExit()
117+
return $proc.ExitCode
118+
}
119+
83120
function Invoke-CapturedProcess {
84121
<#
85122
.SYNOPSIS
@@ -212,15 +249,15 @@ try {
212249
# The PRODUCTION Rust executable, never own-shadow-engine.
213250
$rustArgs = @("ownir") + $ownirArgs
214251
try {
215-
& $rustCore @rustArgs
216-
$rc = $LASTEXITCODE
252+
$rc = Invoke-CandidateProcess -FilePath $rustCore -ArgumentList $rustArgs
217253
}
218254
catch {
219255
# D3.1's seam: the candidate never STARTED — an existing file the
220256
# loader will not run. That is "cannot select the candidate", so it
221257
# is a configuration error (2), not Owen failing internally (5).
222-
# On Windows this is the only point at which a non-runnable
223-
# candidate can be detected, since there is no execute bit to test.
258+
# Reaching this catch is why the call above is a spawn and not the
259+
# call operator: an "open" succeeds on a file that cannot run, and
260+
# a seam nothing can ever arrive at is not a seam.
224261
[Console]::Error.WriteLine(("own-check: the candidate ``own-cli`` binary could not be started: " +
225262
"'$rustCore' ($($_.Exception.Message)). Set OWEN_RUST_CORE to a runnable ``own-cli`` " +
226263
"executable. Owen did not fall back to Python."))

‎tests/test_stage1_ps1.py‎

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -220,27 +220,25 @@ def control_not_started_is_2(sample: Path, tmp: Path) -> None:
220220
221221
This is the case the CI smoke step could not reach: it used a NONEXISTENT
222222
path, which `Test-Path` rejects long before any spawn.
223+
224+
It took two wrong answers to get here, and both were the production code
225+
rather than the platform. I first declared this control Linux-N/A because
226+
PowerShell there returned 0 and printed `xdg-open: no method available for
227+
opening ...`; Windows CI then returned 0 with both streams empty, and the
228+
job's own cleanup terminated an orphaned NOTEPAD. Same defect, two desktop
229+
handlers: own-check.ps1 was ASKING THE PLATFORM TO OPEN the candidate
230+
rather than spawning it, so the seam could not be reached anywhere and
231+
Owen reported a clean, finding-free run having analysed nothing. With a
232+
real spawn (UseShellExecute = $false) the start either succeeds or throws,
233+
and the contract is answerable on both platforms — so this control is
234+
required on both, and the N/A is gone.
235+
236+
The near-miss is worth keeping: on a developer container with no xdg-open
237+
installed, this control PASSED, because the invocation failed and looked
238+
exactly like a refusal. A verdict that turns on which desktop helper
239+
happens to be installed is not measuring the contract.
223240
"""
224241
check = "ps1-not-started-is-2"
225-
if os.name != "nt":
226-
# Measured on a Linux runner: PowerShell there does not refuse a
227-
# non-executable file, it hands it to the DESKTOP OPENER — the run
228-
# exits 0 with `xdg-open: no method available for opening ...`. So
229-
# "the loader will not start this image" is not a state Linux can be
230-
# asked about; it answers a different question and answers it
231-
# successfully.
232-
#
233-
# Worth recording why this was nearly missed: the control PASSED on a
234-
# developer container, for the wrong reason — no xdg-open was
235-
# installed there, so the invocation failed and looked like a refusal.
236-
# A control whose verdict depends on whether a desktop helper happens
237-
# to be present is not measuring the contract. This is exactly the
238-
# case the Windows-native mutation leg exists for.
239-
not_applicable(check, "PowerShell on Linux routes a non-executable file to the desktop "
240-
"opener instead of refusing it, so the spawn seam cannot be posed "
241-
"here; it is required on Windows, where the ps1 mutation campaign "
242-
"runs")
243-
return
244242
unstartable = tmp / "not-a-program.txt"
245243
unstartable.write_text("this is text, not an executable image\n", encoding="utf-8")
246244

0 commit comments

Comments
 (0)