Skip to content

feat(service): checked Windows stop primitives and operations, unwired (#7771 slice 1) - #8564

Merged
lidge-jun merged 5 commits into
devfrom
feat/windows-checked-stop-primitives-2
Nov 11, 2026
Merged

lidge-jun merged 5 commits into
devfrom
feat/windows-checked-stop-primitives-2

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Nov 11, 2026 •

Copy link
Copy Markdown
Owner

Summary

Slice 1 of 3 for #7771 Option B, the owner-approved design in devlog/_plan/261111_windows_service_stop/ (030 is the contract). It adds the checked Windows stop as tested library code. Nothing calls it yet, so behavior is unchanged on every platform; slice 2 wires the callers, and slice 3 (Desktop, #8560) is merged.

Module What it does
src/service/windows-process-native.ts Windows x64 Bun FFI, loaded only from resolveTrustedWindowsSystemDirectory() under the #8487 rules. It reads a process creation time (GetProcessTimes). The terminate opens the process, reads the creation time on that same handle, refuses a mismatch as reused, then calls TerminateProcess on the same handle and closes it in finally. Also an exit watch opened at bind time and the Schedule service PID (QueryServiceStatusEx). Returns null on ARM64 and whenever a test simulates the platform.
src/service/windows-process-evidence.ts One CIM process snapshot with CreationDate. On x64 a native creation time is kept only when it matches CIM to the millisecond, so a PID reused between the two reads counts as unreadable. Also a capped sc.exe queryex Schedule fallback, and the ARM64 PowerShell terminate (StartTime within 1 s, then Kill() on the same object).
src/service/windows-stop-binding.ts The 030 binding rule as a pure function: only wscript.exe, cscript.exe and cmd.exe carrying this home's launcher or service-script path as a whole token, bound by lineage (ancestor of the identity-checked child, child of a bound wrapper, parent gone only while this home's task is registered, or the Schedule service host), with a creation time. The proxy child is never a target. Unreadable candidates make the result unknown; they are never skipped or killed.
src/service/windows-stop-budget.ts The step caps, the admission arithmetic and the Part B window.
src/service/windows-checked-stop.ts Part A, stopWindowsServiceChecked. Every effect is injected; every step is capped except the exit-watch open and the updater authorizer, which run inside the shared terminate window (recorded in 060). Admission runs after the registration probe and returns unverified-budget, changing nothing, when the remaining deadline cannot hold the rest. Outcomes: absent / stopped / survived / denied / unknown, with a reason and the bound set. guardedStopWindowsServiceChecked (for the updater) re-authorizes before each terminate.
src/service/windows-stop-confirm.ts Part B, confirmWindowsStopFreed: the port stays free for a 6 s hold, then a final enumeration catches a relaunched wrapper. A remaining listener is classified as ours, foreign, or an exited owner (not foreign).

Two recorded departures from 030's numbers and wording:

  • Part B's last sample: the last port check can run up to 0.5 s past the 6 s window, and the budget counts it. The admission minimums become 48.5 / 56.5 / 42.5 s; the Desktop exit drain still fits its 72 s, at 70.5 s.
  • An unbound wrapper with nothing registered reports survived / unbound-wrapper, as 040 specifies.

ARM64: the bounded PowerShell fallback exists, but per 050, with no ARM64 device run, slice 2 ships ARM64 with the strict variant (kill nothing, report unknown).

Docs: new structure/ops/windows-checked-stop.md (linked from runtime.md, manifest and INDEX regenerated), and devlog/_plan/261111_windows_service_stop/060_implementation.md with the slice plan.

Refs #7771.

Verification

  • New hermetic tests in tests/windows/ use only an injected platform, injected effects and a virtual clock (no real schtasks, PowerShell or FFI calls): 72 pass, 2 skip (the live Windows x64 FFI cases, which run on the Windows lane), 0 fail, including one test per fail-closed rule (unreadable parent never binds; registered + unreadable parent is unknown; terminate overrun is unknown with no further kill; unreadable task or WinSW state is unknown; unreadable stray candidate is unknown; denied exit watch is denied; the child-ancestor lineage clause binds on its own; an unnamed-image listener is unknown), each shown to fail when its rule is mutated, and a regression test that fractional deadline-derived timeouts are floored before the fallback runs. They cover each 040 bullet for these modules: binding positives and negatives, PID reuse leaving the process untouched, denied stopping every further kill, unreadable evidence giving unknown with nothing killed, a stray wrapper with the task not running, an orphan bound only while registered, WinSW survived, caps giving unknown, admission refusal changing nothing, Part B catching a late relaunch, ours vs foreign, and a ghost listener.
  • Fail-first: with the modules absent every test file fails. Breaking the orphan, denied and late-relaunch rules one at a time fails exactly the intended tests.
  • Existing Windows service suites (guarded manager, process-state, winsw, wrappers, scheduler, stop-deferred-teardown, service): 584 pass, 31 skip, 0 fail. bun run test:changed selects only the new files, so nothing imports the modules yet.
  • bun run typecheck, bun run privacy:scan, bun run structure:check, test-layout, Lab boundary and file-size ratchet: pass (rebased on current dev).

NOT RUN: Windows real device and ARM64; the live FFI tests on this host (Windows CI lane only); full bun run test (nothing calls the new code).

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

…wired

Adds the library half of #7771 option B as tested code that nothing calls yet,
so behavior is unchanged on every platform:

- windows-process-native: x64 FFI from the trusted system directory for the
  process creation time (GetProcessTimes), a handle terminate that compares the
  creation time on the same handle it terminates, a bind-time exit watch, and the
  Task Scheduler service PID (QueryServiceStatusEx).
- windows-process-evidence: the CIM snapshot with creation times (native times
  kept only when they agree with CIM to the ms), an sc.exe queryex fallback, and
  the bounded ARM64 PowerShell terminate (StartTime check within 1 s, Kill() on the
  same object, exit read back).
- windows-stop-binding: the 030 binding rule as a pure function.
- windows-stop-budget: the caps and the admission arithmetic, including the 0.5 s
  closing port sample part B can take past its window (48.5 / 56.5 / 42.5 s).
- windows-checked-stop: part A with every effect injected, admission after the
  registration probe, and the authorizer-gated variant for the updater.
- windows-stop-confirm: part B (6 s hold, final enumeration, ours vs foreign vs
  exited-owner listener) and the port resolution order.

Contract in structure/ops/windows-checked-stop.md; slice plan and the points
where this differs from 030 in devlog/_plan/261111_windows_service_stop/060.

Refs #7771
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner November 11, 2026 16:16
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-11T16:20:07.871785Z c4456bd PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

✅ Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the enhancement New feature or request label Nov 11, 2026
@coderabbitai

coderabbitai Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

Walkthrough

This change adds Windows process-evidence primitives, wrapper-binding rules, checked manager-stop and port-confirmation operations, and associated tests and documentation. The new operations use bounded execution and return outcomes based on process and manager evidence. Existing stop paths do not call them.

Changes: Windows Checked Service Stop

Layer / File(s) Summary
Process identity evidence
src/service/windows-process-native.ts, src/service/windows-process-evidence.ts, tests/windows/windows-process-evidence.test.ts, structure/ops/windows-checked-stop.md
Adds native x64 process identity, termination, exit-watch, and Task Scheduler PID primitives. Adds bounded command-based snapshot and termination fallbacks, with tests for native and fallback behavior.
Wrapper binding and stop budgets
src/service/windows-stop-binding.ts, src/service/windows-stop-budget.ts, tests/windows/windows-stop-binding.test.ts, tests/windows/windows-checked-stop.test.ts, structure/ops/windows-checked-stop.md
Adds path and lineage checks for wrapper candidates, deepest-first binding, and stop-budget calculations. Tests cover binding rules and budget limits.
Checked manager-stop workflow
src/service/windows-checked-stop.ts, tests/helpers/windows-checked-stop-fixtures.ts, tests/windows/windows-checked-stop.test.ts, scripts/test-layout/layout.json, tests/fixtures/test-layout-expected.json, devlog/_plan/261111_windows_service_stop/*, structure/INDEX.md, structure/manifest.json, structure/ops/windows-checked-stop.md, structure/runtime.md
Adds injected, capped manager-stop operations with authorization checks, identity-bound wrapper termination, and post-stop verification. Tests cover outcome, timing, admission, and refusal cases. The plan and documentation state that caller wiring remains for later work.
Port-release confirmation
src/service/windows-stop-confirm.ts, tests/windows/windows-stop-confirm.test.ts, structure/ops/windows-checked-stop.md
Adds bounded port sampling and final process checks. Outcomes distinguish a free port, a foreign listener, a surviving local listener or wrapper, and incomplete or unreadable evidence.

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Stop as stopWindowsServiceChecked
  participant Deps as WindowsCheckedStopDeps
  participant Binding as bindWindowsStopWrappers
  participant Process as Windows process evidence
  Stop->>Deps: Probe manager registration and enumerate processes
  Stop->>Binding: Bind eligible wrappers from process evidence
  Stop->>Deps: Request registered manager stop
  Stop->>Process: Terminate bound wrappers and watch for exit
  Stop->>Deps: Read manager state for final verification
Loading

Possibly related issues

  • Issue 7771 describes a Windows service-wrapper case where a wrapper can relaunch the proxy and keep port 10100 occupied. This change adds checked-stop and port-confirmation operations for that case, but does not wire them to callers.
  • Issue 8545 concerns safe restart and disconnect when runtime ownership cannot be proven. This change adds Windows process evidence, PID-reuse checks, wrapper binding, and checked-stop outcomes, but does not wire them to restart or disconnect callers.

Possibly related PRs

  • lidge-jun/opencodex#8546: Defines the Windows stop contract implemented by these primitives, including manager-first stopping, identity-bound termination, bounded verification, and port confirmation.
  • lidge-jun/opencodex#8560: Implements the later Desktop slice described in this change’s plan, including the Windows drain deadline and readers for stop-result fields.

Suggested labels: enhancement

Pre-merge checks: Passed 2

Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly identifies the main change: adding checked Windows stop primitives and operations that remain unwired in slice 1.
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c4456bda7f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/service/windows-process-evidence.ts Outdated
Comment on lines +266 to +270
const exitWaitMs = Math.max(0, Math.min(FALLBACK_TERMINATE_EXIT_WAIT_MS, options.timeoutMs - 500));
try {
return parseFallbackTerminateOutput(await run({
program: "powershell",
args: powershellArgs(windowsFallbackTerminateScript(pid, boundStartTime, exitWaitMs)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize deadline-derived timeouts before spawning the fallback

When the spawned terminate fallback is used, Part A passes windowEnd - clock.now() through runCapped, so the real performance.now() clock normally produces a fractional timeout. The default runner forwards that directly to execFile, which rejects non-integer timeouts before spawning PowerShell. With less than 1.5 seconds remaining, the computed exitWaitMs also becomes fractional and fails windowsFallbackTerminateScript's integer validation; passing timeoutMs: 1250.5 reproduces unavailable without invoking even an injected runner. Thus the fallback can leave bound wrappers running despite having time available. Floor the positive remaining timeout and derived exit wait to integer milliseconds before constructing the script and invoking the runner.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid; being fixed in this PR: positive remaining timeouts and derived exit waits are floored to integer milliseconds before the fallback script is built and the runner is called, with a regression test.

A cap derived from the deadline (windowEnd - now) is fractional on the
real clock. execFile and windowsFallbackTerminateScript take whole
milliseconds, so the spawned fallback answered unavailable. Floor the
timeout and the derived exit wait, and spawn nothing when it floors to 0,
which execFile would read as no timeout.

Refs #7771
One test per fail-closed branch, each shown to fail when its rule is
mutated: an unreadable parent PID never binds and, with the task
registered, is unreadable; the child-ancestor lineage clause binds on its
own; a terminate overrun, an unreadable task or WinSW state, an
unreadable stray candidate and a denied bind-time watch end Part A as
unknown or denied; an unnamed image carrying this home's path is unknown
in Part B.

Refs #7771
ARM64 ships the strict variant in slice 2 and its adapter must gate it,
because terminateWindowsProcessBound falls back to PowerShell on its own.
Record the Schedule PID read before admission, that openWatch and the
authorizer are not capped, and the route's need for an outer race.

Refs #7771
@lidge-jun

Copy link
Copy Markdown
Owner Author

Maintainer judgment (#7771 Option B, slice 1: checked Windows stop primitives)

  • Value: adds the parts of the owner-approved Option B design that do the risky work: the binding rule that decides which processes may be ended, a terminate that checks the process creation time on the same handle it terminates with, Part A (manager and wrapper stop) and Part B (the port stays free), and the time budget. Slice 2 wires them into ocx stop, /api/stop, service stop/uninstall/install and the updater.
  • UX change: none in this PR. Nothing calls the new modules (test:changed selects only the new tests).
  • Regression evidence for existing users: no existing code path changed. The existing Windows service suites still pass (584 pass, 31 skip). The new hermetic tests (75 pass, 2 skip) pin every applicable 040 bullet and each fail-closed rule, including the unavailable watch and terminate paths. Each rule's test was shown to fail when that rule is mutated, by the implementer, the coordinator and the reviewers.
  • Windows impact: Windows-only code. On x64 it uses Bun FFI loaded from the trusted system directory: kernel32 OpenProcess/GetProcessTimes/TerminateProcess on one handle, and advapi32 for the Schedule service PID. The only processes that can ever be bound for termination are wscript.exe, cscript.exe and cmd.exe carrying this home's launcher or service-script path; the proxy child never is. Unreadable or denied evidence ends with nothing killed. On ARM64 the bounded PowerShell fallback exists but stays unreachable: per 050, slice 2 ships the strict variant until an ARM64 device run.
  • Verification scope: fresh independent general review APPROVE and separate fresh security review SECURITY APPROVE, both on the exact head 4046d43c6c, after three review rounds. Typecheck, privacy, structure, layout and file-size gates pass.
  • NOT RUN (Windows): a real Windows device (the 040 matrix: /end leaving the loop alive, restart-on-failure behavior, an elevated task seen from an unelevated shell, npm vs bundled runtime, logon/unlock triggers, WinSW, slow startup and antivirus, ARM64); the two live x64 FFI tests, including the GetLastError mapping (Windows CI lane only); full bun run test. PR CI had no failures on this head at merge time.

@lidge-jun
lidge-jun merged commit 4ed97f1 into dev Nov 11, 2026
33 checks passed
@lidge-jun
lidge-jun deleted the feat/windows-checked-stop-primitives-2 branch November 11, 2026 16:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant