2017-11-27

Issue Description

Recently I was assigned a BOSD caused by rebooting the Windows guest in KVM. I have made a deep analysis of it. Though I’m not 100% satisfied with the final conclude, it still makes sense and is a good explaination. I have got a lot of help from Wei Wang of intel, Vadim Rozenfeld of redhat, and Paolo Bonzini of redhat, many thanks to them.

This issue is quite directly. Though not every time it causes BSOD, we reboot the Windows guest several times it will almost got BSOD with 0x5c(0x10b,3,0,0). Here is the summary infomation.

    FOLLOWUP_IP: 
    nt!InitBootProcessor+12a
    fffff800`01c01d0a 413ac6          cmp     al,r14b

    SYMBOL_STACK_INDEX:  6

    SYMBOL_NAME:  nt!InitBootProcessor+12a

    FOLLOWUP_NAME:  MachineOwner

    MODULE_NAME: nt

    IMAGE_NAME:  ntkrnlmp.exe

    DEBUG_FLR_IMAGE_TIMESTAMP:  59b946d1

    IMAGE_VERSION:  6.1.7601.23915

    FAILURE_BUCKET_ID:  X64_0x5C_HAL_CLOCK_INTERRUPT_NOT_RECEIVED_nt!InitBootProcessor+12a

    BUCKET_ID:  X64_0x5C_HAL_CLOCK_INTERRUPT_NOT_RECEIVED_nt!InitBootProcessor+12a

    ANALYSIS_SOURCE:  KM

    FAILURE_ID_HASH_STRING:  km:x64_0x5c_hal_clock_interrupt_not_received_nt!initbootprocessor+12a

    FAILURE_ID_HASH:  {829a944d-7639-05f1-a55f-2677354a890e}


    kd> kb
    # RetAddr           : Args to Child                                                           : Call Site
    00 fffff800`017b9662 : 00000000`0000010b fffff800`01854cc0 00000000`00000065 fffff800`01705514 : nt!DbgBreakPointWithStatus
    01 fffff800`017ba44e : 00000000`00000003 00000000`00000000 fffff800`01705d70 00000000`0000005c : nt!KiBugCheckDebugBreak+0x12
    02 fffff800`016c8f04 : 00000000`00000001 fffff800`0161e0b3 00000000`00002a43 00000000`00000000 : nt!KeBugCheck2+0x71e
    03 fffff800`0161e2b4 : 00000000`0000005c 00000000`0000010b 00000000`00000003 00000000`00000000 : nt!KeBugCheckEx+0x104
    04 fffff800`016442a3 : 00000000`00000001 fffff800`0080e4b0 fffff800`0080e4b0 00000000`00000001 : hal!HalpInitializeClock+0x1c9
    05 fffff800`01c01d0a : fffff800`0080e4b0 fffff800`0080e4b0 fffff800`013d8780 fffff800`016c0c86 : hal!HalpInitSystem+0x29b
    06 fffff800`0191cfa3 : fffff800`00000000 fffff800`01846e80 fffff800`013d8780 00000000`00000001 : nt!InitBootProcessor+0x12a
    07 fffff800`0190a8a6 : 00000000`00000230 fffff800`02b28588 fffff800`013d8b30 00000001`00000000 : nt!KiInitializeKernel+0x833
    08 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiSystemStartup+0x196

It is obvious function ‘HalpInitializeClock’ has failed and causes a bugcheck. Disassemb this and it easy to find out this bugcheck is caused when it calls function ‘HalpWaitForPhase0ClockTick’ and the later function return failed.

    kd> uf HalpInitializeClock
    hal!HalpInitializeClock:
    fffff800`01bff0ec 4889742408      mov     qword ptr [rsp+8],rsi
    fffff800`01bff0f1 9c              pushfq
    fffff800`01bff0f2 4883ec50        sub     rsp,50h
    fffff800`01bff0f6 488b0503600100  mov     rax,qword ptr [hal!_security_cookie (fffff800`01c15100)]
    fffff800`01bff0fd 4833c4          xor     rax,rsp
    fffff800`01bff100 4889442440      mov     qword ptr [rsp+40h],rax
    fffff800`01bff105 8b0dc1a20100    mov     ecx,dword ptr [hal!HalpClockSource (fffff800`01c193cc)]
    ...
    hal!HalpInitializeClock+0x19c:
    fffff800`01bff287 b9b80b0000      mov     ecx,0BB8h
    fffff800`01bff28c e8e3fdffff      call    hal!HalpWaitForPhase0ClockTick (fffff800`01bff074)
    fffff800`01bff291 84c0            test    al,al
    fffff800`01bff293 7520            jne     hal!HalpInitializeClock+0x1ca (fffff800`01bff2b5)

    hal!HalpInitializeClock+0x1aa:
    fffff800`01bff295 4c630530a10100  movsxd  r8,dword ptr [hal!HalpClockSource (fffff800`01c193cc)]
    fffff800`01bff29c 488364242000    and     qword ptr [rsp+20h],0
    fffff800`01bff2a2 4533c9          xor     r9d,r9d
    fffff800`01bff2a5 418d495c        lea     ecx,[r9+5Ch]
    fffff800`01bff2a9 ba0b010000      mov     edx,10Bh
    fffff800`01bff2ae ff153c000100    call    qword ptr [hal!_imp_KeBugCheckEx (fffff800`01c0f2f0)]
    fffff800`01bff2b4 cc              int     3
    ...
    fffff800`01bff2da c3              ret

So just copy+paste “HalpWaitForPhase0ClockTick” in the Google, you will find this bugzilla:

    https://bugzilla.redhat.com/show_bug.cgi?id=1387054

Seems the same, just differently in the bugcheck’s second parameter which is ‘1’ in the bugzilla but is ‘3’ in our BSOD. So I find the patch:

    https://github.com/torvalds/linux/commit/4114c27d450bef228be9c7b0c40a888e18a3a636#diff-3e935e2004c0c48a7a669085ee75f1b1

And applied this patch, reboot guest several times, no BSOD. This process just take me ten minutes and seems life is OK again. Over? Ofcourse not, I’m curious about this issue and want to know more under the surface of this BSOD.

Analysis in Windows kernel side

Let’s look at more in detail about the backtrack in windbg.

If you have some backgroud of Windows startup, you should know that this backtrack show the BSOD happend in the Phase0 initialization. In this phase initialization, only one processor get initialized which called boot processor. In the backtrack, we can see Windows is initializing the Clock. From the summary of BSOD, we see “x64_0x5c_hal_clock_interrupt_not_received_nt” this indicate the issue, the windows doesn’t received interrupts.

Let’s see the disassemble of function “HalpWaitForPhase0ClockTick”. This is the main logic of this function.

    char __fastcall HalpWaitForPhase0ClockTick(unsigned int a1)
    {
    unsigned __int64 v1; // rbx

    v1 = ((unsigned __int64)((HalpProc0TSCHz * (unsigned __int64)a1 * (unsigned __int128)0x624DD2F1A9FBE77ui64 >> 64)
                            + ((unsigned __int64)(HalpProc0TSCHz * a1
                                                - (HalpProc0TSCHz
                                                * (unsigned __int64)a1
                                                * (unsigned __int128)0x624DD2F1A9FBE77ui64 >> 64)) >> 1)) >> 9)
        + __rdtsc();
    HalpProcessorFence();
    if ( HalpPhase0ClockInterruptCount )
        return 1;
    while ( __rdtsc() <= v1 )
    {
        if ( HalpPhase0ClockInterruptCount )
        return 1;
    }
    return 0;
    }

    char HalpHpetClockInterruptStub()
    {
    ++HalpPhase0ClockInterruptCount;
    return 1;
    }

Here ‘HalpPhase0ClockInterruptCount’ counts the clock interrupt count, it will increment every timer interrupt. It is easy to understand that this function is waiting interrupt within v1 times (from the redhat bugzilla, it’s 3s). From Vadim Rozenfeld, I know this is a common technique in Windows kernel that the HAL initialization process waits for some period of time which considered to be enough to complete this initialization action, in this case it’s the clock. So the BSOD in Windows kernel is clear, when the guest try to initialize the clock, it waits some time(3s) to ensure timer interrupt has been triggered(through HalpPhase0ClockInterruptCount). But it doesn’t wait this interrupt and think the clock hasn’t worked in a good state so triggers this BSOD.

Analysis in KVM side

As we have know the story in Windows side let’s look at the KVM side. Though I’m familiar with CPU/Memory/Device virtualization in qemu/kvm stack, to be honest, I’m not familiar the interrupt virtualizaton. Let’s see the patch KVM: x86: reset RVI upon system reset, the commit says

    "A bug was reported as follows: when running Windows 7 32-bit guests on qemu-kvm,
    sometimes the guests run into blue screen during reboot. The problem was that a
    guest's RVI was not cleared when it rebooted. This patch has fixed the problem."

This patch clear the RVI when reboot. First let’s look at the reboot path.

    kvm_vcpu_ioctl(CPU(s->cpu), KVM_SET_LAPIC, &kapic);
    
    -->kvm_vcpu_ioctl_set_lapic
    -->kvm_apic_post_state_restore
    -->vmx_hwapic_irr_update
    -->vmx_set_rvi

The later two function was added by the patch. Here is a brief introduction of some registers:

IRR: Interrupt Request Register, if the nth bit is set, the LAPIC has received the nth interrupt but not deliver it to CPU
RVI: Requesting virtual interrupt, This is the low byte of the guest interrupt status. The processor
treats this value as the vector of the highest priority virtual interrupt that is requesting service.
SVI: Servicing virtual interrupt, This is the high byte of the guest interrupt status. The processor
treats this value as the vector of the highest priority virtual interrupt that is in service.
EOI: End of Interrupt, the software write this register in the end of interrupt handler to notify the virtual apic deliver next interrupt.
ISR: In-Service Register, if the nth bit is set, the CPU has processed the nth interrupt, but not complete

RVI and SVI is in the virtual apic only, they characterize part of the guest’s virtual-APIC state and does not correspond to any processor or APIC registers. The general process is this, interrupt was set in IRR, then RVI, when the guest process interrupt, and set the ISR, when it finish the interrupt dispatch it writes EOI register to notifiy virtual apic to deliver another interrupt.

In this BSOD case, the RVI register is not clear and it has higher priority than the timer interrupt, as it is in the eary of Windows initialization, there maybe no corresponding interrupt procedure for the obsolete RVI interrupt so no handler can handle it. As the RVI interrupt has higher priority than timer interrupt, and the ISR in virtual apic can’t be get clear, the virtual apic will not deliver the timer interrupt and make the Widnows BSOD.

Reference

  1. SDM 24.4.2

  2. Mctrain’s Blog: 中断处理的那些事儿



blog comments powered by Disqus