Post-sleep sessions still failed on 0.25.0 — the host was holding open the very device its recovery asks PnP to cycle #119

Merged
enricobuehler merged 3 commits from worktree-vdisplay-reap-pnputil into main 2026-08-08 13:29:11 +00:00
Owner

The field report

A 0.25.0 user still cannot connect after the host wakes from sleep — surviving the b6acbd09 probe fix (0.24.0). Their log shows the probe half working exactly as designed and dying in its terminal branch, every session, ~4.6 s per attempt:

no pf-vdisplay device interface found (0 active, 0 inactive)
pf-vdisplay: the adapter devnode exists but could NOT be reloaded … reason=Generic failure

Both reload levers fail: Disable-PnpDevice throws WMI's catch-all, and the pnputil /restart-device fallback exits non-zero — with the old code discarding why on both counts.

The structural cause (commit 3, the headline)

The control-device sharing contract was "bare HANDLE copies, never closed for the process lifetime": retired handles were deliberately kept alive because the pinger/linger threads and the capture delivery closures held raw copies whose soundness depended on no-close. So after a wake left the driver hostless, the host still held open handles to the dead control device — and an open control handle is exactly what vetoes the PnP disable the recovery leans on. reset-pf-vdisplay.ps1 stops the whole host service precisely to get those handles closed; the in-process recovery structurally could not.

Ownership is now Arc<OwnedHandle> all the way out: the manager's accessors hand out clones, every consumer holds its clone across its IOCTLs (the three capture closures each own one — Arc<OwnedHandle> is Send+Sync, ending the raw-isize smuggling), and DeviceSlot::retired is gone. Retiring drops only the manager's reference, so the handle closes when the last in-flight user drains. The recovery releases that reference at the first absent sighting — the existing 3 s ABSENT_SETTLE doubles as the drain window — and again before a not-ready-deadline reload.

Safe because: the driver attaches no meaning to the control file closing (host-gone is the IOCTL-liveness watchdog; EvtFileClose deliberately unhooked), and the dev_raw helper makes every raw-HANDLE view a borrow of the owning Arc, so a concurrent retire can never close a handle mid-IOCTL. Lock-order note: RECOVERY → device is now taken; the forbidden inverse still never occurs (VdisplayDriver::open never reloads).

The two companion fixes the same log demanded

Commit 2 — a refused reload now says WHY, and never targets a phantom. The REFUSED branch reported only the Disable exception and threw away the pnputil exit code (3010 "needs a reboot" is its own diagnosis), the devnode's state, and whether the right devnode was even targeted: Get-PnpDevice lists not-present phantom devnodes (upgrade leftovers), and Select-Object -First 1 could hand every recovery attempt a phantom — whose disable and restart fail exactly like the field log — while a live node sat unexamined. The selector now prefers present (OK-first) nodes, a phantom-only state gets a truthful "the device node is gone; reinstalling re-creates it" refusal, and the REFUSED line carries devnode counts, PnP Status, ConfigManager problem code, and the restart exit code — one field log now decides between handle-veto, phantom, and problem-state.

Commit 1 — the ghost-monitor reap can no longer fail in silence. The reap that keeps departed monitors from exhausting the IddCx slot budget (the 0x80070490 wedge) launched pnputil by bare name — under a LocalSystem PATH that can miss System32, SilentlyContinue swallowed the miss and the host logged nothing: "no ghosts" and "removed nothing" were byte-identical. Now pnputil is resolved via $env:SystemRoot (a SYSTEM process must not trust PATH — a planted pnputil.exe would run elevated), $LASTEXITCODE is pre-seeded per launch, and found/removed are reported unconditionally. Not this reporter's bug (their log refuted it), but the same vacuous-signal family, in the function next door.

Verification

  • Mac: scripts/xcheck.sh windows clippy (all four Windows-gated crates, --all-targets) + cargo fmt --all --check green.
  • .133 runner, committed tree (bytes proven on-box first): cargo check -p punktfunk-host --release RC=0; cargo test -p pf-vdisplay --release 61 passed / 0 failed / 5 ignored — the first time this suite has run on Windows rather than only compiled.
  • Still owed: an on-glass wake-cycle test — the close-unblocks-disable claim is argued from the mechanism, not yet observed on hardware. The commit-2 diagnostics confirm or refute it from the reporter's next log either way.
## The field report A 0.25.0 user still cannot connect after the host wakes from sleep — surviving the `b6acbd09` probe fix (0.24.0). Their log shows the probe half working exactly as designed and dying in its terminal branch, every session, ~4.6 s per attempt: ``` no pf-vdisplay device interface found (0 active, 0 inactive) pf-vdisplay: the adapter devnode exists but could NOT be reloaded … reason=Generic failure ``` Both reload levers fail: `Disable-PnpDevice` throws WMI's catch-all, and the `pnputil /restart-device` fallback exits non-zero — with the old code discarding *why* on both counts. ## The structural cause (commit 3, the headline) The control-device sharing contract was "bare `HANDLE` copies, never closed for the process lifetime": retired handles were deliberately kept alive because the pinger/linger threads and the capture delivery closures held raw copies whose soundness depended on no-close. So after a wake left the driver hostless, the host still held open handles to the dead control device — **and an open control handle is exactly what vetoes the PnP disable the recovery leans on.** `reset-pf-vdisplay.ps1` stops the whole host service precisely to get those handles closed; the in-process recovery structurally could not. Ownership is now `Arc<OwnedHandle>` all the way out: the manager's accessors hand out clones, every consumer holds its clone across its IOCTLs (the three capture closures each own one — `Arc<OwnedHandle>` is `Send+Sync`, ending the raw-`isize` smuggling), and `DeviceSlot::retired` is gone. Retiring drops only the manager's reference, so the handle **closes when the last in-flight user drains**. The recovery releases that reference at the first absent sighting — the existing 3 s `ABSENT_SETTLE` doubles as the drain window — and again before a not-ready-deadline reload. Safe because: the driver attaches no meaning to the control file closing (host-gone is the IOCTL-liveness watchdog; `EvtFileClose` deliberately unhooked), and the `dev_raw` helper makes every raw-`HANDLE` view a borrow of the owning Arc, so a concurrent retire can never close a handle mid-IOCTL. Lock-order note: `RECOVERY → device` is now taken; the forbidden inverse still never occurs (`VdisplayDriver::open` never reloads). ## The two companion fixes the same log demanded **Commit 2 — a refused reload now says WHY, and never targets a phantom.** The `REFUSED` branch reported only the Disable exception and threw away the pnputil exit code (3010 "needs a reboot" is its own diagnosis), the devnode's state, and whether the right devnode was even targeted: `Get-PnpDevice` lists not-present phantom devnodes (upgrade leftovers), and `Select-Object -First 1` could hand every recovery attempt a phantom — whose disable *and* restart fail exactly like the field log — while a live node sat unexamined. The selector now prefers present (OK-first) nodes, a phantom-only state gets a truthful "the device node is gone; reinstalling re-creates it" refusal, and the `REFUSED` line carries devnode counts, PnP `Status`, ConfigManager problem code, and the restart exit code — one field log now decides between handle-veto, phantom, and problem-state. **Commit 1 — the ghost-monitor reap can no longer fail in silence.** The reap that keeps departed monitors from exhausting the IddCx slot budget (the `0x80070490` wedge) launched pnputil by bare name — under a LocalSystem PATH that can miss System32, `SilentlyContinue` swallowed the miss and the host logged nothing: "no ghosts" and "removed nothing" were byte-identical. Now pnputil is resolved via `$env:SystemRoot` (a SYSTEM process must not trust PATH — a planted `pnputil.exe` would run elevated), `$LASTEXITCODE` is pre-seeded per launch, and found/removed are reported unconditionally. Not this reporter's bug (their log refuted it), but the same vacuous-signal family, in the function next door. ## Verification - Mac: `scripts/xcheck.sh windows clippy` (all four Windows-gated crates, `--all-targets`) + `cargo fmt --all --check` green. - `.133` runner, committed tree (bytes proven on-box first): `cargo check -p punktfunk-host --release` RC=0; `cargo test -p pf-vdisplay --release` **61 passed / 0 failed / 5 ignored** — the first time this suite has *run* on Windows rather than only compiled. - Still owed: an on-glass wake-cycle test — the close-unblocks-disable claim is argued from the mechanism, not yet observed on hardware. The commit-2 diagnostics confirm or refute it from the reporter's next log either way.
enricobuehler added 3 commits 2026-08-08 13:27:06 +00:00
The reap that keeps departed virtual monitors from exhausting the IddCx
monitor-slot budget launched pnputil by BARE NAME — under the LocalSystem
service's PATH that can miss System32, SilentlyContinue swallowed the
miss, and the Rust side logged only when the count was positive: a reap
that removed nothing and a box with no ghosts were byte-identical
(silence). Ghosts then ratcheted up with every sleep cycle until
IOCTL_ADD wedged at 0x80070490 and every session black-screened — and
the wedge self-heal shipped in 0.25.0 retried an ADD behind a reap that
could never remove anything, which is exactly a persistent post-sleep
"no connection" surviving the b6acbd09 probe fix.

Same family and same cure as the adapter-reload path one function down:
resolve pnputil via $env:SystemRoot (a SYSTEM process must not trust
PATH anyway — a planted pnputil.exe would run elevated), pre-seed
$LASTEXITCODE to failure before every launch, and report found AND
removed unconditionally so "no ghosts" and "removed nothing" are
finally distinguishable in a field log. The report parse is split out
and pinned by tests like classify_reload_output.
Field log 2026-08-08 (0.25.0, wake from sleep): every session died on
'the adapter devnode could not be reloaded (Generic failure)' — the WMI
catch-all — because the REFUSED branch reported only the Disable
exception and threw away everything that would identify the failure
mode: why the pnputil /restart-device fallback ALSO failed (its exit
code — 3010 'needs a reboot' is its own diagnosis), what state the
devnode was in, and whether the right devnode was even targeted.

That last one is a real trap, not just missing telemetry: Get-PnpDevice
lists not-present PHANTOM devnodes (upgrade/reinstall leftovers), and
Select-Object -First 1 could hand every recovery attempt a phantom —
whose disable and restart both fail exactly like the field log — while
a live node sat unexamined. The selector now prefers present nodes (OK
before problem-state), and a phantom-only state gets a truthful
refusal: no reload can revive a devnode record whose device is gone;
only reinstalling re-creates it.

The REFUSED line now carries devnode counts, the chosen node's PnP
Status + ConfigManager problem code, and the restart exit code, so the
next field log decides between handle-veto, phantom, and problem-state
instead of reading 'Generic failure'. Decode pinned by test.
fix(host/vdisplay): the host no longer vetoes its own wake-from-sleep recovery — control handles close on retire
ci / bun-nix (pull_request) Successful in 21s
ci / docs-site (pull_request) Successful in 1m23s
ci / web (pull_request) Successful in 1m29s
apple / swift (pull_request) Successful in 1m39s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 2m19s
android / android (pull_request) Successful in 8m16s
ci / rust (pull_request) Successful in 9m59s
fba22c6c64
The control-device sharing contract was 'bare HANDLE copies, never
closed for the process lifetime': retired handles were deliberately kept
alive because the pinger/linger threads and the capture delivery
closures held raw copies whose soundness depended on no-close. The cost
surfaced in the 2026-08-08 field log: after a wake left the driver
hostless, every adapter reload came back REFUSED (Generic failure) —
and an open control handle is exactly what vetoes the PnP disable (and
can wedge the pnputil restart) the recovery leans on.
reset-pf-vdisplay.ps1 stops the whole host service precisely to get
those handles closed; the in-process recovery could not, because the
process could never close them.

Ownership is now Arc all the way out: ensure_device/device_handle/
control_device_handle hand out Arc<OwnedHandle> clones, every consumer
holds its clone across its IOCTLs (the capture closures each own one —
Arc<OwnedHandle> is Send+Sync, ending the isize smuggling), and
retiring drops only the manager's reference, so the handle CLOSES when
the last in-flight user drains. DeviceSlot::retired is gone. The
recovery path now releases the manager's reference at the first absent
sighting — the 3 s ABSENT_SETTLE doubles as the drain window — and
again before a not-ready-deadline reload, so the PnP cycle finally runs
against a device the host is no longer holding open.

The driver attaches no meaning to the control file closing (host-gone
is the IOCTL-liveness watchdog, EvtFileClose deliberately unhooked), so
the close has no driver-side side effects. Lock order note: RECOVERY →
device is now taken (the release hooks); the forbidden inverse still
never occurs — VdisplayDriver::open never reloads.
enricobuehler merged commit 608baf63be into main 2026-08-08 13:29:11 +00:00
enricobuehler deleted branch worktree-vdisplay-reap-pnputil 2026-08-08 13:29:12 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#119