记一次修复Linux Kernel HyperV Bug的经历

最近在做Linux Kernel Kexec的工作,Windows 10的HyperV VM里装了个Fedora 29,惊讶的发现Kexec竟然不能工作。

调用Kexec会直接蹦出这个Panic:

[   19.966336] kexec_core: Starting new kernel
[   19.994408] BUG: unable to handle kernel NULL pointer dereference at 0000000000000000
[   19.995152] PGD 8000000057995067 P4D 8000000057995067 PUD 57990067 PMD 0
[   19.995152] Oops: 0002 [#1] SMP PTI
[   19.995152] CPU: 0 PID: 1016 Comm: kexec Not tainted 4.18.16-300.fc29.x86_64 #1
[   19.995152] Hardware name: Microsoft Corporation Virtual Machine/Virtual Machine, BIOS Hyper-V UEFI Release v3.0 03/02/2018
[   19.995152] RIP: 0010:0xffffc9000001d000
[   19.995152] Code: Bad RIP value.
[   19.995152] RSP: 0018:ffffc9000495bcf0 EFLAGS: 00010046
[   19.995152] RAX: 0000000000000000 RBX: ffffc9000001d000 RCX: 0000000000020015
[   19.995152] RDX: 000000007f553000 RSI: 0000000000000000 RDI: ffffc9000495bd28
[   19.995152] RBP: 0000000000000002 R08: 0000000000000000 R09: ffffffff8238aaf8
[   19.995152] R10: ffffffff8238aae0 R11: 0000000000000000 R12: ffff88007f553008
[   19.995152] R13: 0000000000000001 R14: ffff8800ff553000 R15: 0000000000000000
[   19.995152] FS:  00007ff5c0e67b80(0000) GS:ffff880078e00000(0000) knlGS:0000000000000000
[   19.995152] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[   19.995152] CR2: ffffc9000001cfd6 CR3: 000000004f678006 CR4: 00000000003606f0
[   19.995152] Call Trace:
[   19.995152]  ? __send_ipi_mask+0x1c6/0x2d0
[   19.995152]  ? hv_send_ipi_mask_allbutself+0x6d/0xb0
[   19.995152]  ? mp_save_irq+0x70/0x70
[   19.995152]  ? __ioapic_read_entry+0x32/0x50
[   19.995152]  ? ioapic_read_entry+0x39/0x50
[   19.995152]  ? clear_IO_APIC_pin+0xb8/0x110
[   19.995152]  ? native_stop_other_cpus+0x6e/0x170
[   19.995152]  ? native_machine_shutdown+0x22/0x40
[   19.995152]  ? kernel_kexec+0x136/0x156
[   19.995152]  ? __do_sys_reboot+0x1be/0x210
[   19.995152]  ? kmem_cache_free+0x1b1/0x1e0
[   19.995152]  ? __dentry_kill+0x10b/0x160
[   19.995152]  ? _cond_resched+0x15/0x30
[   19.995152]  ? dentry_kill+0x47/0x170
[   19.995152]  ? dput.part.34+0xc6/0x100
[   19.995152]  ? __fput+0x147/0x220
[   19.995152]  ? _cond_resched+0x15/0x30
[   19.995152]  ? task_work_run+0x38/0xa0
[   19.995152]  ? do_syscall_64+0x5b/0x160
[   19.995152]  ? entry_SYSCALL_64_after_hwframe+0x44/0xa9
[   19.995152] Modules linked in: ip6t_rpfilter ip6t_REJECT nf_reject_ipv6 xt_conntrack ebtable_nat ip6table_nat nf_conntrack_ipv6 nf_defrag_ipv6 nf_nat_ipv6 ip6table_mangle ip6table_raw ip6table_security iptable_nat nf_conntrack_ipv4 nf_defrag_ipv4 nf_nat_ipv4 nf_nat iptable_mangle iptable_raw iptable_security nf_conntrack ip_set nfnetlink ebtable_filter ebtables ip6table_filter ip6_tables sunrpc vfat fat crct10dif_pclmul crc32_pclmul ghash_clmulni_intel intel_rapl_perf hv_balloon joydev xfs libcrc32c hv_storvsc serio_raw scsi_transport_fc hv_netvsc hyperv_keyboard hyperv_fb hid_hyperv crc32c_intel hv_vmbus

跟着Trackback看了下代码,大概是Kexec尝试关闭其他CPU的时候,Send IPI发生了Panic。

# gdb vmlinux 
(gdb) list *(__send_ipi_mask+0x1c6)
0xffffffff81024166 is in __send_ipi_mask (./arch/x86/include/asm/mshyperv.h:138).
133
134     #ifdef CONFIG_X86_64
135             if (!hv_hypercall_pg)
136                     return U64_MAX;
137
138             __asm__ __volatile__("mov %4, %%r8\n"
139                                  CALL_NOSPEC
140                                  : "=a" (hv_status), ASM_CALL_CONSTRAINT,
141                                    "+c" (control), "+d" (input_address)
142                                  :  "r" (output_address),

Panic就发生在那个Inline ASM里面。这个RIP: 0010:0xffffc9000001d000以及Code: Bad RIP value,又是怎么回事呢?

根据汇编代码分析一下:

        x = y + ((x > y) ? phys_base : (__START_KERNEL_map - PAGE_OFFSET));
ffffffff8102413b:       48 c7 c2 00 00 00 80    mov    $0xffffffff80000000,%rdx
ffffffff81024142:       48 2b 15 97 39 16 01    sub    0x1163997(%rip),%rdx        # ffffffff82187ae0 <page_offset_base>
ffffffff81024149:       48 8b 1d a0 62 94 01    mov    0x19462a0(%rip),%rbx        # ffffffff8296a3f0 <hv_hypercall_pg>
ffffffff81024150:       4c 01 f2                add    %r14,%rdx
ffffffff81024153:       48 85 db                test   %rbx,%rbx
ffffffff81024156:       0f 84 59 ff ff ff       je     ffffffff810240b5 <__send_ipi_mask+0x115>
        __asm__ __volatile__("mov %4, %%r8\n"
ffffffff8102415c:       31 c0                   xor    %eax,%eax
ffffffff8102415e:       49 89 c0                mov    %rax,%r8
! ===> ffffffff81024161:       ff d3                   callq  *%rbx  # ! <=== rbx is 0xffffc9000001d000, value of <hv_hypercall_pg>
ffffffff81024163:       90                      nop
ffffffff81024164:       90                      nop
ffffffff81024165:       90                      nop
ffffffff81024166:       48 89 c3                mov    %rax,%rbx
                if ((status & HV_HYPERCALL_RESULT_MASK) != HV_STATUS_SUCCESS)
ffffffff81024169:       66 85 c0                test   %ax,%ax
ffffffff8102416c:       75 0a                   jne    ffffffff81024178 <__send_ipi_mask+0x1d8>

注意0xffffffff81024166之前,有一条mov 0x19462a0(%rip),%rbx,此处把hv_hypercall_pg赋值给了rbx。0xffffc9000001d000应该是此时hv_hypercall_pg的值。

可以看出是hv_hypercall_pg被使用的时候发生了Page Fault,初步估计是什么东西错误地提前把它释放了。

先做一个Bisect吧。

经过10多次recompile,发现commit 引入了这个问题,内容很清晰,用Hypercall去做IPI。

之后又仔细梳理了一下Kexec的过程,发现Panic的地方正好对的上,分别是在和APIC通讯和Send REBOOT_VECTOR这个IPI。分别在这两个步骤之前直接返回失败,kexec便会Fail,不会Panic。

发现HyperV重写了

Leave a Reply

Your email address will not be published. Required fields are marked *