Coverage Report

Created: 2026-01-09 07:21

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/fwupd/libfwupdplugin/fu-hid-descriptor.c
Line
Count
Source
1
/*
2
 * Copyright 2023 Richard Hughes <richard@hughsie.com>
3
 *
4
 * SPDX-License-Identifier: LGPL-2.1-or-later
5
 */
6
7
225k
#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
1.48k
G_DEFINE_TYPE(FuHidDescriptor, fu_hid_descriptor, FU_TYPE_FIRMWARE)
30
1.48k
31
218k
#define FU_HID_DESCRIPTOR_TABLE_LOCAL_SIZE_MAX   1024
32
31.9k
#define FU_HID_DESCRIPTOR_TABLE_LOCAL_DUPES_MAX  16
33
218k
#define FU_HID_DESCRIPTOR_TABLE_GLOBAL_SIZE_MAX  1024
34
40.3k
#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
72.3k
{
39
72.3k
  guint cnt = 0;
40
20.3M
  for (guint i = 0; i < table->len; i++) {
41
20.2M
    FuHidReportItem *item_tmp = g_ptr_array_index(table, i);
42
20.2M
    if (fu_hid_report_item_get_kind(item) == fu_hid_report_item_get_kind(item_tmp) &&
43
20.2M
        fu_hid_report_item_get_value(item) == fu_hid_report_item_get_value(item_tmp) &&
44
10.5M
        fu_firmware_get_idx(FU_FIRMWARE(item)) ==
45
10.5M
      fu_firmware_get_idx(FU_FIRMWARE(item_tmp)))
46
951k
      cnt++;
47
20.2M
  }
48
72.3k
  return cnt;
49
72.3k
}
50
51
static gboolean
52
fu_hid_descriptor_parse(FuFirmware *firmware,
53
      GInputStream *stream,
54
      FuFirmwareParseFlags flags,
55
      GError **error)
56
1.24k
{
57
1.24k
  gsize offset = 0;
58
1.24k
  gsize streamsz = 0;
59
1.24k
  g_autoptr(GPtrArray) table_state =
60
1.24k
      g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref);
61
1.24k
  g_autoptr(GPtrArray) table_local =
62
1.24k
      g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref);
63
1.24k
  if (!fu_input_stream_size(stream, &streamsz, error))
64
0
    return FALSE;
65
219k
  while (offset < streamsz) {
66
218k
    g_autofree gchar *itemstr = NULL;
67
218k
    g_autoptr(FuHidReportItem) item = fu_hid_report_item_new();
68
69
    /* sanity check */
70
218k
    if (table_state->len > FU_HID_DESCRIPTOR_TABLE_GLOBAL_SIZE_MAX) {
71
4
      g_set_error(error,
72
4
            FWUPD_ERROR,
73
4
            FWUPD_ERROR_INVALID_DATA,
74
4
            "HID table state too large, limit is %u",
75
4
            (guint)FU_HID_DESCRIPTOR_TABLE_GLOBAL_SIZE_MAX);
76
4
      return FALSE;
77
4
    }
78
218k
    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
218k
    if (!fu_firmware_parse_stream(FU_FIRMWARE(item), stream, offset, flags, error))
88
128
      return FALSE;
89
218k
    offset += fu_firmware_get_size(FU_FIRMWARE(item));
90
91
    /* only for debugging */
92
218k
    itemstr = fu_firmware_to_string(FU_FIRMWARE(item));
93
218k
    g_debug("add to table-state: %s", itemstr);
94
95
    /* if there is a sane number of duplicate tokens then add to table */
96
218k
    if (fu_hid_report_item_get_kind(item) == FU_HID_ITEM_KIND_GLOBAL) {
97
40.3k
      if (fu_hid_descriptor_count_table_dupes(table_state, item) >
98
40.3k
          FU_HID_DESCRIPTOR_TABLE_GLOBAL_DUPES_MAX) {
99
9
        g_set_error(
100
9
            error,
101
9
            FWUPD_ERROR,
102
9
            FWUPD_ERROR_INVALID_DATA,
103
9
            "table invalid @0x%x, too many duplicate global %s tokens",
104
9
            (guint)offset,
105
9
            fu_firmware_get_id(FU_FIRMWARE(item)));
106
9
        return FALSE;
107
9
      }
108
40.3k
      g_ptr_array_add(table_state, g_object_ref(item));
109
178k
    } else if (fu_hid_report_item_get_kind(item) == FU_HID_ITEM_KIND_LOCAL ||
110
150k
         fu_hid_report_item_get_kind(item) == FU_HID_ITEM_KIND_MAIN) {
111
31.9k
      if (fu_hid_descriptor_count_table_dupes(table_local, item) >
112
31.9k
          FU_HID_DESCRIPTOR_TABLE_LOCAL_DUPES_MAX) {
113
14
        g_set_error(
114
14
            error,
115
14
            FWUPD_ERROR,
116
14
            FWUPD_ERROR_INVALID_DATA,
117
14
            "table invalid @0x%x, too many duplicate %s %s:0x%x tokens",
118
14
            (guint)offset,
119
14
            fu_hid_item_kind_to_string(fu_hid_report_item_get_kind(item)),
120
14
            fu_firmware_get_id(FU_FIRMWARE(item)),
121
14
            fu_hid_report_item_get_value(item));
122
14
        return FALSE;
123
14
      }
124
31.9k
      g_ptr_array_add(table_local, g_object_ref(item));
125
31.9k
    }
126
127
    /* add report */
128
218k
    if (fu_hid_report_item_get_kind(item) == FU_HID_ITEM_KIND_MAIN) {
129
3.99k
      g_autoptr(FuHidReport) report = fu_hid_report_new();
130
131
      /* copy the table state to the new report */
132
174k
      for (guint i = 0; i < table_state->len; i++) {
133
170k
        FuHidReportItem *item_tmp = g_ptr_array_index(table_state, i);
134
170k
        if (!fu_firmware_add_image(FU_FIRMWARE(report),
135
170k
                 FU_FIRMWARE(item_tmp),
136
170k
                 error))
137
0
          return FALSE;
138
170k
      }
139
32.2k
      for (guint i = 0; i < table_local->len; i++) {
140
28.2k
        FuHidReportItem *item_tmp = g_ptr_array_index(table_local, i);
141
28.2k
        if (!fu_firmware_add_image(FU_FIRMWARE(report),
142
28.2k
                 FU_FIRMWARE(item_tmp),
143
28.2k
                 error))
144
0
          return FALSE;
145
28.2k
      }
146
3.99k
      if (!fu_firmware_add_image(firmware, FU_FIRMWARE(report), error))
147
89
        return FALSE;
148
149
      /* remove all the local items */
150
3.90k
      g_ptr_array_set_size(table_local, 0);
151
3.90k
    }
152
218k
  }
153
154
  /* success */
155
1.00k
  return TRUE;
156
1.24k
}
157
158
static gboolean
159
fu_hid_descriptor_write_report_item(FuFirmware *report_item,
160
            GByteArray *buf,
161
            GHashTable *globals,
162
            GError **error)
163
13.2k
{
164
13.2k
  g_autoptr(GBytes) fw = NULL;
165
166
  /* dedupe any globals */
167
13.2k
  if (fu_hid_report_item_get_kind(FU_HID_REPORT_ITEM(report_item)) ==
168
13.2k
      FU_HID_ITEM_KIND_GLOBAL) {
169
9.04k
    guint8 tag = fu_firmware_get_idx(report_item);
170
9.04k
    FuFirmware *report_item_tmp = g_hash_table_lookup(globals, GUINT_TO_POINTER(tag));
171
9.04k
    if (report_item_tmp != NULL &&
172
7.25k
        fu_hid_report_item_get_value(FU_HID_REPORT_ITEM(report_item)) ==
173
7.25k
      fu_hid_report_item_get_value(FU_HID_REPORT_ITEM(report_item_tmp))) {
174
6.67k
      g_debug("skipping duplicate global tag 0x%x", tag);
175
6.67k
      return TRUE;
176
6.67k
    }
177
2.36k
    g_hash_table_insert(globals, GUINT_TO_POINTER(tag), report_item);
178
2.36k
  }
179
6.56k
  fw = fu_firmware_write(report_item, error);
180
6.56k
  if (fw == NULL)
181
0
    return FALSE;
182
6.56k
  fu_byte_array_append_bytes(buf, fw);
183
184
  /* success */
185
6.56k
  return TRUE;
186
6.56k
}
187
188
static gboolean
189
fu_hid_descriptor_write_report(FuFirmware *report,
190
             GByteArray *buf,
191
             GHashTable *globals,
192
             GError **error)
193
2.71k
{
194
2.71k
  g_autoptr(GPtrArray) report_items = fu_firmware_get_images(report);
195
196
  /* for each item */
197
15.9k
  for (guint i = 0; i < report_items->len; i++) {
198
13.2k
    FuFirmware *report_item = g_ptr_array_index(report_items, i);
199
13.2k
    if (!fu_hid_descriptor_write_report_item(report_item, buf, globals, error))
200
0
      return FALSE;
201
13.2k
  }
202
203
  /* success */
204
2.71k
  return TRUE;
205
2.71k
}
206
207
static GByteArray *
208
fu_hid_descriptor_write(FuFirmware *firmware, GError **error)
209
1.00k
{
210
1.00k
  g_autoptr(GByteArray) buf = g_byte_array_new();
211
1.00k
  g_autoptr(GHashTable) globals = g_hash_table_new(g_direct_hash, g_direct_equal);
212
1.00k
  g_autoptr(GPtrArray) reports = fu_firmware_get_images(firmware);
213
214
  /* for each report */
215
3.71k
  for (guint i = 0; i < reports->len; i++) {
216
2.71k
    FuFirmware *report = g_ptr_array_index(reports, i);
217
2.71k
    if (!fu_hid_descriptor_write_report(report, buf, globals, error))
218
0
      return NULL;
219
2.71k
  }
220
221
  /* success */
222
1.00k
  return g_steal_pointer(&buf);
223
1.00k
}
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.48k
{
291
1.48k
  fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_NO_AUTO_DETECTION);
292
1.48k
  fu_firmware_set_size_max(FU_FIRMWARE(self), 64 * 1024);
293
1.48k
#ifdef HAVE_FUZZER
294
1.48k
  fu_firmware_set_images_max(FU_FIRMWARE(self), 10);
295
#else
296
  fu_firmware_set_images_max(FU_FIRMWARE(self), 1024);
297
#endif
298
1.48k
  fu_firmware_add_image_gtype(FU_FIRMWARE(self), FU_TYPE_HID_REPORT);
299
1.48k
}
300
301
static void
302
fu_hid_descriptor_class_init(FuHidDescriptorClass *klass)
303
1
{
304
1
  FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass);
305
1
  firmware_class->parse = fu_hid_descriptor_parse;
306
1
  firmware_class->write = fu_hid_descriptor_write;
307
1
}
308
309
/**
310
 * fu_hid_descriptor_new:
311
 *
312
 * Creates a new #FuFirmware to parse a HID descriptor
313
 *
314
 * Since: 1.9.4
315
 **/
316
FuFirmware *
317
fu_hid_descriptor_new(void)
318
0
{
319
0
  return FU_FIRMWARE(g_object_new(FU_TYPE_HID_DESCRIPTOR, NULL));
320
0
}