linux/drivers/platform/wmi/marshalling.c
Armin Wolf 204b52fadf
platform/wmi: Prepare to reject undersized unmarshalling results
Driver using the buffer-based WMI API usually reject buffers resulting
from WMI method calls or block queries if they contain not enough data.
Prepare the WMI core for assisting in this by automatically rejecting
undersized unmarshalling results.

Signed-off-by: Armin Wolf <W_Armin@gmx.de>
Link: https://patch.msgid.link/20260406203237.2970-4-W_Armin@gmx.de
Reviewed-by: Ilpo Järvinen <ilpo.jarvinen@linux.intel.com>
Signed-off-by: Ilpo Järvinen <ilpo.jarvinen@linux.intel.com>
2026-04-13 14:11:22 +03:00

252 lines
5.5 KiB
C

// SPDX-License-Identifier: GPL-2.0-or-later
/*
* ACPI-WMI buffer marshalling.
*
* Copyright (C) 2025 Armin Wolf <W_Armin@gmx.de>
*/
#include <linux/acpi.h>
#include <linux/align.h>
#include <linux/math.h>
#include <linux/overflow.h>
#include <linux/slab.h>
#include <linux/unaligned.h>
#include <linux/wmi.h>
#include <kunit/visibility.h>
#include "internal.h"
static int wmi_adjust_buffer_length(size_t *length, const union acpi_object *obj)
{
size_t alignment, size;
switch (obj->type) {
case ACPI_TYPE_INTEGER:
/*
* Integers are threated as 32 bit even if the ACPI DSDT
* declares 64 bit integer width.
*/
alignment = 4;
size = sizeof(u32);
break;
case ACPI_TYPE_STRING:
/*
* Strings begin with a single little-endian 16-bit field containing
* the string length in bytes and are encoded as UTF-16LE with a terminating
* nul character.
*/
if (obj->string.length + 1 > U16_MAX / 2)
return -EOVERFLOW;
alignment = 2;
size = struct_size_t(struct wmi_string, chars, obj->string.length + 1);
break;
case ACPI_TYPE_BUFFER:
/*
* Buffers are copied as-is.
*/
alignment = 1;
size = obj->buffer.length;
break;
default:
return -EPROTO;
}
*length = size_add(ALIGN(*length, alignment), size);
return 0;
}
static int wmi_obj_get_buffer_length(const union acpi_object *obj, size_t *length)
{
size_t total = 0;
int ret;
if (obj->type == ACPI_TYPE_PACKAGE) {
for (int i = 0; i < obj->package.count; i++) {
ret = wmi_adjust_buffer_length(&total, &obj->package.elements[i]);
if (ret < 0)
return ret;
}
} else {
ret = wmi_adjust_buffer_length(&total, obj);
if (ret < 0)
return ret;
}
*length = total;
return 0;
}
static int wmi_obj_transform_simple(const union acpi_object *obj, u8 *buffer, size_t *consumed)
{
struct wmi_string *string;
size_t length;
__le32 value;
u8 *aligned;
switch (obj->type) {
case ACPI_TYPE_INTEGER:
aligned = PTR_ALIGN(buffer, 4);
length = sizeof(value);
value = cpu_to_le32(obj->integer.value);
memcpy(aligned, &value, length);
break;
case ACPI_TYPE_STRING:
aligned = PTR_ALIGN(buffer, 2);
string = (struct wmi_string *)aligned;
length = struct_size(string, chars, obj->string.length + 1);
/* We do not have to worry about unaligned accesses here as the WMI
* string will already be aligned on a two-byte boundary.
*/
string->length = cpu_to_le16((obj->string.length + 1) * 2);
for (int i = 0; i < obj->string.length; i++)
string->chars[i] = cpu_to_le16(obj->string.pointer[i]);
/*
* The Windows WMI-ACPI driver always emits a terminating nul character,
* so we emulate this behavior here as well.
*/
string->chars[obj->string.length] = '\0';
break;
case ACPI_TYPE_BUFFER:
aligned = buffer;
length = obj->buffer.length;
memcpy(aligned, obj->buffer.pointer, length);
break;
default:
return -EPROTO;
}
*consumed = (aligned - buffer) + length;
return 0;
}
static int wmi_obj_transform(const union acpi_object *obj, u8 *buffer)
{
size_t consumed;
int ret;
if (obj->type == ACPI_TYPE_PACKAGE) {
for (int i = 0; i < obj->package.count; i++) {
ret = wmi_obj_transform_simple(&obj->package.elements[i], buffer,
&consumed);
if (ret < 0)
return ret;
buffer += consumed;
}
} else {
ret = wmi_obj_transform_simple(obj, buffer, &consumed);
if (ret < 0)
return ret;
}
return 0;
}
int wmi_unmarshal_acpi_object(const union acpi_object *obj, struct wmi_buffer *buffer,
size_t min_size)
{
size_t length, alloc_length;
u8 *data;
int ret;
ret = wmi_obj_get_buffer_length(obj, &length);
if (ret < 0)
return ret;
if (length < min_size)
return -ENODATA;
if (ARCH_KMALLOC_MINALIGN < 8) {
/*
* kmalloc() guarantees that the alignment of the resulting memory allocation is at
* least the largest power-of-two divisor of the allocation size. The WMI buffer
* data needs to be aligned on a 8 byte boundary to properly support 64-bit WMI
* integers, so we have to round the allocation size to the next multiple of 8.
*/
alloc_length = round_up(length, 8);
} else {
alloc_length = length;
}
data = kzalloc(alloc_length, GFP_KERNEL);
if (!data)
return -ENOMEM;
ret = wmi_obj_transform(obj, data);
if (ret < 0) {
kfree(data);
return ret;
}
buffer->length = length;
buffer->data = data;
return 0;
}
EXPORT_SYMBOL_IF_KUNIT(wmi_unmarshal_acpi_object);
int wmi_marshal_string(const struct wmi_buffer *buffer, struct acpi_buffer *out)
{
const struct wmi_string *string;
u16 length, value;
size_t chars;
char *str;
if (buffer->length < sizeof(*string))
return -ENODATA;
string = buffer->data;
length = get_unaligned_le16(&string->length);
if (buffer->length < sizeof(*string) + length)
return -ENODATA;
/* Each character needs to be 16 bits long */
if (length % 2)
return -EINVAL;
chars = length / 2;
str = kmalloc(chars + 1, GFP_KERNEL);
if (!str)
return -ENOMEM;
for (int i = 0; i < chars; i++) {
value = get_unaligned_le16(&string->chars[i]);
/* ACPI only accepts ASCII strings */
if (value > 0x7F) {
kfree(str);
return -EINVAL;
}
str[i] = value & 0xFF;
/*
* ACPI strings should only contain a single nul character at the end.
* Because of this we must not copy any padding from the WMI string.
*/
if (!value) {
/* ACPICA wants the length of the string without the nul character */
out->length = i;
out->pointer = str;
return 0;
}
}
str[chars] = '\0';
out->length = chars;
out->pointer = str;
return 0;
}
EXPORT_SYMBOL_IF_KUNIT(wmi_marshal_string);