From 5c76cb0e27b02d7f100c7e62984b4326f70e8f15 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Sat, 8 Aug 2026 09:41:14 +0900 Subject: [PATCH 1/6] sysctl: --inline requires positional arguments --- man/systemd-sysctl.service.xml | 2 +- src/sysctl/sysctl.c | 4 ++++ test/units/TEST-87-AUX-UTILS-VM.sysctl.sh | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/man/systemd-sysctl.service.xml b/man/systemd-sysctl.service.xml index a817a6b33f..36f446dcc7 100644 --- a/man/systemd-sysctl.service.xml +++ b/man/systemd-sysctl.service.xml @@ -105,7 +105,7 @@ Treat each positional argument as a separate configuration line instead of a file - name. + name. When specified, at least one positional argument must be specified. diff --git a/src/sysctl/sysctl.c b/src/sysctl/sysctl.c index fa95932985..b4d63635a5 100644 --- a/src/sysctl/sysctl.c +++ b/src/sysctl/sysctl.c @@ -401,6 +401,10 @@ static int parse_argv(int argc, char *argv[], char ***remaining_args) { return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Positional arguments are not allowed with --cat-config/--tldr."); + if (arg_inline && strv_isempty(*remaining_args)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "--inline requires positional arguments."); + return 1; } diff --git a/test/units/TEST-87-AUX-UTILS-VM.sysctl.sh b/test/units/TEST-87-AUX-UTILS-VM.sysctl.sh index 9385c05a47..8333ce9787 100755 --- a/test/units/TEST-87-AUX-UTILS-VM.sysctl.sh +++ b/test/units/TEST-87-AUX-UTILS-VM.sysctl.sh @@ -16,6 +16,7 @@ echo "-foo.foo=42" >/tmp/foo.conf assert_rc 0 /usr/lib/systemd/systemd-sysctl /tmp/foo.conf assert_rc 0 /usr/lib/systemd/systemd-sysctl --strict /tmp/foo.conf +assert_rc 1 /usr/lib/systemd/systemd-sysctl --inline assert_rc 0 /usr/lib/systemd/systemd-sysctl --inline "foo.bar=42" assert_rc 1 /usr/lib/systemd/systemd-sysctl --inline --strict "foo.bar=42" assert_rc 0 /usr/lib/systemd/systemd-sysctl --inline -- "-foo.bar=42" From 1c223728cf404d7d5c1802e0d5b149b966a80e0e Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Sat, 8 Aug 2026 19:45:18 +0900 Subject: [PATCH 2/6] sysctl: skip leading whitespaces and ignore arguments start with '#' when --inline --- src/sysctl/sysctl.c | 12 +++++++++--- test/units/TEST-87-AUX-UTILS-VM.sysctl.sh | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/sysctl/sysctl.c b/src/sysctl/sysctl.c index b4d63635a5..f14f885773 100644 --- a/src/sysctl/sysctl.c +++ b/src/sysctl/sysctl.c @@ -425,10 +425,16 @@ static int run(int argc, char *argv[]) { unsigned pos = 0; STRV_FOREACH(arg, args) { - if (arg_inline) + if (arg_inline) { + pos++; + + const char *s = skip_leading_chars(*arg, /* bad= */ NULL); + if (IN_SET(*s, '\0', '#')) + continue; + /* Use (argument):n, where n==1 for the first positional arg */ - RET_GATHER(r, parse_line("(argument)", ++pos, *arg, /* invalid_config= */ NULL, &sysctl_options)); - else + RET_GATHER(r, parse_line("(argument)", pos, s, /* invalid_config= */ NULL, &sysctl_options)); + } else RET_GATHER(r, parse_file(&sysctl_options, *arg, false)); } } else { diff --git a/test/units/TEST-87-AUX-UTILS-VM.sysctl.sh b/test/units/TEST-87-AUX-UTILS-VM.sysctl.sh index 8333ce9787..fe1b5f1b33 100755 --- a/test/units/TEST-87-AUX-UTILS-VM.sysctl.sh +++ b/test/units/TEST-87-AUX-UTILS-VM.sysctl.sh @@ -17,7 +17,7 @@ assert_rc 0 /usr/lib/systemd/systemd-sysctl /tmp/foo.conf assert_rc 0 /usr/lib/systemd/systemd-sysctl --strict /tmp/foo.conf assert_rc 1 /usr/lib/systemd/systemd-sysctl --inline -assert_rc 0 /usr/lib/systemd/systemd-sysctl --inline "foo.bar=42" +assert_rc 0 /usr/lib/systemd/systemd-sysctl --inline " foo.bar=42" "#comment" " " assert_rc 1 /usr/lib/systemd/systemd-sysctl --inline --strict "foo.bar=42" assert_rc 0 /usr/lib/systemd/systemd-sysctl --inline -- "-foo.bar=42" assert_rc 0 /usr/lib/systemd/systemd-sysctl --inline --strict -- "-foo.bar=42" From 45bd01544109ec3bafd0cc42b06a05d01702faf2 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Fri, 7 Aug 2026 22:45:26 +0900 Subject: [PATCH 3/6] sysctl: add --save= option to save loaded settings This may be useful for saving sysctl settings provided on the command line, via standard input, or with --inline. --- src/sysctl/sysctl.c | 123 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 120 insertions(+), 3 deletions(-) diff --git a/src/sysctl/sysctl.c b/src/sysctl/sysctl.c index f14f885773..3a7f64fc14 100644 --- a/src/sysctl/sysctl.c +++ b/src/sysctl/sysctl.c @@ -10,16 +10,20 @@ #include "constants.h" #include "creds-util.h" #include "errno-util.h" +#include "fs-util.h" #include "glob-util.h" #include "hashmap.h" #include "log.h" #include "main-func.h" +#include "mkdir.h" +#include "options.h" #include "pager.h" #include "path-util.h" #include "pretty-print.h" #include "string-util.h" #include "strv.h" #include "sysctl-util.h" +#include "tmpfile-util.h" #include "verbs.h" static char **arg_prefixes = NULL; @@ -28,8 +32,10 @@ static bool arg_strict = false; static bool arg_verify = false; static bool arg_inline = false; static PagerFlags arg_pager_flags = 0; +static char *arg_save_file = NULL; STATIC_DESTRUCTOR_REGISTER(arg_prefixes, strv_freep); +STATIC_DESTRUCTOR_REGISTER(arg_save_file, freep); COMMAND( "systemd-sysctl\0", @@ -226,6 +232,13 @@ static int apply_all(OrderedHashmap *sysctl_options) { return r; } +static void filter_sysctl_options(OrderedHashmap *sysctl_options) { + SysctlOption *option; + ORDERED_HASHMAP_FOREACH(option, sysctl_options) + if (!string_is_glob(option->key) && !test_prefix(option->key)) + sysctl_option_free(ordered_hashmap_remove(sysctl_options, option->key)); +} + static int parse_line(const char *fname, unsigned line, const char *buffer, bool *invalid_config, void *userdata) { OrderedHashmap **sysctl_options = ASSERT_PTR(userdata); _cleanup_free_ char *k = NULL, *v = NULL; @@ -262,9 +275,19 @@ static int parse_line(const char *fname, unsigned line, const char *buffer, bool const char *key = sysctl_normalize(strstrip(k)), *value = strstrip(v); - /* We can't filter out globs at this point, we'll need to do that later. */ - if (!string_is_glob(key) && !test_prefix(key)) - return 0; + if (arg_save_file) { + /* When saving the loaded rules, refuse unsafe characters, especially newlines, in keys + * and values. Otherwise, the saved file may not be parsed correctly. */ + if (!string_is_safe(key, STRING_ALLOW_BACKSLASHES | STRING_ALLOW_QUOTES | STRING_ALLOW_GLOBS) || + (value && !string_is_safe(value, STRING_ALLOW_EMPTY | STRING_ALLOW_BACKSLASHES | STRING_ALLOW_QUOTES | STRING_ALLOW_GLOBS))) + return log_syntax(NULL, LOG_WARNING, fname, line, SYNTHETIC_ERRNO(EINVAL), + "Ignoring unsafe key and/or value for '%s'.", key); + } else { + /* We can't filter out globs at this point, we'll need to do that later. + * Keep them when saving the loaded rules, and filter them after writing. */ + if (!string_is_glob(key) && !test_prefix(key)) + return 0; + } SysctlOption *existing = ordered_hashmap_get(*sysctl_options, key); if (existing) { @@ -325,7 +348,82 @@ static int cat_config(char **files) { return cat_files(NULL, files, arg_cat_flags); } +static int save_sysctl_options(OrderedHashmap *sysctl_options, const char *filename) { + int r; + + assert(filename); + + if (ordered_hashmap_isempty(sysctl_options)) { + log_info("No sysctl settings loaded, not creating '%s'.", filename); + return 0; + } + + r = mkdir_p("/run/sysctl.d/", 0755); + if (r < 0) + return log_error_errno(r, "Failed to create directory '/run/sysctl.d/': %m"); + + _cleanup_free_ char *path = path_join("/run/sysctl.d/", filename); + if (!path) + return log_oom(); + + _cleanup_fclose_ FILE *f = NULL; + _cleanup_(unlink_and_freep) char *temp_path = NULL; + r = fopen_temporary(path, &f, &temp_path); + if (r < 0) + return log_error_errno(r, "Failed to open a temporary file for '%s': %m", path); + + if (fchmod(fileno(f), 0644) < 0) + return log_error_errno(errno, "Failed to chmod '%s': %m", temp_path); + + fputs("# This is generated by systemd-sysctl.\n", f); + + SysctlOption *option; + ORDERED_HASHMAP_FOREACH(option, sysctl_options) + fprintf(f, "%s%s%s%s\n", + option->ignore_failure || !option->value ? "-" : "", + option->key, + option->value ? " = " : "", + strempty(option->value)); + + r = fflush_and_check(f); + if (r < 0) + return log_error_errno(r, "Failed to flush '%s': %m", temp_path); + + r = conservative_rename(temp_path, path); + if (r < 0) + return log_error_errno(r, "Failed to rename '%s' to '%s': %m", temp_path, path); + + temp_path = mfree(temp_path); + return 0; +} + +static int parse_conf_filename(const char *src, const char *option, char **dest) { + assert(option); + assert(dest); + + if (isempty(src)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot use an empty filename for %s.", option); + + if (startswith(src, ".")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Dot-prefixed filenames are not allowed for %s.", option); + + _cleanup_free_ char *name = NULL; + if (!endswith(src, ".conf")) + name = strjoin(src, ".conf"); + else + name = strdup(src); + if (!name) + return log_oom(); + + if (!filename_is_valid(name)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid file name '%s' specified for %s.", name, option); + + return free_and_replace(*dest, name); +} + static int parse_argv(int argc, char *argv[], char ***remaining_args) { + int r; + assert(argc >= 0); assert(argv); assert(remaining_args); @@ -391,6 +489,13 @@ static int parse_argv(int argc, char *argv[], char ***remaining_args) { arg_inline = true; break; + OPTION_LONG("save", "FILENAME", + "Write loaded settings to the specified file"): + r = parse_conf_filename(opts.arg, "--save=", &arg_save_file); + if (r < 0) + return r; + break; + OPTION_COMMON_INTROSPECT_CLI: return introspect_cli(SD_JSON_FORMAT_OFF); } @@ -405,6 +510,10 @@ static int parse_argv(int argc, char *argv[], char ***remaining_args) { return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--inline requires positional arguments."); + if (arg_save_file && strv_isempty(*remaining_args)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "--save= requires positional arguments."); + return 1; } @@ -437,6 +546,14 @@ static int run(int argc, char *argv[]) { } else RET_GATHER(r, parse_file(&sysctl_options, *arg, false)); } + + if (arg_save_file) { + r = save_sysctl_options(sysctl_options, arg_save_file); + if (r < 0) + return r; + + filter_sysctl_options(sysctl_options); + } } else { _cleanup_strv_free_ char **files = NULL; From fb19c2ffa2a4987c95921b7735effe4aca3b6fa4 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Fri, 7 Aug 2026 23:07:40 +0900 Subject: [PATCH 4/6] sysctl: introduce --revert= option to remove runtime sysctl config This may be useful for removing files created by the --save= option. --- src/sysctl/sysctl.c | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/sysctl/sysctl.c b/src/sysctl/sysctl.c index 3a7f64fc14..1e94643cad 100644 --- a/src/sysctl/sysctl.c +++ b/src/sysctl/sysctl.c @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #include +#include #include "sd-json.h" @@ -33,9 +34,11 @@ static bool arg_verify = false; static bool arg_inline = false; static PagerFlags arg_pager_flags = 0; static char *arg_save_file = NULL; +static char *arg_revert_file = NULL; STATIC_DESTRUCTOR_REGISTER(arg_prefixes, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_save_file, freep); +STATIC_DESTRUCTOR_REGISTER(arg_revert_file, freep); COMMAND( "systemd-sysctl\0", @@ -496,6 +499,13 @@ static int parse_argv(int argc, char *argv[], char ***remaining_args) { return r; break; + OPTION_LONG("revert", "FILENAME", + "Remove the specified file from /run/sysctl.d/"): + r = parse_conf_filename(opts.arg, "--revert=", &arg_revert_file); + if (r < 0) + return r; + break; + OPTION_COMMON_INTROSPECT_CLI: return introspect_cli(SD_JSON_FORMAT_OFF); } @@ -529,6 +539,15 @@ static int run(int argc, char *argv[]) { umask(0022); + if (arg_revert_file) { + _cleanup_free_ char *path = path_join("/run/sysctl.d/", arg_revert_file); + if (!path) + return log_oom(); + + if (unlink(path) < 0 && errno != ENOENT) + return log_error_errno(errno, "Failed to remove '%s': %m", path); + } + _cleanup_ordered_hashmap_free_ OrderedHashmap *sysctl_options = NULL; if (!strv_isempty(args)) { unsigned pos = 0; From 80e346af7481ebbd0f337f9b909fa527a4602649 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Sat, 8 Aug 2026 02:59:26 +0900 Subject: [PATCH 5/6] test: add test cases for systemd-sysctl --save= and --revert= --- test/units/TEST-87-AUX-UTILS-VM.sysctl.sh | 131 ++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/test/units/TEST-87-AUX-UTILS-VM.sysctl.sh b/test/units/TEST-87-AUX-UTILS-VM.sysctl.sh index fe1b5f1b33..f19a0ba774 100755 --- a/test/units/TEST-87-AUX-UTILS-VM.sysctl.sh +++ b/test/units/TEST-87-AUX-UTILS-VM.sysctl.sh @@ -92,3 +92,134 @@ EOF assert_eq "$(cat /proc/sys/net/ipv4/conf/hoge/drop_gratuitous_arp)" "1" assert_eq "$(cat /proc/sys/net/ipv4/conf/hoge/bootp_relay)" "1" assert_eq "$(cat /proc/sys/net/ipv4/conf/hoge/disable_policy)" "0" + +########################## +# --save= and --revert= +########################## +(! /usr/lib/systemd/systemd-sysctl --save=) +(! /usr/lib/systemd/systemd-sysctl --save=.) +(! /usr/lib/systemd/systemd-sysctl --save=.conf) +(! /usr/lib/systemd/systemd-sysctl --save=aaa/hoge.conf) +(! /usr/lib/systemd/systemd-sysctl --save=../hoge.conf) +(! /usr/lib/systemd/systemd-sysctl --revert=) +(! /usr/lib/systemd/systemd-sysctl --revert=.) +(! /usr/lib/systemd/systemd-sysctl --revert=.conf) +(! /usr/lib/systemd/systemd-sysctl --revert=aaa/hoge.conf) +(! /usr/lib/systemd/systemd-sysctl --revert=../hoge.conf) + +# --save= requires positional arguments +(! /usr/lib/systemd/systemd-sysctl --save=foo) + +# Do not create an empty conf file. +/usr/lib/systemd/systemd-sysctl \ + --prefix=/net/ipv4/conf/hoge --save=foo --inline "# comment only" +[[ ! -e /run/sysctl.d/foo.conf ]] + +echo 0 >/proc/sys/net/ipv4/conf/hoge/drop_gratuitous_arp +echo 0 >/proc/sys/net/ipv4/conf/hoge/bootp_relay +echo 0 >/proc/sys/net/ipv4/conf/hoge/disable_policy + +/usr/lib/systemd/systemd-sysctl \ + --prefix=/net/ipv4/conf/hoge --save=foo --inline -- \ + '# This argument should be ignored' \ + 'net.ipv4.conf.*.drop_gratuitous_arp=1' \ + 'net.ipv4.*.*.bootp_relay=1' \ + '-net.ipv4.conf.all.bootp_relay' \ + '-net.ipv4.aaa.*.disable_policy=1' \ + '# Arguments with unsafe characters should be ignored' \ + $'foo\nbar.hoge=1' \ + $'foo.bar=4\n2' + +assert_eq "$(cat /proc/sys/net/ipv4/conf/hoge/drop_gratuitous_arp)" "1" +assert_eq "$(cat /proc/sys/net/ipv4/conf/hoge/bootp_relay)" "1" +assert_eq "$(cat /proc/sys/net/ipv4/conf/hoge/disable_policy)" "0" + +diff /run/sysctl.d/foo.conf - </proc/sys/net/ipv4/conf/hoge/drop_gratuitous_arp +echo 0 >/proc/sys/net/ipv4/conf/hoge/bootp_relay +echo 0 >/proc/sys/net/ipv4/conf/hoge/disable_policy + +/usr/lib/systemd/systemd-sysctl --prefix=/net/ipv4/conf/hoge --save=foo - </proc/sys/net/ipv4/conf/hoge/drop_gratuitous_arp +echo 0 >/proc/sys/net/ipv4/conf/hoge/bootp_relay +echo 0 >/proc/sys/net/ipv4/conf/hoge/disable_policy + +# Verify that the saved .conf file is loaded. +/usr/lib/systemd/systemd-sysctl --prefix=/net/ipv4/conf/hoge +assert_eq "$(cat /proc/sys/net/ipv4/conf/hoge/drop_gratuitous_arp)" "1" +assert_eq "$(cat /proc/sys/net/ipv4/conf/hoge/bootp_relay)" "1" +assert_eq "$(cat /proc/sys/net/ipv4/conf/hoge/disable_policy)" "0" + +echo 0 >/proc/sys/net/ipv4/conf/hoge/drop_gratuitous_arp +echo 0 >/proc/sys/net/ipv4/conf/hoge/bootp_relay +echo 0 >/proc/sys/net/ipv4/conf/hoge/disable_policy + +# Check that the saved .conf file can be overridden. +/usr/lib/systemd/systemd-sysctl --prefix=/net/ipv4/conf/hoge --save=foo /run/sysctl.d/foo.conf - </proc/sys/net/ipv4/conf/hoge/drop_gratuitous_arp +echo 0 >/proc/sys/net/ipv4/conf/hoge/bootp_relay +echo 0 >/proc/sys/net/ipv4/conf/hoge/disable_policy + +/usr/lib/systemd/systemd-sysctl --revert=foo --prefix=/net/ipv4/conf/hoge +[[ ! -e /run/sysctl.d/foo.conf ]] + +# Assume that no saved sysctl .conf files modify these variables. +assert_eq "$(cat /proc/sys/net/ipv4/conf/hoge/drop_gratuitous_arp)" "0" +assert_eq "$(cat /proc/sys/net/ipv4/conf/hoge/bootp_relay)" "0" +assert_eq "$(cat /proc/sys/net/ipv4/conf/hoge/disable_policy)" "0" + +echo 0 >/proc/sys/net/ipv4/conf/hoge/drop_gratuitous_arp +echo 0 >/proc/sys/net/ipv4/conf/hoge/bootp_relay +echo 0 >/proc/sys/net/ipv4/conf/hoge/disable_policy + +# Try again to test when foo.conf does not exist +/usr/lib/systemd/systemd-sysctl --revert=foo --prefix=/net/ipv4/conf/hoge + +assert_eq "$(cat /proc/sys/net/ipv4/conf/hoge/drop_gratuitous_arp)" "0" +assert_eq "$(cat /proc/sys/net/ipv4/conf/hoge/bootp_relay)" "0" +assert_eq "$(cat /proc/sys/net/ipv4/conf/hoge/disable_policy)" "0" From 5e2d9c6ed1fea7493835e1b9c7f60e89f80d7eff Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Sat, 8 Aug 2026 03:26:14 +0900 Subject: [PATCH 6/6] man: document systemd-sysctl --save= and --revert= --- man/systemd-sysctl.service.xml | 45 ++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/man/systemd-sysctl.service.xml b/man/systemd-sysctl.service.xml index 36f446dcc7..82b2ec71bb 100644 --- a/man/systemd-sysctl.service.xml +++ b/man/systemd-sysctl.service.xml @@ -110,6 +110,31 @@ + + + + Before applying the loaded sysctl settings, write them to /run/sysctl.d/ + using the specified filename. If the filename does not end with .conf, the + suffix is appended. This option requires one or more positional arguments. May be combined with + . This may be useful for creating runtime sysctl configuration files or + overriding existing sysctl configuration files. + + + + + + + + + Before loading and applying the sysctl settings, removes the specified sysctl configuration + file under /run/sysctl.d/. If the filename does not end with + .conf, the suffix is appended. This may be useful for removing configuration + files created by . + + + + + @@ -186,6 +211,26 @@ kernel.core_pattern = |/usr/libexec/abrt-hook-ccpp %s %c %p %u %g %t %P %I of preference. + + Disable IPv6 on an interface and save the specified setting + + # /usr/lib/systemd/systemd-sysctl --save=99-disable-ipv6-foo - <<EOF +net.ipv6.conf.foo.disable_ipv6=1 +EOF + + This disables IPv6 on the network interface foo, and creates + /run/sysctl.d/99-disable-ipv6-foo.conf. + + + + Remove a runtime sysctl configuration file + + # /usr/lib/systemd/systemd-sysctl --revert=99-disable-ipv6-foo --prefix=/net/ipv6/conf/foo + + This removes /run/sysctl.d/99-disable-ipv6-foo.conf, and applies the + remaining sysctl settings for the network interface foo. + + See sysctl8 for various ways to directly apply sysctl settings.