/src/fwupd/libfwupdplugin/fu-hid-descriptor.c
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | * Copyright 2023 Richard Hughes <richard@hughsie.com> |
3 | | * |
4 | | * SPDX-License-Identifier: LGPL-2.1-or-later |
5 | | */ |
6 | | |
7 | 117k | #define G_LOG_DOMAIN "FuHidDevice" |
8 | | |
9 | | #include "config.h" |
10 | | |
11 | | #include "fu-byte-array.h" |
12 | | #include "fu-hid-descriptor.h" |
13 | | #include "fu-hid-report-item.h" |
14 | | #include "fu-hid-struct.h" |
15 | | #include "fu-input-stream.h" |
16 | | |
17 | | /** |
18 | | * FuHidDescriptor: |
19 | | * |
20 | | * A HID descriptor. |
21 | | * |
22 | | * Each report is a image of this firmware object and each report has children of #FuHidReportItem. |
23 | | * |
24 | | * Documented: https://www.usb.org/sites/default/files/hid1_11.pdf |
25 | | * |
26 | | * See also: [class@FuFirmware] |
27 | | */ |
28 | | |
29 | | G_DEFINE_TYPE(FuHidDescriptor, fu_hid_descriptor, FU_TYPE_FIRMWARE) |
30 | | |
31 | 111k | #define FU_HID_DESCRIPTOR_TABLE_LOCAL_SIZE_MAX 1024 |
32 | 28.6k | #define FU_HID_DESCRIPTOR_TABLE_LOCAL_DUPES_MAX 16 |
33 | 111k | #define FU_HID_DESCRIPTOR_TABLE_GLOBAL_SIZE_MAX 1024 |
34 | 36.4k | #define FU_HID_DESCRIPTOR_TABLE_GLOBAL_DUPES_MAX 64 |
35 | | |
36 | | static guint |
37 | | fu_hid_descriptor_count_table_dupes(GPtrArray *table, FuHidReportItem *item) |
38 | 65.1k | { |
39 | 65.1k | guint cnt = 0; |
40 | 15.5M | for (guint i = 0; i < table->len; i++) { |
41 | 15.4M | FuHidReportItem *item_tmp = g_ptr_array_index(table, i); |
42 | 15.4M | if (fu_hid_report_item_get_kind(item) == fu_hid_report_item_get_kind(item_tmp) && |
43 | 15.4M | fu_hid_report_item_get_value(item) == fu_hid_report_item_get_value(item_tmp) && |
44 | 15.4M | fu_firmware_get_idx(FU_FIRMWARE(item)) == |
45 | 8.56M | fu_firmware_get_idx(FU_FIRMWARE(item_tmp))) |
46 | 825k | cnt++; |
47 | 15.4M | } |
48 | 65.1k | return cnt; |
49 | 65.1k | } |
50 | | |
51 | | static gboolean |
52 | | fu_hid_descriptor_parse(FuFirmware *firmware, |
53 | | GInputStream *stream, |
54 | | FuFirmwareParseFlags flags, |
55 | | GError **error) |
56 | 1.19k | { |
57 | 1.19k | gsize offset = 0; |
58 | 1.19k | gsize streamsz = 0; |
59 | 1.19k | g_autoptr(GPtrArray) table_state = |
60 | 1.19k | g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); |
61 | 1.19k | g_autoptr(GPtrArray) table_local = |
62 | 1.19k | g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); |
63 | 1.19k | if (!fu_input_stream_size(stream, &streamsz, error)) |
64 | 0 | return FALSE; |
65 | 112k | while (offset < streamsz) { |
66 | 111k | g_autofree gchar *itemstr = NULL; |
67 | 111k | g_autoptr(FuHidReportItem) item = fu_hid_report_item_new(); |
68 | | |
69 | | /* sanity check */ |
70 | 111k | if (table_state->len > FU_HID_DESCRIPTOR_TABLE_GLOBAL_SIZE_MAX) { |
71 | 3 | g_set_error(error, |
72 | 3 | FWUPD_ERROR, |
73 | 3 | FWUPD_ERROR_INVALID_DATA, |
74 | 3 | "HID table state too large, limit is %u", |
75 | 3 | (guint)FU_HID_DESCRIPTOR_TABLE_GLOBAL_SIZE_MAX); |
76 | 3 | return FALSE; |
77 | 3 | } |
78 | 111k | if (table_local->len > FU_HID_DESCRIPTOR_TABLE_LOCAL_SIZE_MAX) { |
79 | 1 | g_set_error(error, |
80 | 1 | FWUPD_ERROR, |
81 | 1 | FWUPD_ERROR_INVALID_DATA, |
82 | 1 | "HID table state too large, limit is %u", |
83 | 1 | (guint)FU_HID_DESCRIPTOR_TABLE_LOCAL_SIZE_MAX); |
84 | 1 | return FALSE; |
85 | 1 | } |
86 | | |
87 | 111k | if (!fu_firmware_parse_stream(FU_FIRMWARE(item), stream, offset, flags, error)) |
88 | 106 | return FALSE; |
89 | 111k | offset += fu_firmware_get_size(FU_FIRMWARE(item)); |
90 | | |
91 | | /* only for debugging */ |
92 | 111k | itemstr = fu_firmware_to_string(FU_FIRMWARE(item)); |
93 | 111k | g_debug("add to table-state:\n%s", itemstr); |
94 | | |
95 | | /* if there is a sane number of duplicate tokens then add to table */ |
96 | 111k | if (fu_hid_report_item_get_kind(item) == FU_HID_ITEM_KIND_GLOBAL) { |
97 | 36.4k | if (fu_hid_descriptor_count_table_dupes(table_state, item) > |
98 | 36.4k | FU_HID_DESCRIPTOR_TABLE_GLOBAL_DUPES_MAX) { |
99 | 13 | g_set_error( |
100 | 13 | error, |
101 | 13 | FWUPD_ERROR, |
102 | 13 | FWUPD_ERROR_INVALID_DATA, |
103 | 13 | "table invalid @0x%x, too many duplicate global %s tokens", |
104 | 13 | (guint)offset, |
105 | 13 | fu_firmware_get_id(FU_FIRMWARE(item))); |
106 | 13 | return FALSE; |
107 | 13 | } |
108 | 36.4k | g_ptr_array_add(table_state, g_object_ref(item)); |
109 | 74.6k | } else if (fu_hid_report_item_get_kind(item) == FU_HID_ITEM_KIND_LOCAL || |
110 | 74.6k | fu_hid_report_item_get_kind(item) == FU_HID_ITEM_KIND_MAIN) { |
111 | 28.6k | if (fu_hid_descriptor_count_table_dupes(table_local, item) > |
112 | 28.6k | FU_HID_DESCRIPTOR_TABLE_LOCAL_DUPES_MAX) { |
113 | 10 | g_set_error( |
114 | 10 | error, |
115 | 10 | FWUPD_ERROR, |
116 | 10 | FWUPD_ERROR_INVALID_DATA, |
117 | 10 | "table invalid @0x%x, too many duplicate %s %s:0x%x tokens", |
118 | 10 | (guint)offset, |
119 | 10 | fu_hid_item_kind_to_string(fu_hid_report_item_get_kind(item)), |
120 | 10 | fu_firmware_get_id(FU_FIRMWARE(item)), |
121 | 10 | fu_hid_report_item_get_value(item)); |
122 | 10 | return FALSE; |
123 | 10 | } |
124 | 28.6k | g_ptr_array_add(table_local, g_object_ref(item)); |
125 | 28.6k | } |
126 | | |
127 | | /* add report */ |
128 | 111k | if (fu_hid_report_item_get_kind(item) == FU_HID_ITEM_KIND_MAIN) { |
129 | 3.82k | g_autoptr(FuHidReport) report = fu_hid_report_new(); |
130 | | |
131 | | /* copy the table state to the new report */ |
132 | 150k | for (guint i = 0; i < table_state->len; i++) { |
133 | 146k | FuHidReportItem *item_tmp = g_ptr_array_index(table_state, i); |
134 | 146k | if (!fu_firmware_add_image_full(FU_FIRMWARE(report), |
135 | 146k | FU_FIRMWARE(item_tmp), |
136 | 146k | error)) |
137 | 0 | return FALSE; |
138 | 146k | } |
139 | 25.2k | for (guint i = 0; i < table_local->len; i++) { |
140 | 21.4k | FuHidReportItem *item_tmp = g_ptr_array_index(table_local, i); |
141 | 21.4k | if (!fu_firmware_add_image_full(FU_FIRMWARE(report), |
142 | 21.4k | FU_FIRMWARE(item_tmp), |
143 | 21.4k | error)) |
144 | 0 | return FALSE; |
145 | 21.4k | } |
146 | 3.82k | if (!fu_firmware_add_image_full(firmware, FU_FIRMWARE(report), error)) |
147 | 102 | return FALSE; |
148 | | |
149 | | /* remove all the local items */ |
150 | 3.72k | g_ptr_array_set_size(table_local, 0); |
151 | 3.72k | } |
152 | 111k | } |
153 | | |
154 | | /* success */ |
155 | 958 | return TRUE; |
156 | 1.19k | } |
157 | | |
158 | | static gboolean |
159 | | fu_hid_descriptor_write_report_item(FuFirmware *report_item, |
160 | | GByteArray *buf, |
161 | | GHashTable *globals, |
162 | | GError **error) |
163 | 12.9k | { |
164 | 12.9k | g_autoptr(GBytes) fw = NULL; |
165 | | |
166 | | /* dedupe any globals */ |
167 | 12.9k | if (fu_hid_report_item_get_kind(FU_HID_REPORT_ITEM(report_item)) == |
168 | 12.9k | FU_HID_ITEM_KIND_GLOBAL) { |
169 | 8.86k | guint8 tag = fu_firmware_get_idx(report_item); |
170 | 8.86k | FuFirmware *report_item_tmp = g_hash_table_lookup(globals, GUINT_TO_POINTER(tag)); |
171 | 8.86k | if (report_item_tmp != NULL && |
172 | 8.86k | fu_hid_report_item_get_value(FU_HID_REPORT_ITEM(report_item)) == |
173 | 6.98k | fu_hid_report_item_get_value(FU_HID_REPORT_ITEM(report_item_tmp))) { |
174 | 6.16k | g_debug("skipping duplicate global tag 0x%x", tag); |
175 | 6.16k | return TRUE; |
176 | 6.16k | } |
177 | 2.69k | g_hash_table_insert(globals, GUINT_TO_POINTER(tag), report_item); |
178 | 2.69k | } |
179 | 6.79k | fw = fu_firmware_write(report_item, error); |
180 | 6.79k | if (fw == NULL) |
181 | 0 | return FALSE; |
182 | 6.79k | fu_byte_array_append_bytes(buf, fw); |
183 | | |
184 | | /* success */ |
185 | 6.79k | return TRUE; |
186 | 6.79k | } |
187 | | |
188 | | static gboolean |
189 | | fu_hid_descriptor_write_report(FuFirmware *report, |
190 | | GByteArray *buf, |
191 | | GHashTable *globals, |
192 | | GError **error) |
193 | 2.42k | { |
194 | 2.42k | g_autoptr(GPtrArray) report_items = fu_firmware_get_images(report); |
195 | | |
196 | | /* for each item */ |
197 | 15.3k | for (guint i = 0; i < report_items->len; i++) { |
198 | 12.9k | FuFirmware *report_item = g_ptr_array_index(report_items, i); |
199 | 12.9k | if (!fu_hid_descriptor_write_report_item(report_item, buf, globals, error)) |
200 | 0 | return FALSE; |
201 | 12.9k | } |
202 | | |
203 | | /* success */ |
204 | 2.42k | return TRUE; |
205 | 2.42k | } |
206 | | |
207 | | static GByteArray * |
208 | | fu_hid_descriptor_write(FuFirmware *firmware, GError **error) |
209 | 958 | { |
210 | 958 | g_autoptr(GByteArray) buf = g_byte_array_new(); |
211 | 958 | g_autoptr(GHashTable) globals = g_hash_table_new(g_direct_hash, g_direct_equal); |
212 | 958 | g_autoptr(GPtrArray) reports = fu_firmware_get_images(firmware); |
213 | | |
214 | | /* for each report */ |
215 | 3.37k | for (guint i = 0; i < reports->len; i++) { |
216 | 2.42k | FuFirmware *report = g_ptr_array_index(reports, i); |
217 | 2.42k | if (!fu_hid_descriptor_write_report(report, buf, globals, error)) |
218 | 0 | return NULL; |
219 | 2.42k | } |
220 | | |
221 | | /* success */ |
222 | 958 | return g_steal_pointer(&buf); |
223 | 958 | } |
224 | | |
225 | | typedef struct { |
226 | | const gchar *id; |
227 | | guint32 value; |
228 | | } FuHidDescriptorCondition; |
229 | | |
230 | | /** |
231 | | * fu_hid_descriptor_find_report: |
232 | | * @self: a #FuHidDescriptor |
233 | | * @error: (nullable): optional return location for an error |
234 | | * @...: pairs of string-integer values, ending with %NULL |
235 | | * |
236 | | * Finds the first HID report that matches all the report attributes. |
237 | | * |
238 | | * Returns: (transfer full): A #FuHidReport, or %NULL if not found. |
239 | | * |
240 | | * Since: 1.9.4 |
241 | | **/ |
242 | | FuHidReport * |
243 | | fu_hid_descriptor_find_report(FuHidDescriptor *self, GError **error, ...) |
244 | 0 | { |
245 | 0 | va_list args; |
246 | 0 | g_autoptr(GPtrArray) conditions = g_ptr_array_new_with_free_func(g_free); |
247 | 0 | g_autoptr(GPtrArray) reports = fu_firmware_get_images(FU_FIRMWARE(self)); |
248 | |
|
249 | 0 | g_return_val_if_fail(FU_IS_HID_DESCRIPTOR(self), NULL); |
250 | 0 | g_return_val_if_fail(error == NULL || *error == NULL, NULL); |
251 | | |
252 | | /* parse varargs */ |
253 | 0 | va_start(args, error); |
254 | 0 | for (guint i = 0; i < 1000; i++) { |
255 | 0 | g_autofree FuHidDescriptorCondition *cond = g_new0(FuHidDescriptorCondition, 1); |
256 | 0 | cond->id = va_arg(args, const gchar *); |
257 | 0 | if (cond->id == NULL) |
258 | 0 | break; |
259 | 0 | cond->value = va_arg(args, guint32); |
260 | 0 | g_ptr_array_add(conditions, g_steal_pointer(&cond)); |
261 | 0 | } |
262 | 0 | va_end(args); |
263 | | |
264 | | /* return the first report that matches *all* conditions */ |
265 | 0 | for (guint i = 0; i < reports->len; i++) { |
266 | 0 | FuHidReport *report = g_ptr_array_index(reports, i); |
267 | 0 | gboolean matched = TRUE; |
268 | 0 | for (guint j = 0; j < conditions->len; j++) { |
269 | 0 | FuHidDescriptorCondition *cond = g_ptr_array_index(conditions, j); |
270 | 0 | g_autoptr(FuFirmware) item = |
271 | 0 | fu_firmware_get_image_by_id(FU_FIRMWARE(report), cond->id, NULL); |
272 | 0 | if (item == NULL) { |
273 | 0 | matched = FALSE; |
274 | 0 | break; |
275 | 0 | } |
276 | 0 | if (fu_hid_report_item_get_value(FU_HID_REPORT_ITEM(item)) != cond->value) { |
277 | 0 | matched = FALSE; |
278 | 0 | break; |
279 | 0 | } |
280 | 0 | } |
281 | 0 | if (matched) |
282 | 0 | return g_object_ref(report); |
283 | 0 | } |
284 | 0 | g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no report found"); |
285 | 0 | return NULL; |
286 | 0 | } |
287 | | |
288 | | static void |
289 | | fu_hid_descriptor_init(FuHidDescriptor *self) |
290 | 1.41k | { |
291 | 1.41k | fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_NO_AUTO_DETECTION); |
292 | 1.41k | fu_firmware_set_size_max(FU_FIRMWARE(self), 64 * 1024); |
293 | 1.41k | fu_firmware_set_images_max(FU_FIRMWARE(self), |
294 | 1.41k | g_getenv("FWUPD_FUZZER_RUNNING") != NULL ? 10 : 1024); |
295 | 1.41k | g_type_ensure(FU_TYPE_HID_REPORT); |
296 | 1.41k | g_type_ensure(FU_TYPE_HID_REPORT_ITEM); |
297 | 1.41k | } |
298 | | |
299 | | static void |
300 | | fu_hid_descriptor_class_init(FuHidDescriptorClass *klass) |
301 | 1 | { |
302 | 1 | FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); |
303 | 1 | firmware_class->parse = fu_hid_descriptor_parse; |
304 | 1 | firmware_class->write = fu_hid_descriptor_write; |
305 | 1 | } |
306 | | |
307 | | /** |
308 | | * fu_hid_descriptor_new: |
309 | | * |
310 | | * Creates a new #FuFirmware to parse a HID descriptor |
311 | | * |
312 | | * Since: 1.9.4 |
313 | | **/ |
314 | | FuFirmware * |
315 | | fu_hid_descriptor_new(void) |
316 | 0 | { |
317 | 0 | return FU_FIRMWARE(g_object_new(FU_TYPE_HID_DESCRIPTOR, NULL)); |
318 | 0 | } |