There is a vmalloc out of bounds write vulnerability in the vision DSP kernel driver of Samsung Exynos S20 devices. The vulnerability could potentially be used by a malicious system application to compromise the kernel and gain further privileges.

Vulnerability Details

The Exynos DSP driver implements two distinct ioctl calls that are used to load images and graphs and boot the device. The DSP_IOC_BOOT ioctl loads the dsp’s firmware images, common libraries, an xml global kernel descriptor file and a linker file for linking libraries. The DSP_IOC_LOAD_GRAPH ioctl is responsible for creating a shared memory region between the dsp device and user space and for loading the custom graph models implemented in elf libraries. When these libraries are loaded the linker resolves the relocations based on the relocation headers in the elf file and the loaded linker file. Due to the missing bound checks in the relocation code it is possible to write controlled data at a controlled offset beyond the buffer in which the elf file’s content is stored.

When the DSP_IOC_BOOT ioctl is called on /dev/dsp, among many other images, it loads the relocation rules from dsp_reloc_rules.bin. The rules are processed in dsp_reloc_rule_list_import, where a list of rules is created, each containing a set of expressions and positions. The expressions are used at the time of linking to calculate the relocation value, while the position is used at the same time to select which bits need to be changed.

After the DSP_IOC_LOAD_GRAPH ioctl handler loads the graph binary into the kernel memory and sets up the program and data device memory, it carries out the linking process. The dsp_linker_link_libs function is the entry point for the linker, it goes over each library and processes each relocation header for them in __linker_reloc_list. The vulnerabilities lie in the functions that parse the relocation headers and the associated relocation rules. Due to the lack of input validation there are a series of out of bounds indexing issues all of which could cause a kernel crash.

The __rela_relocation function uses the unsanitized r_info field, coming from the processed elf file, as an array index:

static int __rela_relocation(struct dsp_lib *lib, struct dsp_elf32_rela *rela,

  [...]
  // Retrieves the r_info field, which could be 0-0x00ffffff
  symidx = dsp_elf32_rela_get_sym_idx(rela);
  // The value is used as an index
  sym = &elf->symtab[symidx];
  // The retrieved pointer is dereferenced
  sym_str = elf->strtab + sym->st_name;

The __process_rule function has a similar out of bounds array access:

static int __process_rule(struct dsp_lib *lib, struct dsp_reloc_rule *rule,

  [...]
  // The sh_info field is never verified and it comes from the elf file
  struct dsp_elf32_shdr *data_shdr = &elf->shdr[rela_shdr->sh_info];
  char *reloc_data = elf->data + data_shdr->sh_offset + rela->r_offset;

However, the most interesting vulnerability is in the actual relocation function. The __relocate function patches a selected set of bits with a certain value. The position and the number of the bits is calculated based on the relocation header and the associated relocation rule. Due to missing validation, the final offset, where the relocation value is written, can point out of the elf file’s buffer. A maliciously crafted rule file and elf library can be used to achieve a write primitive from the vmallocated file buffer at a controlled offset with a controlled value.

Before introducing the details of the vulnerability, the complete callchain is presented with a brief description of each involved function.

  • dsp_context_load_graph
  • dsp_graph_load
  • __dsp_graph_add_kernel
  • dsp_kernel_load
  • dsp_dl_load_libraries
  • dsp_linker_link_libs
    • This is the entry point for the linker, iterates over each library being loaded
  • __linker_relocate
    • Calls the relocation function for each section within the elf library
  • __linker_reloc_list
    • Iterates over each relocation header for the given section
  • __rela_relocation
    • Retrieves the relocation rule for the header, calculates the relocation value and calls the __process_rule function
  • __process_rule
    • Interprets the bit position information from the rule and retrieves the offset from the header
  • __relocate
    • Carries out the actual write based on the supplied position and value

The most relevant functions are the last two in the call chain. While the __relocate function does a lot of calculations to determine which bits need to be changed, in essence it writes the relocation value at the provided address with the provided offset.

static void __relocate(struct dsp_reloc_info *r_info, char *data)
  [...]
  value |= (r_info->value & mask) >> r_info->low;
    mask = ((1 << t_bits) - 1) << r_info->sh;
    data[r_info->idx] &= ~mask;
    data[r_info->idx] |= value;

The data pointer and the r_info structure are set up in the __process_rule function that calls relocate. This function first calculates the reloc_data pointer based on the r_offset field from the relocation header. The pointer is correctly verified to point within the elf file. Then it proceeds to set up the r_info structure, where the idx field is calculated based on the selected relocation rule. There are no explicit checks on the idx field, the only constraints are coming from the way it is calculated. It can take any value in the range of 0 - 0x1fffffff which is more than enough to address the entire vmap region of the kernel.

static int __cvt_to_item_idx(unsigned int *sh, int num, int item_cnt,
  int item_align)
{
  int idx;
  num += item_align;
  idx = item_cnt - num / 8 - 1;
  *sh = num % 8;
  return idx;
}

static int __process_rule(struct dsp_lib *lib, struct dsp_reloc_rule *rule,
  struct dsp_elf32_shdr *rela_shdr, unsigned int value,
  struct dsp_elf32_rela *rela)
{
  struct dsp_link_info *l_info = lib->link_info;
  struct dsp_elf32 *elf = l_info->elf;

  struct dsp_elf32_shdr *data_shdr = &elf->shdr[rela_shdr->sh_info];
  // 1. The base pointer will always point within the elf image
  char *reloc_data = elf->data + data_shdr->sh_offset + rela->r_offset;

  int item_bit = rule->cont.type.bit_sz * rule->cont.inst_num;

  int item_cnt = item_bit / 8 + ((item_bit % 8) ? 1 : 0);
  int item_align = item_cnt * 8 - item_bit;

  if ((reloc_data > (elf->data + elf->size)) ||
      (reloc_data < elf->data)) {
    DL_ERROR("reloc_data is out of range(%#lx/%#zx)\n",
        (unsigned long)(reloc_data - elf->data),
        elf->size);
    return -1;
  }

  [...]
  dsp_list_for_each(pos_node, &rule->pos_list) {
  [...]
    r_info.value = value;
    // 2. bit_pos, item_cnt, item_align are all coming from the controlled relocation rule
    r_info.idx = __cvt_to_item_idx(&r_info.sh,
            pos->bit_pos, item_cnt, item_align);
    r_info.low = 0;
    r_info.high = rule->type.bit_sz - 1;
    r_info.h_ext = BIT_NONE;
    r_info.l_ext = BIT_NONE;
    __relocate(&r_info, reloc_data);

The value that is written is retrieved in the __rela_relocation function by calling __calc_link_value. This is a stack based virtual machine, that executes the expressions associated with the selected relocation rule. It is trivial to return an arbitrary 4 byte value as there is a specific expression for that.

The elf library’s buffer is allocated in dsp_binary_alloc_load after the content is retrieved with request_firmware_direct. The buffer is allocated with the vmalloc allocator, thus the vulnerability provides a write primitive into the kernel’s vmalloc region with controlled offset and value. The vmalloc address space is where, between many other things, kernel stacks reside. This vulnerability can be abused to overwrite the stack of a kernel process and gain arbitrary code execution within the kernel. Brandon Azad’s article details how kernel stacks can be sprayed deterministically and how a vmap overflow can be exploited until arbitrary code execution. aSiagaming et al. demonstrates how such exploit can be further improved to circumvent modern protection features on Samsung devices.

Access Control

The /dev/dsp character device has a very relaxed permission set, it can be opened and read by anyone due to the DAC permissions. The vendor_dsp_device selinux context allows various application contexts, including untrusted applications to open and issue ioctl to the dsp device. To exploit this vulnerability the content of the loaded elf file and the linker rule file (dsp_reloc_rules.bin) needs to be controlled. While we demonstrated a path traversal vulnerability previously it is not sufficient to reach this point of the parsing, as it provides no control over the rule file.

The request firmware API searches a predefined list of directories and one that is dynamically set at runtime. While previous versions of the device driver used the request_firmware function, it was recently modified to use the request_firmware_direct version without the user space side-loading fallback. Even with the direct version the dynamic path can be changed through the /sys/module/firmware_class/parameters/path file. This can be used to redirect the firmware request API’s search path to an attacker controlled directory. This sysfs file can only be written by system processes due to its DAC permission and it has the sysfs_firmware_class selinux label. Currently only the system_app, ueventd and vendor_init contexts can write this file.

While it is only available from privileged applications there is a distinct security boundary between the kernel and these apps that can be bridged by this vulnerability.

Affected Devices (Verified)

Samsung S20 Exynos 990, SM-G980F

Fix

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

Timeline

  • 2021.05.25. Bug reported to Samsung Mobile Security, SVE-2021-21957 is assigned
  • 2021.10.01. Samsung releases security bulletin, CVE-2021-25475 is assigned, OTA firmware update distribution begins