Coverage Report

Created: 2026-10-28 07:47

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/fwupd/libfwupdplugin/fu-kernel.c
Line
Count
Source
1
/*
2
 * Copyright 2017 Richard Hughes <richard@hughsie.com>
3
 *
4
 * SPDX-License-Identifier: LGPL-2.1-or-later
5
 */
6
7
0
#define G_LOG_DOMAIN "FuCommon"
8
9
#include "config.h"
10
11
#include <errno.h>
12
#include <glib/gstdio.h>
13
#ifdef HAVE_UTSNAME_H
14
#include <sys/utsname.h>
15
#endif
16
17
#include "fu-compressor-stream.h"
18
#include "fu-input-stream.h"
19
#include "fu-kernel.h"
20
#include "fu-path.h"
21
#include "fu-string.h"
22
#include "fu-version-common.h"
23
24
/**
25
 * fu_kernel_locked_down:
26
 * @pstore: a #FuPathStore
27
 *
28
 * Determines if kernel lockdown in effect
29
 *
30
 * Since: 1.8.2
31
 **/
32
gboolean
33
fu_kernel_locked_down(FuPathStore *pstore)
34
0
{
35
0
#ifdef __linux__
36
0
  gsize len = 0;
37
0
  g_autofree gchar *fname = NULL;
38
0
  g_autofree gchar *data = NULL;
39
0
  g_auto(GStrv) options = NULL;
40
41
0
  fname = fu_path_store_build_filename(pstore,
42
0
               NULL,
43
0
               FU_PATH_KIND_SYSFSDIR_SECURITY,
44
0
               "lockdown",
45
0
               NULL);
46
0
  if (fname == NULL)
47
0
    return FALSE;
48
0
  if (!g_file_test(fname, G_FILE_TEST_EXISTS))
49
0
    return FALSE;
50
0
  if (!g_file_get_contents(fname, &data, &len, NULL))
51
0
    return FALSE;
52
0
  if (len < 1)
53
0
    return FALSE;
54
0
  options = g_strsplit(data, " ", -1);
55
0
  for (guint i = 0; options[i] != NULL; i++) {
56
0
    if (g_strcmp0(options[i], "[none]") == 0)
57
0
      return FALSE;
58
0
  }
59
0
  return TRUE;
60
#else
61
  return FALSE;
62
#endif
63
0
}
64
65
/**
66
 * fu_kernel_check_version:
67
 * @minimum_kernel: (not nullable): The minimum kernel version to check against
68
 * @error: (nullable): optional return location for an error
69
 *
70
 * Determines if the system is running at least a certain required kernel version
71
 *
72
 * Since: 1.8.2
73
 **/
74
gboolean
75
fu_kernel_check_version(const gchar *minimum_kernel, GError **error)
76
0
{
77
#ifdef HAVE_UTSNAME_H
78
  struct utsname name_tmp = {0};
79
80
  g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
81
  g_return_val_if_fail(minimum_kernel != NULL, FALSE);
82
83
  if (uname(&name_tmp) < 0) {
84
    g_set_error_literal(error,
85
            FWUPD_ERROR,
86
            FWUPD_ERROR_INTERNAL,
87
            "failed to read kernel version");
88
    return FALSE;
89
  }
90
  if (fu_version_compare(name_tmp.release, minimum_kernel, FWUPD_VERSION_FORMAT_TRIPLET) <
91
      0) {
92
    g_set_error(error,
93
          FWUPD_ERROR,
94
          FWUPD_ERROR_INTERNAL,
95
          "kernel %s doesn't meet minimum %s",
96
          name_tmp.release,
97
          minimum_kernel);
98
    return FALSE;
99
  }
100
101
  return TRUE;
102
#else
103
0
  g_set_error_literal(error,
104
0
          FWUPD_ERROR,
105
0
          FWUPD_ERROR_INTERNAL,
106
0
          "platform doesn't support checking for minimum Linux kernel");
107
0
  return FALSE;
108
0
#endif
109
0
}
110
111
typedef struct {
112
  GHashTable *hash;
113
  GHashTable *values;
114
} FuKernelConfigHelper;
115
116
static gboolean
117
fu_kernel_parse_config_line_cb(GString *token, guint token_idx, gpointer user_data, GError **error)
118
0
{
119
0
  g_auto(GStrv) kv = NULL;
120
0
  FuKernelConfigHelper *helper = (FuKernelConfigHelper *)user_data;
121
0
  GRefString *value;
122
123
0
  if (token->len == 0)
124
0
    return TRUE;
125
0
  if (token->str[0] == '#')
126
0
    return TRUE;
127
128
0
  kv = g_strsplit(token->str, "=", 2);
129
0
  if (g_strv_length(kv) != 2) {
130
0
    g_set_error(error,
131
0
          FWUPD_ERROR,
132
0
          FWUPD_ERROR_INVALID_DATA,
133
0
          "invalid format for '%s'",
134
0
          token->str);
135
0
    return FALSE;
136
0
  }
137
0
  value = g_hash_table_lookup(helper->values, kv[1]);
138
0
  if (value != NULL) {
139
0
    g_hash_table_insert(helper->hash, g_strdup(kv[0]), g_ref_string_acquire(value));
140
0
  } else {
141
0
    g_hash_table_insert(helper->hash, g_strdup(kv[0]), g_ref_string_new(kv[1]));
142
0
  }
143
0
  return TRUE;
144
0
}
145
146
/**
147
 * fu_kernel_parse_config:
148
 * @buf: (not nullable): cmdline to parse
149
 * @bufsz: size of @bufsz
150
 *
151
 * Parses all the kernel options into a hash table. Commented out options are not included.
152
 *
153
 * Returns: (transfer container) (element-type utf8 utf8): config keys
154
 *
155
 * Since: 1.9.6
156
 **/
157
GHashTable *
158
fu_kernel_parse_config(const gchar *buf, gsize bufsz, GError **error)
159
0
{
160
0
  g_autoptr(GHashTable) hash = g_hash_table_new_full(g_str_hash,
161
0
                 g_str_equal,
162
0
                 g_free,
163
0
                 (GDestroyNotify)g_ref_string_release);
164
0
  g_autoptr(GHashTable) values = g_hash_table_new_full(g_str_hash,
165
0
                   g_str_equal,
166
0
                   NULL,
167
0
                   (GDestroyNotify)g_ref_string_release);
168
0
  FuKernelConfigHelper helper = {.hash = hash, .values = values};
169
0
  const gchar *value_keys[] = {"y", "m", "0", NULL};
170
171
0
  g_return_val_if_fail(buf != NULL, NULL);
172
0
  g_return_val_if_fail(error == NULL || *error == NULL, NULL);
173
174
  /* add 99.9% of the most common keys to avoid thousands of small allocations */
175
0
  for (guint i = 0; value_keys[i] != NULL; i++) {
176
0
    g_hash_table_insert(values,
177
0
            (gpointer)value_keys[i],
178
0
            g_ref_string_new(value_keys[i]));
179
0
  }
180
0
  if (!fu_strsplit_full(buf, bufsz, "\n", fu_kernel_parse_config_line_cb, &helper, error))
181
0
    return NULL;
182
0
  return g_steal_pointer(&hash);
183
0
}
184
185
#ifdef __linux__
186
static gchar *
187
fu_kernel_get_config_path(FuPathStore *pstore, GError **error)
188
0
{
189
#ifdef HAVE_UTSNAME_H
190
  struct utsname name_tmp;
191
  g_autofree gchar *config_fn = NULL;
192
193
  memset(&name_tmp, 0, sizeof(struct utsname));
194
  if (uname(&name_tmp) < 0) {
195
    g_set_error_literal(error,
196
            FWUPD_ERROR,
197
            FWUPD_ERROR_INTERNAL,
198
            "failed to read kernel version");
199
    return NULL;
200
  }
201
  config_fn = g_strdup_printf("config-%s", name_tmp.release);
202
  return fu_path_store_build_filename(pstore,
203
              error,
204
              FU_PATH_KIND_HOSTFS_BOOT,
205
              config_fn,
206
              NULL);
207
#else
208
0
  g_set_error_literal(error,
209
0
          FWUPD_ERROR,
210
0
          FWUPD_ERROR_INTERNAL,
211
0
          "platform does not support uname");
212
0
  return NULL;
213
0
#endif
214
0
}
215
#endif
216
217
/**
218
 * fu_kernel_get_config:
219
 * @pstore: a #FuPathStore
220
 * @error: (nullable): optional return location for an error
221
 *
222
 * Loads all the kernel options into a hash table. Commented out options are not included.
223
 *
224
 * Returns: (transfer container) (element-type utf8 utf8): options from the kernel
225
 *
226
 * Since: 1.8.5
227
 **/
228
GHashTable *
229
fu_kernel_get_config(FuPathStore *pstore, GError **error)
230
0
{
231
0
#ifdef __linux__
232
0
  gsize bufsz = 0;
233
0
  g_autofree gchar *buf = NULL;
234
0
  g_autofree gchar *fn = NULL;
235
0
  g_autofree gchar *config_fngz = NULL;
236
237
0
  g_return_val_if_fail(FU_IS_PATH_STORE(pstore), NULL);
238
0
  g_return_val_if_fail(error == NULL || *error == NULL, NULL);
239
240
  /* try /proc/config.gz -- which will only work with CONFIG_IKCONFIG */
241
0
  config_fngz =
242
0
      fu_path_store_build_filename(pstore, NULL, FU_PATH_KIND_PROCFS, "config.gz", NULL);
243
0
  if (config_fngz != NULL && g_file_test(config_fngz, G_FILE_TEST_EXISTS)) {
244
0
    g_autoptr(GBytes) payload = NULL;
245
0
    g_autoptr(FwupdFileInputStream) istream1 = NULL;
246
0
    g_autoptr(FuInputStream) istream2 = NULL;
247
248
0
    istream1 = fwupd_file_input_stream_new(config_fngz, error);
249
0
    if (istream1 == NULL)
250
0
      return NULL;
251
0
    istream2 = fu_compressor_stream_new_decompress(FWUPD_INPUT_STREAM(istream1),
252
0
                     FWUPD_COMPRESSOR_FORMAT_GZIP,
253
0
                     error);
254
0
    if (istream2 == NULL)
255
0
      return NULL;
256
0
    payload = fu_input_stream_read_bytes(istream2, 0, G_MAXSIZE, NULL, error);
257
0
    if (payload == NULL)
258
0
      return NULL;
259
0
    return fu_kernel_parse_config(g_bytes_get_data(payload, NULL),
260
0
                g_bytes_get_size(payload),
261
0
                error);
262
0
  }
263
264
  /* fall back to /boot/config-$(uname -r) */
265
0
  fn = fu_kernel_get_config_path(pstore, error);
266
0
  if (fn == NULL)
267
0
    return NULL;
268
0
  if (!g_file_get_contents(fn, &buf, &bufsz, error))
269
0
    return NULL;
270
0
  return fu_kernel_parse_config(buf, bufsz, error);
271
#else
272
  g_set_error_literal(error,
273
          FWUPD_ERROR,
274
          FWUPD_ERROR_INTERNAL,
275
          "platform does not support getting the kernel config");
276
  return NULL;
277
#endif
278
0
}
279
280
/**
281
 * fu_kernel_parse_cmdline:
282
 * @buf: (not nullable): cmdline to parse
283
 * @bufsz: size of @bufsz
284
 *
285
 * Parses all the kernel key/values into a hash table, respecting double quotes when required.
286
 *
287
 * Returns: (transfer container) (element-type utf8 utf8): keys from the cmdline
288
 *
289
 * Since: 1.9.1
290
 **/
291
GHashTable *
292
fu_kernel_parse_cmdline(const gchar *buf, gsize bufsz)
293
0
{
294
0
  gboolean is_escape = FALSE;
295
0
  g_autoptr(GHashTable) hash = NULL;
296
0
  g_autoptr(GString) acc = g_string_new(NULL);
297
298
0
  g_return_val_if_fail(buf != NULL, NULL);
299
300
0
  hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
301
0
  if (bufsz == 0)
302
0
    return g_steal_pointer(&hash);
303
0
  for (gsize i = 0; i < bufsz; i++) {
304
0
    if (!is_escape && (buf[i] == ' ' || buf[i] == '\n') && acc->len > 0) {
305
0
      g_auto(GStrv) kv = g_strsplit(acc->str, "=", 2);
306
0
      g_hash_table_insert(hash, g_strdup(kv[0]), g_strdup(kv[1]));
307
0
      g_string_set_size(acc, 0);
308
0
      continue;
309
0
    }
310
0
    if (buf[i] == '"') {
311
0
      is_escape = !is_escape;
312
0
      continue;
313
0
    }
314
0
    g_string_append_c(acc, buf[i]);
315
0
  }
316
0
  if (acc->len > 0) {
317
0
    g_auto(GStrv) kv = g_strsplit(acc->str, "=", 2);
318
0
    g_hash_table_insert(hash, g_strdup(kv[0]), g_strdup(kv[1]));
319
0
  }
320
321
  /* success */
322
0
  return g_steal_pointer(&hash);
323
0
}
324
325
/**
326
 * fu_kernel_get_cmdline:
327
 * @error: (nullable): optional return location for an error
328
 *
329
 * Loads all the kernel /proc/cmdline key/values into a hash table.
330
 *
331
 * Returns: (transfer container) (element-type utf8 utf8): keys from the kernel command line
332
 *
333
 * Since: 1.8.5
334
 **/
335
GHashTable *
336
fu_kernel_get_cmdline(GError **error)
337
0
{
338
0
#ifdef __linux__
339
0
  gsize bufsz = 0;
340
0
  g_autofree gchar *buf = NULL;
341
342
0
  g_return_val_if_fail(error == NULL || *error == NULL, NULL);
343
344
0
  if (!g_file_get_contents("/proc/cmdline", &buf, &bufsz, error))
345
0
    return NULL;
346
0
  return fu_kernel_parse_cmdline(buf, bufsz);
347
#else
348
  g_set_error_literal(error,
349
          FWUPD_ERROR,
350
          FWUPD_ERROR_INTERNAL,
351
          "platform does not support getting the kernel cmdline");
352
  return NULL;
353
#endif
354
0
}
355
356
gboolean
357
fu_kernel_check_cmdline_mutable(FuPathStore *pstore, GError **error)
358
0
{
359
0
  g_autofree gchar *grubby_path = NULL;
360
0
  g_auto(GStrv) config_files = g_new0(gchar *, 3);
361
362
0
  g_return_val_if_fail(FU_IS_PATH_STORE(pstore), FALSE);
363
0
  g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
364
365
  /* not found */
366
0
  grubby_path = fu_path_store_find_program(pstore, "grubby", error);
367
0
  if (grubby_path == NULL)
368
0
    return FALSE;
369
370
  /* check all the config files are writable */
371
0
  config_files[0] = fu_path_store_build_filename(pstore,
372
0
                   error,
373
0
                   FU_PATH_KIND_HOSTFS_BOOT,
374
0
                   "grub2",
375
0
                   "grub.cfg",
376
0
                   NULL);
377
0
  if (config_files[0] == NULL)
378
0
    return FALSE;
379
0
  config_files[1] =
380
0
      fu_path_store_build_filename(pstore, error, FU_PATH_KIND_SYSCONFDIR, "grub.cfg", NULL);
381
0
  if (config_files[1] == NULL)
382
0
    return FALSE;
383
0
  for (guint i = 0; config_files[i] != NULL; i++) {
384
0
    g_autoptr(GFile) file = g_file_new_for_path(config_files[i]);
385
0
    g_autoptr(GFileInfo) info = NULL;
386
0
    g_autoptr(GError) error_local = NULL;
387
388
0
    if (!g_file_query_exists(file, NULL))
389
0
      continue;
390
0
    info = g_file_query_info(file,
391
0
           G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE,
392
0
           G_FILE_QUERY_INFO_NONE,
393
0
           NULL,
394
0
           &error_local);
395
0
    if (info == NULL) {
396
0
      g_warning("failed to get info for %s: %s",
397
0
          config_files[i],
398
0
          error_local->message);
399
0
      continue;
400
0
    }
401
0
    if (!g_file_info_get_attribute_boolean(info, G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE)) {
402
0
      g_set_error(error,
403
0
            FWUPD_ERROR,
404
0
            FWUPD_ERROR_NOT_SUPPORTED,
405
0
            "%s is not writable",
406
0
            config_files[i]);
407
0
      return FALSE;
408
0
    }
409
0
  }
410
411
  /* success */
412
0
  return TRUE;
413
0
}
414
415
static gboolean
416
fu_kernel_set_commandline(FuPathStore *pstore, const gchar *arg, gboolean enable, GError **error)
417
0
{
418
0
  g_autofree gchar *output = NULL;
419
0
  g_autofree gchar *arg_string = NULL;
420
0
  g_autofree gchar *grubby_path = NULL;
421
0
  const gchar *argv_grubby[] = {"", "--update-kernel=DEFAULT", "", NULL};
422
423
0
  grubby_path = fu_path_store_find_program(pstore, "grubby", error);
424
0
  if (grubby_path == NULL) {
425
0
    g_prefix_error_literal(error, "failed to find grubby: ");
426
0
    return FALSE;
427
0
  }
428
0
  if (enable)
429
0
    arg_string = g_strdup_printf("--args=%s", arg);
430
0
  else
431
0
    arg_string = g_strdup_printf("--remove-args=%s", arg);
432
433
0
  argv_grubby[0] = grubby_path;
434
0
  argv_grubby[2] = arg_string;
435
0
  return g_spawn_sync(NULL, /* nocheck:blocked */
436
0
          (gchar **)argv_grubby,
437
0
          NULL,
438
0
          G_SPAWN_DEFAULT,
439
0
          NULL,
440
0
          NULL,
441
0
          &output,
442
0
          NULL,
443
0
          NULL,
444
0
          error);
445
0
}
446
447
/**
448
 * fu_kernel_add_cmdline_arg:
449
 * @pstore: a #FuPathStore
450
 * @arg: (not nullable): key to set
451
 * @error: (nullable): optional return location for an error
452
 *
453
 * Add a kernel command line argument.
454
 *
455
 * Returns: %TRUE if successful
456
 *
457
 * Since: 1.9.5
458
 **/
459
gboolean
460
fu_kernel_add_cmdline_arg(FuPathStore *pstore, const gchar *arg, GError **error)
461
0
{
462
0
  g_return_val_if_fail(FU_IS_PATH_STORE(pstore), FALSE);
463
0
  g_return_val_if_fail(arg != NULL, FALSE);
464
0
  g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
465
0
  return fu_kernel_set_commandline(pstore, arg, TRUE, error);
466
0
}
467
468
/**
469
 * fu_kernel_remove_cmdline_arg:
470
 * @pstore: a #FuPathStore
471
 * @arg: (not nullable): key to set
472
 * @error: (nullable): optional return location for an error
473
 *
474
 * Remove a kernel command line argument.
475
 *
476
 * Returns: %TRUE if successful
477
 *
478
 * Since: 1.9.5
479
 **/
480
gboolean
481
fu_kernel_remove_cmdline_arg(FuPathStore *pstore, const gchar *arg, GError **error)
482
0
{
483
0
  g_return_val_if_fail(FU_IS_PATH_STORE(pstore), FALSE);
484
0
  g_return_val_if_fail(arg != NULL, FALSE);
485
0
  g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
486
0
  return fu_kernel_set_commandline(pstore, arg, FALSE, error);
487
0
}