Coverage Report

Created: 2025-07-01 07:09

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