]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
tmpfiles: add conditionalized execute bit (X) support
authorMike Yuan <me@yhndnzj.com>
Fri, 16 Dec 2022 16:44:06 +0000 (00:44 +0800)
committerMike Yuan <me@yhndnzj.com>
Thu, 27 Apr 2023 07:15:09 +0000 (15:15 +0800)
According to setfacl(1), "the character X stands for
the execute permission if the file is a directory
or already has execute permission for some user."

After this commit, parse_acl() would return 3 acl
objects. The newly-added acl_exec object contains
entries that are subject to conditionalized execute
bit mangling. In tmpfiles, we would iterate the acl_exec
object, check the permission of the target files,
and remove the execute bit if necessary.

Here's an example entry:
A /tmp/test - - - - u:test:rwX

Closes #25114

man/tmpfiles.d.xml
src/shared/acl-util.c
src/shared/acl-util.h
src/tmpfiles/tmpfiles.c

index a23b9c8946e92a90c8afe301408cfeaf32141187..54f3c501cb0bf43021ee027500abccfc140610df 100644 (file)
@@ -446,13 +446,15 @@ L     /tmp/foobar -    -    -     -   /dev/null</programlisting>
           <term><varname>a+</varname></term>
           <listitem><para>Set POSIX ACLs (access control lists), see <citerefentry
           project='man-pages'><refentrytitle>acl</refentrytitle>
-          <manvolnum>5</manvolnum></citerefentry>. If suffixed with <varname>+</varname>, the specified
-          entries will be added to the existing set. <command>systemd-tmpfiles</command> will automatically
-          add the required base entries for user and group based on the access mode of the file, unless base
-          entries already exist or are explicitly specified. The mask will be added if not specified
-          explicitly or already present. Lines of this type accept shell-style globs in place of normal path
-          names. This can be useful for allowing additional access to certain files. Does not follow
-          symlinks.</para></listitem>
+          <manvolnum>5</manvolnum></citerefentry>. Additionally, if 'X' is used, the execute bit is set only
+          if the file is a directory or already has execute permission for some user, as mentioned in
+          <citerefentry project='man-pages'><refentrytitle>setfacl</refentrytitle><manvolnum>1</manvolnum></citerefentry>.
+          If suffixed with <varname>+</varname>, the specified entries will be added to the existing set.
+          <command>systemd-tmpfiles</command> will automatically add the required base entries for user
+          and group based on the access mode of the file, unless base entries already exist or are explicitly
+          specified. The mask will be added if not specified explicitly or already present. Lines of this type
+          accept shell-style globs in place of normal path names. This can be useful for allowing additional
+          access to certain files. Does not follow symlinks.</para></listitem>
         </varlistentry>
 
         <varlistentry>
index b734ee1e0c97f1d7095fe0ce3e2db6dc51884812..5c0c4e21aa06a5f2a202f2c348538564c89636a4 100644 (file)
@@ -209,14 +209,20 @@ int acl_search_groups(const char *path, char ***ret_groups) {
         return ret;
 }
 
-int parse_acl(const char *text, acl_t *ret_acl_access, acl_t *ret_acl_default, bool want_mask) {
-        _cleanup_free_ char **a = NULL, **d = NULL; /* strings are not freed */
-        _cleanup_strv_free_ char **split = NULL;
-        int r = -EINVAL;
-        _cleanup_(acl_freep) acl_t a_acl = NULL, d_acl = NULL;
+int parse_acl(
+                const char *text,
+                acl_t *ret_acl_access,
+                acl_t *ret_acl_access_exec, /* extra rules to apply to inodes subject to uppercase X handling */
+                acl_t *ret_acl_default,
+                bool want_mask) {
+
+        _cleanup_strv_free_ char **a = NULL, **e = NULL, **d = NULL, **split = NULL;
+        _cleanup_(acl_freep) acl_t a_acl = NULL, e_acl = NULL, d_acl = NULL;
+        int r;
 
         assert(text);
         assert(ret_acl_access);
+        assert(ret_acl_access_exec);
         assert(ret_acl_default);
 
         split = strv_split(text, ",");
@@ -224,13 +230,38 @@ int parse_acl(const char *text, acl_t *ret_acl_access, acl_t *ret_acl_default, b
                 return -ENOMEM;
 
         STRV_FOREACH(entry, split) {
-                char *p;
+                _cleanup_strv_free_ char **entry_split = NULL;
+                _cleanup_free_ char *entry_join = NULL;
+                int n;
+
+                n = strv_split_full(&entry_split, *entry, ":", EXTRACT_DONT_COALESCE_SEPARATORS|EXTRACT_RETAIN_ESCAPE);
+                if (n < 0)
+                        return n;
+
+                if (n < 3 || n > 4)
+                        return -EINVAL;
+
+                string_replace_char(entry_split[n-1], 'X', 'x');
+
+                if (n == 4) {
+                        if (!STR_IN_SET(entry_split[0], "default", "d"))
+                                return -EINVAL;
 
-                p = STARTSWITH_SET(*entry, "default:", "d:");
-                if (p)
-                        r = strv_push(&d, p);
-                else
-                        r = strv_push(&a, *entry);
+                        entry_join = strv_join(entry_split + 1, ":");
+                        if (!entry_join)
+                                return -ENOMEM;
+
+                        r = strv_consume(&d, TAKE_PTR(entry_join));
+                } else { /* n == 3 */
+                        entry_join = strv_join(entry_split, ":");
+                        if (!entry_join)
+                                return -ENOMEM;
+
+                        if (!streq(*entry, entry_join))
+                                r = strv_consume(&e, TAKE_PTR(entry_join));
+                        else
+                                r = strv_consume(&a, TAKE_PTR(entry_join));
+                }
                 if (r < 0)
                         return r;
         }
@@ -253,6 +284,20 @@ int parse_acl(const char *text, acl_t *ret_acl_access, acl_t *ret_acl_default, b
                 }
         }
 
+        if (!strv_isempty(e)) {
+                _cleanup_free_ char *join = NULL;
+
+                join = strv_join(e, ",");
+                if (!join)
+                        return -ENOMEM;
+
+                e_acl = acl_from_text(join);
+                if (!e_acl)
+                        return -errno;
+
+                /* The mask must be calculated after deciding whether the execute bit should be set. */
+        }
+
         if (!strv_isempty(d)) {
                 _cleanup_free_ char *join = NULL;
 
@@ -272,6 +317,7 @@ int parse_acl(const char *text, acl_t *ret_acl_access, acl_t *ret_acl_default, b
         }
 
         *ret_acl_access = TAKE_PTR(a_acl);
+        *ret_acl_access_exec = TAKE_PTR(e_acl);
         *ret_acl_default = TAKE_PTR(d_acl);
 
         return 0;
index d3a341fbe612f4062f5d8750e92fb2f40c106905..978389ed1d7abf35797a54733cb9c90508fa3909 100644 (file)
@@ -15,7 +15,12 @@ int acl_find_uid(acl_t acl, uid_t uid, acl_entry_t *entry);
 int calc_acl_mask_if_needed(acl_t *acl_p);
 int add_base_acls_if_needed(acl_t *acl_p, const char *path);
 int acl_search_groups(const char* path, char ***ret_groups);
-int parse_acl(const char *text, acl_t *ret_acl_access, acl_t *ret_acl_default, bool want_mask);
+int parse_acl(
+                const char *text,
+                acl_t *ret_acl_access,
+                acl_t *ret_acl_access_exec,
+                acl_t *ret_acl_default,
+                bool want_mask);
 int acls_for_file(const char *path, acl_type_t type, acl_t new, acl_t *ret);
 int fd_add_uid_acl_permission(int fd, uid_t uid, unsigned mask);
 
index 2eb6e5ea3361ed0aeccaa67c6fbda9f3639e74c9..de72df29087395576833dabbe1fe9c7ac80f7dc5 100644 (file)
@@ -138,6 +138,7 @@ typedef struct Item {
         char **xattrs;
 #if HAVE_ACL
         acl_t acl_access;
+        acl_t acl_access_exec;
         acl_t acl_default;
 #endif
         uid_t uid;
@@ -1127,17 +1128,145 @@ static int parse_acls_from_arg(Item *item) {
         /* If append_or_force (= modify) is set, we will not modify the acl
          * afterwards, so the mask can be added now if necessary. */
 
-        r = parse_acl(item->argument, &item->acl_access, &item->acl_default, !item->append_or_force);
+        r = parse_acl(item->argument, &item->acl_access, &item->acl_access_exec,
+                      &item->acl_default, !item->append_or_force);
         if (r < 0)
-                log_warning_errno(r, "Failed to parse ACL \"%s\": %m. Ignoring", item->argument);
+                log_warning_errno(r, "Failed to parse ACL \"%s\", ignoring: %m", item->argument);
 #else
-        log_warning("ACLs are not supported. Ignoring.");
+        log_warning("ACLs are not supported, ignoring.");
 #endif
 
         return 0;
 }
 
 #if HAVE_ACL
+static int parse_acl_cond_exec(
+                const char *path,
+                acl_t access, /* could be empty (NULL) */
+                acl_t cond_exec,
+                const struct stat *st,
+                bool append,
+                acl_t *ret) {
+
+        _cleanup_(acl_freep) acl_t parsed = NULL;
+        acl_entry_t entry;
+        acl_permset_t permset;
+        bool has_exec;
+        int r;
+
+        assert(path);
+        assert(ret);
+        assert(st);
+
+        parsed = access ? acl_dup(access) : acl_init(0);
+        if (!parsed)
+                return -errno;
+
+        /* Since we substitute 'X' with 'x' in parse_acl(), we just need to copy the entries over
+         * for directories */
+        if (S_ISDIR(st->st_mode)) {
+                for (r = acl_get_entry(cond_exec, ACL_FIRST_ENTRY, &entry);
+                     r > 0;
+                     r = acl_get_entry(cond_exec, ACL_NEXT_ENTRY, &entry)) {
+
+                        acl_entry_t parsed_entry;
+
+                        if (acl_create_entry(&parsed, &parsed_entry) < 0)
+                                return -errno;
+
+                        if (acl_copy_entry(parsed_entry, entry) < 0)
+                                return -errno;
+                }
+                if (r < 0)
+                        return -errno;
+
+                goto finish;
+        }
+
+        has_exec = st->st_mode & S_IXUSR;
+
+        if (!has_exec && append) {
+                _cleanup_(acl_freep) acl_t old = NULL;
+
+                old = acl_get_file(path, ACL_TYPE_ACCESS);
+                if (!old)
+                        return -errno;
+
+                for (r = acl_get_entry(old, ACL_FIRST_ENTRY, &entry);
+                     r > 0;
+                     r = acl_get_entry(old, ACL_NEXT_ENTRY, &entry)) {
+
+                        if (acl_get_permset(entry, &permset) < 0)
+                                return -errno;
+
+                        r = acl_get_perm(permset, ACL_EXECUTE);
+                        if (r < 0)
+                                return -errno;
+                        if (r > 0) {
+                                has_exec = true;
+                                break;
+                        }
+                }
+                if (r < 0)
+                        return -errno;
+        }
+
+        /* Check if we're about to set the execute bit in acl_access */
+        if (!has_exec && access) {
+                for (r = acl_get_entry(access, ACL_FIRST_ENTRY, &entry);
+                     r > 0;
+                     r = acl_get_entry(access, ACL_NEXT_ENTRY, &entry)) {
+
+                        if (acl_get_permset(entry, &permset) < 0)
+                                return -errno;
+
+                        r = acl_get_perm(permset, ACL_EXECUTE);
+                        if (r < 0)
+                                return -errno;
+                        if (r > 0) {
+                                has_exec = true;
+                                break;
+                        }
+                }
+                if (r < 0)
+                        return -errno;
+        }
+
+        for (r = acl_get_entry(cond_exec, ACL_FIRST_ENTRY, &entry);
+             r > 0;
+             r = acl_get_entry(cond_exec, ACL_NEXT_ENTRY, &entry)) {
+
+                acl_entry_t parsed_entry;
+
+                if (acl_create_entry(&parsed, &parsed_entry) < 0)
+                        return -errno;
+
+                if (acl_copy_entry(parsed_entry, entry) < 0)
+                        return -errno;
+
+                if (!has_exec) {
+                        if (acl_get_permset(parsed_entry, &permset) < 0)
+                                return -errno;
+
+                        if (acl_delete_perm(permset, ACL_EXECUTE) < 0)
+                                return -errno;
+                }
+        }
+        if (r < 0)
+                return -errno;
+
+finish:
+        if (!append) { /* want_mask = true */
+                r = calc_acl_mask_if_needed(&parsed);
+                if (r < 0)
+                        return r;
+        }
+
+        *ret = TAKE_PTR(parsed);
+
+        return 0;
+}
+
 static int path_set_acl(
                 const char *path,
                 const char *pretty,
@@ -1202,6 +1331,7 @@ static int fd_set_acls(
 
         int r = 0;
 #if HAVE_ACL
+        _cleanup_(acl_freep) acl_t access_with_exec_parsed = NULL;
         struct stat stbuf;
 
         assert(item);
@@ -1224,7 +1354,18 @@ static int fd_set_acls(
                 return 0;
         }
 
-        if (item->acl_access)
+        if (item->acl_access_exec) {
+                r = parse_acl_cond_exec(FORMAT_PROC_FD_PATH(fd),
+                                        item->acl_access,
+                                        item->acl_access_exec,
+                                        st,
+                                        item->append_or_force,
+                                        &access_with_exec_parsed);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to parse conditionalized execute bit for \"%s\": %m", path);
+
+                r = path_set_acl(FORMAT_PROC_FD_PATH(fd), path, ACL_TYPE_ACCESS, access_with_exec_parsed, item->append_or_force);
+        } else if (item->acl_access)
                 r = path_set_acl(FORMAT_PROC_FD_PATH(fd), path, ACL_TYPE_ACCESS, item->acl_access, item->append_or_force);
 
         /* set only default acls to folders */
@@ -1237,7 +1378,7 @@ static int fd_set_acls(
         }
 
         if (r > 0)
-                return -r; /* already warned */
+                return -r; /* already warned in path_set_acl */
 
         /* The above procfs paths don't work if /proc is not mounted. */
         if (r == -ENOENT && proc_mounted() == 0)
@@ -2867,6 +3008,9 @@ static void item_free_contents(Item *i) {
         if (i->acl_access)
                 acl_free(i->acl_access);
 
+        if (i->acl_access_exec)
+                acl_free(i->acl_access_exec);
+
         if (i->acl_default)
                 acl_free(i->acl_default);
 #endif