projects/linux·blob master · refs

251 lines · 5.5 KiB

1// SPDX-License-Identifier: GPL-2.0-or-later
2/*
3 * ACPI-WMI buffer marshalling.
4 *
5 * Copyright (C) 2025 Armin Wolf <[email protected]>
6 */
7
8#include <linux/acpi.h>
9#include <linux/align.h>
10#include <linux/math.h>
11#include <linux/overflow.h>
12#include <linux/slab.h>
13#include <linux/unaligned.h>
14#include <linux/wmi.h>
15
16#include <kunit/visibility.h>
17
18#include "internal.h"
19
20static int wmi_adjust_buffer_length(size_t *length, const union acpi_object *obj)
21{
22 size_t alignment, size;
23
24 switch (obj->type) {
25 case ACPI_TYPE_INTEGER:
26 /*
27 * Integers are threated as 32 bit even if the ACPI DSDT
28 * declares 64 bit integer width.
29 */
30 alignment = 4;
31 size = sizeof(u32);
32 break;
33 case ACPI_TYPE_STRING:
34 /*
35 * Strings begin with a single little-endian 16-bit field containing
36 * the string length in bytes and are encoded as UTF-16LE with a terminating
37 * nul character.
38 */
39 if (obj->string.length + 1 > U16_MAX / 2)
40 return -EOVERFLOW;
41
42 alignment = 2;
43 size = struct_size_t(struct wmi_string, chars, obj->string.length + 1);
44 break;
45 case ACPI_TYPE_BUFFER:
46 /*
47 * Buffers are copied as-is.
48 */
49 alignment = 1;
50 size = obj->buffer.length;
51 break;
52 default:
53 return -EPROTO;
54 }
55
56 *length = size_add(ALIGN(*length, alignment), size);
57
58 return 0;
59}
60
61static int wmi_obj_get_buffer_length(const union acpi_object *obj, size_t *length)
62{
63 size_t total = 0;
64 int ret;
65
66 if (obj->type == ACPI_TYPE_PACKAGE) {
67 for (int i = 0; i < obj->package.count; i++) {
68 ret = wmi_adjust_buffer_length(&total, &obj->package.elements[i]);
69 if (ret < 0)
70 return ret;
71 }
72 } else {
73 ret = wmi_adjust_buffer_length(&total, obj);
74 if (ret < 0)
75 return ret;
76 }
77
78 *length = total;
79
80 return 0;
81}
82
83static int wmi_obj_transform_simple(const union acpi_object *obj, u8 *buffer, size_t *consumed)
84{
85 struct wmi_string *string;
86 size_t length;
87 __le32 value;
88 u8 *aligned;
89
90 switch (obj->type) {
91 case ACPI_TYPE_INTEGER:
92 aligned = PTR_ALIGN(buffer, 4);
93 length = sizeof(value);
94
95 value = cpu_to_le32(obj->integer.value);
96 memcpy(aligned, &value, length);
97 break;
98 case ACPI_TYPE_STRING:
99 aligned = PTR_ALIGN(buffer, 2);
100 string = (struct wmi_string *)aligned;
101 length = struct_size(string, chars, obj->string.length + 1);
102
103 /* We do not have to worry about unaligned accesses here as the WMI
104 * string will already be aligned on a two-byte boundary.
105 */
106 string->length = cpu_to_le16((obj->string.length + 1) * 2);
107 for (int i = 0; i < obj->string.length; i++)
108 string->chars[i] = cpu_to_le16(obj->string.pointer[i]);
109
110 /*
111 * The Windows WMI-ACPI driver always emits a terminating nul character,
112 * so we emulate this behavior here as well.
113 */
114 string->chars[obj->string.length] = '\0';
115 break;
116 case ACPI_TYPE_BUFFER:
117 aligned = buffer;
118 length = obj->buffer.length;
119
120 memcpy(aligned, obj->buffer.pointer, length);
121 break;
122 default:
123 return -EPROTO;
124 }
125
126 *consumed = (aligned - buffer) + length;
127
128 return 0;
129}
130
131static int wmi_obj_transform(const union acpi_object *obj, u8 *buffer)
132{
133 size_t consumed;
134 int ret;
135
136 if (obj->type == ACPI_TYPE_PACKAGE) {
137 for (int i = 0; i < obj->package.count; i++) {
138 ret = wmi_obj_transform_simple(&obj->package.elements[i], buffer,
139 &consumed);
140 if (ret < 0)
141 return ret;
142
143 buffer += consumed;
144 }
145 } else {
146 ret = wmi_obj_transform_simple(obj, buffer, &consumed);
147 if (ret < 0)
148 return ret;
149 }
150
151 return 0;
152}
153
154int wmi_unmarshal_acpi_object(const union acpi_object *obj, struct wmi_buffer *buffer,
155 size_t min_size)
156{
157 size_t length, alloc_length;
158 u8 *data;
159 int ret;
160
161 ret = wmi_obj_get_buffer_length(obj, &length);
162 if (ret < 0)
163 return ret;
164
165 if (length < min_size)
166 return -ENODATA;
167
168 if (ARCH_KMALLOC_MINALIGN < 8) {
169 /*
170 * kmalloc() guarantees that the alignment of the resulting memory allocation is at
171 * least the largest power-of-two divisor of the allocation size. The WMI buffer
172 * data needs to be aligned on a 8 byte boundary to properly support 64-bit WMI
173 * integers, so we have to round the allocation size to the next multiple of 8.
174 */
175 alloc_length = round_up(length, 8);
176 } else {
177 alloc_length = length;
178 }
179
180 data = kzalloc(alloc_length, GFP_KERNEL);
181 if (!data)
182 return -ENOMEM;
183
184 ret = wmi_obj_transform(obj, data);
185 if (ret < 0) {
186 kfree(data);
187 return ret;
188 }
189
190 buffer->length = length;
191 buffer->data = data;
192
193 return 0;
194}
195EXPORT_SYMBOL_IF_KUNIT(wmi_unmarshal_acpi_object);
196
197int wmi_marshal_string(const struct wmi_buffer *buffer, struct acpi_buffer *out)
198{
199 const struct wmi_string *string;
200 u16 length, value;
201 size_t chars;
202 char *str;
203
204 if (buffer->length < sizeof(*string))
205 return -ENODATA;
206
207 string = buffer->data;
208 length = get_unaligned_le16(&string->length);
209 if (buffer->length < sizeof(*string) + length)
210 return -ENODATA;
211
212 /* Each character needs to be 16 bits long */
213 if (length % 2)
214 return -EINVAL;
215
216 chars = length / 2;
217 str = kmalloc(chars + 1, GFP_KERNEL);
218 if (!str)
219 return -ENOMEM;
220
221 for (int i = 0; i < chars; i++) {
222 value = get_unaligned_le16(&string->chars[i]);
223
224 /* ACPI only accepts ASCII strings */
225 if (value > 0x7F) {
226 kfree(str);
227 return -EINVAL;
228 }
229
230 str[i] = value & 0xFF;
231
232 /*
233 * ACPI strings should only contain a single nul character at the end.
234 * Because of this we must not copy any padding from the WMI string.
235 */
236 if (!value) {
237 /* ACPICA wants the length of the string without the nul character */
238 out->length = i;
239 out->pointer = str;
240 return 0;
241 }
242 }
243
244 str[chars] = '\0';
245
246 out->length = chars;
247 out->pointer = str;
248
249 return 0;
250}
251EXPORT_SYMBOL_IF_KUNIT(wmi_marshal_string);