1 commit 474847ed35dda5fd4c33a717d8cc7c4d17b90232
2 Author: Craig Small <csmall@dropbear.xyz>
3 Date: Mon Sep 13 22:07:37 2021 +1000
5 sysctl: Support systemd glob patterns
7 systemd-sysctl handles glob patterns along with overrides and
8 exceptions. Now the procps sysctl does it too.
10 The return value for sysctl is consistently either 0 or 1.
12 Added tests to check sysctl functions.
17 Signed-off-by: Craig Small <csmall@dropbear.xyz>
19 diff --git a/sysctl.c b/sysctl.c
20 index bbca0b9..d26cd11 100644
24 * along with this program; if not, write to the Free Software
25 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
27 + * Part of this code comes from systemd, especially sysctl.c
30 * - added -p <preload> to preload values from a file
33 #include <sys/types.h>
38 #include "fileutils.h"
39 @@ -66,12 +68,34 @@ static bool PrintName;
40 static bool PrintNewline;
41 static bool IgnoreError;
48 static size_t iolen = LINELEN;
50 +typedef struct SysctlSetting {
54 + bool ignore_failure;
56 + struct SysctlSetting *next;
59 +typedef struct SettingList {
60 + struct SysctlSetting *head;
61 + struct SysctlSetting *tail;
64 +#define GLOB_CHARS "*?["
65 +static inline bool string_is_glob(const char *p)
67 + return !!strpbrk(p, GLOB_CHARS);
71 /* Function prototypes. */
72 static int pattern_match(const char *string, const char *pat);
73 static int DisplayAll(const char *restrict const path);
74 @@ -100,6 +124,81 @@ static void slashdot(char *restrict p, char old, char new)
78 +static void setting_free(SysctlSetting *s) {
88 +static SysctlSetting *setting_new(
91 + bool ignore_failure,
92 + bool glob_exclude) {
94 + SysctlSetting *s = NULL;
98 + proc_len = strlen(PROC_PATH);
99 + /* used to open the file */
100 + path = xmalloc(strlen(key) + proc_len + 2);
101 + strcpy(path, PROC_PATH);
103 + strcat(path + proc_len, key+1);
105 + strcat(path + proc_len, key);
106 + /* change . to / */
107 + slashdot(path + proc_len, '.', '/');
109 + s = xmalloc(sizeof(SysctlSetting));
111 + *s = (SysctlSetting) {
112 + .key = strdup(key),
114 + .value = value? strdup(value): NULL,
115 + .ignore_failure = ignore_failure,
116 + .glob_exclude = glob_exclude,
123 +static void settinglist_add(SettingList *l, SysctlSetting *s) {
124 + SysctlSetting *old_tail;
129 + if (l->head == NULL)
132 + if (l->tail != NULL) {
133 + old_tail = l->tail;
134 + old_tail->next = s;
139 +static SysctlSetting *settinglist_findpath(const SettingList *l, const char *path) {
140 + SysctlSetting *node;
142 + for (node=l->head; node != NULL; node = node->next) {
143 + if (strcmp(node->path, path) == 0)
149 +/* Function prototypes. */
150 +static int pattern_match(const char *string, const char *pat);
151 +static int DisplayAll(const char *restrict const path);
154 * Display the usage format
156 @@ -115,6 +214,7 @@ static void __attribute__ ((__noreturn__))
157 fputs(_(" -A alias of -a\n"), out);
158 fputs(_(" -X alias of -a\n"), out);
159 fputs(_(" --deprecated include deprecated parameters to listing\n"), out);
160 + fputs(_(" --dry-run Print the key and values but do not write\n"), out);
161 fputs(_(" -b, --binary print value without new line\n"), out);
162 fputs(_(" -e, --ignore ignore unknown variables errors\n"), out);
163 fputs(_(" -N, --names print variable names without values\n"), out);
164 @@ -137,6 +237,39 @@ static void __attribute__ ((__noreturn__))
165 exit(out == stderr ? EXIT_FAILURE : EXIT_SUCCESS);
169 + * Strip left/leading side of a string
171 +static char *lstrip(char *line)
175 + if (!line || !*line)
179 + while(isspace(*start)) start++;
185 + * Strip right/trailing side of a string
188 +static void rstrip(char *line)
192 + if (!line || !*line)
195 + end = line + strlen(line) - 1;
196 + while(end > line && isspace(*end)) end--;
202 * Strip the leading and trailing spaces from a string
204 @@ -166,7 +299,7 @@ static char *StripLeadingAndTrailingSpaces(char *oneline)
206 static int ReadSetting(const char *restrict const name)
209 + int rc = EXIT_SUCCESS;
210 char *restrict tmpname;
211 char *restrict outname;
213 @@ -198,7 +331,7 @@ static int ReadSetting(const char *restrict const name)
214 if (stat(tmpname, &ts) < 0) {
216 xwarn(_("cannot stat %s"), tmpname);
222 @@ -215,7 +348,7 @@ static int ReadSetting(const char *restrict const name)
225 if (pattern && !pattern_match(outname, pattern)) {
231 @@ -231,19 +364,19 @@ static int ReadSetting(const char *restrict const name)
234 xwarnx(_("\"%s\" is an unknown key"), outname);
240 xwarnx(_("permission denied on key '%s'"), outname);
244 case EIO: /* Ignore stable_secret below /proc/sys/net/ipv6/conf */
249 xwarn(_("reading key \"%s\""), outname);
255 @@ -279,7 +412,7 @@ static int ReadSetting(const char *restrict const name)
257 xwarnx(_("permission denied on key '%s'"),
264 @@ -291,11 +424,11 @@ static int ReadSetting(const char *restrict const name)
267 case EIO: /* Ignore stable_secret below /proc/sys/net/ipv6/conf */
272 xwarnx(_("reading key \"%s\""), outname);
278 @@ -323,7 +456,7 @@ static int is_deprecated(char *filename)
280 static int DisplayAll(const char *restrict const path)
283 + int rc = EXIT_SUCCESS;
286 struct dirent *restrict de;
287 @@ -333,7 +466,7 @@ static int DisplayAll(const char *restrict const path)
290 xwarnx(_("unable to open directory \"%s\""), path);
294 readdir(dp); /* skip . */
295 readdir(dp); /* skip .. */
296 @@ -369,130 +502,183 @@ static int DisplayAll(const char *restrict const path)
298 * Write a sysctl setting
300 -static int WriteSetting(const char *setting)
303 - const char *name = setting;
305 - const char *equals;
309 - bool ignore_failure;
312 +static int WriteSetting(
316 + const bool ignore_failure) {
318 + int rc = EXIT_SUCCESS;
323 - /* probably don't want to display this err */
326 - equals = strchr(setting, '=');
329 - xwarnx(_("\"%s\" must be of the form name=value"),
334 - /* point to the value in name=value */
335 - value = equals + 1;
337 - if (!*name || name == equals) {
338 - xwarnx(_("malformed setting \"%s\""), setting);
342 - ignore_failure = name[0] == '-';
343 - if (ignore_failure)
346 - /* used to open the file */
347 - tmpname = xmalloc(equals - name + 1 + strlen(PROC_PATH));
348 - strcpy(tmpname, PROC_PATH);
349 - strncat(tmpname, name, (int) (equals - name));
350 - tmpname[equals - name + strlen(PROC_PATH)] = 0;
351 - /* change . to / */
352 - slashdot(tmpname + strlen(PROC_PATH), '.', '/');
356 - /* used to display the output */
357 - outname = xmalloc(equals - name + 1);
358 - strncpy(outname, name, (int) (equals - name));
359 - outname[equals - name] = 0;
360 - /* change / to . */
361 - slashdot(outname, '/', '.');
362 - last_dot = strrchr(outname, '.');
363 - if (last_dot != NULL && is_deprecated(last_dot + 1)) {
364 - xwarnx(_("%s is deprecated, value not set"), outname);
368 - if (stat(tmpname, &ts) < 0) {
369 + if (stat(path, &ts) < 0) {
371 - xwarn(_("cannot stat %s"), tmpname);
373 + xwarn(_("cannot stat %s"), path);
380 if ((ts.st_mode & S_IWUSR) == 0) {
381 - xwarn(_("setting key \"%s\""), outname);
383 + xwarn(_("setting key \"%s\""), key);
387 if (S_ISDIR(ts.st_mode)) {
388 - xwarn(_("setting key \"%s\""), outname);
390 + xwarn(_("setting key \"%s\""), key);
394 - fp = fprocopen(tmpname, "w");
399 - if (!IgnoreError) {
400 - xwarnx(_("\"%s\" is an unknown key%s"), outname, (ignore_failure?_(", ignoring"):""));
402 + if ((fp = fprocopen(path, "w")) == NULL) {
405 + if (!IgnoreError) {
406 + xwarnx(_("\"%s\" is an unknown key%s"),
407 + key, (ignore_failure?_(", ignoring"):""));
416 - xwarnx(_("permission denied on key \"%s\"%s"), outname, (ignore_failure?_(", ignoring"):""));
419 - xwarn(_("setting key \"%s\"%s"), outname, (ignore_failure?_(", ignoring"):""));
422 - if (!ignore_failure && errno != ENOENT)
425 - rc = fprintf(fp, "%s\n", value);
428 - if (close_stream(fp) != 0)
429 - xwarn(_("setting key \"%s\""), outname);
430 - else if (rc == 0 && !Quiet) {
432 - fprintf(stdout, "%s\n", outname);
435 - fprintf(stdout, "%s = %s\n",
439 - fprintf(stdout, "%s\n", value);
441 - fprintf(stdout, "%s", value);
453 + xwarnx(_("permission denied on key \"%s\"%s"),
454 + key, (ignore_failure?_(", ignoring"):""));
457 + xwarn(_("setting key \"%s\"%s"),
458 + key, (ignore_failure?_(", ignoring"):""));
461 + if (!ignore_failure && errno != ENOENT)
464 + if (0 < fprintf(fp, "%s\n", value))
466 + if (close_stream(fp) != 0) {
467 + xwarn(_("setting key \"%s\""), path);
472 + if ((rc == EXIT_SUCCESS && !Quiet) || DryRun) {
474 + printf("%s\n", value);
477 + printf("%s = %s\n", path, value);
480 + printf("%s\n", value);
482 + printf("%s", value);
490 + * parse each configuration line, there are multiple ways of specifying
491 + * a key/value here:
493 + * key = value simple setting
494 + * -key = value ignore errors
495 + * key.pattern.*.with.glob = value set keys that match glob
496 + * -key.pattern.exclude.with.glob dont set this value
497 + * key.pattern.override.with.glob = value set this glob match to value
501 +static SysctlSetting *parse_setting_line(
509 + bool glob_exclude = FALSE;
510 + bool ignore_failure = FALSE;
512 + key = lstrip(line);
513 + if (strlen(key) < 2)
516 + /* skip over comments */
517 + if (key[0] == '#' || key[0] == ';')
520 + if (pattern && !pattern_match(key, pattern))
523 + value = strchr(key, '=');
524 + if (value == NULL) {
525 + if (key[0] == '-') {
526 + glob_exclude = TRUE;
531 + xwarnx(_("%s(%d): invalid syntax, continuing..."),
537 + if (key[0] == '-') {
538 + ignore_failure = TRUE;
541 + value++; // skip over =
542 + value=lstrip(value);
546 + return setting_new(key, value, ignore_failure, glob_exclude);
549 +/* Go through the setting list, expand and sort out
550 + * setting globs and actually write the settings out
552 +static int write_setting_list(const SettingList *sl)
554 + SysctlSetting *node;
555 + int rc = EXIT_SUCCESS;
557 + for (node=sl->head; node != NULL; node=node->next) {
558 + if (node->glob_exclude)
561 + if (string_is_glob(node->path)) {
566 + if (glob(node->path, 0, NULL, &globbuf) != 0)
569 + for(i=0; i < globbuf.gl_pathc; i++) {
570 + if (settinglist_findpath(sl, globbuf.gl_pathv[i]))
571 + continue; // override or exclude
573 + rc |= WriteSetting(node->key, globbuf.gl_pathv[i], node->value,
574 + node->ignore_failure);
577 + rc |= WriteSetting(node->key, node->path, node->value,
578 + node->ignore_failure);
587 static int pattern_match(const char *string, const char *pat)
588 @@ -513,12 +699,12 @@ static int pattern_match(const char *string, const char *pat)
589 * Preload the sysctl's from the conf file. We parse the file and then
590 * reform it (strip out whitespace).
592 -static int Preload(const char *restrict const filename)
593 +static int Preload(SettingList *setlist, const char *restrict const filename)
599 + int rc = EXIT_SUCCESS;
603 @@ -547,62 +733,26 @@ static int Preload(const char *restrict const filename)
604 ? stdin : fopen(globbuf.gl_pathv[j], "r");
606 xwarn(_("cannot open \"%s\""), globbuf.gl_pathv[j]);
609 + return EXIT_FAILURE;
612 while ((rlen = getline(&iobuf, &iolen, fp)) > 0) {
614 + SysctlSetting *setting;
621 - t = StripLeadingAndTrailingSpaces(iobuf);
625 - if (*t == '#' || *t == ';')
628 - name = strtok(t, "=");
629 - if (!name || !*name) {
630 - xwarnx(_("%s(%d): invalid syntax, continuing..."),
631 - globbuf.gl_pathv[j], n);
635 - StripLeadingAndTrailingSpaces(name);
637 - if (pattern && !pattern_match(name, pattern))
640 - offset = strlen(name);
641 - memmove(&iobuf[0], name, offset);
642 - iobuf[offset++] = '=';
644 - value = strtok(NULL, "\n\r");
645 - if (!value || !*value) {
646 - xwarnx(_("%s(%d): invalid syntax, continuing..."),
647 - globbuf.gl_pathv[j], n);
651 - while ((*value == ' ' || *value == '\t') && *value != 0)
654 - /* should NameOnly affect this? */
655 - memmove(&iobuf[offset], value, strlen(value));
656 - offset += strlen(value);
657 - iobuf[offset] = '\0';
659 - rc |= WriteSetting(iobuf);
660 + if ( (setting = parse_setting_line(globbuf.gl_pathv[j], n, iobuf))
663 + settinglist_add(setlist, setting);
672 @@ -618,7 +768,7 @@ static int sortpairs(const void *A, const void *B)
673 return strcmp(a->name, b->name);
676 -static int PreloadSystem(void)
677 +static int PreloadSystem(SettingList *setlist)
680 const char *dirs[] = {
681 @@ -630,7 +780,7 @@ static int PreloadSystem(void)
683 struct pair **cfgs = NULL;
686 + int rc = EXIT_SUCCESS;
688 enum { nprealloc = 16 };
690 @@ -688,14 +838,14 @@ static int PreloadSystem(void)
691 for (i = 0; i < ncfgs; ++i) {
693 printf(_("* Applying %s ...\n"), cfgs[i]->value);
694 - rc |= Preload(cfgs[i]->value);
695 + rc |= Preload(setlist, cfgs[i]->value);
699 if (stat(DEFAULT_PRELOAD, &ts) == 0 && S_ISREG(ts.st_mode)) {
701 printf(_("* Applying %s ...\n"), DEFAULT_PRELOAD);
702 - rc |= Preload(DEFAULT_PRELOAD);
703 + rc |= Preload(setlist, DEFAULT_PRELOAD);
707 @@ -717,15 +867,19 @@ int main(int argc, char *argv[])
708 bool preloadfileOpt = false;
712 const char *preloadfile = NULL;
713 + SettingList *setlist;
716 DEPRECATED_OPTION = CHAR_MAX + 1,
721 static const struct option longopts[] = {
722 {"all", no_argument, NULL, 'a'},
723 {"deprecated", no_argument, NULL, DEPRECATED_OPTION},
724 + {"dry-run", no_argument, NULL, DRYRUN_OPTION},
725 {"binary", no_argument, NULL, 'b'},
726 {"ignore", no_argument, NULL, 'e'},
727 {"names", no_argument, NULL, 'N'},
728 @@ -753,6 +907,10 @@ int main(int argc, char *argv[])
731 IgnoreDeprecated = true;
733 + setlist = xmalloc(sizeof(SettingList));
734 + setlist->head = NULL;
735 + setlist->tail = NULL;
739 @@ -805,7 +963,12 @@ int main(int argc, char *argv[])
743 - return PreloadSystem();
744 + rc |= PreloadSystem(setlist);
745 + rc |= write_setting_list(setlist);
747 + case DRYRUN_OPTION:
751 pattern = xstrdup(optarg);
753 @@ -833,15 +996,16 @@ int main(int argc, char *argv[])
754 int ret = EXIT_SUCCESS, i;
757 - ret |= Preload(DEFAULT_PRELOAD);
758 + ret |= Preload(setlist, DEFAULT_PRELOAD);
761 /* This happens when -pfile option is
762 * used without space. */
763 - ret |= Preload(preloadfile);
764 + ret |= Preload(setlist, preloadfile);
766 for (i = 0; i < argc; i++)
767 - ret |= Preload(argv[i]);
768 + ret |= Preload(setlist, argv[i]);
769 + ret |= write_setting_list(setlist);
773 @@ -855,9 +1019,14 @@ int main(int argc, char *argv[])
774 program_invocation_short_name);
776 for ( ; *argv; argv++) {
777 - if (WriteMode || strchr(*argv, '='))
778 - ReturnCode += WriteSetting(*argv);
780 + if (WriteMode || strchr(*argv, '=')) {
782 + if ( (s = parse_setting_line("command line", 0, *argv)) != NULL)
783 + ReturnCode |= WriteSetting(s->key, s->path, s->value,
784 + s->ignore_failure);
786 + ReturnCode |= EXIT_FAILURE;
788 ReturnCode += ReadSetting(*argv);
791 diff --git a/testsuite/config/unix.exp b/testsuite/config/unix.exp
792 index 4156c3b..ecdc0bf 100644
793 --- a/testsuite/config/unix.exp
794 +++ b/testsuite/config/unix.exp
795 @@ -136,6 +136,15 @@ proc expect_table_dsc { test match_header match_item } {
799 +proc expect_spawn_retval { test retval } {
800 + foreach {pid spawnid os_error_flag value} [wait] break
802 + if {$value == $retval} {
805 + fail "$test (exit value)"
808 proc make_pipeproc { } {
809 global pipeproc_pid pipeproc_spawnid topdir
811 diff --git a/testsuite/sysctl.test/sysctl_write.exp b/testsuite/sysctl.test/sysctl_write.exp
813 index 0000000..5a74dec
815 +++ b/testsuite/sysctl.test/sysctl_write.exp
818 +set sysctl ${topdir}sysctl
820 +set test "sysctl write from command line"
821 +spawn $sysctl --dry-run kernel.hostname=procps-test
822 +expect_pass "$test" "/proc/sys/kernel/hostname = procps-test"
824 +set test "sysctl write from configuration file"
825 +spawn $sysctl --dry-run -f ${topdir}testsuite/sysctl_glob_test.conf
826 +expect_pass "$test" "/proc/sys/fs/protected_fifos = 2\\s+/proc/sys/fs/protected_symlinks = 2\\s+/proc/sys/fs/protected_hardlinks = 1"
828 +set hostname_file "/proc/sys/kernel/hostname"
829 +if {[file exists ${hostname_file}]} {
830 + if {[file writable ${hostname_file}]} {
831 + unsupported "sysctl write: hostname file is writable"
833 + set test "sysctl write unwritable file"
834 + spawn $sysctl -q kernel.hostname=procpstest
835 + expect_pass "$test" "sysctl: permission denied on key \"kernel.hostname\"\\s*$"
836 + expect_spawn_retval "$test" 1
838 + set test "sysctl write unwritable file ignored"
839 + spawn $sysctl -q -- -kernel.hostname=procpstest
840 + expect_pass "$test" "sysctl: permission denied on key \"kernel.hostname\", ignoring\\s*$"
841 + expect_spawn_retval "$test" 0
844 + unsupported "sysctl write: hostname file doe not exist"
846 diff --git a/testsuite/sysctl_glob_test.conf b/testsuite/sysctl_glob_test.conf
848 index 0000000..45ae904
850 +++ b/testsuite/sysctl_glob_test.conf
853 +# Test configuration for for glob in sysctl
856 +fs.protected_hardlinks = 1
857 +-fs.protected_regular