In this advisory we are disclosing a vulnerability in the Huawei log device that allows any unprivileged process to disclose sensitive information from the kernel.
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 zrhung, and it provides information and configuration options to monitor hung processes.
The implementation of the config set ioctl contains a race condition vulnerability that allows an attacker to free the underlying buffer that holds the configuration data. Consecutive config get ioctl calls use the dangling pointers to read and disclose, potentially sensitive, dynamically allocated kernel data. An attacker can use this vulnerability to disclose cryptographic materials, such as kernel stack cookies, or potentially user identifying personal data. 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 zrhung config can be set through the LOGGER_SET_HCFG
ioctl call and can be read through the LOGGER_GET_HCFG
ioctl.
Furthermore, the LOGGER_SET_FEATURE
ioctl can be used to set the value of the global ctx.cfg_feature
variable.
The vulnerability is in the hcfgk_set_cfg()
function, that implements the config set feature.
The call chain to this function and its annotated code is presented below.
logger_ioctl()
zrhung_ioctl()
hcfgk_set_cfg()
int hcfgk_set_cfg(struct file *file, const void __user *arg)
{
[...]
ret = copy_from_user(&len, arg, sizeof(len));
if (ret) {
pr_err("copy hung config table from user failed\n");
return ret;
}
if (len > HCFG_VAL_SIZE_MAX)
return -EINVAL;
// 1.
spin_lock(&lock);
// 2.
if (ctx.cfg_feature != HCFG_FEATURE_VERSION) {
spin_unlock(&lock);
pr_err("cfg_feature is invalid\n");
return -EINVAL;
}
table_len = sizeof(struct hcfg_table_version);
entry_num = ZRHUNG_CFG_ENTRY_NUM;
// 3.
spin_unlock(&lock);
mem_size = PAGE_ALIGN(table_len + len);
user_table = vmalloc(mem_size);
if (!user_table)
return -ENOMEM;
memset(user_table, 0, mem_size);
ret = copy_from_user(user_table, arg, table_len + len);
if (ret) {
pr_err("copy hung config table from user failed\n");
vfree(user_table);
return ret;
}
// 4.
spin_lock(&lock);
// 5.
tmp = ctx.user_table;
ctx.user_table = user_table;
user_table = tmp;
ctx.mem_size = mem_size;
ctx.entry_num = entry_num;
// 6.
if (ctx.cfg_feature != HCFG_FEATURE_VERSION) {
spin_unlock(&lock);
pr_err("cfg_feature is invalid\n");
vfree(user_table);
return -EINVAL;
}
// 7.
t = ctx.user_table;
ctx.table.entry = t->entry;
ctx.table.data = t->data;
/* make sure last byte in data is 0 terminated */
ctx.table.len = len;
ctx.table.data[len - 1] = '\0';
spin_unlock(&lock);
if (user_table != NULL)
vfree(user_table);
return ret;
}
The set config function first acquires the global spinlock, called lock
(1), then checks if the global ctx.cfg_feature
variable is set to the expected value (2).
If not the function aborts.
After this check, the global lock (3) is released, and a new config buffer is allocated with the user provided size.
Then the function proceeds to acquire the global lock again (4) and swaps the old ctx.user_table
pointer with the address of the freshly allocated buffer (5).
Note, that ctx.user_table
is not used directly to access the config data, instead the ctx.table.entry
and ctx.table.data
pointers are maintained for that purpose.
At (6) the feature variable is checked again, and if it is not set to the correct value, the old config buffer is released and the function exits.
If the feature value is correct, the function updates the ctx.table.entry
and ctx.table.data
pointers to point into the new config buffer (7).
These are the pointers that are used to access and read the configuration by the get config function.
The get config function simply returns data to the user space from the ctx.table.data
location.
The root cause of the vulnerability is the additional feature variable check at (6).
If this check fails, the old config buffer is released and the function terminates.
Thus the ctx.table.data
pointer is not updated (7) with the address of the new config buffer.
The global config is not invalidated in any ways.
Future calls to read the config would use the dangling pointers, that now point into the freed vmalloc buffer, to read back the config data.
While the feature variable is checked before (2), the global lock is released between (3) and (4), thus a competing thread can change its value before it is checked again at (6).
The vulnerability can be exploited by a malicious process to disclose arbitrary information from the kernel’s vmalloc address space.
Once the vulnerability is triggered the attacker can spray the victim data (e.g. kernel stacks, see Brandon Azad’s article for details), that would overtake the freed vmalloc buffer’s location.
The ctx.table.data
still points to the original location, where now the victim data resides, and the next get config ioctl would return its content.
If the attacker fails to win the race, the function simply returns at (2) without any side effect. The attacker can attempt the race as many times as needed, they can win it very reliably. The size of the config buffer is also controlled by the attacker in the 4808-312608 bytes range, allowing a large variety of potentially overlapping vmalloc allocations.
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 contexts 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 Critical severity
- 2022.04.01. Huawei releases security bulletin