NetBSD Problem Report #60655

From zakinko@bambi.snowrabbit.org  Thu Aug 27 01:55:28 2026
Return-Path: <zakinko@bambi.snowrabbit.org>
Received: from mail.netbsd.org (mail.netbsd.org [199.233.217.200])
	(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
	 key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256
	 client-signature RSA-PSS (2048 bits) client-digest SHA256)
	(Client CN "mail.netbsd.org", Issuer "YR2" (not verified))
	by mollari.NetBSD.org (Postfix) with ESMTPS id B66AE1A923B
	for <gnats-bugs@gnats.NetBSD.org>; Thu, 27 Aug 2026 01:55:28 +0000 (UTC)
Message-Id: <20260827015524.C5FE9C92F0@bambi.snowrabbit.org>
Date: Thu, 27 Aug 2026 10:55:24 +0900 (JST)
From: Showta Ishizaki <zakinko@snowrabbit.org>
To: gnats-bugs@NetBSD.org
Cc: zakinko@snowrabbit.org
Subject: port-arm/: vmt(4) probes the VMware backdoor unconditionally on aarch64 and panics
X-Send-Pr-Version: 3.95
X-From4GNATS: "Showta Ishizaki via gnats" <gnats-admin@NetBSD.org>

>Number:         60655
>Category:       port-arm
>Synopsis:       vmt(4) probes the VMware backdoor unconditionally on aarch64 and panics the kernel on hypervisors that do not implement it
>Confidential:   no
>Severity:       serious
>Priority:       medium
>Responsible:    port-arm-maintainer
>State:          analyzed
>Class:          sw-bug
>Submitter-Id:   net
>Arrival-Date:   Thu Aug 27 02:00:00 +0000 2026
>Closed-Date:    
>Last-Modified:  Sat Sep 12 12:50:01 +0000 2026
>Originator:     Showta Ishizaki
>Release:        NetBSD 11.99.7 (also NetBSD 11.0)
>Organization:
>Environment:
System: NetBSD 11.99.7 (GENERIC64) evbarm/aarch64, built from -current
Architecture: aarch64
Machine: evbarm
Host: qemu-system-aarch64 -M virt -accel hvf -cpu host, Apple M4, macOS
Also reproduced with the released NetBSD 11.0 evbarm-aarch64 arm64.img.

>Description:

sys/arch/aarch64/aarch64/vmt.c attaches at cpufeaturebus and its match
routine calls vmt_probe() with no guard:

	static int
	vmt_match(device_t parent, cfdata_t match, void *aux)
	{
		/* vmt should not attach to more than a single CPU. */
		if (vmt_attached)
			return 0;

		return vmt_probe();
	}

vmt_probe() reaches BACKDOOR_OP_AARCH64 in sys/dev/vmt/vmtvar.h, whose
operative instruction is

	mrs xzr, mdccsr_el0

On VMware this access is intercepted by the hypervisor and becomes the
backdoor call.  Elsewhere it is an ordinary read of a real system
register, and a hypervisor that neither implements nor emulates it
delivers an undefined instruction exception instead.  Apple's
Hypervisor.framework does exactly that, and since the access happens at
EL1, trap_el1h_sync() falls through to its default case and panics before
the root file system is mounted:

	[   1.0000000] cpu0 at acpi0: unknown CPU (ID = 0x610f0000), id 0x0
	[   1.0000000] cpu0: package 0, core 0, smt 0, numa 0
	[   1.0000000] panic: Trap: fatal Unknown Reason (Illegal Instruction):
	               pc=ffffc000003fa318 sp=ffffc000010262e0 esr=02000000
	[   1.0000000] fp ffffc00001026880 config_found_acquire() at ...
	[   1.0000000] fp ffffc00001026910 acpi_fdt_attach() at ...
	[   1.0000000] cpu0: End traceback...
	Stopped in pid 0.0 (system) at netbsd:cpu_Debugger+0xc

pc resolves to vm_cmd+0x28.

The same unconditional probe on x86 is harmless because the backdoor there
is an inl from an I/O port, and reading an unimplemented port returns
0xffffffff rather than faulting.  aarch64 offers no equivalent guarantee,
so the same shape of code is not safe here.

This is not specific to Apple's hypervisor.  Any aarch64 hypervisor that
does not implement the VMware backdoor will panic NetBSD at boot the same
way.

>How-To-Repeat:

On an Apple silicon host with qemu installed:

	ftp https://cdn.NetBSD.org/pub/NetBSD/NetBSD-11.0/evbarm-aarch64/binary/gzimg/arm64.img.gz
	gunzip arm64.img.gz
	qemu-img resize -f raw arm64.img 40G
	cp /opt/homebrew/share/qemu/edk2-aarch64-code.fd .
	qemu-img create -f raw varstore.img 64M

	qemu-system-aarch64 -M virt -accel hvf -cpu host -smp 4 -m 4G \
	  -drive if=pflash,format=raw,readonly=on,file=edk2-aarch64-code.fd \
	  -drive if=pflash,format=raw,file=varstore.img \
	  -drive if=none,file=arm64.img,format=raw,id=hd0 \
	  -device virtio-blk-pci,drive=hd0 -nographic

The kernel panics as above.  -M virt,acpi=off panics in the same place.

The same image with -accel tcg -cpu cortex-a72 boots to a login prompt.
That is the control: the image and qemu are fine, and the only difference
is the hypervisor.

>Fix:

Two pieces.  NetBSD already has the machinery for "poke something that may
not be there": cpu_set_onfault() and the faultbuf that bus_space_peek()
uses, and trap_el1h_error() already consults it for an SError.  It was
simply never wired up for an undefined instruction.

The first hunk does that wiring; the second has the probe use it.  Built
from -current and verified under -accel hvf on an Apple M4: the kernel now
walks past vmt and reaches a login prompt.

--- sys/arch/aarch64/aarch64/trap.c
+++ sys/arch/aarch64/aarch64/trap.c
@@ -245,6 +245,24 @@
 #endif
 		break;

+	case ESR_EC_UNKNOWN: {
+		/*
+		 * An undefined instruction in the kernel is normally fatal,
+		 * but a driver may be probing for an instruction that only
+		 * some machines implement, in the same way bus_space_peek()
+		 * probes for a device that may not be there.  Let such a
+		 * probe unwind through its faultbuf, as trap_el1h_error()
+		 * already does for an SError.
+		 */
+		struct faultbuf * const fb = cpu_disable_onfault();
+
+		if (fb != NULL) {
+			cpu_jump_onfault(tf, fb, EFAULT);
+			return;
+		}
+		goto fatal;
+	}
+
 	case ESR_EC_FP_ACCESS:
 		if ((curlwp->l_flag & (LW_SYSTEM|LW_SYSTEM_FPU)) ==
 		    (LW_SYSTEM|LW_SYSTEM_FPU)) {
@@ -258,6 +276,7 @@
 	case ESR_EC_ILL_STATE:
 	case ESR_EC_BTE_A64:
 	default:
+	fatal:
 		panic("Trap: fatal %s: pc=%016" PRIx64 " sp=%016" PRIx64
 		    " esr=%08x", eclass_trapname(eclass), tf->tf_pc, tf->tf_sp,
 		    esr);

--- sys/arch/aarch64/aarch64/vmt.c
+++ sys/arch/aarch64/aarch64/vmt.c
@@ -33,6 +33,8 @@
 #include <sys/device.h>
 #include <sys/module.h>

+#include <aarch64/machdep.h>
+
 #include <dev/vmt/vmtreg.h>
 #include <dev/vmt/vmtvar.h>

@@ -48,11 +50,25 @@
 static int
 vmt_match(device_t parent, cfdata_t match, void *aux)
 {
+	struct faultbuf fb;
+	int rv;
+
 	/* vmt should not attach to more than a single CPU. */
 	if (vmt_attached)
 		return 0;

-	return vmt_probe();
+	/*
+	 * The backdoor VMware listens on is an access to a real system
+	 * register.  A hypervisor that does not implement it delivers an
+	 * undefined instruction exception instead, so run the probe under a
+	 * faultbuf and read a fault as "not VMware".
+	 */
+	if (cpu_set_onfault(&fb) != 0)
+		return 0;
+	rv = vmt_probe();
+	cpu_unset_onfault();
+
+	return rv;
 }

>Release-Note:

>Audit-Trail:

State-Changed-From-To: open->feedback
State-Changed-By: skrll@NetBSD.org
State-Changed-When: Fri, 04 Sep 2026 06:57:08 +0000
State-Changed-Why:
Thanks for the report.
This doesn't fail for me. What version of qemu-system-aarch64 are you using?


From: zakinko@snowrabbit.org
To: gnats-bugs@NetBSD.org
Cc: port-arm-maintainer@NetBSD.org, netbsd-bugs@NetBSD.org,
	gnats-admin@NetBSD.org, skrll@NetBSD.org, zakinko@snowrabbit.org
Subject: Re: port-arm/60655: vmt(4) probes the VMware backdoor unconditionally on aarch64 and panics the kernel on hypervisors that do not implement it
Date: Sat,  5 Sep 2026 00:40:00 +0900 (JST)

 qemu-system-aarch64 11.1.0 from Homebrew, on macOS 26.6.2 (build 25G83),
 on an Apple M4.  The accelerator is what matters here, and I have now
 measured both.

 	-accel hvf	panics
 	-accel tcg	boots to login

 Same image, same GENERIC64 kernel (NetBSD 11.99.7; 11.0 behaves the same).
 The only difference is the accelerator, so if you were using TCG that would
 explain why it does not fail for you.

 With hvf:

 	qemu-system-aarch64 \
 		-M virt -accel hvf -cpu host -smp 6 -m 8G \
 		-drive if=pflash,format=raw,readonly=on,file=edk2-aarch64-code.fd \
 		-drive if=pflash,format=raw,file=varstore.img \
 		-drive if=none,file=arm64.img,format=raw,id=hd0 \
 		-device virtio-blk-pci,drive=hd0 \
 		-display none -serial unix:console.sock,server,nowait

 	[   1.0000000] cpu0 at acpi0: unknown CPU (ID = 0x610f0000), id 0x0
 	[   1.0000000] panic: Trap: fatal Unknown Reason (Illegal Instruction)

 With tcg, the same invocation but -accel tcg -cpu cortex-a72:

 	[   1.0000000] cpu0 at acpi0: Arm Cortex-A72 r0p3 (v8-A), id 0x0
 	...
 	login:

 The read of `mrs xzr, mdccsr_el0` succeeds under TCG because QEMU implements
 that system register itself, so vmt_probe() returns and vmt(4) simply decides
 it is not running under VMware.  Under hvf the instruction reaches the real
 CPU through Hypervisor.framework, nothing intercepts it, and it raises an
 undefined instruction exception at EL1.

 The CPU line differs too: hvf reports the host M4 as an unknown CPU, TCG
 reports the modelled Cortex-A72.

 I cannot test KVM here; this host is macOS, where the only accelerators are
 hvf and tcg.

 One caveat on the TCG run, so as not to overstate it: it panicked once with
 "cpu0: softints stuck for 16 seconds" and rebooted, which I take to be TCG
 being slow rather than anything to do with vmt(4).  It reached login on the
 following boot.  There is no vmt-related panic anywhere in that log.

From: Nick Hudson <nick.hudson@gmx.co.uk>
To: zakinko@snowrabbit.org,
 "gnats-bugs@netbsd.org" <gnats-bugs@NetBSD.org>
Cc: "port-arm-maintainer@netbsd.org" <port-arm-maintainer@NetBSD.org>,
 "netbsd-bugs@netbsd.org" <netbsd-bugs@NetBSD.org>,
 "gnats-admin@netbsd.org" <gnats-admin@NetBSD.org>
Subject: Re: port-arm/60655: vmt(4) probes the VMware backdoor unconditionally
 on aarch64 and panics the kernel on hypervisors that do not implement it
Date: Fri, 4 Sep 2026 17:02:46 +0100

 On 04/09/2026 17:40, zakinko@snowrabbit.org wrote:
 > qemu-system-aarch64 11.1.0 from Homebrew, on macOS 26.6.2 (build =
 25G83),
 > on an Apple M4.

 I tested qemu-system-aarch64 10.2 from homebrew. I see the error with =
 11.1.

 I think this is related to qemu 11.1 support for

 ARM: =E2=80=98virt=E2=80=99 board support for specifying cache topology, =
 =E2=80=98hvf=E2=80=99 accelerator now supports nested virtualization and =
 vGIC

From: zakinko@snowrabbit.org
To: gnats-bugs@NetBSD.org
Cc: port-arm-maintainer@NetBSD.org, netbsd-bugs@NetBSD.org,
	gnats-admin@NetBSD.org, skrll@NetBSD.org, zakinko@snowrabbit.org
Subject: Re: port-arm/60655: vmt(4) probes the VMware backdoor unconditionally on aarch64 and panics the kernel on hypervisors that do not implement it
Date: Sat,  5 Sep 2026 10:27:02 +0900 (JST)

 You were right that the version is the variable, and it narrows further than
 that.  It is one commit, and it can be shown without rebuilding anything:

 	qemu 11.1.0, -M virt (= virt-11.1)		panics
 	qemu 11.1.0, -M virt-11.0			boots
 	qemu 11.1.0, -M virt,kernel-irqchip=off		boots
 	qemu 10.2.0, -M virt				boots

 Same host, same NetBSD 11.0 GENERIC64 arm64.img, same command line otherwise.
 The commit is 37863fff59 ("hvf: arm: enable vGIC by default for virt-11.1 and
 later"), which makes kernel-irqchip default to on for the new machine type.

 With the vGIC in play the MDCCSR_EL0 read is trapped out to QEMU, and QEMU's
 hvf path has no case for that register, so it injects an undefined
 instruction.  Its own trace point says so, at the pc from the panic:

 	hvf_unhandled_sysreg_read unhandled sysreg read at
 	  pc=0xffffc000003fa318: 0x0020c002 (op0=2 op1=3 crn=0 crm=1 op2=0)

 The TCG path does not do this: debug_helper.c defines MDCCSR_EL0 as
 ARM_CP_CONST with resetvalue 0, and the comment there says it is RAZ
 specifically to avoid spurious SIGILLs "as the support cannot be probed for".
 I have sent a patch to qemu-devel making hvf do the same, under the subject
 "target/arm/hvf: implement MDCCSR_EL0 as RAZ"; it has not appeared in the
 archive yet.  It is not fixed upstream either: master is 1168 commits past
 v11.1.0 and none of them touch accel/hvf or target/arm/hvf, and 11.1.1 has
 none.

 That said, I do not think this is only QEMU's problem, and I would like your
 opinion on the NetBSD side.

 A hypervisor is free to trap MDCCSR_EL0 and refuse it, and QEMU's own comment
 is the point: the guest cannot ask beforehand.  The register is
 architecturally mandatory, so testing for its presence answers "yes" and the
 probe dies all the same.  On x86 the same driver is safe by construction --
 the backdoor there is an inl from an unused I/O port, which cannot fault -- so
 the exposure is specific to aarch64, where the probe is an instruction a
 hypervisor can decline.

 The machinery to survive it is already in the tree.  trap_el1h_error() does
 exactly this for SErrors, with a comment saying it would otherwise panic
 unconditionally but bus_space_peek(9) has to probe.  trap_el1h_sync() has no
 such escape, so ESR_EC_UNKNOWN falls to the default arm and panics.

 Built from trunk, cross-compiled for evbarm-aarch64, -Werror -Wall clean.
 With that kernel, a stock unpatched QEMU 11.1.0 on -M virt -accel hvf reaches
 login, while the trace point above still fires once -- so the kernel is
 surviving the injected undefined instruction, rather than the register having
 become readable.

 One thing I am unsure of, and would rather raise myself: this reuses the
 onfault machinery, which copyin/copyout also use.  A genuine kernel bug that
 executed an undefined instruction while an onfault was set would now return
 EFAULT instead of panicking.  A dedicated flag would be narrower.  I went this
 way because trap_el1h_error() already took the broad approach for the same
 kind of probe, but if you would rather have something narrower I will redo it.

 --- sys/arch/aarch64/aarch64/trap.c.orig
 +++ sys/arch/aarch64/aarch64/trap.c
 @@ -244,6 +244,25 @@
  		panic("No debugger in kernel");
  #endif
  		break;
 +
 +	case ESR_EC_UNKNOWN:
 +		/*
 +		 * An undefined instruction in the kernel is normally fatal,
 +		 * but a driver may have to execute one on purpose: an
 +		 * instruction that some hypervisors answer and others refuse,
 +		 * with no way to ask beforehand.  vmt(4) does this to look
 +		 * for the VMware backdoor.  Let such a probe recover the way
 +		 * bus_space_peek(9) recovers from an SError, see
 +		 * trap_el1h_error() below.
 +		 */
 +		if (curcpu()->ci_intr_depth == 0) {
 +			struct faultbuf * const fb = cpu_disable_onfault();
 +			if (fb != NULL) {
 +				cpu_jump_onfault(tf, fb, EFAULT);
 +				break;
 +			}
 +		}
 +		goto fatal;

  	case ESR_EC_FP_ACCESS:
  		if ((curlwp->l_flag & (LW_SYSTEM|LW_SYSTEM_FPU)) ==
 @@ -258,6 +277,7 @@
  	case ESR_EC_ILL_STATE:
  	case ESR_EC_BTE_A64:
  	default:
 + fatal:
  		panic("Trap: fatal %s: pc=%016" PRIx64 " sp=%016" PRIx64
  		    " esr=%08x", eclass_trapname(eclass), tf->tf_pc, tf->tf_sp,
  		    esr);
 --- sys/arch/aarch64/aarch64/vmt.c.orig
 +++ sys/arch/aarch64/aarch64/vmt.c
 @@ -33,6 +33,8 @@
  #include <sys/device.h>
  #include <sys/module.h>

 +#include <aarch64/machdep.h>
 +
  #include <dev/vmt/vmtreg.h>
  #include <dev/vmt/vmtvar.h>

 @@ -48,11 +50,27 @@
  static int
  vmt_match(device_t parent, cfdata_t match, void *aux)
  {
 +	struct faultbuf fb;
 +	int rv;
 +
  	/* vmt should not attach to more than a single CPU. */
  	if (vmt_attached)
  		return 0;

 -	return vmt_probe();
 +	/*
 +	 * On aarch64 the backdoor is a read of MDCCSR_EL0 with a magic
 +	 * value in x7.  The register is mandatory, but a hypervisor is
 +	 * free to trap it and answer with an undefined instruction, and
 +	 * the guest cannot ask which one it is on beforehand.  QEMU 11.1
 +	 * with the Apple vGIC does exactly that, and the probe took the
 +	 * whole kernel down with it.  Catch the trap and report no vmt.
 +	 */
 +	if (cpu_set_onfault(&fb) != 0)
 +		return 0;
 +	rv = vmt_probe();
 +	cpu_unset_onfault();
 +
 +	return rv;
  }

  static void

From: Nick Hudson <nick.hudson@gmx.co.uk>
To: gnats-bugs@netbsd.org,
 port-arm-maintainer@netbsd.org,
 gnats-admin@netbsd.org,
 netbsd-bugs@netbsd.org,
 Showta Ishizaki <zakinko@snowrabbit.org>
Cc: 
Subject: Re: port-arm/60655: vmt(4) probes the VMware backdoor unconditionally
 on aarch64 and panics the kernel on hypervisors that do not implement it
Date: Sat, 5 Sep 2026 13:49:45 +0100

 On 05/09/2026 03:30, zakinko@snowrabbit.org via gnats wrote:
 [snip]
 > I have sent a patch to qemu-devel making hvf do the same, under the =
 subject
 > "target/arm/hvf: implement MDCCSR_EL0 as RAZ"; it has not appeared in =
 the
 > archive yet. It is not fixed upstream either: master is 1168 commits =
 past
 > v11.1.0 and none of them touch accel/hvf or target/arm/hvf, and 11.1.1 =
 has
 > none.
 >=20

 Definitely a qemu bug and I see Mohamed Mediouni agrees.

 https://lists.gnu.org/archive/html/qemu-devel/2026-09/msg01794.html


 >=20
 > That said, I do not think this is only QEMU's problem, and I would =
 like your
 > opinion on the NetBSD side.

 The patch is not correct.

 We can do better by not probing when

 arm64# acpidump -dt | grep HypervisorId
         HypervisorId=3D0x00000000554d4551
 arm64#


State-Changed-From-To: feedback->analyzed
State-Changed-By: skrll@NetBSD.org
State-Changed-When: Sat, 05 Sep 2026 13:57:49 +0000
State-Changed-Why:
Problem is understood


From: zakinko@snowrabbit.org
To: gnats-bugs@NetBSD.org
Cc: port-arm-maintainer@NetBSD.org, netbsd-bugs@NetBSD.org,
	gnats-admin@NetBSD.org, skrll@NetBSD.org, zakinko@snowrabbit.org
Subject: Re: port-arm/60655: vmt(4) probes the VMware backdoor unconditionally on aarch64 and panics the kernel on hypervisors that do not implement it
Date: Sun,  6 Sep 2026 11:42:58 +0900 (JST)

 Thanks for the pointer to HypervisorId -- that was the right place to look.
 I have rewritten the patch to read the FADT instead: the old version let
 the probe run and recovered from the trap, while this one never executes
 the instruction.  On stock QEMU 11.1.0 (-M virt -accel hvf) the guest
 reaches login with no hvf_unhandled_sysreg_read firing, where the old patch
 left one trap per boot.

 I checked all three cases on real hypervisors, not QEMU alone.  On VMware
 Fusion 26 (Apple Silicon), with the same kernel:

   HypervisorId = 0x0000657261774d56 = "VMware"
   vmt0 at cpu0: VMware Tools driver
   vmt0: UUID: 564de88b-...

 so the comparison matches a real VMware guest and vmt(4) attaches and talks
 to the backdoor as before.  With the FADT string forced to "VMware" or left
 at zero on QEMU, vmt_probe() still runs (and panics there, since that QEMU
 refuses MDCCSR_EL0) -- so the check is not just disabling vmt(4), and a
 hypervisor that leaves the field empty keeps the old behaviour.

 Let me know if this isn't the shape you had in mind.

 --- sys/arch/aarch64/aarch64/vmt.c.orig
 +++ sys/arch/aarch64/aarch64/vmt.c
 @@ -32,7 +32,16 @@

  #include <sys/device.h>
  #include <sys/module.h>
 +#include <sys/systm.h>

 +#ifdef _KERNEL_OPT
 +#include "acpica.h"
 +#endif
 +
 +#if NACPICA > 0
 +#include <dev/acpi/acpivar.h>
 +#endif
 +
  #include <dev/vmt/vmtreg.h>
  #include <dev/vmt/vmtvar.h>

 @@ -45,6 +54,39 @@

  static bool vmt_attached = false;

 +#if NACPICA > 0
 +/*
 + * The ACPI 6.0 FADT carries a Hypervisor Vendor Identity: eight bytes of
 + * ASCII, NUL padded.  Skip the probe when the hypervisor identifies
 + * itself as something other than VMware; there is nothing there to find.
 + *
 + * On aarch64 the backdoor is a read of MDCCSR_EL0 with a magic value in
 + * x7.  The register is architecturally mandatory, but a hypervisor is
 + * free to trap it and answer with an undefined instruction rather than a
 + * value, and that trap is fatal in kernel mode.  QEMU 11.1 does exactly
 + * this once the Apple vGIC is in use, so the machine panics during
 + * autoconfiguration.  See port-arm/60655.
 + *
 + * Firmware that leaves the field zero tells us nothing, so probe as
 + * before.  The bytes are compared in table order rather than through the
 + * UINT64, so that this reads the same on aarch64eb.
 + */
 +static bool
 +vmt_hypervisor_may_be_vmware(void)
 +{
 +	char name[sizeof(AcpiGbl_FADT.HypervisorId) + 1];
 +
 +	if (AcpiGbl_FADT.HypervisorId == 0)
 +		return true;
 +
 +	memcpy(name, &AcpiGbl_FADT.HypervisorId,
 +	    sizeof(AcpiGbl_FADT.HypervisorId));
 +	name[sizeof(name) - 1] = '\0';
 +
 +	return strncmp(name, "VMware", 6) == 0;
 +}
 +#endif /* NACPICA > 0 */
 +
  static int
  vmt_match(device_t parent, cfdata_t match, void *aux)
  {
 @@ -52,6 +94,11 @@
  	if (vmt_attached)
  		return 0;

 +#if NACPICA > 0
 +	if (!vmt_hypervisor_may_be_vmware())
 +		return 0;
 +#endif
 +
  	return vmt_probe();
  }


From: Nick Hudson <nick.hudson@gmx.co.uk>
To: gnats-bugs@netbsd.org,
 port-arm-maintainer@netbsd.org,
 gnats-admin@netbsd.org,
 netbsd-bugs@netbsd.org,
 Showta Ishizaki <zakinko@snowrabbit.org>
Cc: 
Subject: Re: port-arm/60655: vmt(4) probes the VMware backdoor unconditionally
 on aarch64 and panics the kernel on hypervisors that do not implement it
Date: Sun, 6 Sep 2026 11:56:50 +0100

 Thanks for doing this.

 Think you need to check the FADT version is at least 6 too, though.

From: zakinko@snowrabbit.org
To: gnats-bugs@NetBSD.org
Cc: port-arm-maintainer@NetBSD.org, netbsd-bugs@NetBSD.org,
	gnats-admin@NetBSD.org, nick.hudson@gmx.co.uk, zakinko@snowrabbit.org
Subject: Re: port-arm/60655: vmt(4) probes the VMware backdoor unconditionally on aarch64 and panics the kernel on hypervisors that do not implement it
Date: Mon,  7 Sep 2026 02:53:44 +0900 (JST)

 Thanks for catching that -- I had missed it.  I read the pointer as:
 HypervisorId only exists from FADT revision 6, so the revision (and the
 length) should be checked before the field is read.  Is that the right
 understanding?

 Two things I went on to do.  I am not confident about either, so I would
 be grateful for your view:

 - The FADT reading moved into a small helper in dev/acpi,
   acpi_fadt_hypervisor_id(), so that the next user of the field need not
   copy it.

 - vmt_match() now probes only when the firmware names VMware, rather
   than skipping only when it names something else.  The difference shows
   on a hypervisor that traps MDCCSR_EL0 but leaves the field zero: the
   earlier form still panics there (I checked with a QEMU 11.1.0 patched
   to write zeros).  The ACPI 6.0 note for the field reads to me as
   "probe only on a vendor you recognise", but I may be reading too much
   into it -- if you would rather keep the old behaviour on a zero field,
   it is a single condition to flip.

 Rebuilt from trunk, -Werror -Wall clean.  Boots on stock QEMU 11.1 (hvf;
 "QEMU", vmt does not attach), on the zero-field QEMU, and on VMware
 Fusion 26 ("VMware", vmt0 attaches and answers the host).

 --- sys/dev/acpi/acpi_util.h.orig
 +++ sys/dev/acpi/acpi_util.h
 @@ -114,4 +114,6 @@
  ACPI_STATUS	 acpi_claim_childdevs(device_t, struct acpi_devnode *,
  			const char *);

 +bool		 acpi_fadt_hypervisor_id(char *, size_t);
 +
  #endif	/* !_SYS_DEV_ACPI_ACPI_UTIL_H */
 --- sys/dev/acpi/acpi_util.c.orig
 +++ sys/dev/acpi/acpi_util.c
 @@ -1422,3 +1422,42 @@

  	return AE_OK;
  }
 +
 +/*
 + * Return the Hypervisor Vendor Identity from the FADT as a NUL-terminated
 + * string, or false if there is none to report.
 + *
 + * ACPI 6.0 added the field (5.2.9, FADT Format, offset 268): eight bytes
 + * that name the hypervisor vendor, "usually following the name of the
 + * hypervisor product", with no version information in it.  QEMU writes
 + * "QEMU" and VMware writes "VMware", zero padded.  Firmware places zero
 + * bytes in it when no hypervisor is present, so zero is reported as
 + * absence.  The specification's own note is that a guest "can consult it
 + * and act on the result, based on whether it recognized the vendor",
 + * which is what a caller does with the string.
 + *
 + * The field only exists from FADT revision 6.  ACPICA zeroes its copy of
 + * the FADT before filling it from the firmware's table, so on an older,
 + * shorter table the field reads as zero rather than as garbage.  That is
 + * a property of the copy and not something to lean on, so both the
 + * revision and the length are checked before the field is read, as
 + * open-vm-tools does.  The bytes are copied in table order rather than
 + * through the UINT64, so that this reads the same on big-endian machines.
 + */
 +bool
 +acpi_fadt_hypervisor_id(char *buf, size_t buflen)
 +{
 +	const size_t len = sizeof(AcpiGbl_FADT.HypervisorId);
 +
 +	KASSERT(buflen > len);
 +
 +	if (AcpiGbl_FADT.Header.Revision < 6 ||
 +	    AcpiGbl_FADT.Header.Length < ACPI_FADT_V6_SIZE)
 +		return false;
 +	if (AcpiGbl_FADT.HypervisorId == 0)
 +		return false;
 +
 +	memcpy(buf, &AcpiGbl_FADT.HypervisorId, len);
 +	buf[len] = '\0';
 +	return true;
 +}
 --- sys/arch/aarch64/aarch64/vmt.c.orig
 +++ sys/arch/aarch64/aarch64/vmt.c
 @@ -32,7 +32,16 @@

  #include <sys/device.h>
  #include <sys/module.h>
 +#include <sys/systm.h>

 +#ifdef _KERNEL_OPT
 +#include "acpica.h"
 +#endif
 +
 +#if NACPICA > 0
 +#include <dev/acpi/acpivar.h>
 +#endif
 +
  #include <dev/vmt/vmtreg.h>
  #include <dev/vmt/vmtvar.h>

 @@ -48,10 +57,29 @@
  static int
  vmt_match(device_t parent, cfdata_t match, void *aux)
  {
 +#if NACPICA > 0
 +	char hv[sizeof(AcpiGbl_FADT.HypervisorId) + 1];
 +#endif
 +
  	/* vmt should not attach to more than a single CPU. */
  	if (vmt_attached)
  		return 0;

 +#if NACPICA > 0
 +	/*
 +	 * The backdoor is a read of MDCCSR_EL0 with a magic value in x7.  A
 +	 * hypervisor other than VMware is free to answer that read with an
 +	 * undefined instruction trap, which is fatal in kernel mode; QEMU
 +	 * does so once the Apple vGIC is in use.  Probe only when the
 +	 * firmware names VMware; a hypervisor that names something else,
 +	 * or nothing, is not one whose API this driver knows.  See
 +	 * port-arm/60655.
 +	 */
 +	if (!acpi_fadt_hypervisor_id(hv, sizeof(hv)) ||
 +	    strncmp(hv, "VMware", 6) != 0)
 +		return 0;
 +#endif
 +
  	return vmt_probe();
  }


From: Nick Hudson <nick.hudson@gmx.co.uk>
To: zakinko@snowrabbit.org,
 "gnats-bugs@netbsd.org" <gnats-bugs@NetBSD.org>
Cc: "port-arm-maintainer@netbsd.org" <port-arm-maintainer@NetBSD.org>,
 "netbsd-bugs@netbsd.org" <netbsd-bugs@NetBSD.org>,
 "gnats-admin@netbsd.org" <gnats-admin@NetBSD.org>
Subject: Re: port-arm/60655: vmt(4) probes the VMware backdoor unconditionally
 on aarch64 and panics the kernel on hypervisors that do not implement it
Date: Thu, 10 Sep 2026 07:50:57 +0100

 --Apple-Mail=_AEE1818F-833B-4FFD-BCBD-5418E5C5662C
 Content-Transfer-Encoding: quoted-printable
 Content-Type: text/plain;
 	charset=utf-8


 On 06/09/2026 19:53, zakinko@snowrabbit.org =
 <mailto:zakinko@snowrabbit.org> wrote:
 > Thanks for catching that -- I had missed it.  I read the pointer as:
 > HypervisorId only exists from FADT revision 6, so the revision (and =
 the
 > length) should be checked before the field is read.  Is that the right
 > understanding?
 >=20
 > Two things I went on to do.  I am not confident about either, so I =
 would
 > be grateful for your view:
 >=20
 > - The FADT reading moved into a small helper in dev/acpi,
 >   acpi_fadt_hypervisor_id(), so that the next user of the field need =
 not
 >   copy it.
 Not a big fan of this helper, but OK, I guess.

 >=20
 > - vmt_match() now probes only when the firmware names VMware, rather
 >   than skipping only when it names something else.
 Sounds good.
 >   The difference shows
 >   on a hypervisor that traps MDCCSR_EL0 but leaves the field zero: the
 >   earlier form still panics there (I checked with a QEMU 11.1.0 =
 patched
 >   to write zeros).

 Any hypervisor that doesn=E2=80=99t provide a RAZ MDCCSR_EL0 is broken.

 >   The ACPI 6.0 note for the field reads to me as
 >   "probe only on a vendor you recognise", but I may be reading too =
 much
 >   into it -- if you would rather keep the old behaviour on a zero =
 field,
 >   it is a single condition to flip.
 >=20
 I think the code in the message is fine.
 > =20
 > +#if NACPICA > 0
 > +	/*
 > +                                                                    =
 ...  A
 > +	 * hypervisor other than VMware is free to answer that read with =
 an
 > +	 * undefined instruction trap, which is fatal in kernel mode; =
 QEMU
 > +	 * does so once the Apple vGIC is in use.
 This is wrong.

 QEMU was broken - it is now hopefully fixed with your diff.=

 --Apple-Mail=_AEE1818F-833B-4FFD-BCBD-5418E5C5662C
 Content-Transfer-Encoding: quoted-printable
 Content-Type: text/html;
 	charset=utf-8

 <html aria-label=3D"message body"><head><meta http-equiv=3D"content-type" =
 content=3D"text/html; charset=3Dutf-8"></head><body =
 style=3D"overflow-wrap: break-word; -webkit-nbsp-mode: space; =
 line-break: after-white-space;">

  =20
     <meta http-equiv=3D"Content-Type" content=3D"text/html; =
 charset=3DUTF-8">
  =20
   <div><p><br>
     </p>
     <div class=3D"moz-cite-prefix">On 06/09/2026 19:53,
       <a class=3D"moz-txt-link-abbreviated" =
 href=3D"mailto:zakinko@snowrabbit.org">zakinko@snowrabbit.org</a> =
 wrote:<br>
     </div>
     <blockquote type=3D"cite" =
 cite=3D"mid:20260906175344.36CA7C9311@bambi.snowrabbit.org">
       <pre wrap=3D"" class=3D"moz-quote-pre">Thanks for catching that -- =
 I had missed it.  I read the pointer as:
 HypervisorId only exists from FADT revision 6, so the revision (and the
 length) should be checked before the field is read.  Is that the right
 understanding?

 Two things I went on to do.  I am not confident about either, so I would
 be grateful for your view:

 - The FADT reading moved into a small helper in dev/acpi,
   acpi_fadt_hypervisor_id(), so that the next user of the field need not
   copy it.</pre>
     </blockquote><p>Not a big fan of this helper, but OK, I guess.</p>
     <blockquote type=3D"cite" =
 cite=3D"mid:20260906175344.36CA7C9311@bambi.snowrabbit.org">
       <pre wrap=3D"" class=3D"moz-quote-pre">
 - vmt_match() now probes only when the firmware names VMware, rather
   than skipping only when it names something =
 else.</pre></blockquote>Sounds good.<br><blockquote type=3D"cite" =
 cite=3D"mid:20260906175344.36CA7C9311@bambi.snowrabbit.org"><pre wrap=3D""=
  class=3D"moz-quote-pre">  The difference shows
   on a hypervisor that traps MDCCSR_EL0 but leaves the field zero: the
   earlier form still panics there (I checked with a QEMU 11.1.0 patched
   to write zeros).</pre></blockquote><div><br></div>Any hypervisor that =
 doesn=E2=80=99t provide a RAZ MDCCSR_EL0 is =
 broken.</div><div><br><blockquote type=3D"cite" =
 cite=3D"mid:20260906175344.36CA7C9311@bambi.snowrabbit.org"><pre wrap=3D""=
  class=3D"moz-quote-pre">  The ACPI 6.0 note for the field reads to me =
 as
   "probe only on a vendor you recognise", but I may be reading too much
   into it -- if you would rather keep the old behaviour on a zero field,
   it is a single condition to flip.

 </pre></blockquote>I think the code in the message is =
 fine.<br><blockquote type=3D"cite" =
 cite=3D"mid:20260906175344.36CA7C9311@bambi.snowrabbit.org"><pre wrap=3D""=
  class=3D"moz-quote-pre">=20
 +#if NACPICA &gt; 0
 +	/*
 +                                                                    ... =
  A
 +	 * hypervisor other than VMware is free to answer that read with =
 an
 +	 * undefined instruction trap, which is fatal in kernel mode; =
 QEMU
 +	 * does so once the Apple vGIC is in use.</pre></blockquote>This =
 is wrong.</div><div><br></div><div>QEMU was broken - it is now hopefully =
 fixed with your diff.</div>

 </body></html>=

 --Apple-Mail=_AEE1818F-833B-4FFD-BCBD-5418E5C5662C--

From: zakinko@snowrabbit.org
To: gnats-bugs@NetBSD.org
Cc: port-arm-maintainer@NetBSD.org, netbsd-bugs@NetBSD.org,
	gnats-admin@NetBSD.org, nick.hudson@gmx.co.uk, zakinko@snowrabbit.org
Subject: Re: port-arm/60655: vmt(4) probes the VMware backdoor unconditionally on aarch64 and panics the kernel on hypervisors that do not implement it
Date: Fri, 11 Sep 2026 07:34:13 +0900 (JST)

 Thanks for looking at it again.

 I have taken out the sentence about a hypervisor being free to trap
 the read.  A hypervisor that does not give a RAZ MDCCSR_EL0 is simply
 broken, and QEMU was; the comment in vmt_match() now says that and
 notes that QEMU has fixed it (Peter Maydell applied the patch to
 target-arm.next on 8 September with a cc:stable tag).  Nothing else in
 the diff changed.

 I kept the helper since it seemed acceptable to you.  If you would
 rather have the FADT read inline in vmt_match(), please say so and I
 will send that form instead.

 Rebuilt GENERIC64 from the same trunk tree with the comment change,
 -Werror -Wall clean, and booted it on stock QEMU 11.1.0 under hvf: it
 reaches login, vmt does not attach, and QEMU reports no unhandled
 sysreg read.  The zero-field QEMU and VMware Fusion runs reported
 before were with this code minus the comment, so I did not repeat
 them.

 --- sys/dev/acpi/acpi_util.h.orig
 +++ sys/dev/acpi/acpi_util.h
 @@ -114,4 +114,6 @@ ACPI_STATUS	 acpi_dsm_query(ACPI_HANDLE, uint8_t *, ACPI_INTEGER,
  ACPI_STATUS	 acpi_claim_childdevs(device_t, struct acpi_devnode *,
  			const char *);

 +bool		 acpi_fadt_hypervisor_id(char *, size_t);
 +
  #endif	/* !_SYS_DEV_ACPI_ACPI_UTIL_H */
 --- sys/dev/acpi/acpi_util.c.orig
 +++ sys/dev/acpi/acpi_util.c
 @@ -1422,3 +1422,42 @@ acpi_claim_childdevs(device_t dev, struct acpi_devnode *devnode,

  	return AE_OK;
  }
 +
 +/*
 + * Return the Hypervisor Vendor Identity from the FADT as a NUL-terminated
 + * string, or false if there is none to report.
 + *
 + * ACPI 6.0 added the field (5.2.9, FADT Format, offset 268): eight bytes
 + * that name the hypervisor vendor, "usually following the name of the
 + * hypervisor product", with no version information in it.  QEMU writes
 + * "QEMU" and VMware writes "VMware", zero padded.  Firmware places zero
 + * bytes in it when no hypervisor is present, so zero is reported as
 + * absence.  The specification's own note is that a guest "can consult it
 + * and act on the result, based on whether it recognized the vendor",
 + * which is what a caller does with the string.
 + *
 + * The field only exists from FADT revision 6.  ACPICA zeroes its copy of
 + * the FADT before filling it from the firmware's table, so on an older,
 + * shorter table the field reads as zero rather than as garbage.  That is
 + * a property of the copy and not something to lean on, so both the
 + * revision and the length are checked before the field is read, as
 + * open-vm-tools does.  The bytes are copied in table order rather than
 + * through the UINT64, so that this reads the same on big-endian machines.
 + */
 +bool
 +acpi_fadt_hypervisor_id(char *buf, size_t buflen)
 +{
 +	const size_t len = sizeof(AcpiGbl_FADT.HypervisorId);
 +
 +	KASSERT(buflen > len);
 +
 +	if (AcpiGbl_FADT.Header.Revision < 6 ||
 +	    AcpiGbl_FADT.Header.Length < ACPI_FADT_V6_SIZE)
 +		return false;
 +	if (AcpiGbl_FADT.HypervisorId == 0)
 +		return false;
 +
 +	memcpy(buf, &AcpiGbl_FADT.HypervisorId, len);
 +	buf[len] = '\0';
 +	return true;
 +}
 --- sys/arch/aarch64/aarch64/vmt.c.orig
 +++ sys/arch/aarch64/aarch64/vmt.c
 @@ -32,6 +32,15 @@

  #include <sys/device.h>
  #include <sys/module.h>
 +#include <sys/systm.h>
 +
 +#ifdef _KERNEL_OPT
 +#include "acpica.h"
 +#endif
 +
 +#if NACPICA > 0
 +#include <dev/acpi/acpivar.h>
 +#endif

  #include <dev/vmt/vmtreg.h>
  #include <dev/vmt/vmtvar.h>
 @@ -48,10 +57,29 @@ static bool vmt_attached = false;
  static int
  vmt_match(device_t parent, cfdata_t match, void *aux)
  {
 +#if NACPICA > 0
 +	char hv[sizeof(AcpiGbl_FADT.HypervisorId) + 1];
 +#endif
 +
  	/* vmt should not attach to more than a single CPU. */
  	if (vmt_attached)
  		return 0;

 +#if NACPICA > 0
 +	/*
 +	 * The backdoor is a read of MDCCSR_EL0 with a magic value in x7.
 +	 * Probe only when the firmware names VMware; a hypervisor that
 +	 * names something else, or nothing, is not one whose API this
 +	 * driver knows, and there is nothing to find.  (QEMU 11.1 under
 +	 * hvf also trapped the read as an undefined instruction, which is
 +	 * fatal in kernel mode.  That was a QEMU bug, since fixed.)  See
 +	 * port-arm/60655.
 +	 */
 +	if (!acpi_fadt_hypervisor_id(hv, sizeof(hv)) ||
 +	    strncmp(hv, "VMware", 6) != 0)
 +		return 0;
 +#endif
 +
  	return vmt_probe();
  }


From: "Nick Hudson" <skrll@netbsd.org>
To: gnats-bugs@gnats.NetBSD.org
Cc: 
Subject: PR/60655 CVS commit: src/sys
Date: Sat, 12 Sep 2026 12:47:59 +0000

 Module Name:	src
 Committed By:	skrll
 Date:		Sat Sep 12 12:47:59 UTC 2026

 Modified Files:
 	src/sys/arch/aarch64/aarch64: vmt.c
 	src/sys/dev/acpi: acpi_util.c acpi_util.h

 Log Message:
 PR/60655: vmt(4) probes the VMware backdoor unconditionally on aarch64

 On ACPI systems only probe for vmt(4) when Hypervisor_Id in FADT reports
 VMWare.

 This helps avoid buggy hypervisors that don't implement a RAZ mdccsr_el0

 Patch from Showta Ishizaki and slightly updated by me.


 To generate a diff of this commit:
 cvs rdiff -u -r1.2 -r1.3 src/sys/arch/aarch64/aarch64/vmt.c
 cvs rdiff -u -r1.37 -r1.38 src/sys/dev/acpi/acpi_util.c
 cvs rdiff -u -r1.16 -r1.17 src/sys/dev/acpi/acpi_util.h

 Please note that diffs are not public domain; they are subject to the
 copyright notices on the relevant files.

>Unformatted:

NetBSD Home
NetBSD PR Database Search

(Contact us) $NetBSD: query-full-pr,v 1.51 2026/08/10 02:28:17 riastradh Exp $
$NetBSD: gnats_config.sh,v 1.10 2026/05/13 22:00:09 riastradh Exp $
Copyright © 1994-2026 The NetBSD Foundation, Inc. ALL RIGHTS RESERVED.