blob: 63a92c4ebab53c6232b37b66afe0bf476ef7ba03 [file] [edit]
// 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 length, alloc_length;
u8 *data;
int ret;
ret = wmi_obj_get_buffer_length(obj, &length);
if (ret < 0)
return ret;
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);