diff --git a/arch/x64/apic.cc b/arch/x64/apic.cc
index 2bcd227f9178d474feb960aaaa8b5415cfbf6031..904e3bfac8c5e0b089518cdce9170947541686bb 100644
--- a/arch/x64/apic.cc
+++ b/arch/x64/apic.cc
@@ -1,5 +1,6 @@
 #include "apic.hh"
 #include "msr.hh"
+#include "xen.hh"
 #include <osv/percpu.hh>
 #include <cpuid.hh>
 #include <processor.hh>
@@ -118,7 +119,24 @@ void x2apic::write(apicreg reg, u32 value)
 
 u32 x2apic::id()
 {
-    return processor::rdmsr(msr::X2APIC_ID);
+    u32 id = processor::rdmsr(msr::X2APIC_ID);
+    if (!is_xen())
+        return id;
+
+    // The x2APIC specification says that reading from the X2APIC_ID MSR should
+    // return the physical apic id of the current processor. However, the Xen
+    // implementation (as of 4.2.2) is broken, and reads actually return old
+    // style xAPIC id. Even if they fix it, we still have HVs deployed around
+    // that will return the wrong ID. We can work around this by testing if the
+    // returned APIC id is in the form (id << 24), since in that case, the
+    // first 24 bits will all be zeroed. Then at least we can get this working
+    // everywhere. This may pose a problem if we want to ever support more than
+    // 1 << 24 vCPUs (or if any other HV has some random x2apic ids), but that
+    // is highly unlikely anyway.
+    if (((id & 0xffffff) == 0) && ((id >> 24) != 0)) {
+        id = (id >> 24);
+    }
+    return id;
 }
 
 apic_driver* create_apic_driver()
diff --git a/arch/x64/xen.hh b/arch/x64/xen.hh
index 6c090d436fea5f1689173db159f37d5d245fa82e..0704a7fd621473898c686a3ac51dd8eabc97e983 100644
--- a/arch/x64/xen.hh
+++ b/arch/x64/xen.hh
@@ -12,8 +12,10 @@
 extern char hypercall_page[];
 extern uint8_t xen_features[];
 extern struct start_info* xen_start_info;
+extern "C" shared_info_t *HYPERVISOR_shared_info;
 
 #define XENPV_ALTERNATIVE(x, y) ALTERNATIVE((xen_start_info != nullptr), x, y)
+#define is_xen() (HYPERVISOR_shared_info != nullptr)
 
 // We don't support 32 bit
 struct xen_vcpu_info {