/src/fwupd/libfwupdplugin/fu-ioctl.c
Line | Count | Source |
1 | | /* |
2 | | * Copyright 2024 Richard Hughes <richard@hughsie.com> |
3 | | * |
4 | | * SPDX-License-Identifier: LGPL-2.1-or-later |
5 | | */ |
6 | | |
7 | 0 | #define G_LOG_DOMAIN "FuIoctl" |
8 | | |
9 | | #include "config.h" |
10 | | |
11 | | #include "fu-ioctl-private.h" |
12 | | #include "fu-udev-device-private.h" |
13 | | |
14 | | struct _FuIoctl { |
15 | | GObject parent_instance; |
16 | | FuUdevDevice *udev_device; /* ref */ |
17 | | GString *event_id; |
18 | | GPtrArray *fixups; /* of FuIoctlFixup */ |
19 | | }; |
20 | | |
21 | 0 | G_DEFINE_TYPE(FuIoctl, fu_ioctl, G_TYPE_OBJECT) |
22 | 0 |
|
23 | 0 | typedef struct { |
24 | 0 | gchar *key; |
25 | 0 | gboolean is_mutable; |
26 | 0 | guint8 *buf; |
27 | 0 | gsize bufsz; |
28 | 0 | FuIoctlFixupFunc fixup_cb; |
29 | 0 | } FuIoctlFixup; |
30 | 0 |
|
31 | 0 | static void |
32 | 0 | fu_ioctl_fixup_free(FuIoctlFixup *fixup) |
33 | 0 | { |
34 | 0 | g_free(fixup->key); |
35 | 0 | g_free(fixup); |
36 | 0 | } |
37 | | |
38 | | static gchar * |
39 | | fu_ioctl_fixup_build_key(FuIoctlFixup *fixup, const gchar *suffix) |
40 | 0 | { |
41 | 0 | return g_strdup_printf("%s%s", fixup->key != NULL ? fixup->key : "", suffix); |
42 | 0 | } |
43 | | |
44 | | /** |
45 | | * fu_ioctl_set_name: |
46 | | * @self: a #FuIoctl |
47 | | * @name: (nullable): a string, e.g. `Nvme` |
48 | | * |
49 | | * Adds a name for the ioctl, preserving compatibility with existing emulation data. |
50 | | * |
51 | | * NOTE: For new devices this is not required. |
52 | | * |
53 | | * Since: 2.0.2 |
54 | | **/ |
55 | | void |
56 | | fu_ioctl_set_name(FuIoctl *self, const gchar *name) |
57 | 0 | { |
58 | 0 | g_return_if_fail(FU_IS_IOCTL(self)); |
59 | 0 | g_string_truncate(self->event_id, 0); |
60 | 0 | g_string_append_printf(self->event_id, "%sIoctl:", name != NULL ? name : ""); |
61 | 0 | } |
62 | | |
63 | | /* private */ |
64 | | FuIoctl * |
65 | | fu_ioctl_new(FuUdevDevice *udev_device) |
66 | 0 | { |
67 | 0 | g_autoptr(FuIoctl) self = g_object_new(FU_TYPE_IOCTL, NULL); |
68 | |
|
69 | 0 | g_return_val_if_fail(FU_IS_UDEV_DEVICE(udev_device), NULL); |
70 | | |
71 | 0 | self->udev_device = g_object_ref(udev_device); |
72 | 0 | return g_steal_pointer(&self); |
73 | 0 | } |
74 | | |
75 | | static void |
76 | | fu_ioctl_append_key(GString *event_id, const gchar *key, const gchar *value) |
77 | 0 | { |
78 | 0 | if (event_id->len > 0 && !g_str_has_suffix(event_id->str, ":")) |
79 | 0 | g_string_append_c(event_id, ','); |
80 | 0 | g_string_append_printf(event_id, "%s=%s", key, value); |
81 | 0 | } |
82 | | |
83 | | static void |
84 | | fu_ioctl_append_key_as_u8(GString *event_id, const gchar *key, gsize value) |
85 | 0 | { |
86 | 0 | g_autofree gchar *value2 = g_strdup_printf("0x%02x", (guint)value); |
87 | 0 | fu_ioctl_append_key(event_id, key, value2); |
88 | 0 | } |
89 | | |
90 | | static void |
91 | | fu_ioctl_append_key_as_u16(GString *event_id, const gchar *key, gsize value) |
92 | 0 | { |
93 | 0 | g_autofree gchar *value2 = g_strdup_printf("0x%04x", (guint)value); |
94 | 0 | fu_ioctl_append_key(event_id, key, value2); |
95 | 0 | } |
96 | | |
97 | | static void |
98 | | fu_ioctl_append_key_as_u32(GString *event_id, const gchar *key, guint32 value) |
99 | 0 | { |
100 | 0 | g_autofree gchar *value2 = g_strdup_printf("0x%08x", (guint)value); |
101 | 0 | fu_ioctl_append_key(event_id, key, value2); |
102 | 0 | } |
103 | | |
104 | | static void |
105 | | fu_ioctl_append_key_from_buf(GString *event_id, const gchar *key, const guint8 *buf, gsize bufsz) |
106 | 0 | { |
107 | 0 | g_autofree gchar *key_data = g_strdup_printf("%sData", key != NULL ? key : ""); |
108 | 0 | g_autofree gchar *value_data = g_base64_encode(buf, bufsz); |
109 | 0 | g_autofree gchar *key_length = g_strdup_printf("%sLength", key != NULL ? key : ""); |
110 | 0 | g_autofree gchar *value_length = g_strdup_printf("0x%x", (guint)bufsz); |
111 | |
|
112 | 0 | fu_ioctl_append_key(event_id, key_data, value_data); |
113 | 0 | fu_ioctl_append_key(event_id, key_length, value_length); |
114 | 0 | } |
115 | | |
116 | | /** |
117 | | * fu_ioctl_add_key_as_u8: |
118 | | * @self: a #FuIoctl |
119 | | * @key: a string, e.g. `Opcode` |
120 | | * @value: a integer value |
121 | | * |
122 | | * Adds a key for the emulation, formatting it as `0x%02x`. |
123 | | * |
124 | | * Since: 2.0.2 |
125 | | **/ |
126 | | void |
127 | | fu_ioctl_add_key_as_u8(FuIoctl *self, const gchar *key, gsize value) |
128 | 0 | { |
129 | 0 | g_return_if_fail(FU_IS_IOCTL(self)); |
130 | 0 | g_return_if_fail(key != NULL); |
131 | 0 | fu_ioctl_append_key_as_u8(self->event_id, key, value); |
132 | 0 | } |
133 | | |
134 | | /** |
135 | | * fu_ioctl_add_key_as_u16: |
136 | | * @self: a #FuIoctl |
137 | | * @key: a string, e.g. `Opcode` |
138 | | * @value: a integer value |
139 | | * |
140 | | * Adds a key for the emulation, formatting it as `0x%04x`. |
141 | | * |
142 | | * Since: 2.0.2 |
143 | | **/ |
144 | | void |
145 | | fu_ioctl_add_key_as_u16(FuIoctl *self, const gchar *key, gsize value) |
146 | 0 | { |
147 | 0 | g_return_if_fail(FU_IS_IOCTL(self)); |
148 | 0 | g_return_if_fail(key != NULL); |
149 | 0 | fu_ioctl_append_key_as_u16(self->event_id, key, value); |
150 | 0 | } |
151 | | |
152 | | static void |
153 | | fu_ioctl_add_buffer(FuIoctl *self, |
154 | | const gchar *key, |
155 | | guint8 *buf, |
156 | | gsize bufsz, |
157 | | gboolean is_mutable, |
158 | | FuIoctlFixupFunc fixup_cb) |
159 | 0 | { |
160 | 0 | fu_ioctl_append_key_from_buf(self->event_id, key, buf, bufsz); |
161 | 0 | if (fixup_cb != NULL) { |
162 | 0 | FuIoctlFixup *fixup = g_new0(FuIoctlFixup, 1); |
163 | 0 | fixup->key = g_strdup(key); |
164 | 0 | fixup->is_mutable = is_mutable; |
165 | 0 | fixup->buf = buf; |
166 | 0 | fixup->bufsz = bufsz; |
167 | 0 | fixup->fixup_cb = fixup_cb; |
168 | 0 | g_ptr_array_add(self->fixups, fixup); |
169 | 0 | } |
170 | 0 | } |
171 | | |
172 | | /** |
173 | | * fu_ioctl_add_mutable_buffer: |
174 | | * @self: a #FuIoctl |
175 | | * @key: a string, e.g. `Cdb` |
176 | | * @buf: (nullable): an optional buffer |
177 | | * @bufsz: Size of @buf |
178 | | * @fixup_cb: (scope forever): a function to call on the structure |
179 | | * |
180 | | * Adds a mutable buffer that can be used to fix up the ioctl-defined structure with the buffer and |
181 | | * size, and adds a key for the emulation. |
182 | | * |
183 | | * Since: 2.0.2 |
184 | | **/ |
185 | | void |
186 | | fu_ioctl_add_mutable_buffer(FuIoctl *self, |
187 | | const gchar *key, |
188 | | guint8 *buf, |
189 | | gsize bufsz, |
190 | | FuIoctlFixupFunc fixup_cb) |
191 | 0 | { |
192 | 0 | fu_ioctl_add_buffer(self, key, buf, bufsz, TRUE, fixup_cb); |
193 | 0 | } |
194 | | |
195 | | /** |
196 | | * fu_ioctl_add_const_buffer: |
197 | | * @self: a #FuIoctl |
198 | | * @key: a string, e.g. `Cdb` |
199 | | * @buf: (nullable): an optional buffer |
200 | | * @bufsz: Size of @buf |
201 | | * @fixup_cb: (scope forever): a function to call on the structure |
202 | | * |
203 | | * Adds a constant buffer that can be used to fix up the ioctl-defined structure with the buffer |
204 | | * and size, and adds a key for the emulation. |
205 | | * |
206 | | * Since: 2.0.2 |
207 | | **/ |
208 | | void |
209 | | fu_ioctl_add_const_buffer(FuIoctl *self, |
210 | | const gchar *key, |
211 | | const guint8 *buf, |
212 | | gsize bufsz, |
213 | | FuIoctlFixupFunc fixup_cb) |
214 | 0 | { |
215 | 0 | fu_ioctl_add_buffer(self, key, (guint8 *)buf, bufsz, FALSE, fixup_cb); |
216 | 0 | } |
217 | | |
218 | | /** |
219 | | * fu_ioctl_execute: |
220 | | * @self: a #FuIoctl |
221 | | * @request: request number |
222 | | * @buf: a buffer to use, which *must* be large enough for the request |
223 | | * @bufsz: the size of @buf |
224 | | * @rc: (out) (nullable): the raw return value from the ioctl |
225 | | * @timeout: timeout in ms for the retry action, see %FU_IOCTL_FLAG_RETRY |
226 | | * @flags: some #FuIoctlFlags, e.g. %FU_IOCTL_FLAG_RETRY |
227 | | * @error: (nullable): optional return location for an error |
228 | | * |
229 | | * Executes the ioctl, emulating as required. Each fixup defined using fu_ioctl_add_mutable_buffer() |
230 | | * of fu_ioctl_add_const_buffer() is run before the ioctl is executed. |
231 | | * |
232 | | * If there are no fixups defined, the @buf is emulated, and so you must ensure that there are no |
233 | | * ioctl wrapper structures that use indirect pointer values. |
234 | | * |
235 | | * Returns: %TRUE for success |
236 | | * |
237 | | * Since: 2.0.2 |
238 | | **/ |
239 | | gboolean |
240 | | fu_ioctl_execute(FuIoctl *self, |
241 | | gulong request, |
242 | | gpointer buf, |
243 | | gsize bufsz, |
244 | | gint *rc, |
245 | | guint timeout, |
246 | | FuIoctlFlags flags, |
247 | | GError **error) |
248 | 0 | { |
249 | 0 | FuDeviceEvent *event = NULL; |
250 | 0 | g_autoptr(GString) event_id = NULL; |
251 | | |
252 | | /* need event ID */ |
253 | 0 | if (fu_device_has_flag(FU_DEVICE(self->udev_device), FWUPD_DEVICE_FLAG_EMULATED) || |
254 | 0 | fu_context_has_flag(fu_device_get_context(FU_DEVICE(self->udev_device)), |
255 | 0 | FU_CONTEXT_FLAG_SAVE_EVENTS)) { |
256 | 0 | event_id = g_string_new(self->event_id->str); |
257 | 0 | if (g_strcmp0(event_id->str, "Ioctl:") == 0) { |
258 | 0 | fu_ioctl_append_key_as_u16(event_id, "Request", request); |
259 | 0 | if (flags & FU_IOCTL_FLAG_PTR_AS_INTEGER) { |
260 | 0 | fu_ioctl_append_key_as_u32(event_id, NULL, GPOINTER_TO_UINT(buf)); |
261 | 0 | } else { |
262 | 0 | fu_ioctl_append_key_from_buf(event_id, NULL, buf, bufsz); |
263 | 0 | } |
264 | 0 | } |
265 | 0 | } |
266 | | |
267 | | /* emulated */ |
268 | 0 | if (fu_device_has_flag(FU_DEVICE(self->udev_device), FWUPD_DEVICE_FLAG_EMULATED)) { |
269 | 0 | event = fu_device_load_event(FU_DEVICE(self->udev_device), event_id->str, error); |
270 | 0 | if (event == NULL) |
271 | 0 | return FALSE; |
272 | 0 | if (self->fixups->len == 0) { |
273 | 0 | if ((flags & FU_IOCTL_FLAG_PTR_AS_INTEGER) == 0) { |
274 | 0 | if (!fu_device_event_copy_data(event, |
275 | 0 | "DataOut", |
276 | 0 | buf, |
277 | 0 | bufsz, |
278 | 0 | NULL, |
279 | 0 | error)) |
280 | 0 | return FALSE; |
281 | 0 | } |
282 | 0 | } |
283 | 0 | for (guint i = 0; i < self->fixups->len; i++) { |
284 | 0 | FuIoctlFixup *fixup = g_ptr_array_index(self->fixups, i); |
285 | 0 | g_autofree gchar *key = fu_ioctl_fixup_build_key(fixup, "DataOut"); |
286 | 0 | if (!fixup->is_mutable) |
287 | 0 | continue; |
288 | 0 | if (!fu_device_event_copy_data(event, |
289 | 0 | key, |
290 | 0 | fixup->buf, |
291 | 0 | fixup->bufsz, |
292 | 0 | NULL, |
293 | 0 | error)) |
294 | 0 | return FALSE; |
295 | 0 | } |
296 | 0 | if (rc != NULL) { |
297 | 0 | gint64 rc_tmp = fu_device_event_get_i64(event, "Rc", NULL); |
298 | 0 | if (rc_tmp != G_MAXINT64) |
299 | 0 | *rc = (gint)rc_tmp; |
300 | 0 | } |
301 | 0 | return TRUE; |
302 | 0 | } |
303 | | |
304 | | /* save */ |
305 | 0 | if (fu_context_has_flag(fu_device_get_context(FU_DEVICE(self->udev_device)), |
306 | 0 | FU_CONTEXT_FLAG_SAVE_EVENTS)) { |
307 | 0 | event = fu_device_save_event(FU_DEVICE(self->udev_device), event_id->str); |
308 | 0 | } |
309 | | |
310 | | /* the buffer might be specified indirectly */ |
311 | 0 | if (buf != NULL) { |
312 | 0 | for (guint i = 0; i < self->fixups->len; i++) { |
313 | 0 | FuIoctlFixup *fixup = g_ptr_array_index(self->fixups, i); |
314 | 0 | if (!fixup->fixup_cb(self, buf, fixup->buf, fixup->bufsz, error)) |
315 | 0 | return FALSE; |
316 | 0 | } |
317 | 0 | } |
318 | 0 | if (!fu_udev_device_ioctl(self->udev_device, |
319 | 0 | request, |
320 | 0 | buf, |
321 | 0 | bufsz, |
322 | 0 | rc, |
323 | 0 | timeout, |
324 | 0 | flags, |
325 | 0 | error)) |
326 | 0 | return FALSE; |
327 | | |
328 | | /* save response */ |
329 | 0 | if (event != NULL) { |
330 | 0 | if (rc != NULL && *rc != 0) |
331 | 0 | fu_device_event_set_i64(event, "Rc", *rc); |
332 | 0 | if (self->fixups->len == 0) { |
333 | 0 | if ((flags & FU_IOCTL_FLAG_PTR_AS_INTEGER) == 0) |
334 | 0 | fu_device_event_set_data(event, "DataOut", buf, bufsz); |
335 | 0 | } |
336 | 0 | for (guint i = 0; i < self->fixups->len; i++) { |
337 | 0 | FuIoctlFixup *fixup = g_ptr_array_index(self->fixups, i); |
338 | 0 | g_autofree gchar *key = fu_ioctl_fixup_build_key(fixup, "DataOut"); |
339 | 0 | if (!fixup->is_mutable) |
340 | 0 | continue; |
341 | 0 | fu_device_event_set_data(event, key, fixup->buf, fixup->bufsz); |
342 | 0 | } |
343 | 0 | } |
344 | | |
345 | | /* success */ |
346 | 0 | return TRUE; |
347 | 0 | } |
348 | | |
349 | | static void |
350 | | fu_ioctl_init(FuIoctl *self) |
351 | 0 | { |
352 | 0 | self->event_id = g_string_new("Ioctl:"); |
353 | 0 | self->fixups = g_ptr_array_new_with_free_func((GDestroyNotify)fu_ioctl_fixup_free); |
354 | 0 | } |
355 | | |
356 | | static void |
357 | | fu_ioctl_finalize(GObject *object) |
358 | 0 | { |
359 | 0 | FuIoctl *self = FU_IOCTL(object); |
360 | |
|
361 | 0 | g_string_free(self->event_id, TRUE); |
362 | 0 | g_ptr_array_unref(self->fixups); |
363 | 0 | if (self->udev_device != NULL) |
364 | 0 | g_object_unref(self->udev_device); |
365 | |
|
366 | 0 | G_OBJECT_CLASS(fu_ioctl_parent_class)->finalize(object); |
367 | 0 | } |
368 | | |
369 | | static void |
370 | | fu_ioctl_class_init(FuIoctlClass *klass) |
371 | 0 | { |
372 | 0 | GObjectClass *object_class = G_OBJECT_CLASS(klass); |
373 | 0 | object_class->finalize = fu_ioctl_finalize; |
374 | 0 | } |