Skip to content

fix: decode task definitions before the pilot launcher check - #260

Open
PLN wants to merge 2 commits into
mainfrom
fix/msicheck-task-encoding
Open

PLN wants to merge 2 commits into
mainfrom
fix/msicheck-task-encoding

Conversation

@PLN

@PLN PLN commented Sep 25, 2026

Copy link
Copy Markdown
Owner

Tracks #259.

Symptom

The MSI preflight's launcher check matched the ASCII spelling of winunitd.exe against raw task-file bytes. Task Scheduler stores registered definitions as UTF-16LE with a byte order mark, so a scheduled task that names winunitd.exe directly was never detected on a real task store. Install, repair and upgrade therefore did not raise the incompatible-pilot conflict for it.

Fix

  • tools/msi-check/preflight.go decodes each task definition before the unchanged case-insensitive match:
    • UTF-8, with or without a byte order mark.
    • UTF-16LE or UTF-16BE with a byte order mark.
    • UTF-16 without a byte order mark, recognized by the scanner's own two-byte heuristic and then fully validated.
  • Malformed input fails closed with the existing could not inspect machine launchers conflict: invalid UTF-8, an odd UTF-16 length, unpaired surrogates, NUL characters. NUL bytes are never stripped to force a match.
  • Unchanged: the machine Run/RunOnce string match, the count, depth and size bounds, reparse refusal, and the conflict text.
  • New regressions in preflight_test.go run countTaskLaunchers on complete task definitions:
    • Every accepted encoding.
    • Nine malformed inputs.
    • A mixed nested store.
    • A wrapper-only task (documented boundary).
    • The exact conflict for install, repair and upgrade, with uninstall exempt.
  • docs/INSTALLATION.md, docs/MSI-INSTALLER-PLAN.md and docs/R6-EVIDENCE.md now state the detection contract:
    • The check covers explicit winunitd.exe references in task-definition text and in 64-bit machine Run/RunOnce values, whatever the task's principal.
    • Wrapper, script and per-user launchers are discovered and stopped by explicit migration, not by the MSI.
    • R6-EVIDENCE records that the launcher case still needs native re-qualification.

The fix changes availability: one task-store file that isn't valid UTF-8 or UTF-16 now blocks install, repair and upgrade. This is intentional, because silently skipping such a file would reopen the false-negative path. It is covered by the whole-store native lane below.

Verification

  • Exact-source CI: run 36133154795 (workflow_dispatch on 417ee80bc59353438b7f82a44c5b7b79ca520c00) succeeded: windows, test (linux) and compile (linux, GOOS=windows). Artifacts: windows-amd64 10863167915, cross-windows-amd64-manifest 10862497977.
  • Local, Windows Go 1.27.1, unelevated:
    • go test ./tools/msi-check -race -count=1: every new test passes. The two existing symlink tests (TestTaskScanDoesNotFollowReparse, TestServicingDoesNotFollowMutableReparse) fail at symlink creation for lack of privilege, the same as on unmodified main.
    • go vet for Windows and Linux, staticcheck 2026.2.1 and gofmt are clean.
  • Review: approved with wording changes, which are applied in the second commit. No runtime change since review.

Native qualification — pending

This must pass before merge, on a disposable Windows guest from a fresh snapshot and fixture namespace. No fixture task is ever started.

  1. Register a no-trigger SYSTEM task and an interactive-principal task, each with a direct winunitd.exe action. Record the actual registered file bytes and byte order mark. The corrected scanner, running as SYSTEM, must count both. The previous scanner must return zero on the same UTF-16 bytes.
  2. Whole-store lane: scan the guest's entire real task store as SYSTEM with the corrected scanner. Genuine task definitions must produce zero decode refusals, and the per-encoding counts are recorded. The previous scanner's count is recorded for comparison.
  3. In a reviewed disposable layout that satisfies the directory and SCM gates, the exact-source MSI install, repair and upgrade must each stop with preflight conflict: incompatible pilot launches winunitd; explicit migration is required before quiesce. SCM, payload, data, registration and the task must be unchanged. Uninstall's exemption is verified in a package-owned layout.
  4. The package tests run with the privileges needed for the reparse and SCM tests.
  5. Only the owned tasks are removed, restoration is verified independently, and the sanitized record goes into R6-EVIDENCE. Merge requires an equal tested/merged tree.

🤖 Generated with Claude Code

PLN and others added 2 commits September 25, 2026 13:53
The MSI preflight matched the ASCII spelling of winunitd.exe against raw task-file bytes. Task Scheduler stores registered definitions as UTF-16LE with a byte order mark, so a direct machine task launcher was never detected on a real task store. Decode UTF-8 and UTF-16 definitions (with or without a byte order mark) before the case-insensitive match. Refuse malformed encodings with the existing launcher-inspection conflict. Registry Run values keep their separate string match.

Add regressions for every accepted encoding, malformed input, a mixed store, a wrapper-only task and the install/repair/upgrade conflict with the uninstall exemption. Document the detection contract: explicit references in task definitions and machine Run/RunOnce values only; wrapper, script and per-user launchers stay with explicit migration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Describe the BOM-less UTF-16 detection as the scanner's own two-byte heuristic, broader than the XML Appendix F signatures and not an XML parser. Say that a wrapper task is still reported when its definition names winunitd.exe. Match each test fixture's XML declaration to the bytes it emits. No runtime change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant