GitHub
10/24/2026, 9:50 AMmsiexec timeout handling and a per-sync service check
Follow-up to the Windows osquery upgrades in #34.
Problem
If msiexec timed out, osctrld killed only the msiexec client. The Windows Installer service could finish the install later, replacing the binary and possibly resetting the osqueryd service command line (losing --flagfile), while osctrld had already restored the config, started osqueryd and moved on. Nothing re-checked the service afterwards, so a host could end up running osqueryd unenrolled without a report.
Change
Timeout handling
• runMSI now returns a matchable error, errMSITimeout, when it times out.
• On Windows, when the install error is a timeout, osctrld waits once, for up to 10 minutes, for the Windows Installer to go idle. It polls OpenMutex on Global\_MSIExecute every 5 seconds. The polling decision is a pure function (pollInstallerIdle) with fakes in the tests. A mutex that exists, or that cannot be opened because access is denied, means an install is running.
• If the installer goes idle, osctrld decides by the installed version exactly as after a normal msiexec return:
• new version: the normal verification and success path
• old version still installed: the existing install_failed skip (config restored, osqueryd started, 6 hour backoff, no rejection)
• unreadable version: the existing handling
• If the installer is still busy, or the wait fails:
• the pending upgrade stays in place
• osqueryd is not restarted, because the installer could overwrite the restored config mid-flight, or stop the service
• it records an install_failed skip with the 6 hour backoff, and no rejection
• the next sync's resume decides by installed version
• Resume on Windows waits once for the installer before deciding whenever an upgrade is pending and never took effect. It always decides after that bounded wait, so a stuck unrelated installer cannot keep osqueryd stopped forever. A cancelled context only defers the decision.
Per-sync service check (closes the late-install gap)
• ensureServiceSettings now reports whether it changed anything.
• On Windows, every sync with no pending upgrade, right after resumeUpgrade, rechecks that the osqueryd service starts automatically and that its command line still has --flagfile=<configured flag file>. It repairs them if the MSI reset them, logs a warning and restarts osqueryd once. A repair that fails is logged and the sync continues. A correct service costs one query and is silent.
• This also covers an installer that finishes after osctrld has decided, and manual MSI repairs unrelated to osctrld.
README is updated for both behaviors.
Behavior notes
• A Windows host whose osqueryd start type was deliberately set to Manual or Disabled is forced back to automatic and restarted on its first sync.
• A host with no osqueryd service, or with a flag file path osctrld refuses to write (a double quote or trailing backslash), logs a warning on every sync.
• If the service was repaired and the flags also changed in the same sync, osqueryd restarts twice. Both restarts are safe.
• While a sync waits for the installer, the health watcher does not tick, so a down report can be delayed by up to the wait.
• Linux and macOS are unchanged. Every new branch is gated on the Windows platform, and the non-Windows stubs are no-ops.
Security and operational impact
• No new network calls, files or privileges. The check reads and updates only the osqueryd service configuration osctrld already manages.
• This removes the case where an upgrade leaves osqueryd running without its flag file and nothing reports it.
Validation
• go test ./... -count=1, and go test -race ./cmd/osctrld
• GOOS=windows go vet ./..., GOOS=darwin go vet ./..., GOOS=windows GOARCH=arm64 go vet ./...
• gofmt -l cmd
• golangci-lint run ./... and `GOOS=windows golangci-lint run ./...`: 0 issues
• New tests cover:
• the timeout error matching
• `pollInstallerIdle`: idle at once, busy then idle, busy forever, an error, a cancelled context
• timeout then idle with the new, the old and an unreadable version
• timeout then busy (with and without a wait error): the upgrade stays pending, osqueryd is not restarted, nothing is rejected, nothing is re-downloaded, and the next resume verifies, forgets or fails by version
• resume deciding even while the installer is busy, and not waiting when the upgrade already took effect
• the per-sync check: no restart when correct, one restart and a warning on repair, an error that does not stop the sync, no call while an upgrade is pending, no call on Linux or macOS, and a full syncOnce ordering test
Not tested on a real Windows host
The mutex check, the service update and the per-sync repair compile for Windows but have not run there. The access-denied mapping has no test, because it lives in Windows-only code that calls the real OS. Please check on a real host:
• a forced msiexec timeout with the installer still running
• a late install that resets the service flag file, repaired on the next sync
• the per-sync check on a host with a hand-made quick-add service
jmpsec/osctrldGitHub
10/24/2026, 9:52 AM