Skip to content

fix(tuning): stop dockur granting +invtsc on hosts whose kernel rejected the TSC - #782

Open
p4tit0 wants to merge 2 commits into
kernalix7:mainfrom
birb-labs:fix/invtsc-clocksource-cross-check
Open

fix(tuning): stop dockur granting +invtsc on hosts whose kernel rejected the TSC#782
p4tit0 wants to merge 2 commits into
kernalix7:mainfrom
birb-labs:fix/invtsc-clocksource-cross-check

Conversation

@p4tit0

@p4tit0 p4tit0 commented Jul 22, 2026

Copy link
Copy Markdown

Summary

  • detect_tuning_capability() (src/winpodx/utils/specs.py) granted +invtsc based solely on the static constant_tsc/nonstop_tsc CPUID 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.
  • On a host where that check fails (dmesg: tsc: Marking TSC unstable due to check_tsc_sync_source failed), the kernel falls back its own clocksource away from "tsc" (typically to hpet/acpi_pm) while the CPUID bits stay set regardless — so invtsc was still reported True, and -cpu host,...,+invtsc,... was handed to the guest.
  • UEFI/DXE calibrates its boot delay loop directly off RDTSC, before any guest-side clocksource fallback exists. Telling the guest to trust a clock the host itself had just rejected produces a vCPU thread pinned at 100% host/kernel time, 0% guest time — Windows never gets past the "Windows started successfully" firmware splash.
  • First fix: detect_tuning_capability() now also requires /sys/devices/system/clocksource/clocksource0/current_clocksource == "tsc" before granting invtsc, deferring to the kernel's own verdict instead of trusting CPUID alone. Missing/unreadable sysfs reports True (unaffected) so hosts where the check can't run aren't punished.
  • This alone was not sufficient — empirically verified on the affected host. dockur's own 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) / tsc_scaling (Intel) CPUID flag — a check that never cross-references the kernel's clocksource verdict either, and runs regardless of what WinPodX puts in CPU_FLAGS. Since dockur assembles -cpu $CPU_MODEL,$CPU_FEATURES,$CPU_FLAGS and QEMU applies -cpu sub-flags left-to-right with the last occurrence of a feature winning, merely omitting our own +invtsc left dockur's addition in effect.
  • Second fix: _cpu_flags_for_host() (src/winpodx/core/pod/compose.py) now explicitly emits -invtsc whenever cap.invtsc is False, overriding dockur's addition instead of just not adding our own. This is gated on cap.invtsc (the actual hardware+kernel verdict), not profile.apply_invtsc (which also folds in user preference) — so a user who sets tuning_profile = off on 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_scale CPUID 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>/io and per-thread /proc/<tid>/stat, and via QMP info registers showing a completely static RIP across multiple samples). After both fixes: Windows Setup completes, reboots through the OEM pass, and RDP comes up — winpodx doctor reports all green.

Opened against #780.

Test plan

  • Added TestHostClocksourceGatesInvtsc in tests/test_specs.py (5 cases covering the clocksource cross-check + detect_tuning_capability integration)
  • Updated test_compose_cpu_flags_x86_64 / test_compose_cpu_flags_unknown_arch_falls_through_to_x86 in tests/test_compose_arch.py to explicitly mock an invtsc-capable host, since tuning_profile = "off" alone no longer isolates them from the real host's TSC state (the -invtsc override is intentionally unconditional on cap.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/ — clean
  • ruff format --check src/ tests/ — clean
  • Live end-to-end verification on the affected host: fresh winpodx setup with the fix reaches OK Windows ready / OK OEM reboot pass complete and a responding RDP port, where it previously hung indefinitely

p4tit0 added 2 commits July 22, 2026 17:17
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.
@p4tit0 p4tit0 changed the title fix(tuning): cross-check host clocksource before granting +invtsc fix(tuning): stop dockur granting +invtsc on hosts whose kernel rejected the TSC Jul 22, 2026
@kernalix7 kernalix7 self-assigned this Jul 23, 2026

@kernalix7 kernalix7 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tracking this down. Before merge, please:

  • Add direct exact-output tests for generated CPU_FLAGS: cap.invtsc=False must emit arch_capabilities=off,-invtsc under both auto and off, while cap.invtsc=True with profile off must not emit -invtsc.
  • To resolve the tsc=reliable ambiguity, share the successful host’s /proc/cmdline, active clocksource, and either the generated CPU_FLAGS or assembled QEMU -cpu ordering. Please redact any unrelated sensitive values.

A local integration probe found the production patch and these added tests pass: 58 focused tests, Ruff clean.

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.

2 participants