tmpfiles: add conditionalized execute bit (X) support

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
This commit is contained in:
Mike Yuan 2022-12-17 00:44:06 +08:00
parent 49c778e6bf
commit 26d98cdd78
No known key found for this signature in database
GPG key ID: 417471C0A40F58B3
4 changed files with 221 additions and 24 deletions

View 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>

View 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;
p = STARTSWITH_SET(*entry, "default:", "d:");
if (p)
r = strv_push(&d, p);
else
r = strv_push(&a, *entry);
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;
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;

View 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);

View 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