Triển khai mô-đun nhà cung cấp pKVM

Trang này giải thích cách triển khai mô-đun nhà cung cấp máy ảo dựa trên nhân được bảo vệ (pKVM).

Đối với android16-6.12 trở lên, khi hoàn tất các bước này, bạn sẽ có một cây thư mục tương tự như sau:

BUILD.bazel
el1.c
hyp/
    BUILD.bazel
    el2.c

Để xem ví dụ hoàn chỉnh, hãy xem phần Tạo mô-đun pKVM bằng DDK.

Đối với android15-6.6 trở xuống:

Makefile
el1.c
hyp/
    Makefile
    el2.c
  1. Thêm mã siêu giám sát EL2 (el2.c). Tối thiểu, mã này phải khai báo một hàm init chấp nhận một tham chiếu đến cấu trúc pkvm_module_ops:

    #include <asm/kvm_pkvm_module.h>
    
    int pkvm_driver_hyp_init(const struct pkvm_module_ops *ops)
    {
      /* Init the EL2 code */
    
      return 0;
    }
    

    API mô-đun nhà cung cấp pKVM là một cấu trúc đóng gói các lệnh gọi lại cho trình giám sát ảo pKVM. Cấu trúc này tuân theo các quy tắc ABI giống như các giao diện GKI.

  2. Tạo hyp/Makefile để tạo mã trình giám sát siêu ảo:

    hyp-obj-y := el2.o
    include $(srctree)/arch/arm64/kvm/hyp/nvhe/Makefile.module
    
  3. Thêm mã nhân EL1 (el1.c). Phần khởi động của mã này phải chứa một lệnh gọi đến pkvm_load_el2 module để tải mã trình giám sát siêu ảo EL2 từ bước 1.

    #include <linux/init.h>
    #include <linux/module.h>
    #include <linux/kernel.h>
    #include <asm/kvm_pkvm_module.h>
    
    int __kvm_nvhe_pkvm_driver_hyp_init(const struct pkvm_module_ops *ops);
    
    static int __init pkvm_driver_init(void)
    {
        unsigned long token;
    
        return pkvm_load_el2_module(__kvm_nvhe_pkvm_driver_hyp_init, &token);
    }
    module_init(pkvm_driver_init);
    
  4. Tạo các quy tắc xây dựng.

    Đối với android16-6.12 trở lên, hãy tham khảo Tạo mô-đun pKVM bằng DDK để tạo ddk_library() cho EL2 và ddk_module() cho EL1.

    Đối với android15-6.6 trở xuống, hãy tạo makefile gốc để liên kết mã EL1 và EL2 với nhau:

    ifneq ($(KERNELRELEASE),)
    clean-files := hyp/hyp.lds hyp/hyp-reloc.S
    
    obj-m := pkvm_module.o
    pkvm_module-y := el1.o hyp/kvm_nvhe.o
    
    $(PWD)/hyp/kvm_nvhe.o: FORCE
             $(Q)$(MAKE) $(build)=$(obj)/hyp $(obj)/hyp/kvm_nvhe.o
    else
    all:
            make -C $(KDIR) M=$(PWD) modules
    clean:
            make -C $(KDIR) M=$(PWD) clean
    endif
    

Tải mô-đun pKVM

Tương tự như các mô-đun nhà cung cấp GKI, các mô-đun nhà cung cấp pKVM có thể được tải bằng modprobe. Tuy nhiên, vì lý do bảo mật, quá trình tải phải diễn ra trước khi tước đặc quyền. Để tải một mô-đun pKVM, bạn phải đảm bảo các mô-đun của mình có trong hệ thống tệp gốc (initramfs) và bạn phải thêm nội dung sau vào dòng lệnh của hạt nhân:

kvm-arm.protected_modules=mod1,mod2,mod3,...

Các mô-đun nhà cung cấp pKVM được lưu trữ trong initramfs sẽ kế thừa chữ ký và khả năng bảo vệ của initramfs.

Nếu một trong các mô-đun nhà cung cấp pKVM không tải được, thì hệ thống sẽ được coi là không an toàn và bạn sẽ không thể khởi động máy ảo được bảo vệ.

Gọi một hàm EL2 (trình ảo hoá) từ EL1 (mô-đun kernel)

Lệnh gọi hypervisor (HVC) là một chỉ dẫn cho phép nhân gọi hypervisor. Khi các mô-đun nhà cung cấp pKVM được giới thiệu, HVC có thể được dùng để gọi một hàm chạy ở EL2 (trong mô-đun trình ảo hoá) từ EL1 (mô-đun kernel):

  1. Trong mã EL2 (el2.c), hãy khai báo trình xử lý EL2:

Android 14

   void pkvm_driver_hyp_hvc(struct kvm_cpu_context *ctx)
   {
     /* Handle the call */

     cpu_reg(ctx, 1) = 0;
   }

Android 15 trở lên

   void pkvm_driver_hyp_hvc(struct user_pt_regs *regs)
   {
     /* Handle the call */

     regs->regs[0] = SMCCC_RET_SUCCESS;
     regs->regs[1] = 0;
   }
  1. Trong mã EL1 (el1.c), hãy đăng ký trình xử lý EL2 trong mô-đun nhà cung cấp pKVM:

    int __kvm_nvhe_pkvm_driver_hyp_init(const struct pkvm_module_ops *ops);
    void __kvm_nvhe_pkvm_driver_hyp_hvc(struct kvm_cpu_context *ctx); // Android14
    void __kvm_nvhe_pkvm_driver_hyp_hvc(struct user_pt_regs *regs);   // Android15
    
    static int hvc_number;
    
    static int __init pkvm_driver_init(void)
    {
      long token;
      int ret;
    
      ret = pkvm_load_el2_module(__kvm_nvhe_pkvm_driver_hyp_init,token);
      if (ret)
        return ret;
    
      ret = pkvm_register_el2_mod_call(__kvm_nvhe_pkvm_driver_hyp_hvc, token)
      if (ret < 0)
        return ret;
    
      hvc_number = ret;
    
      return 0;
    }
    module_init(pkvm_driver_init);
    
  2. Trong mã EL1 (el1.c), hãy gọi HVC:

    pkvm_el2_mod_call(hvc_number);
    

Gỡ lỗi và phân tích mã EL2

Phần này chứa một số lựa chọn để gỡ lỗi mã EL2 của mô-đun pKVM.

Phát và đọc các sự kiện theo dõi trình giám sát siêu ảo hoá

Tracefs hỗ trợ trình ảo hoá pKVM. Người dùng gốc có quyền truy cập vào giao diện này, nằm trong /sys/kernel/tracing/hypervisor/:

  • tracing_on: Bật hoặc tắt tính năng theo dõi.
  • Việc ghi vào tệp này sẽ đặt lại dấu vết.trace
  • trace_pipe: Việc đọc tệp này sẽ in các sự kiện của trình giám sát siêu ảo.
  • buffer_size_kb: Kích thước của vùng đệm trên mỗi CPU chứa các sự kiện. Tăng giá trị này nếu sự kiện bị mất.

Theo mặc định, các sự kiện sẽ bị tắt. Để bật các sự kiện, hãy sử dụng tệp /sys/kernel/tracing/hypervisor/events/my_event/enable tương ứng trong Tracefs. Bạn cũng có thể bật mọi sự kiện trình ảo hoá tại thời điểm khởi động bằng dòng lệnh của nhân hyp_event=event1,event2.

Trước khi khai báo một sự kiện, mã EL2 của mô-đun phải khai báo đoạn mã sau đây, trong đó pkvm_opsstruct pkvm_module_ops * được truyền đến hàm init của mô-đun:

  #include "events.h"
  #define HYP_EVENT_FILE ../../../../relative/path/to/hyp/events.h
  #include <nvhe/define_events.h>

  #ifdef CONFIG_TRACING
  void *tracing_reserve_entry(unsigned long length)
  {
      return pkvm_ops->tracing_reserve_entry(length);
  }

  void tracing_commit_entry(void)
  {
      pkvm_ops->tracing_commit_entry();
  }
  #endif

Khai báo sự kiện

Khai báo các sự kiện trong tệp .h riêng:

  $ cat hyp/events.h
  #if !defined(__PKVM_DRIVER_HYPEVENTS_H_) || defined(HYP_EVENT_MULTI_READ)
  #define __PKVM_DRIVER_HYPEVENTS_H_

  #ifdef __KVM_NVHE_HYPERVISOR__
  #include <nvhe/trace.h>
  #endif

  HYP_EVENT(pkvm_driver_event,
          HE_PROTO(u64 id),
          HE_STRUCT(
                  he_field(u64, id)
          ),
          HE_ASSIGN(
                  __entry->id = id;
          ),
          HE_PRINTK("id=0x%08llx", __entry->id)
  );
  #endif

Phát ra sự kiện

Bạn có thể ghi lại các sự kiện trong mã EL2 bằng cách gọi hàm C đã tạo:

  trace_pkvm_driver_event(id);

Thêm thông tin đăng ký bổ sung (Android 15 trở xuống)

Đối với Android 15 trở xuống, hãy thêm một quy trình đăng ký khác trong quá trình khởi tạo mô-đun. Bạn không bắt buộc phải làm việc này trong Android 16 trở lên.

  #ifdef CONFIG_TRACING
  extern char __hyp_event_ids_start[];
  extern char __hyp_event_ids_end[];
  #endif

  int pkvm_driver_hyp_init(const struct pkvm_module_ops *ops)
  {
  #ifdef CONFIG_TRACING
      ops->register_hyp_event_ids((unsigned long)__hyp_event_ids_start,
                                        (unsigned long)__hyp_event_ids_end);
  #endif

      /* init module ... */

      return 0;
  }

Phát ra các sự kiện mà không cần khai báo trước (Android 16 trở lên)

Việc khai báo các sự kiện có thể gây phiền toái cho việc gỡ lỗi nhanh. trace_hyp_printk() cho phép người gọi truyền tối đa 4 đối số vào một chuỗi định dạng mà không cần khai báo sự kiện:

  trace_hyp_printk("This is my debug");
  trace_hyp_printk("This is my variable: %d", (int)foo);
  trace_hyp_printk("This is my address: 0x%llx", phys);

Bạn cũng phải có một đoạn mã chuẩn trong mã EL2. trace_hyp_printk() là một macro gọi hàm trace___hyp_printk():

  #include <nvhe/trace.h>

  #ifdef CONFIG_TRACING
  void trace___hyp_printk(u8 fmt_id, u64 a, u64 b, u64 c, u64 d)
  {
          pkvm_ops->tracing_mod_hyp_printk(fmt_id, a, b, c, d);
  }
  #endif

Bật sự kiện __hyp_printk trong /sys/kernel/tracing/hypervisor/events/ hoặc khi khởi động bằng dòng lệnh của nhân hyp_event=__hyp_printk.

Chuyển hướng các sự kiện đến dmesg

Tham số dòng lệnh của hạt nhân hyp_trace_printk=1 làm cho giao diện theo dõi trình giám sát siêu dữ liệu chuyển tiếp từng sự kiện đã ghi nhật ký đến dmesg của hạt nhân. Điều này hữu ích khi đọc các sự kiện khi không truy cập được vào trace_pipe.

Kết xuất các sự kiện trong quá trình lỗi kernel (Android 16 trở lên)

Các sự kiện của trình giám sát siêu ảo được thăm dò. Do đó, có một khoảng thời gian giữa lần thăm dò cuối cùng và sự cố nghiêm trọng của hệ điều hành, trong đó các sự kiện đã được phát ra nhưng chưa được kết xuất vào bảng điều khiển. Lựa chọn cấu hình kernel CONFIG_PKVM_DUMP_TRACE_ON_PANIC cố gắng kết xuất các sự kiện gần đây nhất trong bảng điều khiển nếu hyp_trace_printk đã được bật.

Tuỳ chọn này bị tắt theo mặc định đối với GKI.

Sử dụng Ftrace để theo dõi lệnh gọi hàm và trả về (Android 16 trở lên)

Ftrace là một tính năng của nhân cho phép bạn theo dõi từng lệnh gọi hàm và giá trị trả về. Tương tự, trình giám sát ảo pKVM cung cấp 2 sự kiện funcfunc_ret.

Bạn có thể chọn các hàm được theo dõi bằng dòng lệnh của nhân hyp_ftrace_filter= hoặc bằng một trong các tệp tracefs:

  • /sys/kernel/tracing/hypervisor/set_ftrace_filter
  • /sys/kernel/tracing/hypervisor/set_ftrace_notrace

Bộ lọc sử dụng tính năng so khớp mẫu chung theo kiểu shell.

Bộ lọc sau đây theo dõi các hàm bắt đầu bằng pkvm_hyp_driver:

  echo "__kvm_nvhe_pkvm_hyp_driver*" > /sys/kernel/tracing/hypervisor/set_ftrace_filter

Sự kiện funcfunc_ret chỉ có trong CONFIG_PKVM_FTRACE=y. Tuỳ chọn này bị tắt theo mặc định đối với GKI.