<#35 Windows follow-ups: safe timeout handling an...
# osctrl
g
#35 Windows follow-ups: safe timeout handling and a per-sync service check Pull request opened by javuto Windows follow-ups: safe
msiexec
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/osctrld