Coverage Report

Created: 2026-05-30 06:50

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