In this advisory we are disclosing a memory corruption vulnerability in the Huawei log device that allows any unprivileged process to trigger a kernel crash and reboot the device.
Huawei kernels are shipped with custom log devices (/dev/hwlog_dubai
, /dev/hwlog_exception
and /dev/hwlog_jank
) that facilitate better system diagnostics through a series of ioctl calls.
One of these diagnostics module is referred to as memcheck, and it provides detailed statistics about the system memory usage, including the allocations made by the SLAB kernel allocator.
The implementation of this SLAB tracer contains a series of race condition vulnerabilities that could lead to kernel memory corruption and kernel deadlocks. As an example of exploiting this memory corruption, any compromised or malicious application can use this to mount a permanent DoS attack, by crashing the kernel upon each reboot until the device is factory reset. Other corruption scenarios may exist as well. Due to an access control configuration error, this interface is exposed to untrusted and isolated application contexts, as a result any unprivileged process can exploit this vulnerability.
Vulnerability Details
The SLAB tracer, implemented by the hwlog devices, can be preinitialised by the LOGGER_MEMCHECK_DETAIL_READ
ioctl.
This would save the name of the most used (largest) SLAB on the system.
The LOGGER_MEMCHECK_COMMAND
ioctl is used to enable or disable the tracing for the selected SLAB, while the LOGGER_MEMCHECK_STACK_READ
ioctl can be used to read the trace results.
The trace logs are stored in dynamically allocated ringbuffers and the pointer to this buffer is stored in a global structure.
Access to this global structure is controlled by a global variable instead of locking.
The guard variable is meant to ensure that tracking can only be enabled once, and the ringbuffers cannot be freed or reallocated while they are being used.
However, due to a race condition vulnerability in the implementation of trace enable (hisi_slub_track_on()
) and trace disable (hisi_slub_track_off()
) functions it is possible to corrupt this global structure, free the trace buffer and zero out its pointer while it is still being used.
This results in kernel NULL pointer dereference panic.
logger_ioctl()
memcheck_ioctl()
process_do_command()
memcheck_do_command()
memcheck_do_kernel_command()
hisi_page_trace_on()
hisi_slub_track_on()
Above is a call chain to one of the vulnerable functions, there is no explicit locking implemented by any of these functions, the trace enable, trace disable and trace read functions can execute in parallel.
The slub_track_flag
is the global variable that is supposed to prevent overlapping or double ringbuffer initialisation and freeing.
While the srb->lock
spinlock mediates access to the ringbuffer itself.
The relevant pseudo code snippets are presented below with detailed explanation following.
int hisi_slub_track_on(char *name)
{
// 1.
if (slub_track_flag)
return 0;
mutex_lock(&slab_mutex);
// Find and enable tracking for the slab
[...]
mutex_unlock(&slab_mutex);
[...]
// 2.
ret = slub_create_ringbuf();
// 3.
slub_track_flag = 1;
}
static int slub_create_ringbuf(void)
{
int ret;
ret = __slub_create_ringbuf(slub_alloc);
[...]
ret = __slub_create_ringbuf(slub_free);
[...]
return 0;
}
static int __slub_create_ringbuf(int type)
{
void *buf = NULL;
size_t size = SLUB_RINGBUF_LEN;
// 4.
struct slub_ring_buf *srb = &srb_array[type];
[...]
buf = vmalloc(size * sizeof(long));
if (!buf)
return -ENOMEM;
srb->in = 0;
srb->out = 0;
// 5.
srb->buf = buf;
srb->size = size;
srb->type = type;
spin_lock_init(&srb->lock);
return 0;
}
int hisi_slub_track_off(char *name)
{
// 6.
if (!slub_track_flag)
return 0;
mutex_lock(&slab_mutex);
[...]
mutex_unlock(&slab_mutex);
[...]
// 7.
slub_track_flag = 0;
slub_fetch_node(slub_alloc);
slub_fetch_node(slub_free);
// 8.
slub_del_ringbuf();
[...]
}
static int slub_del_ringbuf(void)
{
__slub_del_ringbuf(SLUB_ALLOC);
__slub_del_ringbuf(SLUB_FREE);
return 0;
}
static void __slub_del_ringbuf(int type)
{
unsigned long flags;
struct slub_ring_buf *srb = &srb_array[type];
spin_lock_irqsave(&srb->lock, flags);
if (srb->buf) {
// 9.
vfree(srb->buf);
srb->buf = NULL;
}
spin_unlock_irqrestore(&srb->lock, flags);
}
The very first issue is that hisi_slub_stack_read()
completely ignores the slub_track_flag
and can be executed before tracing is enabled.
As a result, it tries to access the uninitialised ringbuffer and acquire an uninitialised spinlock, which triggers a kernel BUG macro.
The real vulnerability is in how the slub_track_flag
used between the hisi_slub_ track_on()
and hisi_slub_track_off()
functions.
The hisi_slub_track_on()
function checks if tracing has been enabled (1), if not it initialises the ringbuffers (2) by calling __slub_create_ringbuf()
.
This function accesses the global structure (4), allocates the ring buffer with vmalloc, and saves its address in the global structure (5).
Once this is done, hisi_slub_track_on()
sets the slub_track_flag
(3) to signal that initialisation is complete.
Note, that there are already problems with this “locking” model, the global flag doesn’t ensure mutually exclusive access to the shared resource.
Multiple parallel trace enable calls could pass the check at (1) before one of them reaches (3), resulting in only the last buffer being saved and memory being leaked.
The hisi_slub_track_off()
works similarly, it reads the slub_track_flag
(6) to check if tracing has been enabled then it proceeds to reset the flag (7) and release the ringbuffers (8).
Consequently, __slub_del_ringbuf()
is called that frees the buffer and nulls the global pointer.
The main vulnerability is that the slub_track_flag
flag is reset (7) before the ringbuffers are released (8).
As soon as (7) is reached a parallel trace enable call can pass the check at (1) and initialise a new ringbuffer (5) while the trace disable call executes the slub_fetch_node()
functions.
In that case the trace disable call would reach (8) and release the freshly allocated ringbuffers (9) while tracing remains enabled, creating a use-after-free scenario for the vmallocated ringbuffers.
Please note that other race windows are present as well that would lead to different memory corruption or deadlock states. The root cause of these is the same, the global shared resource is not protected by a lock and exclusive access is not guaranteed to threads while they modify it.
Access Control
The Linux DAC permission allows any process to open the hwlog devices for writing, which is sufficient for issuing ioctl commands.
Each device has a different Selinux label (see the listing below), however their ioctl interface is identical.
The domain
type attribute, which includes a series of unprivileged context such as isolated and untrusted app, is allowed to open, write and issue ioctls on these devices.
ls -lZ /dev/hwlog_*
crw-rw--w- 1 root system u:object_r:dubai_log_device:s0 /dev/hwlog_dubai
crw-rw--w- 1 root log u:object_r:exception_device:s0 /dev/hwlog_exception
crw-rw--w- 1 root system u:object_r:jank_device:s0 /dev/hwlog_jank
Selinux extended permissions are used to create a whitelist of ioctls on the /dev/hwlog_exception
and /dev/hwlog_jank
devices, that only allow a small set of ioctls to be called by the domain
type attribute.
However, these extended permissions are not applied for the dubai_log_device
context, consequently, through the /dev/hwlog_dubai
device every hwlog ioctl command is exposed to all the unprivileged processes.
Affected Devices (Verified)
-
Kirin 990
- Huawei Mate 30 Pro (LIO)
- Huawei P40 Pro (ELS)
- Huawei P40 (ANA)
-
Kirin 9000/9000E
- Huawei Mate40 Pro (NOH) EMUI
- Huawei MatePad Pro 12 (WGH) HarmonyOS
Fix
Huawei OTA images, released after April 2022, contain the fix for the vulnerability.
Timeline
- 2022.01.21. Bug reported to Huawei PSIRT via Harmony Bug Bounty Program
- 2022.03.23. Huawei confirms vulnerability, assigns CVE and Medium severity
- 2022.04.01. Huawei releases security bulletin