[hypervisor][x86] Check CPL when handling a VMCALL

Only allow guest CPL 0 to make a hypercall. If anything other than the
guest kernel attempts a VMCALL, return NOT_PERMITTED.

Bug: b/154791063
Change-Id: I08d7f0d00df002ef90bb9be14c52697027df03bb
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/383975
Testability-Review: Abdulla Kamar <abdulla@google.com>
Reviewed-by: Venkatesh Srinivas <venkateshs@google.com>
Commit-Queue: Abdulla Kamar <abdulla@google.com>
diff --git a/zircon/kernel/arch/x86/hypervisor/vcpu_priv.h b/zircon/kernel/arch/x86/hypervisor/vcpu_priv.h
index 10efb14..3c29d35 100644
--- a/zircon/kernel/arch/x86/hypervisor/vcpu_priv.h
+++ b/zircon/kernel/arch/x86/hypervisor/vcpu_priv.h
@@ -71,6 +71,7 @@
 static const uint32_t kGuestXxAccessRightsTypeCode      = 1u << 3;
 // See Volume 3, Section 3.4.5.1 for valid non-system selector types.
 static const uint32_t kGuestXxAccessRightsS             = 1u << 4;
+static const uint32_t kGuestXxAccessRightsDplUser       = 3u << 5;
 static const uint32_t kGuestXxAccessRightsP             = 1u << 7;
 static const uint32_t kGuestXxAccessRightsL             = 1u << 13;
 static const uint32_t kGuestXxAccessRightsD             = 1u << 14;
diff --git a/zircon/kernel/arch/x86/hypervisor/vmexit.cc b/zircon/kernel/arch/x86/hypervisor/vmexit.cc
index a8d130f..2707456 100644
--- a/zircon/kernel/arch/x86/hypervisor/vmexit.cc
+++ b/zircon/kernel/arch/x86/hypervisor/vmexit.cc
@@ -1066,14 +1066,21 @@
                                  hypervisor::GuestPhysicalAddressSpace* gpas,
                                  GuestState* guest_state) {
   next_rip(exit_info, vmcs);
-  vmcs->Invalidate();
 
+  const uint32_t access_rights = vmcs->Read(VmcsField32::GUEST_SS_ACCESS_RIGHTS);
+  if ((access_rights & kGuestXxAccessRightsDplUser) != 0) {
+    // We only accept a VMCALL if CPL is 0.
+    guest_state->rax = VmCallStatus::NOT_PERMITTED;
+    return ZX_OK;
+  }
+
+  vmcs->Invalidate();
   VmCallInfo info(guest_state);
   switch (info.type) {
     case VmCallType::CLOCK_PAIRING: {
       if (info.arg[1] != 0) {
         dprintf(INFO, "CLOCK_PAIRING hypercall doesn't support clock type %lu\n", info.arg[1]);
-        guest_state->rax = VmCallStatus::OP_NOT_SUPPORTED;
+        guest_state->rax = VmCallStatus::NOT_SUPPORTED;
         break;
       }
       zx_status_t status = pv_clock_populate_offset(gpas, info.arg[0]);
@@ -1086,10 +1093,10 @@
       break;
     }
     default:
-      dprintf(INFO, "Unknown VMCALL(%lu) (arg0=%#lx, arg1=%#lx, arg2=%#lx, arg3=%#lx)\n",
+      dprintf(INFO, "Unknown hypercall %lu (arg0=%#lx, arg1=%#lx, arg2=%#lx, arg3=%#lx)\n",
               static_cast<unsigned long>(info.type), info.arg[0], info.arg[1], info.arg[2],
               info.arg[3]);
-      guest_state->rax = VmCallStatus::NO_SYS;
+      guest_state->rax = VmCallStatus::UNKNOWN_HYPERCALL;
       break;
   }
   // We never fail in case of hypercalls, we just return/propagate errors to the caller.
diff --git a/zircon/kernel/arch/x86/hypervisor/vmexit_priv.h b/zircon/kernel/arch/x86/hypervisor/vmexit_priv.h
index b8eab72..e2fdef4 100644
--- a/zircon/kernel/arch/x86/hypervisor/vmexit_priv.h
+++ b/zircon/kernel/arch/x86/hypervisor/vmexit_priv.h
@@ -233,9 +233,10 @@
 
 enum VmCallStatus : int64_t {
     OK                  = 0,
+    NOT_PERMITTED       = -1,
     FAULT               = -14,
-    OP_NOT_SUPPORTED    = -95,
-    NO_SYS              = -1000,
+    NOT_SUPPORTED       = -95,
+    UNKNOWN_HYPERCALL   = -1000,
 };
 
 enum class VmCallType : uint64_t {
diff --git a/zircon/system/utest/hypervisor/guest.cc b/zircon/system/utest/hypervisor/guest.cc
index be935bb..bda8f3c 100644
--- a/zircon/system/utest/hypervisor/guest.cc
+++ b/zircon/system/utest/hypervisor/guest.cc
@@ -65,7 +65,8 @@
 DECLARE_TEST_FUNCTION(vcpu_syscall)
 DECLARE_TEST_FUNCTION(vcpu_sysenter)
 DECLARE_TEST_FUNCTION(vcpu_sysenter_compat)
-DECLARE_TEST_FUNCTION(vcpu_vmcall)
+DECLARE_TEST_FUNCTION(vcpu_vmcall_invalid_number)
+DECLARE_TEST_FUNCTION(vcpu_vmcall_invalid_cpl)
 DECLARE_TEST_FUNCTION(vcpu_extended_registers)
 DECLARE_TEST_FUNCTION(guest_set_trap_with_io)
 #endif
@@ -114,7 +115,8 @@
 
 #ifdef __aarch64__
 
-static zx_status_t get_interrupt_controller_info(fuchsia::sysinfo::InterruptControllerInfoPtr* info) {
+static zx_status_t get_interrupt_controller_info(
+    fuchsia::sysinfo::InterruptControllerInfoPtr* info) {
   fuchsia::sysinfo::SysInfoSyncPtr sysinfo = get_sysinfo();
 
   zx_status_t fidl_status;
@@ -948,11 +950,11 @@
   END_TEST;
 }
 
-static bool vcpu_vmcall() {
+static bool vcpu_vmcall_invalid_number() {
   BEGIN_TEST;
 
   test_t test;
-  ASSERT_TRUE(setup(&test, vcpu_vmcall_start, vcpu_vmcall_end));
+  ASSERT_TRUE(setup(&test, vcpu_vmcall_invalid_number_start, vcpu_vmcall_invalid_number_end));
   if (!test.supported) {
     // The hypervisor isn't supported, so don't run the test.
     return true;
@@ -963,8 +965,29 @@
   zx_vcpu_state_t vcpu_state;
   ASSERT_EQ(test.vcpu.read_state(ZX_VCPU_STATE, &vcpu_state, sizeof(vcpu_state)), ZX_OK);
 
-  const uint64_t kVmCallNoSys = -1000;
-  EXPECT_EQ(vcpu_state.rax, kVmCallNoSys);
+  const uint64_t kUnknownHypercall = -1000;
+  EXPECT_EQ(vcpu_state.rax, kUnknownHypercall);
+
+  END_TEST;
+}
+
+static bool vcpu_vmcall_invalid_cpl() {
+  BEGIN_TEST;
+
+  test_t test;
+  ASSERT_TRUE(setup(&test, vcpu_vmcall_invalid_cpl_start, vcpu_vmcall_invalid_cpl_end));
+  if (!test.supported) {
+    // The hypervisor isn't supported, so don't run the test.
+    return true;
+  }
+
+  ASSERT_TRUE(resume_and_clean_exit(&test));
+
+  zx_vcpu_state_t vcpu_state;
+  ASSERT_EQ(test.vcpu.read_state(ZX_VCPU_STATE, &vcpu_state, sizeof(vcpu_state)), ZX_OK);
+
+  const uint64_t kNotPermitted = -1;
+  EXPECT_EQ(vcpu_state.rax, kNotPermitted);
 
   END_TEST;
 }
@@ -1099,7 +1122,8 @@
 RUN_TEST(vcpu_syscall)
 RUN_TEST(vcpu_sysenter)
 RUN_TEST(vcpu_sysenter_compat)
-RUN_TEST(vcpu_vmcall)
+RUN_TEST(vcpu_vmcall_invalid_number)
+RUN_TEST(vcpu_vmcall_invalid_cpl)
 RUN_TEST(vcpu_extended_registers)
 RUN_TEST(vcpu_write_state_io_invalid_size)
 RUN_TEST(guest_set_trap_with_io)
diff --git a/zircon/system/utest/hypervisor/x64.S b/zircon/system/utest/hypervisor/x64.S
index 91b06b6..2dcdf50 100644
--- a/zircon/system/utest/hypervisor/x64.S
+++ b/zircon/system/utest/hypervisor/x64.S
@@ -405,11 +405,29 @@
     movq $0, (EXIT_TEST_ADDR)
 FUNCTION vcpu_sysenter_compat_end
 
-FUNCTION vcpu_vmcall_start
+FUNCTION vcpu_vmcall_invalid_number_start
     movq $(INVALID_HYPERCALL), %rax
     vmcall
     movq $0, (EXIT_TEST_ADDR)
-FUNCTION vcpu_vmcall_end
+FUNCTION vcpu_vmcall_invalid_number_end
+
+FUNCTION vcpu_vmcall_invalid_cpl_start
+    init_interrupt_handling vcpu_vmcall_invalid_cpl_start
+
+    xor %eax, %eax
+    mov $((USER_CODE_64_SELECTOR << 16) | CODE_64_SELECTOR), %edx
+    mov $X86_MSR_IA32_STAR, %ecx
+    wrmsr
+
+    pushfq
+    mov $ABSOLUTE_ADDR(vcpu_vmcall_invalid_cpl_start, vcpu_vmcall_invalid_cpl_ring3), %rcx
+    sysretq
+
+vcpu_vmcall_invalid_cpl_ring3:
+    movq $(INVALID_HYPERCALL), %rax
+    vmcall
+    movq $0, (EXIT_TEST_ADDR)
+FUNCTION vcpu_vmcall_invalid_cpl_end
 
 FUNCTION vcpu_extended_registers_start
     // Enable SSE instructions.