fix(tuning): stop dockur granting +invtsc on hosts whose kernel rejected the TSC - #782
Open
p4tit0 wants to merge 2 commits into
Open
fix(tuning): stop dockur granting +invtsc on hosts whose kernel rejected the TSC#782p4tit0 wants to merge 2 commits into
p4tit0 wants to merge 2 commits into
Conversation
constant_tsc/nonstop_tsc in /proc/cpuinfo are static CPUID bits reporting hardware capability only; they say nothing about whether this kernel's own cross-core TSC sync check at boot actually passed. On hosts where that check fails (dmesg: "Marking TSC unstable due to check_tsc_sync_source failed"), the kernel falls back its clocksource away from "tsc" while the CPUID bits stay set regardless -- detect_tuning_capability() granted +invtsc anyway, telling the Windows guest to trust a clock the host itself had just rejected. UEFI/DXE calibrates its boot delay loop directly off RDTSC before any guest-side clocksource fallback exists, and the guest's calibration never converges: the vCPU thread pins at 100% host/kernel time, 0% guest time, and Windows never gets past the firmware splash. detect_tuning_capability() now also requires /sys/devices/system/clocksource/clocksource0/current_clocksource == "tsc" before granting invtsc, so it defers to the kernel's own verdict instead of trusting CPUID alone. Hosts where the sync check passes (i.e. every host invtsc already worked on) are unaffected -- only hosts where the kernel already rejected the TSC change to invtsc=False, which is exactly the population that hung on boot.
The previous commit stopped WinPodX's own CPU_FLAGS from adding +invtsc when the host's clocksource check fails, but that alone did not fix the boot hang: dockur's base image (qemus/qemu's proc.sh, configureKvmAmdFeatures/configureKvmIntelFeatures) independently adds its own +invtsc to CPU_FEATURES whenever the host reports the tsc_scale (AMD) or tsc_scaling (Intel) CPUID flag -- a check that never cross-references the kernel's own clocksource verdict either, and runs regardless of what WinPodX puts in CPU_FLAGS. dockur assembles `-cpu $CPU_MODEL,$CPU_FEATURES,$CPU_FLAGS`, so WinPodX's CPU_FLAGS always lands after dockur's own CPU_FEATURES in the final -cpu string. QEMU applies -cpu sub-flags left-to-right with the last occurrence of a given feature winning, so merely omitting +invtsc on our side left dockur's own addition in effect. _cpu_flags_for_host() now explicitly emits -invtsc whenever cap.invtsc is False, overriding dockur's addition instead of just not adding our own. Gated on cap.invtsc (not profile.apply_invtsc) so a user who sets tuning_profile=off on an otherwise-healthy host doesn't get an unnecessary override -- only hosts where the capability check itself failed are affected. Verified end-to-end on the affected host (AMD Ryzen 7 4800H, kernel rejected TSC sync, tsc_scale flag present): before this fix the guest hung indefinitely at the firmware splash with zero disk I/O and a vCPU thread pinned at 100% host/kernel time; after both fixes, Windows Setup completes, reboots through the OEM pass, and RDP comes up. Two arch-only compose tests (test_compose_cpu_flags_x86_64, test_compose_cpu_flags_unknown_arch_falls_through_to_x86) previously relied on tuning_profile="off" alone to stay independent of the real host's TSC capability; since the override is now correctly unconditional on cap.invtsc, they now mock detect_tuning_capability explicitly to an invtsc-True host so they keep testing only the arch-detection branch.
kernalix7
requested changes
Aug 4, 2026
kernalix7
left a comment
Owner
There was a problem hiding this comment.
Thanks for tracking this down. Before merge, please:
- Add direct exact-output tests for generated
CPU_FLAGS:cap.invtsc=Falsemust emitarch_capabilities=off,-invtscunder bothautoandoff, whilecap.invtsc=Truewith profileoffmust not emit-invtsc. - To resolve the
tsc=reliableambiguity, share the successful host’s/proc/cmdline, active clocksource, and either the generatedCPU_FLAGSor assembled QEMU-cpuordering. Please redact any unrelated sensitive values.
A local integration probe found the production patch and these added tests pass: 58 focused tests, Ruff clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
detect_tuning_capability()(src/winpodx/utils/specs.py) granted+invtscbased solely on the staticconstant_tsc/nonstop_tscCPUID bits in/proc/cpuinfo. Those bits report hardware capability for an invariant TSC — they say nothing about whether this kernel's own cross-core synchronization check at boot actually accepted the TSC as usable.dmesg:tsc: Marking TSC unstable due to check_tsc_sync_source failed), the kernel falls back its own clocksource away from"tsc"(typically tohpet/acpi_pm) while the CPUID bits stay set regardless — soinvtscwas still reportedTrue, and-cpu host,...,+invtsc,...was handed to the guest.detect_tuning_capability()now also requires/sys/devices/system/clocksource/clocksource0/current_clocksource == "tsc"before grantinginvtsc, deferring to the kernel's own verdict instead of trusting CPUID alone. Missing/unreadable sysfs reportsTrue(unaffected) so hosts where the check can't run aren't punished.qemus/qemu'sproc.sh,configureKvmAmdFeatures/configureKvmIntelFeatures) independently adds its own+invtsctoCPU_FEATURESwhenever the host reports thetsc_scale(AMD) /tsc_scaling(Intel) CPUID flag — a check that never cross-references the kernel's clocksource verdict either, and runs regardless of what WinPodX puts inCPU_FLAGS. Since dockur assembles-cpu $CPU_MODEL,$CPU_FEATURES,$CPU_FLAGSand QEMU applies-cpusub-flags left-to-right with the last occurrence of a feature winning, merely omitting our own+invtscleft dockur's addition in effect._cpu_flags_for_host()(src/winpodx/core/pod/compose.py) now explicitly emits-invtscwhenevercap.invtscisFalse, overriding dockur's addition instead of just not adding our own. This is gated oncap.invtsc(the actual hardware+kernel verdict), notprofile.apply_invtsc(which also folds in user preference) — so a user who setstuning_profile = offon an otherwise-healthy host does not get an unnecessary override; only hosts where the capability check itself failed are affected.Verified end-to-end on the host that motivated this fix (AMD Ryzen 7 4800H, Nobara 44, kernel rejected TSC sync,
tsc_scaleCPUID flag present): before both fixes, the guest hung indefinitely at the firmware splash with zero forward disk I/O and a vCPU thread pinned at 100% host/kernel time (confirmed via/proc/<qemu_pid>/ioand per-thread/proc/<tid>/stat, and via QMPinfo registersshowing a completely staticRIPacross multiple samples). After both fixes: Windows Setup completes, reboots through the OEM pass, and RDP comes up —winpodx doctorreports all green.Opened against #780.
Test plan
TestHostClocksourceGatesInvtscintests/test_specs.py(5 cases covering the clocksource cross-check +detect_tuning_capabilityintegration)test_compose_cpu_flags_x86_64/test_compose_cpu_flags_unknown_arch_falls_through_to_x86intests/test_compose_arch.pyto explicitly mock an invtsc-capable host, sincetuning_profile = "off"alone no longer isolates them from the real host's TSC state (the-invtscoverride is intentionally unconditional oncap.invtsc, not gated on profile)pytest tests/ -v— 2170 passed, 27 skipped, 0 failed (full suite, pre-second-fix baseline) + 536 passed / 1 skipped on every test file touching compose/specs (post-second-fix, targeted re-run)ruff check src/ tests/— cleanruff format --check src/ tests/— cleanwinpodx setupwith the fix reachesOK Windows ready/OK OEM reboot pass completeand a responding RDP port, where it previously hung indefinitely