GitHub
10/24/2026, 8:46 AMmsiexec, restore the configuration, start osqueryd, confirm the new version is reported.
No rollback. MSI refuses to install an older version over a newer one, so a failed Windows upgrade is not rolled back. It restores the configuration snapshot, starts osqueryd, waits for it, clears the pending marker and the stash, and records a rejection with the new reason rollback_unavailable. The host is not retried under the same request id. osctrl counts it as a rollback, so the rollout pauses.
Target only. Windows does not download, stash or check the previous version's MSI. A server answer with a target and no previous is valid on Windows. On other platforms a missing previous is still an invalid_resolution skip.
msiexec
• Exit codes 3010 and 1641 (reboot pending) are not errors by themselves. If the new version is running, the upgrade succeeds. If the old binary is still installed, the upgrade is skipped as install_failed with the existing 6 hour backoff and is not recorded as a rejection.
• It runs with a 10 minute timeout. A timeout, or a privilege failure with the version unchanged, is the same install_failed skip.
• A clean exit is a success even if a child process holds the output pipe past the wait delay.
Service settings. Before every osqueryd start after msiexec (success, skip, failure path and resume), osctrld checks that the osqueryd service starts automatically and that its command line still has --flagfile=<configured flag file>, and reapplies both if the MSI reset them. The command line is rewritten by a pure function that refuses flag file paths containing a double quote or ending in a backslash, and matches the flag name case-sensitively.
Upgrade folder safety (Windows)
• Ownership check: a path counts as trusted only when it is owned by Administrators (S-1-5-32-544) or SYSTEM (S-1-5-18).
• The stash folder gets a protected access list with Administrators and SYSTEM only, because snapshot.json holds a copy of the enrollment secret.
• The check runs in this order: real directory (junctions and symlinks are refused), trusted owner, then the access list. When an existing stash is read back, the access list is re-applied best-effort, so a failure there never blocks restoring the configuration.
Crash and reboot recovery. On Windows, every resume case that would roll back goes through the failure path above. A pending upgrade whose old version is still installed and never took effect is forgotten without a rejection, after restoring what is possible and reapplying the service settings.
amd64 only for now. The MSI upgrade path supports amd64 only. On any other architecture the upgrade is skipped as target_unavailable with the usual backoff and nothing is downloaded. osctrld install is unchanged and still fetches the x64 MSI on Windows ARM64.
Reporting. rollback_unavailable is added to the reported reasons.
Security and operational impact
• The upgrade assumes osctrld runs elevated (SYSTEM or an administrator). Without that, the upgrade is skipped as install_failed.
• The enrollment secret copy in the upgrade stash is no longer readable by ordinary users on Windows.
• No change on Linux and macOS. Every new branch is gated on the Windows platform.
• Older osctrld releases still refuse Windows. A new osctrld talking to an older osctrl is safe, because the older osctrl answers unsupported_platform for Windows.
Validation
• go test ./... -count=1, and go test -race ./cmd/osctrld
• GOOS=windows go vet ./..., GOOS=darwin go vet ./... and GOOS=windows GOARCH=arm64 go vet ./...
• gofmt -l cmd
• golangci-lint run ./... and `GOOS=windows golangci-lint run ./...`: 0 issues
• New tests cover:
• the pure logic: trustedOwnerSID, the access list string, msiExitResult, and the service command-line parser and updater
• runMSI end to end with a fake process: success, a non-zero exit, a timeout, and a lingering child
• the Windows upgrade path end to end through the existing test seams: target-only success, 3010 with the new and old version, a privilege failure, a service settings error, an access list error, and the ARM64 skip
• the failure path: config restored, osqueryd running, rollback_unavailable recorded, no second msiexec, no retry under the same request id
• resume after a crash in each state
• Linux and macOS still requiring a previous version
Not tested on a real Windows host
The Windows-only code (the access list and owner lookup, and the svc/mgr service update) is compile-checked only. Please run these on a real host:
• an upgrade on a host enrolled by the quick-add script and on one with an MSI-created service: configuration preserved, service flag file and start type preserved, new version reported
• a forced failure that reports rollback_unavailable with osqueryd still running on its old configuration
• a freshly created stash folder passing the ownership check under an elevated administrator (if the check fails, the result is a safe install_failed skip)
• the 3010 reboot-pending flow
• what the MSI does to the quick-add script's hand-made service (it may drop `--flagfile`; the service settings check should repair it)
• go test ./cmd/osctrld/ on Windows
Known gap: if msiexec times out, osctrld kills only the client. The Windows Installer service may finish the install later and reset the service, with nothing reported. This should block calling Windows upgrades production-ready until it is checked.
Compatibility
Deploy osctrl first or in either order. An osctrld that has Windows upgrades does nothing until osctrl answers a Windows host with a target.
jmpsec/osctrldGitHub
10/24/2026, 8:48 AM