Page Menu
Home
FreeBSD
Search
Configure Global Search
Log In
Files
F164486825
D58012.id183077.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
16 KB
Referenced Files
None
Subscribers
None
D58012.id183077.diff
View Options
diff --git a/sys/security/mac_do/mac_do.c b/sys/security/mac_do/mac_do.c
--- a/sys/security/mac_do/mac_do.c
+++ b/sys/security/mac_do/mac_do.c
@@ -31,6 +31,7 @@
#include <sys/sysctl.h>
#include <sys/ucred.h>
#include <sys/vnode.h>
+#include <sys/imgact.h>
#include <security/mac/mac_policy.h>
@@ -79,6 +80,7 @@
static unsigned osd_jail_slot;
static unsigned osd_thread_slot;
+static unsigned osd_thread_exec_slot;
#define IT_INVALID 0 /* Must stay 0. */
#define IT_UID 1
@@ -200,6 +202,9 @@
id_nb_t gids_nb;
struct id_spec *uids;
struct id_spec *gids;
+ u_int exec_paths_nb;
+ char **exec_paths;
+ bool exec_is_blacklist;
};
STAILQ_HEAD(rulehead, rule);
@@ -390,7 +395,11 @@
STAILQ_FOREACH_SAFE(rule, head, r_entries, rule_next) {
free(rule->uids, M_MAC_DO);
free(rule->gids, M_MAC_DO);
+ for (u_int i = 0; i < rule->exec_paths_nb; i++)
+ free(rule->exec_paths[i], M_MAC_DO);
+ free(rule->exec_paths, M_MAC_DO);
free(rule, M_MAC_DO);
+
}
STAILQ_INIT(head);
}
@@ -477,18 +486,22 @@
* delimiter characters (so it doesn't prevent tokens containing spaces and tabs
* in the middle).
*/
+static inline char *
+skip_ws(char *p)
+{
+ return (p + strspn(p, " \t"));
+}
+
static char *
strsep_noblanks(char **const stringp, const char *delim)
{
char *p = *stringp;
char *ret, *wsp;
- size_t idx;
if (p == NULL)
return (NULL);
- idx = strspn(p, " \t");
- p += idx;
+ p = skip_ws(p);
ret = strsep(&p, delim);
@@ -933,6 +946,100 @@
return (EINVAL);
}
+static int
+parse_rule_exec(char *exec_str, struct rule *const rule,
+ const char *const start, struct parse_error **const parse_error)
+{
+ char **paths, *p, *path_p, *tok, *key;
+ u_int nb, capacity;
+ bool negated;
+
+ nb = 0;
+ capacity = 4;
+ negated = false;
+ p = exec_str;
+
+ /* Check for '!' before the option key (consistent with gid flags). */
+ p = skip_ws(p);
+ if (*p == '!') {
+ negated = true;
+ p++;
+ p = skip_ws(p);
+ }
+
+ key = strsep_noblanks(&p, "=");
+ if (key == NULL || strcmp(key, "exec") != 0) {
+ make_parse_error(parse_error,
+ (key != NULL ? key : exec_str) - start,
+ "Unknown option '%.10s' (only 'exec' is supported).",
+ key != NULL ? key : "");
+ return (EINVAL);
+ }
+ if (p == NULL) {
+ make_parse_error(parse_error, key - start,
+ "Missing '=' after 'exec'.");
+ return (EINVAL);
+ }
+
+ p = skip_ws(p);
+
+ if (strcmp(p, "*") == 0) {
+ if (negated) {
+ make_parse_error(parse_error, p - start,
+ "Negated wildcard is not allowed.");
+ return (EINVAL);
+ }
+ rule->exec_paths_nb = 0;
+ rule->exec_paths = NULL;
+ return (0);
+ }
+
+ rule->exec_is_blacklist = negated;
+
+ /*
+ * Single pass with a dynamically growing array.
+ *
+ * A two-pass approach (count then copy) would require first making
+ * a copy of the string, since strsep() modifies it in place by
+ * replacing ':' with '\0'. Not worth the trouble.
+ */
+ paths = malloc(capacity * sizeof(char *), M_MAC_DO, M_WAITOK);
+ path_p = p;
+ while ((tok = strsep_noblanks(&path_p, ":")) != NULL) {
+ if (*tok == '\0')
+ continue;
+ if (tok[0] != '/') {
+ make_parse_error(parse_error, tok - start,
+ "Exec path '%s' is not absolute.", tok);
+ goto einval;
+ }
+ if (nb >= capacity) {
+ capacity *= 2;
+ paths = realloc(paths,
+ capacity * sizeof(char *), M_MAC_DO, M_WAITOK);
+ }
+ paths[nb] = strdup(tok, M_MAC_DO);
+ nb++;
+ }
+
+ if (nb == 0) {
+ make_parse_error(parse_error, p - start,
+ "Empty exec path list.");
+ goto einval;
+ }
+
+ /* Shrink allocation to actual size. */
+ rule->exec_paths = realloc(paths, nb * sizeof(char *),
+ M_MAC_DO, M_WAITOK);
+ rule->exec_paths_nb = nb;
+ return (0);
+
+einval:
+ rule->exec_paths = paths;
+ rule->exec_paths_nb = nb;
+ return (EINVAL);
+}
+
/*
* See also the herald comment for parse_rules() below.
*
@@ -953,7 +1060,9 @@
{
const char *const start = rule;
const char *from_type, *from_id, *p;
- char *to_list;
+ char *to, *to_list;
+ char *options;
+ char *target_str;
struct id_list uid_list, gid_list;
struct id_elem *ie, *ie_next;
struct rule *new;
@@ -994,6 +1103,36 @@
goto einval;
}
+ /*
+ * Extract optional options block from end of rule.
+ * Must happen before <to> comma parsing (which would
+ * consume the rule string).
+ *
+ * After strsep_noblanks(&rule, "("):
+ * rule != NULL → '(' was found, rule points past it.
+ * rule == NULL → no '(' in the string, no options block.
+ *
+ * After strsep_noblanks(&rule, ")"):
+ * rule == NULL → no ')' found (unclosed).
+ * rule != NULL → ')' found, rule points past it (to '\0' if
+ * at end of string, or to trailing content).
+ */
+ target_str = strsep_noblanks(&rule, "(");
+ options = NULL;
+ if (rule != NULL) {
+ options = strsep_noblanks(&rule, ")");
+ if (rule == NULL) {
+ make_parse_error(parse_error, target_str - start,
+ "Unclosed options block.");
+ goto einval;
+ }
+ if (*skip_ws(rule) != '\0') {
+ make_parse_error(parse_error, options - start,
+ "Unexpected content after options block.");
+ goto einval;
+ }
+ }
+
/*
* We will now parse the "to" list.
*
@@ -1006,21 +1145,22 @@
* allows to minimize memory allocations and enables searching IDs in
* O(log(n)) instead of linearly.
*/
- to_list = strsep_noblanks(&rule, ",");
- if (to_list == NULL) {
+ if (target_str == NULL) {
make_parse_error(parse_error, 0, "No target list.");
goto einval;
}
+ to_list = target_str;
+ to = strsep_noblanks(&to_list, ",");
do {
- error = parse_target_clause(to_list, new, &uid_list, &gid_list,
+ error = parse_target_clause(to, new, &uid_list, &gid_list,
parse_error);
if (error != 0) {
- (*parse_error)->pos += to_list - start;
+ (*parse_error)->pos += to - start;
goto einval;
}
- to_list = strsep_noblanks(&rule, ",");
- } while (to_list != NULL);
+ to = strsep_noblanks(&to_list, ",");
+ } while (to != NULL);
if (new->uids_nb != 0) {
new->uids = malloc(sizeof(*new->uids) * new->uids_nb, M_MAC_DO,
@@ -1055,11 +1195,21 @@
check_type_and_type_flags(IT_GID, new->gid_flags);
}
+ /* Parse options block (if present). */
+ if (options != NULL) {
+ error = parse_rule_exec(options, new, start, parse_error);
+ if (error != 0)
+ goto einval;
+ }
+
STAILQ_INSERT_TAIL(&rules->head, new, r_entries);
MPASS(error == 0 && *parse_error == NULL);
return (0);
einval:
+ for (u_int i = 0; i < new->exec_paths_nb; i++)
+ free(new->exec_paths[i], M_MAC_DO);
+ free(new->exec_paths, M_MAC_DO);
free(new->gids, M_MAC_DO);
free(new->uids, M_MAC_DO);
free(new, M_MAC_DO);
@@ -1454,6 +1604,15 @@
dst_rule->gids = malloc(gids_size, M_MAC_DO, M_WAITOK);
bcopy(src_rule->gids, dst_rule->gids, gids_size);
}
+ if (src_rule->exec_paths_nb > 0) {
+ dst_rule->exec_paths = malloc(
+ src_rule->exec_paths_nb * sizeof(char *), M_MAC_DO, M_WAITOK);
+ for (u_int i = 0; i < src_rule->exec_paths_nb; i++)
+ dst_rule->exec_paths[i] = strdup(src_rule->exec_paths[i],
+ M_MAC_DO);
+ dst_rule->exec_paths_nb = src_rule->exec_paths_nb;
+ dst_rule->exec_is_blacklist = src_rule->exec_is_blacklist;
+ }
STAILQ_INSERT_TAIL(&dst->head, dst_rule, r_entries);
}
@@ -2114,6 +2273,12 @@
return (hdr);
}
+struct mac_do_setcred_data {
+ struct mac_do_data_header hdr;
+ const struct ucred *new_cred;
+ u_int setcred_flags;
+};
+
/* Destructor for 'osd_thread_slot'. */
static void
dealloc_thread_osd(void *const value)
@@ -2121,6 +2286,54 @@
free(value, M_MAC_DO);
}
+/*
+ * Exec constraint data, stored in a separate OSD slot with a longer
+ * lifecycle than the transient setcred data. Set in priv_grant(),
+ * read in vnode_check_exec(), persists across multiple exec calls.
+ * Cleared only by the next setcred_enter(), thread exit, or module unload.
+ *
+ * 'conf != NULL' indicates the slot is armed.
+ */
+struct mac_do_exec_data {
+ struct conf *conf;
+ u_int nb_rules;
+ const struct rule **rules;
+};
+
+static void *
+fetch_exec_data(void)
+{
+ return (osd_thread_get_unlocked(curthread, osd_thread_exec_slot));
+}
+
+static void
+clear_exec_data(struct mac_do_exec_data *data)
+{
+ if (data == NULL)
+ return;
+ if (data->conf != NULL) {
+ drop_conf(data->conf);
+ data->conf = NULL;
+ }
+ free(data->rules, M_MAC_DO);
+ data->rules = NULL;
+ data->nb_rules = 0;
+}
+
+/* Destructor for 'osd_thread_exec_slot'. */
+static void
+dealloc_exec_osd(void *const value)
+{
+ struct mac_do_exec_data *const d = value;
+
+ if (d != NULL) {
+ if (d->conf != NULL)
+ drop_conf(d->conf);
+ free(d->rules, M_MAC_DO);
+ }
+ free(value, M_MAC_DO);
+}
+
/*
* Whether to grant access to some primary group according to flags.
*
@@ -2438,20 +2651,17 @@
/*
* To pass data between check_setcred() and priv_grant() (on PRIV_CRED_SETCRED).
*/
-struct mac_do_setcred_data {
- struct mac_do_data_header hdr;
- const struct ucred *new_cred;
- u_int setcred_flags;
-};
static int
mac_do_priv_grant(struct ucred *cred, int priv)
{
struct mac_do_setcred_data *const data = fetch_data();
+ struct mac_do_exec_data *exec_data;
struct rules *rules;
const struct ucred *new_cred;
const struct rule *rule;
u_int setcred_flags;
+ u_int matching_nb;
int error;
/* Bail out fast if we aren't concerned. */
@@ -2487,16 +2697,66 @@
/*
* Browse rules, and for those that match the requestor, call specific
* privilege granting functions interpreting the "to"/"target" part.
+ *
+ * We must check ALL rules, not just the first match: multiple rules
+ * may allow the same credential transition with different exec
+ * constraints, and the exec check must consider all of them.
*/
error = EPERM;
- STAILQ_FOREACH(rule, &rules->head, r_entries)
- if (rule_applies(rule, cred)) {
- error = rule_grant_setcred(rule, cred, new_cred);
- if (error != EPERM)
- break;
- }
+ matching_nb = 0;
+ STAILQ_FOREACH(rule, &rules->head, r_entries) {
+ if (!rule_applies(rule, cred))
+ continue;
+ if (rule_grant_setcred(rule, cred, new_cred) == 0) {
+ error = 0;
+ if (rule->exec_paths_nb > 0 || rule->exec_is_blacklist)
+ matching_nb++;
+ }
+ }
- return (error);
+ if (error != 0)
+ return (error);
+
+ /*
+ * At least one rule granted the transition. Store all granting rules
+ * that carry exec constraints in the exec OSD slot so that
+ * vnode_check_exec can evaluate them per-rule (any-match semantics).
+ */
+ exec_data = fetch_exec_data();
+ clear_exec_data(exec_data);
+
+ if (matching_nb > 0) {
+ if (exec_data == NULL) {
+ exec_data = malloc(sizeof(*exec_data), M_MAC_DO,
+ M_WAITOK | M_ZERO);
+ int e = osd_thread_set(curthread, osd_thread_exec_slot,
+ exec_data);
+ if (e != 0) {
+ void **rsv = osd_reserve(osd_thread_exec_slot);
+ e = osd_thread_set_reserved(curthread,
+ osd_thread_exec_slot, rsv, exec_data);
+ MPASS(e == 0);
+ }
+ }
+ exec_data->rules = malloc(matching_nb * sizeof(struct rule *),
+ M_MAC_DO, M_WAITOK);
+ exec_data->nb_rules = 0;
+ exec_data->conf = data->hdr.conf;
+ hold_conf(exec_data->conf);
+
+ STAILQ_FOREACH(rule, &rules->head, r_entries) {
+ if (!rule_applies(rule, cred))
+ continue;
+ if (rule_grant_setcred(rule, cred, new_cred) == 0) {
+ if (rule->exec_paths_nb > 0 ||
+ rule->exec_is_blacklist)
+ exec_data->rules[exec_data->nb_rules++] =
+ rule;
+ }
+ }
+ }
+
+ return (0);
}
static int
@@ -2548,10 +2808,17 @@
mac_do_setcred_enter(void)
{
struct prison *const pr = curproc->p_ucred->cr_prison;
- struct mac_do_setcred_data * data;
+ struct mac_do_setcred_data *data;
struct conf *conf;
int error;
+ /*
+ * Always clear any prior exec constraint, even if mac_do is
+ * disabled or this process isn't trusted. A new setcred()
+ * replaces the old exec constraint.
+ */
+ clear_exec_data(fetch_exec_data());
+
/*
* If not enabled, don't prepare data. Other hooks will check for that
* to know if they have to do something.
@@ -2579,6 +2846,7 @@
data = fetch_data();
if (!is_data_reusable(data, sizeof(*data)))
data = alloc_data(data, sizeof(*data));
+
set_data_header(data, sizeof(*data), PRIV_CRED_SETCRED, conf);
/* Not really necessary, but helps to catch programming errors. */
data->new_cred = NULL;
@@ -2623,6 +2891,112 @@
clear_data(data);
}
+static int
+mac_do_vnode_check_exec(struct ucred *cred __unused,
+ struct vnode *vp __unused, struct label *vplabel __unused,
+ struct image_params *imgp, struct label *execlabel __unused)
+{
+ struct mac_do_exec_data *data;
+ const char *path;
+ char *to_free;
+ int error;
+
+ if (do_enabled == 0)
+ return (0);
+
+ data = fetch_exec_data();
+ if (data == NULL || data->conf == NULL)
+ return (0); /* No exec constraint. */
+
+ if (data->nb_rules == 0)
+ return (0); /* No rules with exec constraints. */
+
+ /*
+ * Determine the path of the binary being exec'd.
+ *
+ * imgp->interpreted != 0 means an image activator already ran on a
+ * prior pass of do_execve's interpret loop (shell or binmisc
+ * interpreter: IMGACT_SHELL / IMGACT_BINMISC set on the main imgp),
+ * or this is the fresh sub-imgp used by elf64_load_interp_file for
+ * the ELF dynamic linker (IMGACT_INTERP_ELF set before the call).
+ * In every such case, this vnode_check_exec fires for an interpreter
+ * being loaded by the kernel as part of exec'ing an already-checked
+ * binary, not a path chosen by the user, so allow it without further
+ * checking. The constraint persists, so any later execve(2) issued
+ * from within the interpreter still hits this check.
+ *
+ * For execve(2) (imgp->args->fname != NULL), imgp->execpath is the
+ * user-provided path for absolute paths, or resolved via
+ * vn_fullpath_hardlink() for relative paths. Using it avoids
+ * namecache-dependent issues where vn_fullpath_jail() can return a
+ * cached path from a prior lookup of the same inode by another process.
+ *
+ * For fexecve(2) (imgp->args->fname == NULL), imgp->execpath is set
+ * via vn_fullpath() which has the same namecache limitations, and
+ * may be NULL if that call failed. Fall back to vn_fullpath_jail()
+ * on imgp->vp, which is the best we can do without a user-provided
+ * path.
+ */
+ if (imgp->interpreted != 0)
+ return (0); /* Interpreter loading: allow. */
+
+ to_free = NULL;
+ if (imgp->args->fname != NULL) {
+ path = imgp->execpath;
+ } else {
+ char *resolved;
+
+ if (vn_fullpath_jail(imgp->vp, &resolved, &to_free) != 0)
+ return (EPERM); /* Cannot resolve: deny. */
+ path = resolved;
+ }
+
+ /*
+ * Per-rule evaluation: allow if ANY matching rule permits this exec.
+ *
+ * - Rule with no exec constraint (paths_nb == 0, not blacklist):
+ * allows any exec.
+ * - Whitelist rule: allows if path is in the list.
+ * - Blacklist rule: allows if path is NOT in the list.
+ *
+ * The constraint persists across multiple exec calls — it is NOT
+ * cleared here. Only a new setcred(), thread exit, or module
+ * unload clears it.
+ */
+ error = EPERM; /* Default deny. */
+ for (u_int r = 0; r < data->nb_rules; r++) {
+ const struct rule *rule = data->rules[r];
+
+ if (rule->exec_paths_nb == 0 && !rule->exec_is_blacklist) {
+ error = 0; /* No constraint: allow any. */
+ break;
+ }
+
+ bool path_match = false;
+ for (u_int i = 0; i < rule->exec_paths_nb; i++) {
+ if (strcmp(rule->exec_paths[i], path) == 0) {
+ path_match = true;
+ break;
+ }
+ }
+
+ if (rule->exec_is_blacklist) {
+ if (!path_match) {
+ error = 0; /* Not blacklisted: allow. */
+ break;
+ }
+ } else {
+ if (path_match) {
+ error = 0; /* Whitelisted: allow. */
+ break;
+ }
+ }
+ }
+
+ free(to_free, M_TEMP);
+ return (error);
+}
+
static void
mac_do_init(struct mac_policy_conf *mpc)
{
@@ -2640,6 +3014,7 @@
drop_conf(default_conf);
osd_thread_slot = osd_thread_register(dealloc_thread_osd);
+ osd_thread_exec_slot = osd_thread_register(dealloc_exec_osd);
}
static void
@@ -2649,6 +3024,7 @@
* osd_thread_deregister() must be called before osd_jail_deregister(),
* for the reason explained in dealloc_jail_osd().
*/
+ osd_thread_deregister(osd_thread_exec_slot);
osd_thread_deregister(osd_thread_slot);
osd_jail_deregister(osd_jail_slot);
rm_destroy(&mac_do_rml);
@@ -2661,6 +3037,7 @@
.mpo_cred_check_setcred = mac_do_check_setcred,
.mpo_cred_setcred_exit = mac_do_setcred_exit,
.mpo_priv_grant = mac_do_priv_grant,
+ .mpo_vnode_check_exec = mac_do_vnode_check_exec,
};
MAC_POLICY_SET(&do_ops, mac_do, "MAC/do", MPC_LOADTIME_FLAG_UNLOADOK, NULL);
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sun, Aug 2, 8:20 AM (3 h, 12 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
35754159
Default Alt Text
D58012.id183077.diff (16 KB)
Attached To
Mode
D58012: MAC/do: add exec whitelist/blacklist support for mac_do consumers
Attached
Detach File
Event Timeline
Log In to Comment