diff options
-rw-r--r-- | man/sysusers.d.xml | 40 | ||||
-rw-r--r-- | src/shared/util.c | 205 | ||||
-rw-r--r-- | src/shared/util.h | 7 | ||||
-rw-r--r-- | src/sysusers/sysusers.c | 174 | ||||
-rw-r--r-- | src/test/test-util.c | 109 |
5 files changed, 468 insertions, 67 deletions
diff --git a/man/sysusers.d.xml b/man/sysusers.d.xml index 58f24a62f5..1832ecf0e6 100644 --- a/man/sysusers.d.xml +++ b/man/sysusers.d.xml @@ -77,13 +77,14 @@ configuration.</para> <para>The file format is one line per user or group - containing name, ID and GECOS field description:</para> + containing name, ID, GECOS field description and home directory:</para> <programlisting># Type Name ID GECOS u httpd 440 "HTTP User" u authd /usr/bin/authd "Authorization user" g input - - -m authd input</programlisting> +m authd input +u root 0 "Superuser" /root</programlisting> <refsect2> <title>Type</title> @@ -103,11 +104,13 @@ m authd input</programlisting> bearing the same name. The user's shell will be set to <filename>/sbin/nologin</filename>, - the home directory to - <filename>/</filename>. The - account will be created - disabled, so that logins are - not allowed.</para></listitem> + the home directory to the + specified home directory, or + <filename>/</filename> if none + is given. The account will be + created disabled, so that + logins are not + allowed.</para></listitem> </varlistentry> <varlistentry> @@ -160,8 +163,8 @@ m authd input</programlisting> <varname>g</varname> the numeric 32bit UID or GID of the user/group. Do not use IDs 65535 or 4294967295, as they have special placeholder - meanings. Specify "-" for automatic UID/GID - allocation for the user or + meanings. Specify <literal>-</literal> for + automatic UID/GID allocation for the user or group. Alternatively, specify an absolute path in the file system. In this case the UID/GID is read from the path's owner/group. This is @@ -183,7 +186,24 @@ m authd input</programlisting> <para>Only applies to lines of type <varname>u</varname> and should otherwise be - left unset.</para> + left unset, or be set to + <literal>-</literal>.</para> + </refsect2> + + <refsect2> + <title>Home Directory</title> + + <para>The home directory for a new system + user. If omitted defaults to the root + directory. It is recommended to not + unnecessarily specify home directories for + system users, unless software strictly + requires one to be set.</para> + + <para>Only applies to lines of type + <varname>u</varname> and should otherwise be + left unset, or be set to + <literal>-</literal>.</para> </refsect2> </refsect1> diff --git a/src/shared/util.c b/src/shared/util.c index 18d40f398f..2bb3b5ef1c 100644 --- a/src/shared/util.c +++ b/src/shared/util.c @@ -6934,3 +6934,208 @@ int is_symlink(const char *path) { return 0; } + +int unquote_first_word(const char **p, char **ret) { + _cleanup_free_ char *s = NULL; + size_t allocated = 0, sz = 0; + + enum { + START, + VALUE, + VALUE_ESCAPE, + SINGLE_QUOTE, + SINGLE_QUOTE_ESCAPE, + DOUBLE_QUOTE, + DOUBLE_QUOTE_ESCAPE, + SPACE, + } state = START; + + assert(p); + assert(*p); + assert(ret); + + /* Parses the first word of a string, and returns it in + * *ret. Removes all quotes in the process. When parsing fails + * (because of an uneven number of quotes or similar), leaves + * the pointer *p at the first invalid character. */ + + for (;;) { + char c = **p; + + switch (state) { + + case START: + if (c == 0) + goto finish; + else if (strchr(WHITESPACE, c)) + break; + + state = VALUE; + /* fallthrough */ + + case VALUE: + if (c == 0) + goto finish; + else if (c == '\'') + state = SINGLE_QUOTE; + else if (c == '\\') + state = VALUE_ESCAPE; + else if (c == '\"') + state = DOUBLE_QUOTE; + else if (strchr(WHITESPACE, c)) + state = SPACE; + else { + if (!GREEDY_REALLOC(s, allocated, sz+2)) + return -ENOMEM; + + s[sz++] = c; + } + + break; + + case VALUE_ESCAPE: + if (c == 0) + return -EINVAL; + + if (!GREEDY_REALLOC(s, allocated, sz+2)) + return -ENOMEM; + + s[sz++] = c; + state = VALUE; + + break; + + case SINGLE_QUOTE: + if (c == 0) + return -EINVAL; + else if (c == '\'') + state = VALUE; + else if (c == '\\') + state = SINGLE_QUOTE_ESCAPE; + else { + if (!GREEDY_REALLOC(s, allocated, sz+2)) + return -ENOMEM; + + s[sz++] = c; + } + + break; + + case SINGLE_QUOTE_ESCAPE: + if (c == 0) + return -EINVAL; + + if (!GREEDY_REALLOC(s, allocated, sz+2)) + return -ENOMEM; + + s[sz++] = c; + state = SINGLE_QUOTE; + break; + + case DOUBLE_QUOTE: + if (c == 0) + return -EINVAL; + else if (c == '\"') + state = VALUE; + else if (c == '\\') + state = DOUBLE_QUOTE_ESCAPE; + else { + if (!GREEDY_REALLOC(s, allocated, sz+2)) + return -ENOMEM; + + s[sz++] = c; + } + + break; + + case DOUBLE_QUOTE_ESCAPE: + if (c == 0) + return -EINVAL; + + if (!GREEDY_REALLOC(s, allocated, sz+2)) + return -ENOMEM; + + s[sz++] = c; + state = DOUBLE_QUOTE; + break; + + case SPACE: + if (c == 0) + goto finish; + if (!strchr(WHITESPACE, c)) + goto finish; + + break; + } + + (*p) ++; + } + +finish: + if (!s) { + *ret = NULL; + return 0; + } + + s[sz] = 0; + *ret = s; + s = NULL; + + return 1; +} + +int unquote_many_words(const char **p, ...) { + va_list ap; + char **l; + int n = 0, i, c, r; + + /* Parses a number of words from a string, stripping any + * quotes if necessary. */ + + assert(p); + + /* Count how many words are expected */ + va_start(ap, p); + for (;;) { + if (!va_arg(ap, char **)) + break; + n++; + } + va_end(ap); + + if (n <= 0) + return 0; + + /* Read all words into a temporary array */ + l = newa0(char*, n); + for (c = 0; c < n; c++) { + + r = unquote_first_word(p, &l[c]); + if (r < 0) { + int j; + + for (j = 0; j < c; j++) { + free(l[j]); + return r; + } + } + + if (r == 0) + break; + } + + /* If we managed to parse all words, return them in the passed + * in parameters */ + va_start(ap, p); + for (i = 0; i < n; i++) { + char **v; + + v = va_arg(ap, char **); + assert(v); + + *v = l[i]; + } + va_end(ap); + + return c; +} diff --git a/src/shared/util.h b/src/shared/util.h index bd8bbb268f..43f4b089b4 100644 --- a/src/shared/util.h +++ b/src/shared/util.h @@ -128,6 +128,8 @@ bool streq_ptr(const char *a, const char *b) _pure_; #define newa(t, n) ((t*) alloca(sizeof(t)*(n))) +#define newa0(t, n) ((t*) alloca0(sizeof(t)*(n))) + #define newdup(t, p, n) ((t*) memdup_multiply(p, sizeof(t), (n))) #define malloc0(n) (calloc((n), 1)) @@ -967,4 +969,7 @@ bool is_localhost(const char *hostname); int take_password_lock(const char *root); -int is_symlink(const char *path);
\ No newline at end of file +int is_symlink(const char *path); + +int unquote_first_word(const char **p, char **ret); +int unquote_many_words(const char **p, ...) _sentinel_; diff --git a/src/sysusers/sysusers.c b/src/sysusers/sysusers.c index f78fb4fff3..eedc1e067e 100644 --- a/src/sysusers/sysusers.c +++ b/src/sysusers/sysusers.c @@ -51,6 +51,7 @@ typedef struct Item { char *uid_path; char *gid_path; char *description; + char *home; gid_t gid; uid_t uid; @@ -556,19 +557,19 @@ static int write_files(void) { .pw_uid = i->uid, .pw_gid = i->gid, .pw_gecos = i->description, + + /* "x" means the password is stored in + * the shadow file */ .pw_passwd = (char*) "x", - }; - /* Initialize the home directory and the shell - * to nologin, with one exception: for root we - * patch in something special */ - if (i->uid == 0) { - n.pw_shell = (char*) "/bin/sh"; - n.pw_dir = (char*) "/root"; - } else { - n.pw_shell = (char*) "/sbin/nologin"; - n.pw_dir = (char*) "/"; - } + /* We default to the root directory as home */ + .pw_dir = i->home ? i->home : (char*) "/", + + /* Initialize the shell to nologin, + * with one exception: for root we + * patch in something special */ + .pw_shell = i->uid == 0 ? (char*) "/bin/sh" : (char*) "/sbin/nologin", + }; errno = 0; if (putpwent(&n, passwd) != 0) { @@ -1292,6 +1293,9 @@ static bool item_equal(Item *a, Item *b) { if (a->gid_set && a->gid != b->gid) return false; + if (!streq_ptr(a->home, b->home)) + return false; + return true; } @@ -1330,6 +1334,9 @@ static bool valid_user_group_name(const char *u) { static bool valid_gecos(const char *d) { + if (!d) + return false; + if (!utf8_is_valid(d)) return false; @@ -1343,6 +1350,30 @@ static bool valid_gecos(const char *d) { return true; } +static bool valid_home(const char *p) { + + if (isempty(p)) + return false; + + if (!utf8_is_valid(p)) + return false; + + if (string_has_cc(p, NULL)) + return false; + + if (!path_is_absolute(p)) + return false; + + if (!path_is_safe(p)) + return false; + + /* Colons are used as field separators, and hence not OK */ + if (strchr(p, ':')) + return false; + + return true; +} + static int parse_line(const char *fname, unsigned line, const char *buffer) { static const Specifier specifier_table[] = { @@ -1353,27 +1384,34 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) { {} }; - _cleanup_free_ char *action = NULL, *name = NULL, *id = NULL, *resolved_name = NULL; + _cleanup_free_ char *action = NULL, *name = NULL, *id = NULL, *resolved_name = NULL, *description = NULL, *home = NULL; _cleanup_(item_freep) Item *i = NULL; Item *existing; Hashmap *h; - int r, n = -1; + int r; + const char *p; assert(fname); assert(line >= 1); assert(buffer); - r = sscanf(buffer, - "%ms %ms %ms %n", - &action, - &name, - &id, - &n); - if (r < 2) { + /* Parse columns */ + p = buffer; + r = unquote_many_words(&p, &action, &name, &id, &description, &home, NULL); + if (r < 0) { log_error("[%s:%u] Syntax error.", fname, line); - return -EIO; + return r; + } + if (r < 2) { + log_error("[%s:%u] Missing action and name columns.", fname, line); + return -EINVAL; + } + if (*p != 0) { + log_error("[%s:%u] Trailing garbage.", fname, line); + return -EINVAL; } + /* Verify action */ if (strlen(action) != 1) { log_error("[%s:%u] Unknown modifier '%s'", fname, line, action); return -EINVAL; @@ -1384,6 +1422,7 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) { return -EBADMSG; } + /* Verify name */ r = specifier_printf(name, specifier_table, NULL, &resolved_name); if (r < 0) { log_error("[%s:%u] Failed to replace specifiers: %s", fname, line, name); @@ -1395,11 +1434,20 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) { return -EINVAL; } - if (n >= 0) { - n += strspn(buffer+n, WHITESPACE); + /* Simplify remaining columns */ + if (isempty(id) || streq(id, "-")) { + free(id); + id = NULL; + } + + if (isempty(description) || streq(description, "-")) { + free(description); + description = NULL; + } - if (STR_IN_SET(buffer + n, "", "-")) - n = -1; + if (isempty(home) || streq(home, "-")) { + free(home); + home = NULL; } switch (action[0]) { @@ -1408,13 +1456,19 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) { _cleanup_free_ char *resolved_id = NULL; char **l; - r = hashmap_ensure_allocated(&members, string_hash_func, string_compare_func); - if (r < 0) - return log_oom(); - /* Try to extend an existing member or group item */ - if (!id || streq(id, "-")) { + if (description) { + log_error("[%s:%u] Lines of type 'm' don't take a GECOS field.", fname, line); + return -EINVAL; + } + + if (home) { + log_error("[%s:%u] Lines of type 'm' don't take a home directory field.", fname, line); + return -EINVAL; + } + + if (!id) { log_error("[%s:%u] Lines of type 'm' require a group name in the third field.", fname, line); return -EINVAL; } @@ -1430,10 +1484,9 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) { return -EINVAL; } - if (n >= 0) { - log_error("[%s:%u] Lines of type 'm' don't take a GECOS field.", fname, line); - return -EINVAL; - } + r = hashmap_ensure_allocated(&members, string_hash_func, string_compare_func); + if (r < 0) + return log_oom(); l = hashmap_get(members, resolved_id); if (l) { @@ -1476,15 +1529,12 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) { if (!i) return log_oom(); - if (id && !streq(id, "-")) { - + if (id) { if (path_is_absolute(id)) { - i->uid_path = strdup(id); - if (!i->uid_path) - return log_oom(); + i->uid_path = id; + id = NULL; path_kill_slashes(i->uid_path); - } else { r = parse_uid(id, &i->uid); if (r < 0) { @@ -1496,40 +1546,53 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) { } } - if (n >= 0) { - i->description = unquote(buffer+n, "\""); - if (!i->description) - return log_oom(); + if (description) { + if (!valid_gecos(description)) { + log_error("[%s:%u] '%s' is not a valid GECOS field.", fname, line, description); + return -EINVAL; + } + + i->description = description; + description = NULL; + } - if (!valid_gecos(i->description)) { - log_error("[%s:%u] '%s' is not a valid GECOS field.", fname, line, i->description); + if (home) { + if (!valid_home(home)) { + log_error("[%s:%u] '%s' is not a valid home directory field.", fname, line, home); return -EINVAL; } + + i->home = home; + home = NULL; } h = users; break; case ADD_GROUP: - r = hashmap_ensure_allocated(&groups, string_hash_func, string_compare_func); - if (r < 0) - return log_oom(); - if (n >= 0) { + if (description) { log_error("[%s:%u] Lines of type 'g' don't take a GECOS field.", fname, line); return -EINVAL; } + if (home) { + log_error("[%s:%u] Lines of type 'g' don't take a home directory field.", fname, line); + return -EINVAL; + } + + r = hashmap_ensure_allocated(&groups, string_hash_func, string_compare_func); + if (r < 0) + return log_oom(); + i = new0(Item, 1); if (!i) return log_oom(); - if (id && !streq(id, "-")) { - + if (id) { if (path_is_absolute(id)) { - i->gid_path = strdup(id); - if (!i->gid_path) - return log_oom(); + i->gid_path = id; + id = NULL; path_kill_slashes(i->gid_path); } else { @@ -1543,7 +1606,6 @@ static int parse_line(const char *fname, unsigned line, const char *buffer) { } } - h = groups; break; default: diff --git a/src/test/test-util.c b/src/test/test-util.c index e876248935..b74abac154 100644 --- a/src/test/test-util.c +++ b/src/test/test-util.c @@ -1101,6 +1101,113 @@ static void test_execute_directory(void) { rm_rf_dangerous(tempdir, false, true, false); } +static void test_unquote_first_word(void) { + const char *p, *original; + char *t; + + p = original = "foobar waldo"; + assert_se(unquote_first_word(&p, &t) > 0); + assert_se(streq(t, "foobar")); + free(t); + assert_se(p == original + 7); + + assert_se(unquote_first_word(&p, &t) > 0); + assert_se(streq(t, "waldo")); + free(t); + assert_se(p == original + 12); + + assert_se(unquote_first_word(&p, &t) == 0); + assert_se(!t); + assert_se(p == original + 12); + + p = original = "\"foobar\" \'waldo\'"; + assert_se(unquote_first_word(&p, &t) > 0); + assert_se(streq(t, "foobar")); + free(t); + assert_se(p == original + 9); + + assert_se(unquote_first_word(&p, &t) > 0); + assert_se(streq(t, "waldo")); + free(t); + assert_se(p == original + 16); + + assert_se(unquote_first_word(&p, &t) == 0); + assert_se(!t); + assert_se(p == original + 16); + + p = original = "\""; + assert_se(unquote_first_word(&p, &t) == -EINVAL); + assert_se(p == original + 1); + + p = original = "\'"; + assert_se(unquote_first_word(&p, &t) == -EINVAL); + assert_se(p == original + 1); + + p = original = "yay\'foo\'bar"; + assert_se(unquote_first_word(&p, &t) > 0); + assert_se(streq(t, "yayfoobar")); + free(t); + assert_se(p == original + 11); + + p = original = " foobar "; + assert_se(unquote_first_word(&p, &t) > 0); + assert_se(streq(t, "foobar")); + free(t); + assert_se(p == original + 12); +} + +static void test_unquote_many_words(void) { + const char *p, *original; + char *a, *b, *c; + + p = original = "foobar waldi piep"; + assert_se(unquote_many_words(&p, &a, &b, &c, NULL) == 3); + assert_se(p == original + 17); + assert_se(streq_ptr(a, "foobar")); + assert_se(streq_ptr(b, "waldi")); + assert_se(streq_ptr(c, "piep")); + free(a); + free(b); + free(c); + + p = original = "'foobar' wa\"ld\"i "; + assert_se(unquote_many_words(&p, &a, &b, &c, NULL) == 2); + assert_se(p == original + 19); + assert_se(streq_ptr(a, "foobar")); + assert_se(streq_ptr(b, "waldi")); + assert_se(streq_ptr(c, NULL)); + free(a); + free(b); + + p = original = ""; + assert_se(unquote_many_words(&p, &a, &b, &c, NULL) == 0); + assert_se(p == original); + assert_se(streq_ptr(a, NULL)); + assert_se(streq_ptr(b, NULL)); + assert_se(streq_ptr(c, NULL)); + + p = original = " "; + assert_se(unquote_many_words(&p, &a, &b, &c, NULL) == 0); + assert_se(p == original+2); + assert_se(streq_ptr(a, NULL)); + assert_se(streq_ptr(b, NULL)); + assert_se(streq_ptr(c, NULL)); + + p = original = "foobar"; + assert_se(unquote_many_words(&p, NULL) == 0); + assert_se(p == original); + + p = original = "foobar waldi"; + assert_se(unquote_many_words(&p, &a, NULL) == 1); + assert_se(p == original+7); + assert_se(streq_ptr(a, "foobar")); + + p = original = " foobar "; + assert_se(unquote_many_words(&p, &a, NULL) == 1); + assert_se(p == original+15); + assert_se(streq_ptr(a, "foobar")); +} + int main(int argc, char *argv[]) { log_parse_environment(); log_open(); @@ -1168,6 +1275,8 @@ int main(int argc, char *argv[]) { test_search_and_fopen_nulstr(); test_glob_exists(); test_execute_directory(); + test_unquote_first_word(); + test_unquote_many_words(); return 0; } |