Coverage Report

Created: 2026-04-09 06:28

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
157k
#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.40k
G_DEFINE_TYPE(FuHidDescriptor, fu_hid_descriptor, FU_TYPE_FIRMWARE)
30
1.40k
31
150k
#define FU_HID_DESCRIPTOR_TABLE_LOCAL_SIZE_MAX   1024
32
29.9k
#define FU_HID_DESCRIPTOR_TABLE_LOCAL_DUPES_MAX  16
33
150k
#define FU_HID_DESCRIPTOR_TABLE_GLOBAL_SIZE_MAX  1024
34
37.1k
#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
67.1k
{
39
67.1k
  guint cnt = 0;
40
18.4M
  for (guint i = 0; i < table->len; i++) {
41
18.3M
    FuHidReportItem *item_tmp = g_ptr_array_index(table, i);
42
18.3M
    if (fu_hid_report_item_get_kind(item) == fu_hid_report_item_get_kind(item_tmp) &&
43
18.3M
        fu_hid_report_item_get_value(item) == fu_hid_report_item_get_value(item_tmp) &&
44
9.67M
        fu_firmware_get_idx(FU_FIRMWARE(item)) ==
45
9.67M
      fu_firmware_get_idx(FU_FIRMWARE(item_tmp)))
46
873k
      cnt++;
47
18.3M
  }
48
67.1k
  return cnt;
49
67.1k
}
50
51
static gboolean
52
fu_hid_descriptor_parse(FuFirmware *firmware,
53
      GInputStream *stream,
54
      FuFirmwareParseFlags flags,
55
      GError **error)
56
1.16k
{
57
1.16k
  gsize offset = 0;
58
1.16k
  gsize streamsz = 0;
59
1.16k
  g_autoptr(GPtrArray) table_state =
60
1.16k
      g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref);
61
1.16k
  g_autoptr(GPtrArray) table_local =
62
1.16k
      g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref);
63
1.16k
  if (!fu_input_stream_size(stream, &streamsz, error))
64
0
    return FALSE;
65
151k
  while (offset < streamsz) {
66
150k
    g_autofree gchar *itemstr = NULL;
67
150k
    g_autoptr(FuHidReportItem) item = fu_hid_report_item_new();
68
69
    /* sanity check */
70
150k
    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
150k
    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
150k
    if (!fu_firmware_parse_stream(FU_FIRMWARE(item), stream, offset, flags, error))
88
124
      return FALSE;
89
150k
    offset += fu_firmware_get_size(FU_FIRMWARE(item));
90
91
    /* only for debugging */
92
150k
    itemstr = fu_firmware_to_string(FU_FIRMWARE(item));
93
150k
    g_debug("add to table-state: %s", itemstr);
94
95
    /* if there is a sane number of duplicate tokens then add to table */
96
150k
    if (fu_hid_report_item_get_kind(item) == FU_HID_ITEM_KIND_GLOBAL) {
97
37.1k
      if (fu_hid_descriptor_count_table_dupes(table_state, item) >
98
37.1k
          FU_HID_DESCRIPTOR_TABLE_GLOBAL_DUPES_MAX) {
99
8
        g_set_error(
100
8
            error,
101
8
            FWUPD_ERROR,
102
8
            FWUPD_ERROR_INVALID_DATA,
103
8
            "table invalid @0x%x, too many duplicate global %s tokens",
104
8
            (guint)offset,
105
8
            fu_firmware_get_id(FU_FIRMWARE(item)));
106
8
        return FALSE;
107
8
      }
108
37.1k
      g_ptr_array_add(table_state, g_object_ref(item));
109
113k
    } else if (fu_hid_report_item_get_kind(item) == FU_HID_ITEM_KIND_LOCAL ||
110
87.0k
         fu_hid_report_item_get_kind(item) == FU_HID_ITEM_KIND_MAIN) {
111
29.9k
      if (fu_hid_descriptor_count_table_dupes(table_local, item) >
112
29.9k
          FU_HID_DESCRIPTOR_TABLE_LOCAL_DUPES_MAX) {
113
17
        g_set_error(
114
17
            error,
115
17
            FWUPD_ERROR,
116
17
            FWUPD_ERROR_INVALID_DATA,
117
17
            "table invalid @0x%x, too many duplicate %s %s:0x%x tokens",
118
17
            (guint)offset,
119
17
            fu_hid_item_kind_to_string(fu_hid_report_item_get_kind(item)),
120
17
            fu_firmware_get_id(FU_FIRMWARE(item)),
121
17
            fu_hid_report_item_get_value(item));
122
17
        return FALSE;
123
17
      }
124
29.9k
      g_ptr_array_add(table_local, g_object_ref(item));
125
29.9k
    }
126
127
    /* add report */
128
150k
    if (fu_hid_report_item_get_kind(item) == FU_HID_ITEM_KIND_MAIN) {
129
3.75k
      g_autoptr(FuHidReport) report = fu_hid_report_new();
130
131
      /* copy the table state to the new report */
132
170k
      for (guint i = 0; i < table_state->len; i++) {
133
166k
        FuHidReportItem *item_tmp = g_ptr_array_index(table_state, i);
134
166k
        if (!fu_firmware_add_image(FU_FIRMWARE(report),
135
166k
                 FU_FIRMWARE(item_tmp),
136
166k
                 error))
137
0
          return FALSE;
138
166k
      }
139
30.1k
      for (guint i = 0; i < table_local->len; i++) {
140
26.3k
        FuHidReportItem *item_tmp = g_ptr_array_index(table_local, i);
141
26.3k
        if (!fu_firmware_add_image(FU_FIRMWARE(report),
142
26.3k
                 FU_FIRMWARE(item_tmp),
143
26.3k
                 error))
144
0
          return FALSE;
145
26.3k
      }
146
3.75k
      if (!fu_firmware_add_image(firmware, FU_FIRMWARE(report), error))
147
85
        return FALSE;
148
149
      /* remove all the local items */
150
3.67k
      g_ptr_array_set_size(table_local, 0);
151
3.67k
    }
152
150k
  }
153
154
  /* success */
155
922
  return TRUE;
156
1.16k
}
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.0k
{
164
13.0k
  g_autoptr(GBytes) fw = NULL;
165
166
  /* dedupe any globals */
167
13.0k
  if (fu_hid_report_item_get_kind(FU_HID_REPORT_ITEM(report_item)) ==
168
13.0k
      FU_HID_ITEM_KIND_GLOBAL) {
169
9.09k
    guint8 tag = fu_firmware_get_idx(report_item);
170
9.09k
    FuFirmware *report_item_tmp = g_hash_table_lookup(globals, GUINT_TO_POINTER(tag));
171
9.09k
    if (report_item_tmp != NULL &&
172
7.29k
        fu_hid_report_item_get_value(FU_HID_REPORT_ITEM(report_item)) ==
173
7.29k
      fu_hid_report_item_get_value(FU_HID_REPORT_ITEM(report_item_tmp))) {
174
6.75k
      g_debug("skipping duplicate global tag 0x%x", tag);
175
6.75k
      return TRUE;
176
6.75k
    }
177
2.33k
    g_hash_table_insert(globals, GUINT_TO_POINTER(tag), report_item);
178
2.33k
  }
179
6.29k
  fw = fu_firmware_write(report_item, error);
180
6.29k
  if (fw == NULL)
181
0
    return FALSE;
182
6.29k
  fu_byte_array_append_bytes(buf, fw);
183
184
  /* success */
185
6.29k
  return TRUE;
186
6.29k
}
187
188
static gboolean
189
fu_hid_descriptor_write_report(FuFirmware *report,
190
             GByteArray *buf,
191
             GHashTable *globals,
192
             GError **error)
193
2.52k
{
194
2.52k
  g_autoptr(GPtrArray) report_items = fu_firmware_get_images(report);
195
196
  /* for each item */
197
15.5k
  for (guint i = 0; i < report_items->len; i++) {
198
13.0k
    FuFirmware *report_item = g_ptr_array_index(report_items, i);
199
13.0k
    if (!fu_hid_descriptor_write_report_item(report_item, buf, globals, error))
200
0
      return FALSE;
201
13.0k
  }
202
203
  /* success */
204
2.52k
  return TRUE;
205
2.52k
}
206
207
static GByteArray *
208
fu_hid_descriptor_write(FuFirmware *firmware, GError **error)
209
922
{
210
922
  g_autoptr(GByteArray) buf = g_byte_array_new();
211
922
  g_autoptr(GHashTable) globals = g_hash_table_new(g_direct_hash, g_direct_equal);
212
922
  g_autoptr(GPtrArray) reports = fu_firmware_get_images(firmware);
213
214
  /* for each report */
215
3.44k
  for (guint i = 0; i < reports->len; i++) {
216
2.52k
    FuFirmware *report = g_ptr_array_index(reports, i);
217
2.52k
    if (!fu_hid_descriptor_write_report(report, buf, globals, error))
218
0
      return NULL;
219
2.52k
  }
220
221
  /* success */
222
922
  return g_steal_pointer(&buf);
223
922
}
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.40k
{
291
1.40k
  fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_NO_AUTO_DETECTION);
292
1.40k
  fu_firmware_set_size_max(FU_FIRMWARE(self), 64 * 1024);
293
1.40k
#ifdef HAVE_FUZZER
294
1.40k
  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.40k
  fu_firmware_add_image_gtype(FU_FIRMWARE(self), FU_TYPE_HID_REPORT);
299
1.40k
}
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
}