Coverage Report

Created: 2025-07-12 06:33

/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
}