1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
2 
3 #include <errno.h>
4 #include <getopt.h>
5 #include <limits.h>
6 #include <stdbool.h>
7 #include <stdio.h>
8 #include <stdlib.h>
9 #include <sys/stat.h>
10 #include <sys/types.h>
11 
12 #include "conf-files.h"
13 #include "def.h"
14 #include "errno-util.h"
15 #include "fd-util.h"
16 #include "fileio.h"
17 #include "glob-util.h"
18 #include "hashmap.h"
19 #include "log.h"
20 #include "main-func.h"
21 #include "pager.h"
22 #include "path-util.h"
23 #include "pretty-print.h"
24 #include "string-util.h"
25 #include "strv.h"
26 #include "sysctl-util.h"
27 
28 static char **arg_prefixes = NULL;
29 static bool arg_cat_config = false;
30 static PagerFlags arg_pager_flags = 0;
31 
32 STATIC_DESTRUCTOR_REGISTER(arg_prefixes, strv_freep);
33 
34 typedef struct Option {
35         char *key;
36         char *value;
37         bool ignore_failure;
38 } Option;
39 
option_free(Option * o)40 static Option *option_free(Option *o) {
41         if (!o)
42                 return NULL;
43 
44         free(o->key);
45         free(o->value);
46 
47         return mfree(o);
48 }
49 
50 DEFINE_TRIVIAL_CLEANUP_FUNC(Option*, option_free);
51 DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR(option_hash_ops, char, string_hash_func, string_compare_func, Option, option_free);
52 
test_prefix(const char * p)53 static bool test_prefix(const char *p) {
54         if (strv_isempty(arg_prefixes))
55                 return true;
56 
57         STRV_FOREACH(i, arg_prefixes) {
58                 const char *t;
59 
60                 t = path_startswith(*i, "/proc/sys/");
61                 if (!t)
62                         t = *i;
63 
64                 if (path_startswith(p, t))
65                         return true;
66         }
67 
68         return false;
69 }
70 
option_new(const char * key,const char * value,bool ignore_failure)71 static Option *option_new(
72                 const char *key,
73                 const char *value,
74                 bool ignore_failure) {
75 
76         _cleanup_(option_freep) Option *o = NULL;
77 
78         assert(key);
79 
80         o = new(Option, 1);
81         if (!o)
82                 return NULL;
83 
84         *o = (Option) {
85                 .key = strdup(key),
86                 .value = value ? strdup(value) : NULL,
87                 .ignore_failure = ignore_failure,
88         };
89 
90         if (!o->key)
91                 return NULL;
92         if (value && !o->value)
93                 return NULL;
94 
95         return TAKE_PTR(o);
96 }
97 
sysctl_write_or_warn(const char * key,const char * value,bool ignore_failure)98 static int sysctl_write_or_warn(const char *key, const char *value, bool ignore_failure) {
99         int r;
100 
101         r = sysctl_write(key, value);
102         if (r < 0) {
103                 /* If the sysctl is not available in the kernel or we are running with reduced privileges and
104                  * cannot write it, then log about the issue, and proceed without failing. (EROFS is treated
105                  * as a permission problem here, since that's how container managers usually protected their
106                  * sysctls.) In all other cases log an error and make the tool fail. */
107                 if (ignore_failure || r == -EROFS || ERRNO_IS_PRIVILEGE(r))
108                         log_debug_errno(r, "Couldn't write '%s' to '%s', ignoring: %m", value, key);
109                 else if (r == -ENOENT)
110                         log_info_errno(r, "Couldn't write '%s' to '%s', ignoring: %m", value, key);
111                 else
112                         return log_error_errno(r, "Couldn't write '%s' to '%s': %m", value, key);
113         }
114 
115         return 0;
116 }
117 
apply_all(OrderedHashmap * sysctl_options)118 static int apply_all(OrderedHashmap *sysctl_options) {
119         Option *option;
120         int r = 0;
121 
122         ORDERED_HASHMAP_FOREACH(option, sysctl_options) {
123                 int k;
124 
125                 /* Ignore "negative match" options, they are there only to exclude stuff from globs. */
126                 if (!option->value)
127                         continue;
128 
129                 if (string_is_glob(option->key)) {
130                         _cleanup_strv_free_ char **paths = NULL;
131                         _cleanup_free_ char *pattern = NULL;
132 
133                         pattern = path_join("/proc/sys", option->key);
134                         if (!pattern)
135                                 return log_oom();
136 
137                         k = glob_extend(&paths, pattern, GLOB_NOCHECK);
138                         if (k < 0) {
139                                 if (option->ignore_failure || ERRNO_IS_PRIVILEGE(k))
140                                         log_debug_errno(k, "Failed to resolve glob '%s', ignoring: %m",
141                                                         option->key);
142                                 else {
143                                         log_error_errno(k, "Couldn't resolve glob '%s': %m",
144                                                         option->key);
145                                         if (r == 0)
146                                                 r = k;
147                                 }
148 
149                         } else if (strv_isempty(paths))
150                                 log_debug("No match for glob: %s", option->key);
151 
152                         STRV_FOREACH(s, paths) {
153                                 const char *key;
154 
155                                 assert_se(key = path_startswith(*s, "/proc/sys"));
156 
157                                 if (!test_prefix(key))
158                                         continue;
159 
160                                 if (ordered_hashmap_contains(sysctl_options, key)) {
161                                         log_debug("Not setting %s (explicit setting exists).", key);
162                                         continue;
163                                 }
164 
165                                 k = sysctl_write_or_warn(key, option->value, option->ignore_failure);
166                                 if (r == 0)
167                                         r = k;
168                         }
169 
170                 } else {
171                         k = sysctl_write_or_warn(option->key, option->value, option->ignore_failure);
172                         if (r == 0)
173                                 r = k;
174                 }
175         }
176 
177         return r;
178 }
179 
parse_file(OrderedHashmap ** sysctl_options,const char * path,bool ignore_enoent)180 static int parse_file(OrderedHashmap **sysctl_options, const char *path, bool ignore_enoent) {
181         _cleanup_fclose_ FILE *f = NULL;
182         _cleanup_free_ char *pp = NULL;
183         unsigned c = 0;
184         int r;
185 
186         assert(path);
187 
188         r = search_and_fopen(path, "re", NULL, (const char**) CONF_PATHS_STRV("sysctl.d"), &f, &pp);
189         if (r < 0) {
190                 if (ignore_enoent && r == -ENOENT)
191                         return 0;
192 
193                 return log_error_errno(r, "Failed to open file '%s', ignoring: %m", path);
194         }
195 
196         log_debug("Parsing %s", pp);
197         for (;;) {
198                 _cleanup_(option_freep) Option *new_option = NULL;
199                 _cleanup_free_ char *l = NULL;
200                 bool ignore_failure = false;
201                 Option *existing;
202                 char *p, *value;
203                 int k;
204 
205                 k = read_line(f, LONG_LINE_MAX, &l);
206                 if (k == 0)
207                         break;
208                 if (k < 0)
209                         return log_error_errno(k, "Failed to read file '%s', ignoring: %m", pp);
210 
211                 c++;
212 
213                 p = strstrip(l);
214 
215                 if (isempty(p))
216                         continue;
217                 if (strchr(COMMENTS "\n", *p))
218                         continue;
219 
220                 value = strchr(p, '=');
221                 if (value) {
222                         if (p[0] == '-') {
223                                 ignore_failure = true;
224                                 p++;
225                         }
226 
227                         *value = 0;
228                         value++;
229                         value = strstrip(value);
230 
231                 } else {
232                         if (p[0] == '-')
233                                 /* We have a "negative match" option. Let's continue with value==NULL. */
234                                 p++;
235                         else {
236                                 log_syntax(NULL, LOG_WARNING, pp, c, 0,
237                                            "Line is not an assignment, ignoring: %s", p);
238                                 if (r == 0)
239                                         r = -EINVAL;
240                                 continue;
241                         }
242                 }
243 
244                 p = strstrip(p);
245                 p = sysctl_normalize(p);
246 
247                 /* We can't filter out globs at this point, we'll need to do that later. */
248                 if (!string_is_glob(p) &&
249                     !test_prefix(p))
250                         continue;
251 
252                 if (ordered_hashmap_ensure_allocated(sysctl_options, &option_hash_ops) < 0)
253                         return log_oom();
254 
255                 existing = ordered_hashmap_get(*sysctl_options, p);
256                 if (existing) {
257                         if (streq_ptr(value, existing->value)) {
258                                 existing->ignore_failure = existing->ignore_failure || ignore_failure;
259                                 continue;
260                         }
261 
262                         log_debug("Overwriting earlier assignment of %s at '%s:%u'.", p, pp, c);
263                         option_free(ordered_hashmap_remove(*sysctl_options, p));
264                 }
265 
266                 new_option = option_new(p, value, ignore_failure);
267                 if (!new_option)
268                         return log_oom();
269 
270                 k = ordered_hashmap_put(*sysctl_options, new_option->key, new_option);
271                 if (k < 0)
272                         return log_error_errno(k, "Failed to add sysctl variable %s to hashmap: %m", p);
273 
274                 TAKE_PTR(new_option);
275         }
276 
277         return r;
278 }
279 
help(void)280 static int help(void) {
281         _cleanup_free_ char *link = NULL;
282         int r;
283 
284         r = terminal_urlify_man("systemd-sysctl.service", "8", &link);
285         if (r < 0)
286                 return log_oom();
287 
288         printf("%s [OPTIONS...] [CONFIGURATION FILE...]\n\n"
289                "Applies kernel sysctl settings.\n\n"
290                "  -h --help             Show this help\n"
291                "     --version          Show package version\n"
292                "     --cat-config       Show configuration files\n"
293                "     --prefix=PATH      Only apply rules with the specified prefix\n"
294                "     --no-pager         Do not pipe output into a pager\n"
295                "\nSee the %s for details.\n",
296                program_invocation_short_name,
297                link);
298 
299         return 0;
300 }
301 
parse_argv(int argc,char * argv[])302 static int parse_argv(int argc, char *argv[]) {
303 
304         enum {
305                 ARG_VERSION = 0x100,
306                 ARG_CAT_CONFIG,
307                 ARG_PREFIX,
308                 ARG_NO_PAGER,
309         };
310 
311         static const struct option options[] = {
312                 { "help",       no_argument,       NULL, 'h'            },
313                 { "version",    no_argument,       NULL, ARG_VERSION    },
314                 { "cat-config", no_argument,       NULL, ARG_CAT_CONFIG },
315                 { "prefix",     required_argument, NULL, ARG_PREFIX     },
316                 { "no-pager",   no_argument,       NULL, ARG_NO_PAGER   },
317                 {}
318         };
319 
320         int c;
321 
322         assert(argc >= 0);
323         assert(argv);
324 
325         while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0)
326 
327                 switch (c) {
328 
329                 case 'h':
330                         return help();
331 
332                 case ARG_VERSION:
333                         return version();
334 
335                 case ARG_CAT_CONFIG:
336                         arg_cat_config = true;
337                         break;
338 
339                 case ARG_PREFIX: {
340                         char *p;
341 
342                         /* We used to require people to specify absolute paths
343                          * in /proc/sys in the past. This is kinda useless, but
344                          * we need to keep compatibility. We now support any
345                          * sysctl name available. */
346                         sysctl_normalize(optarg);
347 
348                         if (path_startswith(optarg, "/proc/sys"))
349                                 p = strdup(optarg);
350                         else
351                                 p = path_join("/proc/sys", optarg);
352                         if (!p)
353                                 return log_oom();
354 
355                         if (strv_consume(&arg_prefixes, p) < 0)
356                                 return log_oom();
357 
358                         break;
359                 }
360 
361                 case ARG_NO_PAGER:
362                         arg_pager_flags |= PAGER_DISABLE;
363                         break;
364 
365                 case '?':
366                         return -EINVAL;
367 
368                 default:
369                         assert_not_reached();
370                 }
371 
372         if (arg_cat_config && argc > optind)
373                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
374                                        "Positional arguments are not allowed with --cat-config");
375 
376         return 1;
377 }
378 
run(int argc,char * argv[])379 static int run(int argc, char *argv[]) {
380         _cleanup_(ordered_hashmap_freep) OrderedHashmap *sysctl_options = NULL;
381         int r, k;
382 
383         r = parse_argv(argc, argv);
384         if (r <= 0)
385                 return r;
386 
387         log_setup();
388 
389         umask(0022);
390 
391         if (argc > optind) {
392                 int i;
393 
394                 r = 0;
395 
396                 for (i = optind; i < argc; i++) {
397                         k = parse_file(&sysctl_options, argv[i], false);
398                         if (k < 0 && r == 0)
399                                 r = k;
400                 }
401         } else {
402                 _cleanup_strv_free_ char **files = NULL;
403 
404                 r = conf_files_list_strv(&files, ".conf", NULL, 0, (const char**) CONF_PATHS_STRV("sysctl.d"));
405                 if (r < 0)
406                         return log_error_errno(r, "Failed to enumerate sysctl.d files: %m");
407 
408                 if (arg_cat_config) {
409                         pager_open(arg_pager_flags);
410 
411                         return cat_files(NULL, files, 0);
412                 }
413 
414                 STRV_FOREACH(f, files) {
415                         k = parse_file(&sysctl_options, *f, true);
416                         if (k < 0 && r == 0)
417                                 r = k;
418                 }
419         }
420 
421         k = apply_all(sysctl_options);
422         if (k < 0 && r == 0)
423                 r = k;
424 
425         return r;
426 }
427 
428 DEFINE_MAIN_FUNCTION(run);
429