Coverage Report

Created: 2025-11-09 07:06

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
164k
#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.44k
G_DEFINE_TYPE(FuHidDescriptor, fu_hid_descriptor, FU_TYPE_FIRMWARE)
30
1.44k
31
157k
#define FU_HID_DESCRIPTOR_TABLE_LOCAL_SIZE_MAX   1024
32
35.5k
#define FU_HID_DESCRIPTOR_TABLE_LOCAL_DUPES_MAX  16
33
157k
#define FU_HID_DESCRIPTOR_TABLE_GLOBAL_SIZE_MAX  1024
34
37.8k
#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
73.3k
{
39
73.3k
  guint cnt = 0;
40
19.2M
  for (guint i = 0; i < table->len; i++) {
41
19.1M
    FuHidReportItem *item_tmp = g_ptr_array_index(table, i);
42
19.1M
    if (fu_hid_report_item_get_kind(item) == fu_hid_report_item_get_kind(item_tmp) &&
43
19.1M
        fu_hid_report_item_get_value(item) == fu_hid_report_item_get_value(item_tmp) &&
44
9.24M
        fu_firmware_get_idx(FU_FIRMWARE(item)) ==
45
9.24M
      fu_firmware_get_idx(FU_FIRMWARE(item_tmp)))
46
885k
      cnt++;
47
19.1M
  }
48
73.3k
  return cnt;
49
73.3k
}
50
51
static gboolean
52
fu_hid_descriptor_parse(FuFirmware *firmware,
53
      GInputStream *stream,
54
      FuFirmwareParseFlags flags,
55
      GError **error)
56
1.20k
{
57
1.20k
  gsize offset = 0;
58
1.20k
  gsize streamsz = 0;
59
1.20k
  g_autoptr(GPtrArray) table_state =
60
1.20k
      g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref);
61
1.20k
  g_autoptr(GPtrArray) table_local =
62
1.20k
      g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref);
63
1.20k
  if (!fu_input_stream_size(stream, &streamsz, error))
64
0
    return FALSE;
65
158k
  while (offset < streamsz) {
66
157k
    g_autofree gchar *itemstr = NULL;
67
157k
    g_autoptr(FuHidReportItem) item = fu_hid_report_item_new();
68
69
    /* sanity check */
70
157k
    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
157k
    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
157k
    if (!fu_firmware_parse_stream(FU_FIRMWARE(item), stream, offset, flags, error))
88
119
      return FALSE;
89
157k
    offset += fu_firmware_get_size(FU_FIRMWARE(item));
90
91
    /* only for debugging */
92
157k
    itemstr = fu_firmware_to_string(FU_FIRMWARE(item));
93
157k
    g_debug("add to table-state: %s", itemstr);
94
95
    /* if there is a sane number of duplicate tokens then add to table */
96
157k
    if (fu_hid_report_item_get_kind(item) == FU_HID_ITEM_KIND_GLOBAL) {
97
37.8k
      if (fu_hid_descriptor_count_table_dupes(table_state, item) >
98
37.8k
          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
37.8k
      g_ptr_array_add(table_state, g_object_ref(item));
109
119k
    } else if (fu_hid_report_item_get_kind(item) == FU_HID_ITEM_KIND_LOCAL ||
110
87.8k
         fu_hid_report_item_get_kind(item) == FU_HID_ITEM_KIND_MAIN) {
111
35.5k
      if (fu_hid_descriptor_count_table_dupes(table_local, item) >
112
35.5k
          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
35.5k
      g_ptr_array_add(table_local, g_object_ref(item));
125
35.5k
    }
126
127
    /* add report */
128
157k
    if (fu_hid_report_item_get_kind(item) == FU_HID_ITEM_KIND_MAIN) {
129
3.79k
      g_autoptr(FuHidReport) report = fu_hid_report_new();
130
131
      /* copy the table state to the new report */
132
149k
      for (guint i = 0; i < table_state->len; i++) {
133
145k
        FuHidReportItem *item_tmp = g_ptr_array_index(table_state, i);
134
145k
        if (!fu_firmware_add_image(FU_FIRMWARE(report),
135
145k
                 FU_FIRMWARE(item_tmp),
136
145k
                 error))
137
0
          return FALSE;
138
145k
      }
139
33.7k
      for (guint i = 0; i < table_local->len; i++) {
140
29.9k
        FuHidReportItem *item_tmp = g_ptr_array_index(table_local, i);
141
29.9k
        if (!fu_firmware_add_image(FU_FIRMWARE(report),
142
29.9k
                 FU_FIRMWARE(item_tmp),
143
29.9k
                 error))
144
0
          return FALSE;
145
29.9k
      }
146
3.79k
      if (!fu_firmware_add_image(firmware, FU_FIRMWARE(report), error))
147
80
        return FALSE;
148
149
      /* remove all the local items */
150
3.71k
      g_ptr_array_set_size(table_local, 0);
151
3.71k
    }
152
157k
  }
153
154
  /* success */
155
977
  return TRUE;
156
1.20k
}
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.31k
    guint8 tag = fu_firmware_get_idx(report_item);
170
9.31k
    FuFirmware *report_item_tmp = g_hash_table_lookup(globals, GUINT_TO_POINTER(tag));
171
9.31k
    if (report_item_tmp != NULL &&
172
7.39k
        fu_hid_report_item_get_value(FU_HID_REPORT_ITEM(report_item)) ==
173
7.39k
      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.55k
    g_hash_table_insert(globals, GUINT_TO_POINTER(tag), report_item);
178
2.55k
  }
179
6.51k
  fw = fu_firmware_write(report_item, error);
180
6.51k
  if (fw == NULL)
181
0
    return FALSE;
182
6.51k
  fu_byte_array_append_bytes(buf, fw);
183
184
  /* success */
185
6.51k
  return TRUE;
186
6.51k
}
187
188
static gboolean
189
fu_hid_descriptor_write_report(FuFirmware *report,
190
             GByteArray *buf,
191
             GHashTable *globals,
192
             GError **error)
193
2.55k
{
194
2.55k
  g_autoptr(GPtrArray) report_items = fu_firmware_get_images(report);
195
196
  /* for each item */
197
15.8k
  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.55k
  return TRUE;
205
2.55k
}
206
207
static GByteArray *
208
fu_hid_descriptor_write(FuFirmware *firmware, GError **error)
209
977
{
210
977
  g_autoptr(GByteArray) buf = g_byte_array_new();
211
977
  g_autoptr(GHashTable) globals = g_hash_table_new(g_direct_hash, g_direct_equal);
212
977
  g_autoptr(GPtrArray) reports = fu_firmware_get_images(firmware);
213
214
  /* for each report */
215
3.53k
  for (guint i = 0; i < reports->len; i++) {
216
2.55k
    FuFirmware *report = g_ptr_array_index(reports, i);
217
2.55k
    if (!fu_hid_descriptor_write_report(report, buf, globals, error))
218
0
      return NULL;
219
2.55k
  }
220
221
  /* success */
222
977
  return g_steal_pointer(&buf);
223
977
}
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.44k
{
291
1.44k
  fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_NO_AUTO_DETECTION);
292
1.44k
  fu_firmware_set_size_max(FU_FIRMWARE(self), 64 * 1024);
293
1.44k
#ifdef HAVE_FUZZER
294
1.44k
  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.44k
  g_type_ensure(FU_TYPE_HID_REPORT);
299
1.44k
  g_type_ensure(FU_TYPE_HID_REPORT_ITEM);
300
1.44k
}
301
302
static void
303
fu_hid_descriptor_class_init(FuHidDescriptorClass *klass)
304
1
{
305
1
  FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass);
306
1
  firmware_class->parse = fu_hid_descriptor_parse;
307
1
  firmware_class->write = fu_hid_descriptor_write;
308
1
}
309
310
/**
311
 * fu_hid_descriptor_new:
312
 *
313
 * Creates a new #FuFirmware to parse a HID descriptor
314
 *
315
 * Since: 1.9.4
316
 **/
317
FuFirmware *
318
fu_hid_descriptor_new(void)
319
0
{
320
0
  return FU_FIRMWARE(g_object_new(FU_TYPE_HID_DESCRIPTOR, NULL));
321
0
}