There is a sensitive information disclosure vulnerability in the vision DSP kernel driver of S20 Exynos devices. The vulnerability can be used by malicious privileged applications to read the kernel’s and other application’s memory.

Vulnerability Details

The Exynos DSP driver implements an ioctl call that allows applications to upload a custom model (graph) for the dsp device. The DSP_IOC_LOAD_GRAPH ioctl handler of the /dev/dsp device receives an array of Unix paths of the graph files to be loaded. This array has a complex structure, it begins with an array of integers that contains the length of each path. The array of lengths is followed by the actual path strings, one after the other, delimited by the length values. This entire structure is provided by the user space and copied into a kmallocated kernel buffer. A dedicated field is used in the main ioctl argument structure to signal the number of graphs to be loaded. The same field also determines the length of the integer array and the initial offset of the file names.

The vulnerability comes from the lack of verification of the length fields and the initial offset. They are not checked against the bounds of the buffer and they can point out of it. In that case, arbitrary kernel data is interpreted as the path to the graph and recorded into the kernel logs. The over-read buffer is on the kernel heap in the kernel’s linear address space. The initial offset is an unsigned integer value that indexes an integer array, as a result 4 * UINT_MAX bytes (~16GB) of linear memory can be addressed and leaked.

As a result kmalloc returns a kernel linear address, and indexing out of this page could reach any type of memory that happens to be in the physical memory after the kmalloc buffer’s page. This might be sensitive kernel dynamic data, including other heap allocations containing cryptographic key materials, pages containing network data, cached file system data or even the contents of kernel stacks and their secret cookies. Again, despite the fact that the + 4 * UINT_MAX offset does not allow a wraparound, meaning that we couldn’t reach the virtual addresses themselves that are returned by vmap allocations or those that correspond to the KASLR-slid base image, we still can reach instead the virtual addresses of the pages in the linear address range that correspond to those same physical pages!

The vulnerability can be reached by the following chain of calls:

  • dsp_ioctl
  • dsp_context_load_graph
  • dsp_graph_load
  • __dsp_graph_add_kernel
  • dsp_kernel_alloc
  • dsp_binary_alloc_load
  • request_firmware
  • _request_firmware

The dsp_context_load_graph function in drivers/vision/dsp/dsp-context.c allocates the kernel buffer called kernel_name that holds the path strings. The length of the array is determined by args->kernel_size. __dsp_context_get_graph_info function is used to copy the data array form the user space buffer. The dsp_graph_load function in drivers/vision/dsp/dsp-graph.c initialises the graph->kernel_count variable from the user provided graph info structure. This is the value that is used as the initial offset and it is never checked against the args->kernel_size value.

The __dsp_graph_add_kernel function sets up the address of the path string based on the provided offsets. Below is a commented digest of the function.

static int __dsp_graph_add_kernel(struct dsp_graph *graph, void *kernel_name)
{
  int ret;
  struct dsp_kernel_manager *kmgr;
  unsigned int kernel_count;
  unsigned int *length;
  unsigned long offset;

  [...]

  // 1. This is a user provided arbitrary unsigned int value
  kernel_count = graph->kernel_count;
  // 2. The beginning of the length/path array, the content is user provided
  length = kernel_name;
  // 3. This can point out of the kernel heap buffer since there is no bound check
  offset = (unsigned long)&length[kernel_count];

  [...]

  for (idx = 0; idx < kernel_count; ++idx) {
    // 4. The name pointer is controlled at this point
    graph->dl_libs[idx].name = (const char *)offset;

    kernel = dsp_kernel_alloc(kmgr, length[idx],
        &graph->dl_libs[idx]);
    if (IS_ERR(kernel)) {
      ret = PTR_ERR(kernel);
      dsp_err("Failed to alloc kernel(%u/%u)\n",
          idx, kernel_count);
      goto p_err_alloc;
    }

    list_add_tail(&kernel->graph_list, &graph->kernel_list);
    // 5. These lengths are also unchecked and could be abused the same way
    offset += length[idx];
  }

  [...]
}

The dsp_kernel_alloc function in drivers/vision/dsp/dsp-kernel.c copies the path from the provided address into a new structure that is used to load the file later. Find the annotated code below:

struct dsp_kernel *dsp_kernel_alloc(struct dsp_kernel_manager *kmgr,
    unsigned int name_length, struct dsp_dl_lib_info *dl_lib)
{

  [...]

  // 1. The length is also controlled,
  //    it only limits the maximum number of bytes that can be disclosed at once
  new->name_length = name_length + 4;
  new->name = kzalloc(new->name_length, GFP_KERNEL);
  if (!new->name) {
    ret = -ENOMEM;
    dsp_err("Failed to allocate kernel_name[%s]\n", dl_lib->name);
    goto p_err_name;
  }
  // 2. The out of bounds data is copied into the name buffer
  snprintf(new->name, new->name_length, "%s.elf", dl_lib->name);
  [...]
}

Finally, the _request_firmware function in drivers/base/firmware_loader/main.c would record the file name, containing the leaked information, into the kernel logs.

static int
_request_firmware(const struct firmware **firmware_p, const char *name,
      struct device *device, void *buf, size_t size,
      enum fw_opt opt_flags)
{
  [...]
      dev_warn(device,
           "Direct firmware load for %s failed with error %d\n",
           name, ret);
  [...]
}

The vulnerability is available through the /dev/dsp device which is accessible for reading and ioctl operations to all users. This character device has the vendor_dsp_device selinux label which is exposed to quite a number of unprivileged selinux contexts, including untrusted applications.

The kernel logs can be accessed multiple ways, all of which requires different contexts and privileges. Together they provide a significant attack surface. The following methods can be used to retrieve kernel logs and the sensitive data leaked into them:

  • Executing the dmesg utility program or the syslog system call
  • Reading the /dev/kmsg character device
  • Reading the /proc/kmsg pseudo file
  • Reading the /proc/last_kmsg pseudo file to obtain the leaks after a reboot

In case of a crash or device malfunction the kernel logs are backed up and saved to logfiles in the /data/log directory under the dumplog_data_file label. These files are accessible to a wide range of selinux context including application contexts.

Affected Devices (Verified)

Samsung S20 Exynos 990, SM-G980F

Fix

Samsung OTA images, released after September 2021, contain the fix for the vulnerability.

Timeline

  • 2021.04.27. Bug reported to Samsung Mobile Security
  • 2021.05.21. Samsung Mobile Security confirmes the vulnerability, SVE-2021-21619 is assigned
  • 2021.09.01. Samsung releases security bulletin, CVE-2021-25457 is assigned, OTA firmware update distribution begins