Summary

The NPU device’s kernel driver implements a set of ioctl handlers one of which uses unsanitized user data as an offset to retrieve a kernel structure. Fields of the structure are written with user provided values. A malicious actor can use this vulnerability to overwrite kernel memory with controlled data.

The mmap handler is exposed through the /dev/davinci0 character device. Due to the applied selinux policy, access to this device is restricted to the hiaiserver system process. Because of these limitations a practical attack would need to target the hiaiserver first.

The /dev/davinci0 device exposes a series of ioctl handlers, one of which is devdrv_ioctl_load_stream_buff. This handler is defined in the drivers/hisi/npu/device/core/npu_ioctl_services.c source file. This function reads input from the user, retrieves a devdrv_stream_info structure based on a user provided 32 bit unsigned stream id. The highest legitimate stream id on the investigated device should be 124, however this is never verified and the user supplied arbitrary value is used as an index to calculate the offset of the structure within a kernel buffer. Once the structure is obtained it is written with data coming from user space, creating a strong kernel write primitive.

int devdrv_ioctl_load_stream_buff(struct devdrv_proc_ctx *proc_ctx, struct devdrv_dev_ctx *dev_ctx, u64 arg)
{
  devdrv_stream_buff_info_t stream_buff_info = {0};
  unsigned long stream_buf_addr;
  u16 ssid;
  int order, sqe_num, ret;

  struct devdrv_stream_info *stream_info = NULL;
  struct devdrv_hwts_sq_info *hwts_sq_info = NULL;
  struct devdrv_entity_info *hwts_sq_sub_info = NULL;

  NPU_DRV_DEBUG("devdrv_ioctl_load_stream_buff enter\n");
  
  // 1. stream_buff_info is copied from user space
  ret = copy_from_user_safe(&stream_buff_info, (void __user *)(uintptr_t)arg, sizeof(stream_buff_info));
  if (ret != 0) {
    NPU_DRV_ERR("fail to copy hwts sqcq address, ret = %d\n", ret);
    return -EINVAL;
  }

  //...

  // 2. stream_info is retrieved based on the provided stream_id
  stream_info = devdrv_calc_stream_info(proc_ctx->devid, stream_buff_info.stream_id);
  // 3. stream info is written with the provided priority value
  stream_info->priority = stream_buff_info.priority;

  //...

Unlike other similar functions devdrv_calc_stream_info lacks bounds checking for the provided index value.

struct devdrv_stream_info *devdrv_calc_stream_info(u8 devid, u32 index)
{
  struct devdrv_stream_info *stream_info = NULL;

  u64 addr = g_shm_desc[devid][DEVDRV_INFO_MEM].virt_addr;
  stream_info = (struct devdrv_stream_info *)(uintptr_t) (addr +
    DEVDRV_SQ_INFO_OCCUPY_SIZE + DEVDRV_CQ_INFO_OCCUPY_SIZE +
    (long)(unsigned)sizeof(struct devdrv_stream_info) * (index));
  return stream_info;
}

The g_shm_desc[devid][DEVDRV_INFO_MEM].virt_addr memory is allocated with __get_free_pages which uses the gfp allocator to return pages. This is the same allocator that is used internally by kmalloc and it returns pages in the same address range. Thus, this vulnerability can be abused by a malicious actor to overwrite memory with controlled data on the kernel heap. The offset can be an arbitrary 32 bit unsigned integer which is more than enough to index the entire kernel heap region.

Affected Devices (Verified)

  • Kirin 990
    • Huawei Mate 30 Pro (LIO)
    • Huawei P40 Pro (ELS)
    • Huawei P40 (ANA)

Fix

Huawei OTA images, released after February 2021, contain the fix for the vulnerability.

Timeline

  • 2020.10.30. Bug reported to Huawei PSIRT
  • 2020.11.25. Huawei PSIRT confirms vulnerability, confirms fix plans
  • 2021.01.31. OTA distribution of the fix, mitigating the vulnerability, starts
  • 2021.06.09. Huawei assigns CVEs
  • 2021.06.30. Huawei releases security bulletin