β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5123

hypercall_md() inline asm violates the GCC extended-asm contract: %r8 lifetime unprotected between two separate asm statements; indirect-call asm missing caller-saved-register and memory clobbers

Summary

r8 is declared clobbered in the first asm yet must survive into the second (the call) - the compiler demonstrably emits code in the gap (gcc 8.3 -O2 objdump: mov %rsi,%rcx between them); the call asm also lacks r9-r11/rsi/rdi/cc/memory clobbers though hypercalls touch memory. Latent miscompile hazard (corrupted output-page GFN / reordered message traffic on a future compiler/LTO); nothing manifests on the shipping build. Single-merged-asm fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5123 Β· 10 files
FileTypeDescriptionSize
README.md β€” 2.8 KB ↓ raw
VERDICT.md β€” 3.2 KB ↓ raw
hypercall_md.c β€” 1.3 KB view raw
build.sh β€” 87 B view raw
run.sh β€” 111 B view raw
build.log β€” 925 B view raw
objdump_gcc83_O2.txt β€” 925 B view raw
fix.diff β€” 873 B view raw
env.txt β€” 416 B view raw
verdict.json β€” 3.3 KB view raw

DF-5123 β€” hypercall_md() inline asm violates the GCC asm contract: r8 lifetime unprotected between two asm statements, indirect-call asm missing clobbers

Severity: Info (latent miscompile hazard) Β· Confidence: likely (the contract violation is certain by inspection; an actual miscompile was not produced on the shipping compiler β€” see verdict)

What this is

sys/dev/virtual/hyperv/x86_64/hyperv_machdep.c:74-84:

    __asm__ __volatile__ ("mov %0, %%r8" : : "r" (out_paddr): "r8");
    __asm__ __volatile__ ("call *%3" : "=a" (status) :
        "c" (in_val), "d" (in_paddr), "m" (hc_addr));

Three contract violations in one function:

  1. r8 lifetime. The first asm declares r8 clobbered while depending on r8 surviving until the second asm executes. The compiler is entitled to allocate r8 for anything between the two statements (including the input-register setup moves for the second asm). The Hyper-V hypercall ABI argument in r8 (output-page GFN) is therefore carried across a gap the compiler knows nothing about β€” silently dropping or corrupting out_paddr.
  2. Missing call clobbers. call *%3 can clobber every caller-saved register (rax,rcx,rdx,rsi,rdi,r8-r11) and flags; the asm declares none of r9-r11/rsi/rdi/"cc".
  3. No "memory" clobber. Hypercalls read their input page and write their output page in memory; without "memory" the compiler may reorder/cache memory traffic around the asm (relevant if the function is ever inlined β€” e.g. under LTO).

Inherited verbatim from FreeBSD's hyperv_machdep.c.

Reproduce (codegen evidence β€” done on the audit guest)

build.sh compiles an exact copy of the function (plus an always_inline variant exercising the inlined case) with the guest's gcc 8.3 at -O2 and disassembles. Observed (objdump_gcc83_O2.txt):

hypercall_md:
   0: mov    %rdi,-0x8(%rsp)
   5: mov    %rcx,%r8          <- asm #1
   8: mov    %rsi,%rcx         <- compiler-emitted, BETWEEN the two asms
   b: callq  *-0x8(%rsp)       <- asm #2

The mov %rsi,%rcx sitting between the two asm statements is the demonstration: the compiler does place instructions in exactly the gap where r8 must be preserved, and nothing in the asm constraints stops a different allocation decision from putting a conflicting use of r8 there. With gcc 8.3 at -O2 the standalone function happens to leave r8 intact β€” so the consequence (hypercall invoked with a garbage output-page GFN / stale hypercall pages) does not manifest today; the defect is real but latent, hence Info.

Merge into a single asm and declare the full call effect (see fix.diff):

    __asm__ __volatile__ ("movq %3, %%r8\n\tcall *%4"
        : "=a" (status)
        : "c" (in_val), "d" (in_paddr), "r" (out_paddr), "m" (hc_addr)
        : "r8", "r9", "r10", "r11", "memory", "cc");
VERDICT.md
↓ download raw

VERDICT β€” DF-5123

status: not_reproduced (no miscompile manifests on the shipping compiler) reproduced: false Β· impact: none (latent) Β· confidence: likely

Narrative

The claim has two parts: (a) the inline asm in hypercall_md() (hyperv_machdep.c:74-84) violates the GCC extended-asm contract β€” certain, by inspection; (b) the violation can miscompile into a hypercall with a corrupted r8 (output-page GFN) or reordered hypercall page traffic β€” latent: not observed with the shipping compiler.

What was run

hypercall_md.c (exact copy of the function + an always_inline variant) was compiled on the audit guest (DragonFly 6.5-DEVELOPMENT, gcc 8.3 "cc 8.3 [DragonFly] Release/2019-02-22") at -O2 and disassembled (build.sh, objdump_gcc83_O2.txt).

What the disassembly proves

  • In the standalone function, gcc emits the operand-setup move mov %rsi,%rcx between mov %rcx,%r8 (asm #1) and callq *-0x8(%rsp) (asm #2). This is the structural precondition of the hazard: the gap between the two volatile asm statements is not a compiler-free zone, and the first asm telling the compiler "r8 is clobbered" means gcc considers r8 dead from that point β€” it may legally allocate r8 for the inter-asm moves of a different input set, silently destroying out_paddr before the call.
  • In the always_inline variant (simulating what an LTO build could do to the real call site in hyperv.c:108), the same pattern appears inline in the caller; the missing "memory" clobber means any non-volatile loads/stores around the hypercall (i.e. the hypercall message-page writes done by hypercall_post_message()'s callers in vmbus.c) are freely reorderable across the asm β€” the demo's surrounding stores were volatile so no reorder was expected to show; that sub-claim remains inspection-based.

What was NOT observed

With gcc 8.3 at -O2, r8 survives the gap in both shapes; no hypercall can actually be issued on the guest anyway (it is a KVM guest β€” VMM/Hypervisor: Origin="KVMKVMKVM" in env.txt β€” so hyperv_identify() fails and the whole file is dormant, hyperv.c:207-208). Therefore the exploitable consequence does not reproduce on this system: status not_reproduced / impact none, while the underlying defect is certain.

Why it still matters

The function is the only hypercall gateway on Hyper-V systems; a future compiler upgrade, LTO, or an added statement between the asms turns the latent contract violation into: hypercalls invoked with an attacker- irrelevant but kernel-corrupting wrong output GFN, or message pages read stale β€” failure modes that would be nearly undebuggable in production. The one-statement fix (see fix.diff) removes the entire class.

Fix validation

Not applicable (Info-class hardening; no runtime behavior to compare β€” the code path is dormant on KVM and the miscompile does not manifest on gcc 8.3).

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

Fix not built into a kernel: the miscompile does not manifest on the shipping compiler and the path is dormant on the KVM guest, so no baseline/patched runtime comparison exists; the corrected asm is mechanically equivalent to the current codegen (verified shape in objdump) minus the contract violations.

findings/poc/DF-5123/fix.diff
↓ fix.diffper-fix-DF-5123

Confirmed kernel references

Detail

Evidence (decisive lines)

objdump_gcc83_O2.txt / build.log: disassembly of hypercall_md showing 'mov %rsi,%rcx' emitted between 'mov %rcx,%r8' (asm 1) and 'callq *-0x8(%rsp)' (asm 2) - proof the inter-asm gap carries compiler code while r8's lifetime is undeclared; VERDICT.md walks the analysis; env.txt documents guest compiler (cc 8.3 DragonFly) and KVM (dormant path)

PoC changes

Seed sketch replaced entirely: the PoC is a codegen demonstration (exact function copy + always_inline variant) compiled on the guest, not a runtime trigger - a runtime hypercall PoC is impossible on a KVM guest.

Verified recommended fix

Single asm: asm volatile("movq %3, %%r8\n\tcall *%4" : "=a"(status) : "c"(in_val), "d"(in_paddr), "r"(out_paddr), "m"(hc_addr) : "r8","r9","r10","r11","memory","cc");

Verdict

The inline-asm contract violation in hypercall_md() (hyperv_machdep.c:74-84) is certain by inspection and structurally confirmed by codegen evidence: compiling an exact copy of the function with the guest's gcc 8.3 at -O2 shows the compiler emitting an operand-setup instruction (mov %rsi,%rcx) BETWEEN the two asm statements - i.e. the gap in which %r8 must survive is not compiler-free, and the first asm declares r8 clobbered, so nothing prevents a future allocation from destroying out_paddr before the indirect call. The indirect-call asm also lacks r9-r11/rsi/rdi/cc clobbers and a memory clobber (hypercalls read/write their in/out pages). However, with the shipping compiler (gcc 8.3, -O2, no LTO) the standalone function and an always_inline variant both keep r8 intact, and the guest is KVM so the Hyper-V code is dormant - no miscompile manifests: hence not_reproduced / latent. Impact if it ever manifests: hypercalls invoked with a corrupted output-page GFN or stale message-page contents.