Files
systemd/src/basic/proc-cmdline.c
Lennart Poettering da65941c3e coredump,analyze: use read_full_file() for reading various top-level /proc/ files
Kernel API file systems typically use either "raw" or "seq_file" to
implement their various interface files. The former are really simple
(to point I'd call them broken), in that they have no understanding of
file offsets, and return their contents again and again on every read(),
and thus EOF is indicated by a short read, not by a zero read. The
latter otoh works like a typical file: you read until you get a
zero-sized read back.

We have read_virtual_file() to read the "raw" files, and can use regular
read_full_file() to read the "seq_file" ones.

Apparently all files in the top-level /proc/ directory use 'seq_file'.
but we accidentally used read_virtual_file() for them. Fix that.

Also clarify in a comment what the rules are.

Fixes: #36131
2025-03-13 14:18:46 +01:00

418 lines
13 KiB
C

/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <stdbool.h>
#include <stddef.h>
#include "alloc-util.h"
#include "efivars.h"
#include "extract-word.h"
#include "fileio.h"
#include "getopt-defs.h"
#include "initrd-util.h"
#include "macro.h"
#include "parse-util.h"
#include "proc-cmdline.h"
#include "process-util.h"
#include "string-util.h"
#include "strv.h"
#include "virt.h"
int proc_cmdline_filter_pid1_args(char **argv, char ***ret) {
enum {
COMMON_GETOPT_ARGS,
SYSTEMD_GETOPT_ARGS,
SHUTDOWN_GETOPT_ARGS,
};
static const struct option options[] = {
COMMON_GETOPT_OPTIONS,
SYSTEMD_GETOPT_OPTIONS,
SHUTDOWN_GETOPT_OPTIONS,
};
static const char *short_options = SYSTEMD_GETOPT_SHORT_OPTIONS;
_cleanup_strv_free_ char **filtered = NULL;
int state, r;
assert(argv);
assert(ret);
/* Currently, we do not support '-', '+', and ':' at the beginning. */
assert(!IN_SET(short_options[0], '-', '+', ':'));
/* Filter out all known options. */
state = no_argument;
STRV_FOREACH(p, strv_skip(argv, 1)) {
int prev_state = state;
const char *a = *p;
/* Reset the state for the next step. */
state = no_argument;
if (prev_state == required_argument ||
(prev_state == optional_argument && a[0] != '-'))
/* Handled as an argument of the previous option, filtering out the string. */
continue;
if (a[0] != '-') {
/* Not an option, accepting the string. */
r = strv_extend(&filtered, a);
if (r < 0)
return r;
continue;
}
if (a[1] == '-') {
if (a[2] == '\0') {
/* "--" is specified, accepting remaining strings. */
r = strv_extend_strv(&filtered, strv_skip(p, 1), /* filter_duplicates = */ false);
if (r < 0)
return r;
break;
}
/* long option, e.g. --foo */
FOREACH_ELEMENT(option, options) {
const char *q = startswith(a + 2, option->name);
if (!q || !IN_SET(q[0], '=', '\0'))
continue;
/* Found matching option, updating the state if necessary. */
if (q[0] == '\0' && option->has_arg == required_argument)
state = required_argument;
break;
}
continue;
}
/* short option(s), e.g. -x or -xyz */
while (a && *++a != '\0')
for (const char *q = short_options; *q != '\0'; q++) {
if (*q != *a)
continue;
/* Found matching short option. */
if (q[1] == ':') {
/* An argument is required or optional, and remaining part
* is handled as argument if exists. */
state = a[1] != '\0' ? no_argument :
q[2] == ':' ? optional_argument : required_argument;
a = NULL; /* Not necessary to parse remaining part. */
}
break;
}
}
*ret = TAKE_PTR(filtered);
return 0;
}
int proc_cmdline(char **ret) {
const char *e;
assert(ret);
/* For testing purposes it is sometimes useful to be able to override what we consider /proc/cmdline to be */
e = secure_getenv("SYSTEMD_PROC_CMDLINE");
if (e)
return strdup_to(ret, e);
if (detect_container() > 0)
return pid_get_cmdline(1, SIZE_MAX, 0, ret);
return read_full_file("/proc/cmdline", ret, /* ret_size= */ NULL);
}
static int proc_cmdline_strv_internal(char ***ret, bool filter_pid1_args) {
const char *e;
int r;
assert(ret);
/* For testing purposes it is sometimes useful to be able to override what we consider /proc/cmdline to be */
e = secure_getenv("SYSTEMD_PROC_CMDLINE");
if (e)
return strv_split_full(ret, e, NULL, EXTRACT_UNQUOTE|EXTRACT_RELAX|EXTRACT_RETAIN_ESCAPE);
if (detect_container() > 0) {
_cleanup_strv_free_ char **args = NULL;
r = pid_get_cmdline_strv(1, /* flags = */ 0, &args);
if (r < 0)
return r;
if (filter_pid1_args)
return proc_cmdline_filter_pid1_args(args, ret);
*ret = TAKE_PTR(args);
return 0;
} else {
_cleanup_free_ char *s = NULL;
r = read_full_file("/proc/cmdline", &s, NULL);
if (r < 0)
return r;
return strv_split_full(ret, s, NULL, EXTRACT_UNQUOTE|EXTRACT_RELAX|EXTRACT_RETAIN_ESCAPE);
}
}
int proc_cmdline_strv(char ***ret) {
return proc_cmdline_strv_internal(ret, /* filter_pid1_args = */ false);
}
static char *mangle_word(const char *word, ProcCmdlineFlags flags) {
char *c;
c = startswith(word, "rd.");
if (c) {
/* Filter out arguments that are intended only for the initrd */
if (!in_initrd())
return NULL;
if (FLAGS_SET(flags, PROC_CMDLINE_STRIP_RD_PREFIX))
return c;
} else if (FLAGS_SET(flags, PROC_CMDLINE_RD_STRICT) && in_initrd())
/* And optionally filter out arguments that are intended only for the host */
return NULL;
return (char*) word;
}
static int proc_cmdline_parse_strv(char **args, proc_cmdline_parse_t parse_item, void *data, ProcCmdlineFlags flags) {
int r;
assert(parse_item);
STRV_FOREACH(word, args) {
char *key, *value;
key = mangle_word(*word, flags);
if (!key)
continue;
value = strchr(key, '=');
if (value)
*(value++) = '\0';
r = parse_item(key, value, data);
if (r < 0)
return r;
}
return 0;
}
int proc_cmdline_parse(proc_cmdline_parse_t parse_item, void *data, ProcCmdlineFlags flags) {
_cleanup_strv_free_ char **args = NULL;
int r;
assert(parse_item);
/* The PROC_CMDLINE_VALUE_OPTIONAL and PROC_CMDLINE_TRUE_WHEN_MISSING flags don't really make sense
* for proc_cmdline_parse(), let's make this clear. */
assert(!(flags & (PROC_CMDLINE_VALUE_OPTIONAL|PROC_CMDLINE_TRUE_WHEN_MISSING)));
r = proc_cmdline_strv_internal(&args, /* filter_pid1_args = */ true);
if (r < 0)
return r;
return proc_cmdline_parse_strv(args, parse_item, data, flags);
}
static bool relaxed_equal_char(char a, char b) {
return a == b ||
(a == '_' && b == '-') ||
(a == '-' && b == '_');
}
char* proc_cmdline_key_startswith(const char *s, const char *prefix) {
assert(s);
assert(prefix);
/* Much like startswith(), but considers "-" and "_" the same */
for (; *prefix != 0; s++, prefix++)
if (!relaxed_equal_char(*s, *prefix))
return NULL;
return (char*) s;
}
bool proc_cmdline_key_streq(const char *x, const char *y) {
assert(x);
assert(y);
/* Much like streq(), but considers "-" and "_" the same */
for (; *x != 0 || *y != 0; x++, y++)
if (!relaxed_equal_char(*x, *y))
return false;
return true;
}
static int cmdline_get_key(char **args, const char *key, ProcCmdlineFlags flags, char **ret_value) {
_cleanup_free_ char *v = NULL;
bool found = false;
int r;
assert(key);
STRV_FOREACH(p, args) {
const char *word;
word = mangle_word(*p, flags);
if (!word)
continue;
if (ret_value) {
const char *e;
e = proc_cmdline_key_startswith(word, key);
if (!e)
continue;
if (*e == '=') {
r = free_and_strdup(&v, e+1);
if (r < 0)
return r;
found = true;
} else if (*e == 0 && FLAGS_SET(flags, PROC_CMDLINE_VALUE_OPTIONAL))
found = true;
} else {
if (proc_cmdline_key_streq(word, key)) {
found = true;
break; /* we found what we were looking for */
}
}
}
if (ret_value)
*ret_value = TAKE_PTR(v);
return found;
}
int proc_cmdline_get_key(const char *key, ProcCmdlineFlags flags, char **ret_value) {
_cleanup_strv_free_ char **args = NULL;
int r;
/* Looks for a specific key on the kernel command line. Supports three modes:
*
* a) The "ret_value" parameter is used. In this case a parameter beginning with the "key" string followed by
* "=" is searched for, and the value following it is returned in "ret_value".
*
* b) as above, but the PROC_CMDLINE_VALUE_OPTIONAL flag is set. In this case if the key is found as a separate
* word (i.e. not followed by "=" but instead by whitespace or the end of the command line), then this is
* also accepted, and "value" is returned as NULL.
*
* c) The "ret_value" parameter is NULL. In this case a search for the exact "key" parameter is performed.
*
* In all three cases, > 0 is returned if the key is found, 0 if not. */
/* PROC_CMDLINE_TRUE_WHEN_MISSING doesn't really make sense for proc_cmdline_get_key(). */
assert(!FLAGS_SET(flags, PROC_CMDLINE_TRUE_WHEN_MISSING));
if (isempty(key))
return -EINVAL;
if (FLAGS_SET(flags, PROC_CMDLINE_VALUE_OPTIONAL) && !ret_value)
return -EINVAL;
r = proc_cmdline_strv_internal(&args, /* filter_pid1_args = */ true);
if (r < 0)
return r;
return cmdline_get_key(args, key, flags, ret_value);
}
int proc_cmdline_get_bool(const char *key, ProcCmdlineFlags flags, bool *ret) {
_cleanup_free_ char *v = NULL;
int r;
assert(ret);
r = proc_cmdline_get_key(key, (flags & ~PROC_CMDLINE_TRUE_WHEN_MISSING) | PROC_CMDLINE_VALUE_OPTIONAL, &v);
if (r < 0)
return r;
if (r == 0) { /* key not specified at all */
*ret = FLAGS_SET(flags, PROC_CMDLINE_TRUE_WHEN_MISSING);
return 0;
}
if (v) { /* key with parameter passed */
r = parse_boolean(v);
if (r < 0)
return r;
*ret = r;
} else /* key without parameter passed */
*ret = true;
return 1;
}
static int cmdline_get_key_ap(ProcCmdlineFlags flags, char* const* args, va_list ap) {
int r, ret = 0;
for (;;) {
char **v;
const char *k, *e;
k = va_arg(ap, const char*);
if (!k)
break;
assert_se(v = va_arg(ap, char**));
STRV_FOREACH(p, args) {
const char *word;
word = mangle_word(*p, flags);
if (!word)
continue;
e = proc_cmdline_key_startswith(word, k);
if (e && *e == '=') {
r = free_and_strdup(v, e + 1);
if (r < 0)
return r;
ret++;
}
}
}
return ret;
}
int proc_cmdline_get_key_many_internal(ProcCmdlineFlags flags, ...) {
_cleanup_strv_free_ char **args = NULL;
int r;
va_list ap;
/* The PROC_CMDLINE_VALUE_OPTIONAL and PROC_CMDLINE_TRUE_WHEN_MISSING flags don't really make sense
* for proc_cmdline_get_key_many, let's make this clear. */
assert(!(flags & (PROC_CMDLINE_VALUE_OPTIONAL|PROC_CMDLINE_TRUE_WHEN_MISSING)));
/* This call may clobber arguments on failure! */
r = proc_cmdline_strv(&args);
if (r < 0)
return r;
va_start(ap, flags);
r = cmdline_get_key_ap(flags, args, ap);
va_end(ap);
return r;
}