From fd8bef4162319b67d97832e8109965b183909e94 Mon Sep 17 00:00:00 2001 From: Stephen Smalley Date: Fri, 7 Aug 2026 15:13:43 -0400 Subject: [PATCH 01/13] selinux: tighten policydb_context_isvalid() checks policydb_context_isvalid() bounds each of user, role, and type but does not require the value to be defined. A crafted policy can declare more identifiers that it defines, so an initial SID or ocontext context whose type is not defined can be inserted into the sidtab and later reach type_attribute_bounds_av() from context_struct_compute_av(), thereby reaching the BUG_ON(!type_val_to_struct[type - 1]). A user with no datum can reach context_struct_to_string() and thereby trigger strlen(NULL). The user and role datums are already NULL-checked further down but only when role is not OBJECT_R_VAL, so an object context can carry an undefined value through. Use policydb_{role,user,type}_isvalid(), which check both the range and that the value has a name, in place of the inline range checks. The name and datum arrays are populated by *_index(), so a value with a name also has a datum. This runs after policydb_index(), so the arrays are populated. The now-redundant NULL guards on role and usrdatum in the OBJECT_R_VAL block can then be dropped. Signed-off-by: Stephen Smalley Signed-off-by: Paul Moore --- security/selinux/ss/policydb.c | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/security/selinux/ss/policydb.c b/security/selinux/ss/policydb.c index b75c977c6673..4342258cb148 100644 --- a/security/selinux/ss/policydb.c +++ b/security/selinux/ss/policydb.c @@ -1037,13 +1037,13 @@ bool policydb_context_isvalid(const struct policydb *p, const struct context *c) const struct role_datum *role; const struct user_datum *usrdatum; - if (!c->role || c->role > p->p_roles.nprim) + if (!policydb_role_isvalid(p, c->role)) return false; - if (!c->user || c->user > p->p_users.nprim) + if (!policydb_user_isvalid(p, c->user)) return false; - if (!c->type || c->type > p->p_types.nprim) + if (!policydb_simpletype_isvalid(p, c->type)) return false; if (c->role != OBJECT_R_VAL) { @@ -1051,7 +1051,7 @@ bool policydb_context_isvalid(const struct policydb *p, const struct context *c) * Role must be authorized for the type. */ role = p->role_val_to_struct[c->role - 1]; - if (!role || !ebitmap_get_bit(&role->types, c->type - 1)) + if (!ebitmap_get_bit(&role->types, c->type - 1)) /* role may not be associated with type */ return false; @@ -1059,9 +1059,6 @@ bool policydb_context_isvalid(const struct policydb *p, const struct context *c) * User must be authorized for the role. */ usrdatum = p->user_val_to_struct[c->user - 1]; - if (!usrdatum) - return false; - if (!ebitmap_get_bit(&usrdatum->roles, c->role - 1)) /* user may not be associated with role */ return false; From fc68b6a45f1668bbe03264343115d1c97013c7ea Mon Sep 17 00:00:00 2001 From: Paul Moore Date: Tue, 25 Aug 2026 12:23:54 -0400 Subject: [PATCH 02/13] selinux: convert selinuxfs inode numbers from unsigned long to u64 Starting with commit 0b2600f81cef ("treewide: change inode->i_ino from unsigned long to u64") inode numbers were changed from an 'unsigned long' type to a 'u64' but somehow selinuxfs was lost in the conversion. This patch corrects this oversight and converts selinuxfs to 'u64' inode numbers. Acked-by: Stephen Smalley Signed-off-by: Paul Moore --- security/selinux/selinuxfs.c | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/security/selinux/selinuxfs.c b/security/selinux/selinuxfs.c index c7d91476971c..292302eb60f3 100644 --- a/security/selinux/selinuxfs.c +++ b/security/selinux/selinuxfs.c @@ -75,8 +75,8 @@ struct selinux_fs_info { char **bool_pending_names; int *bool_pending_values; struct dentry *class_dir; - unsigned long last_class_ino; - unsigned long last_ino; + u64 last_class_ino; + u64 last_ino; struct super_block *sb; }; @@ -304,15 +304,14 @@ static int sel_make_bools(struct selinux_policy *newpolicy, struct dentry *bool_ int **bool_pending_values); static int sel_make_classes(struct selinux_policy *newpolicy, struct dentry *class_dir, - unsigned long *last_class_ino); + u64 *last_class_ino); /* declaration for sel_make_class_dirs */ static struct dentry *sel_make_dir(struct dentry *dir, const char *name, - unsigned long *ino); + u64 *ino); /* declaration for sel_make_policy_nodes */ -static struct dentry *sel_make_swapover_dir(struct super_block *sb, - unsigned long *ino); +static struct dentry *sel_make_swapover_dir(struct super_block *sb, u64 *ino); static ssize_t sel_read_mls(struct file *filp, char __user *buf, size_t count, loff_t *ppos) @@ -467,7 +466,7 @@ static int sel_make_policy_nodes(struct selinux_fs_info *fsi, unsigned int bool_num = 0; char **bool_names = NULL; int *bool_values = NULL; - unsigned long tmp_ino = fsi->last_ino; /* Don't increment last_ino in this function */ + u64 tmp_ino = fsi->last_ino; /* Don't increment last_ino in this function */ tmp_parent = sel_make_swapover_dir(fsi->sb, &tmp_ino); if (IS_ERR(tmp_parent)) @@ -1568,22 +1567,22 @@ static int sel_make_initcon_files(struct dentry *dir) return err; } -static inline unsigned long sel_class_to_ino(u16 class) +static inline u64 sel_class_to_ino(u16 class) { return (class * (SEL_VEC_MAX + 1)) | SEL_CLASS_INO_OFFSET; } -static inline u16 sel_ino_to_class(unsigned long ino) +static inline u16 sel_ino_to_class(u64 ino) { return (ino & SEL_INO_MASK) / (SEL_VEC_MAX + 1); } -static inline unsigned long sel_perm_to_ino(u16 class, u32 perm) +static inline u64 sel_perm_to_ino(u16 class, u32 perm) { return (class * (SEL_VEC_MAX + 1) + perm) | SEL_CLASS_INO_OFFSET; } -static inline u32 sel_ino_to_perm(unsigned long ino) +static inline u32 sel_ino_to_perm(u64 ino) { return (ino & SEL_INO_MASK) % (SEL_VEC_MAX + 1); } @@ -1591,7 +1590,7 @@ static inline u32 sel_ino_to_perm(unsigned long ino) static ssize_t sel_read_class(struct file *file, char __user *buf, size_t count, loff_t *ppos) { - unsigned long ino = file_inode(file)->i_ino; + u64 ino = file_inode(file)->i_ino; char res[TMPBUFLEN]; ssize_t len = scnprintf(res, sizeof(res), "%d", sel_ino_to_class(ino)); return simple_read_from_buffer(buf, count, ppos, res, len); @@ -1605,7 +1604,7 @@ static const struct file_operations sel_class_ops = { static ssize_t sel_read_perm(struct file *file, char __user *buf, size_t count, loff_t *ppos) { - unsigned long ino = file_inode(file)->i_ino; + u64 ino = file_inode(file)->i_ino; char res[TMPBUFLEN]; ssize_t len = scnprintf(res, sizeof(res), "%d", sel_ino_to_perm(ino)); return simple_read_from_buffer(buf, count, ppos, res, len); @@ -1622,7 +1621,7 @@ static ssize_t sel_read_policycap(struct file *file, char __user *buf, int value; char tmpbuf[TMPBUFLEN]; ssize_t length; - unsigned long i_ino = file_inode(file)->i_ino; + u64 i_ino = file_inode(file)->i_ino; value = security_policycap_supported(i_ino & SEL_INO_MASK); length = scnprintf(tmpbuf, TMPBUFLEN, "%d", value); @@ -1698,7 +1697,7 @@ static int sel_make_class_dir_entries(struct selinux_policy *newpolicy, static int sel_make_classes(struct selinux_policy *newpolicy, struct dentry *class_dir, - unsigned long *last_class_ino) + u64 *last_class_ino) { u32 i, nclasses; int rc; @@ -1763,7 +1762,7 @@ static int sel_make_policycap(struct dentry *dir) } static struct dentry *sel_make_dir(struct dentry *dir, const char *name, - unsigned long *ino) + u64 *ino) { struct inode *inode; @@ -1792,8 +1791,7 @@ static const struct inode_operations swapover_dir_inode_operations = { .permission = reject_all, }; -static struct dentry *sel_make_swapover_dir(struct super_block *sb, - unsigned long *ino) +static struct dentry *sel_make_swapover_dir(struct super_block *sb, u64 *ino) { struct dentry *dentry; struct inode *inode; From 3b0744a6358624d46f7ee2573faca2b7d92cf32b Mon Sep 17 00:00:00 2001 From: Stephen Smalley Date: Fri, 28 Aug 2026 16:23:50 -0400 Subject: [PATCH 03/13] selinux: remove unused mls_setup_user_range() mls_setup_user_range() was a helper for security_get_user_sids(), which was removed by commit ad1ac3d740cc6b8 ("selinux: prune /sys/fs/selinux/user"). Since mls_setup_user_range() has no remaining users, remove it too and any other helpers that only it was using. Signed-off-by: Stephen Smalley Signed-off-by: Paul Moore --- security/selinux/ss/mls.c | 38 --------------------------------- security/selinux/ss/mls.h | 3 --- security/selinux/ss/mls_types.h | 3 --- 3 files changed, 44 deletions(-) diff --git a/security/selinux/ss/mls.c b/security/selinux/ss/mls.c index 40c62600650e..b48f8256dd4c 100644 --- a/security/selinux/ss/mls.c +++ b/security/selinux/ss/mls.c @@ -400,44 +400,6 @@ int mls_range_set(struct context *context, struct mls_range *range) return rc; } -int mls_setup_user_range(struct policydb *p, struct context *fromcon, - struct user_datum *user, struct context *usercon) -{ - if (p->mls_enabled) { - struct mls_level *fromcon_sen = &(fromcon->range.level[0]); - struct mls_level *fromcon_clr = &(fromcon->range.level[1]); - struct mls_level *user_low = &(user->range.level[0]); - struct mls_level *user_clr = &(user->range.level[1]); - struct mls_level *user_def = &(user->dfltlevel); - struct mls_level *usercon_sen = &(usercon->range.level[0]); - struct mls_level *usercon_clr = &(usercon->range.level[1]); - - /* Honor the user's default level if we can */ - if (mls_level_between(user_def, fromcon_sen, fromcon_clr)) - *usercon_sen = *user_def; - else if (mls_level_between(fromcon_sen, user_def, user_clr)) - *usercon_sen = *fromcon_sen; - else if (mls_level_between(fromcon_clr, user_low, user_def)) - *usercon_sen = *user_low; - else - return -EINVAL; - - /* Lower the clearance of available contexts - if the clearance of "fromcon" is lower than - that of the user's default clearance (but - only if the "fromcon" clearance dominates - the user's computed sensitivity level) */ - if (mls_level_dom(user_clr, fromcon_clr)) - *usercon_clr = *fromcon_clr; - else if (mls_level_dom(fromcon_clr, user_clr)) - *usercon_clr = *user_clr; - else - return -EINVAL; - } - - return 0; -} - /* * Convert the MLS fields in the security context * structure `oldc' from the values specified in the diff --git a/security/selinux/ss/mls.h b/security/selinux/ss/mls.h index 93cde1b22992..c107b5182fb7 100644 --- a/security/selinux/ss/mls.h +++ b/security/selinux/ss/mls.h @@ -46,9 +46,6 @@ int mls_compute_sid(struct policydb *p, struct context *scontext, struct context *tcontext, u16 tclass, u32 specified, struct context *newcontext, bool sock); -int mls_setup_user_range(struct policydb *p, struct context *fromcon, - struct user_datum *user, struct context *usercon); - #ifdef CONFIG_NETLABEL void mls_export_netlbl_lvl(struct policydb *p, struct context *context, struct netlbl_lsm_secattr *secattr); diff --git a/security/selinux/ss/mls_types.h b/security/selinux/ss/mls_types.h index 51df2ebd1211..d36a03f104c2 100644 --- a/security/selinux/ss/mls_types.h +++ b/security/selinux/ss/mls_types.h @@ -42,9 +42,6 @@ static inline int mls_level_dom(const struct mls_level *l1, #define mls_level_incomp(l1, l2) \ (!mls_level_dom((l1), (l2)) && !mls_level_dom((l2), (l1))) -#define mls_level_between(l1, l2, l3) \ - (mls_level_dom((l1), (l2)) && mls_level_dom((l3), (l1))) - #define mls_range_contains(r1, r2) \ (mls_level_dom(&(r2).level[0], &(r1).level[0]) && \ mls_level_dom(&(r1).level[1], &(r2).level[1])) From 8be71e1ba093cb3aee90c7cf6b403d1fd71a77e9 Mon Sep 17 00:00:00 2001 From: Stephen Smalley Date: Fri, 11 Sep 2026 12:37:35 -0400 Subject: [PATCH 04/13] selinux: mark status and policy inodes as immutable, drop status mmap write checks The selinuxfs "status" and "policy" files are read-only interfaces that are also mmap'd by userspace. They are created 0444 by simple_fill_super() but a CAP_DAC_OVERRIDE caller can still open them O_WRONLY/O_RDWR, open(O_RDONLY|O_TRUNC) them, or truncate(2) them. Mark both inodes S_IMMUTABLE at fill_super time. inode_permission() tests IS_IMMUTABLE before the DAC / capability checks, so all of the above are rejected at the VFS layer without ever reaching the file operations. Since a writable file can no longer exist, do_mmap() clear VM_MAYWRITE for MAP_SHARED mappings on its own, and the sel_mmap_handle_status() write/mprotect guards are dead; drop them. MAP_PRIVATE writable mappings become permitted (they were previously -EPERM) and CoW harmlessly to a private page, matching how sel_mmap_policy() has always treated the private case. The sel_mmap_policy() VM_SHARED guard becomes redundant for the same reason; leave dropping it to the pending "selinux: reject writable opens of policy file, drop mmap shared/write check" patch so that the patches do not conflict. Link: https://lore.kernel.org/selinux/aqPUqU3eZeFrykj3@gremlin/ cc: ljs@kernel.org cc: jannh@google.com cc: jack@suse.cz cc: cgzones@googlemail.com cc: brauner@kernel.org Suggested-by: Jan Kara Signed-off-by: Stephen Smalley Reviewed-by: Jan Kara Acked-by: Lorenzo Stoakes (ARM) Signed-off-by: Paul Moore --- security/selinux/selinuxfs.c | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/security/selinux/selinuxfs.c b/security/selinux/selinuxfs.c index 292302eb60f3..0941ce79ea0b 100644 --- a/security/selinux/selinuxfs.c +++ b/security/selinux/selinuxfs.c @@ -247,11 +247,6 @@ static int sel_mmap_handle_status(struct file *filp, /* only allows one page from the head */ if (vma->vm_pgoff > 0 || size != PAGE_SIZE) return -EIO; - /* disallow writable mapping */ - if (vma->vm_flags & VM_WRITE) - return -EPERM; - /* disallow mprotect() turns it into writable */ - vm_flags_clear(vma, VM_MAYWRITE); return remap_pfn_range(vma, vma->vm_start, page_to_pfn(status), @@ -1818,6 +1813,17 @@ static struct dentry *sel_make_swapover_dir(struct super_block *sb, u64 *ino) #define NULL_FILE_NAME "null" +static void sel_mark_immutable(struct dentry *root, const char *name) +{ + struct qstr q = QSTR(name); + struct dentry *dentry = try_lookup_noperm(&q, root); + + if (!IS_ERR_OR_NULL(dentry)) { + d_inode(dentry)->i_flags |= S_IMMUTABLE; + dput(dentry); + } +} + static int sel_fill_super(struct super_block *sb, struct fs_context *fc) { struct selinux_fs_info *fsi; @@ -1857,6 +1863,9 @@ static int sel_fill_super(struct super_block *sb, struct fs_context *fc) if (ret) goto err; + sel_mark_immutable(sb->s_root, "status"); + sel_mark_immutable(sb->s_root, "policy"); + fsi = sb->s_fs_info; fsi->bool_dir = sel_make_dir(sb->s_root, BOOL_DIR_NAME, &fsi->last_ino); if (IS_ERR(fsi->bool_dir)) { From b4e7b9a70de3cb6261be55d5771fe209f7394eeb Mon Sep 17 00:00:00 2001 From: Paul Moore Date: Tue, 15 Sep 2026 16:29:19 -0400 Subject: [PATCH 05/13] selinux: avoid 64-bit division of inode numbers As reported by Arnd, a previous change to convert the selinuxfs inode numbers to u64 to match the rest of the kernel caused a problem on 32-bit systems: arm-linux-gnueabi-ld: .../selinuxfs.o: in function `sel_read_class': selinuxfs.c:(.text+0xdcc): undefined reference to `__aeabi_uldivmod' arm-linux-gnueabi-ld: .../selinuxfs.o: in function `sel_read_perm': selinuxfs.c:(.text+0xed0): undefined reference to `__aeabi_uldivmod' Since we don't currently use more than 24-bits in the selinuxfs inodes (see SEL_INO_MASK), fix this by applying the SEL_INO_MASK to the selinuxfs inode numbers and casting to a u32 before performing any division operations. Fixes: fc68b6a45f16 ("selinux: convert selinuxfs inode numbers from unsigned long to u64") Reported-by: Arnd Bergmann Signed-off-by: Paul Moore --- security/selinux/selinuxfs.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/security/selinux/selinuxfs.c b/security/selinux/selinuxfs.c index 0941ce79ea0b..b3eaf3823fb1 100644 --- a/security/selinux/selinuxfs.c +++ b/security/selinux/selinuxfs.c @@ -1569,7 +1569,8 @@ static inline u64 sel_class_to_ino(u16 class) static inline u16 sel_ino_to_class(u64 ino) { - return (ino & SEL_INO_MASK) / (SEL_VEC_MAX + 1); + u32 ino_masked = ino & SEL_INO_MASK; + return ino_masked / (SEL_VEC_MAX + 1); } static inline u64 sel_perm_to_ino(u16 class, u32 perm) @@ -1579,7 +1580,8 @@ static inline u64 sel_perm_to_ino(u16 class, u32 perm) static inline u32 sel_ino_to_perm(u64 ino) { - return (ino & SEL_INO_MASK) % (SEL_VEC_MAX + 1); + u32 ino_masked = ino & SEL_INO_MASK; + return ino_masked % (SEL_VEC_MAX + 1); } static ssize_t sel_read_class(struct file *file, char __user *buf, From e6b4f0bac981dcb5c1fd682be0d7a8d69892f5e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6ttsche?= Date: Thu, 17 Sep 2026 16:13:16 +0200 Subject: [PATCH 06/13] selinux: constify avc function parameters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Constify read-only decision structs to ease reasoning where decisions are actually modified, and where only processed. Signed-off-by: Christian Göttsche Acked-by: Stephen Smalley [PM: fixed line length issues] Signed-off-by: Paul Moore --- security/selinux/avc.c | 34 ++++++++++++++++++---------------- security/selinux/include/avc.h | 7 ++++--- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/security/selinux/avc.c b/security/selinux/avc.c index a9401d6c2e5f..d8c9dd6411d7 100644 --- a/security/selinux/avc.c +++ b/security/selinux/avc.c @@ -190,8 +190,8 @@ avc_xperms_decision_lookup(u8 driver, u8 base_perm, } static inline unsigned int -avc_xperms_has_perm(struct extended_perms_decision *xpd, - u8 perm, u8 which) +avc_xperms_has_perm(const struct extended_perms_decision *xpd, + u8 perm, u8 which) { unsigned int rc = 0; @@ -247,7 +247,7 @@ static void avc_xperms_free(struct avc_xperms_node *xp_node) } static void avc_copy_xperms_decision(struct extended_perms_decision *dest, - struct extended_perms_decision *src) + const struct extended_perms_decision *src) { dest->base_perm = src->base_perm; dest->driver = src->driver; @@ -269,7 +269,7 @@ static void avc_copy_xperms_decision(struct extended_perms_decision *dest, */ static inline void avc_quick_copy_xperms_decision(u8 perm, struct extended_perms_decision *dest, - struct extended_perms_decision *src) + const struct extended_perms_decision *src) { /* * compute index of the u32 of the 256 bits (8 u32s) that contain this @@ -323,7 +323,7 @@ static struct avc_xperms_decision_node } static int avc_add_xperms_decision(struct avc_node *node, - struct extended_perms_decision *src) + const struct extended_perms_decision *src) { struct avc_xperms_decision_node *dest_xpd; @@ -348,7 +348,7 @@ static struct avc_xperms_node *avc_xperms_alloc(void) } static int avc_xperms_populate(struct avc_node *node, - struct avc_xperms_node *src) + const struct avc_xperms_node *src) { struct avc_xperms_node *dest; struct avc_xperms_decision_node *dest_xpd; @@ -381,11 +381,11 @@ static int avc_xperms_populate(struct avc_node *node, } static inline u32 avc_xperms_audit_required(u32 requested, - struct av_decision *avd, - struct extended_perms_decision *xpd, - u8 perm, - int result, - u32 *deniedp) + const struct av_decision *avd, + const struct extended_perms_decision *xpd, + u8 perm, + int result, + u32 *deniedp) { u32 denied, audited; @@ -411,8 +411,8 @@ static inline u32 avc_xperms_audit_required(u32 requested, } static inline int avc_xperms_audit(u32 ssid, u32 tsid, u16 tclass, - u32 requested, struct av_decision *avd, - struct extended_perms_decision *xpd, + u32 requested, const struct av_decision *avd, + const struct extended_perms_decision *xpd, u8 perm, int result, struct common_audit_data *ad) { @@ -509,7 +509,8 @@ static struct avc_node *avc_alloc_node(void) return node; } -static void avc_node_populate(struct avc_node *node, u32 ssid, u32 tsid, u16 tclass, struct av_decision *avd) +static void avc_node_populate(struct avc_node *node, u32 ssid, u32 tsid, + u16 tclass, const struct av_decision *avd) { node->ae.ssid = ssid; node->ae.tsid = tsid; @@ -603,7 +604,8 @@ static int avc_latest_notif_update(u32 seqno, int is_insert) * the access vectors into a cache entry. */ static void avc_insert(u32 ssid, u32 tsid, u16 tclass, - struct av_decision *avd, struct avc_xperms_node *xp_node) + const struct av_decision *avd, + const struct avc_xperms_node *xp_node) { struct avc_node *pos, *node = NULL; u32 hvalue; @@ -828,7 +830,7 @@ int __init avc_add_callback(int (*callback)(u32 event), u32 events) */ static int avc_update_node(u32 event, u32 perms, u8 driver, u8 base_perm, u8 xperm, u32 ssid, u32 tsid, u16 tclass, u32 seqno, - struct extended_perms_decision *xpd, u32 flags) + const struct extended_perms_decision *xpd, u32 flags) { u32 hvalue; int rc = 0; diff --git a/security/selinux/include/avc.h b/security/selinux/include/avc.h index 01b5167fee1a..87027a934a11 100644 --- a/security/selinux/include/avc.h +++ b/security/selinux/include/avc.h @@ -61,8 +61,9 @@ struct selinux_audit_data { void __init avc_init(void); -static inline u32 avc_audit_required(u32 requested, struct av_decision *avd, - int result, u32 auditdeny, u32 *deniedp) +static inline u32 avc_audit_required(u32 requested, + const struct av_decision *avd, int result, + u32 auditdeny, u32 *deniedp) { u32 denied, audited; @@ -121,7 +122,7 @@ int slow_avc_audit(u32 ssid, u32 tsid, u16 tclass, u32 requested, u32 audited, * before calling the auditing code. */ static inline int avc_audit(u32 ssid, u32 tsid, u16 tclass, u32 requested, - struct av_decision *avd, int result, + const struct av_decision *avd, int result, struct common_audit_data *a) { u32 audited, denied; From 508d1fcc2d099e1e8d9b6fc14cb150adb8c4b092 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6ttsche?= Date: Thu, 17 Sep 2026 16:20:38 +0200 Subject: [PATCH 07/13] selinux: reject a policy that both rejects and allows unknown classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit handle-unknown is a single three-valued choice - deny, reject or allow - but the policydb header spells it as two independent bits and policydb_read() takes any combination of them. libsepol writes exactly one of the three values (POLICYDB_CONFIG_UNKNOWN_MASK & p->handle_unknown), so no policy built by the reference toolchain carries this combination. Signed-off-by: Christian Göttsche Acked-by: Stephen Smalley Signed-off-by: Paul Moore --- security/selinux/ss/policydb.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/security/selinux/ss/policydb.c b/security/selinux/ss/policydb.c index 4342258cb148..7eeaf7076c52 100644 --- a/security/selinux/ss/policydb.c +++ b/security/selinux/ss/policydb.c @@ -2844,7 +2844,8 @@ int policydb_read(struct policydb *p, struct policy_file *fp) goto bad; } - if ((le32_to_cpu(buf[1]) & POLICYDB_CONFIG_MLS)) { + val = le32_to_cpu(buf[1]); + if (val & POLICYDB_CONFIG_MLS) { p->mls_enabled = 1; rc = -EINVAL; @@ -2855,8 +2856,13 @@ int policydb_read(struct policydb *p, struct policy_file *fp) goto bad; } } - p->reject_unknown = !!(le32_to_cpu(buf[1]) & REJECT_UNKNOWN); - p->allow_unknown = !!(le32_to_cpu(buf[1]) & ALLOW_UNKNOWN); + rc = -EINVAL; + if ((val & (REJECT_UNKNOWN | ALLOW_UNKNOWN)) == (REJECT_UNKNOWN | ALLOW_UNKNOWN)) { + pr_err("SELinux: policydb configuration both rejects and allows unknown classes and permissions\n"); + goto bad; + } + p->reject_unknown = !!(val & REJECT_UNKNOWN); + p->allow_unknown = !!(val & ALLOW_UNKNOWN); if (p->policyvers >= POLICYDB_VERSION_POLCAP) { rc = ebitmap_read(&p->policycaps, fp); From 641bd5750517d2e5c128c566669c190857260693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6ttsche?= Date: Thu, 3 Sep 2026 13:35:38 +0200 Subject: [PATCH 08/13] selinux: reject duplicate symbol values during policy load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The symbol index functions assign unconditionally, so two symbols that share a value silently overwrite each other and leave another value with no entry. Reject a value whose slot is already populated, for commons, classes, roles, types, users, sensitivities and categories; booleans already have their own "declared but not defined" check. Signed-off-by: Christian Göttsche Reviewed-by: Stephen Smalley Signed-off-by: Paul Moore --- security/selinux/ss/policydb.c | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/security/selinux/ss/policydb.c b/security/selinux/ss/policydb.c index 7eeaf7076c52..6119df551f23 100644 --- a/security/selinux/ss/policydb.c +++ b/security/selinux/ss/policydb.c @@ -560,6 +560,9 @@ static int common_index(void *key, void *datum, void *datap) if (!comdatum->value || comdatum->value > p->p_commons.nprim) return -EINVAL; + if (p->sym_val_to_name[SYM_COMMONS][comdatum->value - 1]) + return -EINVAL; + p->sym_val_to_name[SYM_COMMONS][comdatum->value - 1] = key; return 0; @@ -575,6 +578,9 @@ static int class_index(void *key, void *datum, void *datap) if (!cladatum->value || cladatum->value > p->p_classes.nprim) return -EINVAL; + if (p->class_val_to_struct[cladatum->value - 1]) + return -EINVAL; + p->sym_val_to_name[SYM_CLASSES][cladatum->value - 1] = key; p->class_val_to_struct[cladatum->value - 1] = cladatum; return 0; @@ -591,6 +597,9 @@ static int role_index(void *key, void *datum, void *datap) role->bounds > p->p_roles.nprim) return -EINVAL; + if (p->role_val_to_struct[role->value - 1]) + return -EINVAL; + p->sym_val_to_name[SYM_ROLES][role->value - 1] = key; p->role_val_to_struct[role->value - 1] = role; return 0; @@ -613,6 +622,8 @@ static int type_index(void *key, void *datum, void *datap) } if (typdatum->primary) { + if (p->type_val_to_struct[typdatum->value - 1]) + return -EINVAL; p->sym_val_to_name[SYM_TYPES][typdatum->value - 1] = key; p->type_val_to_struct[typdatum->value - 1] = typdatum; } @@ -631,6 +642,9 @@ static int user_index(void *key, void *datum, void *datap) usrdatum->bounds > p->p_users.nprim) return -EINVAL; + if (p->user_val_to_struct[usrdatum->value - 1]) + return -EINVAL; + p->sym_val_to_name[SYM_USERS][usrdatum->value - 1] = key; p->user_val_to_struct[usrdatum->value - 1] = usrdatum; return 0; @@ -647,8 +661,11 @@ static int sens_index(void *key, void *datum, void *datap) if (!levdatum->level.sens || levdatum->level.sens > p->p_levels.nprim) return -EINVAL; - if (!levdatum->isalias) + if (!levdatum->isalias) { + if (p->sym_val_to_name[SYM_LEVELS][levdatum->level.sens - 1]) + return -EINVAL; p->sym_val_to_name[SYM_LEVELS][levdatum->level.sens - 1] = key; + } return 0; } @@ -664,8 +681,11 @@ static int cat_index(void *key, void *datum, void *datap) if (!catdatum->value || catdatum->value > p->p_cats.nprim) return -EINVAL; - if (!catdatum->isalias) + if (!catdatum->isalias) { + if (p->sym_val_to_name[SYM_CATS][catdatum->value - 1]) + return -EINVAL; p->sym_val_to_name[SYM_CATS][catdatum->value - 1] = key; + } return 0; } From 0037abbab6fb9cb18539b9ee2b59d9c3056d3bee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6ttsche?= Date: Thu, 3 Sep 2026 13:35:34 +0200 Subject: [PATCH 09/13] selinux: bounds-check filename transition source types at load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit filename_trans_read_helper() validates the target type, target class and each datum's otype, but the per-datum stypes ebitmap is read without any bound on its set bits. Reject a stypes bitmap whose highest set bit is not a defined type. Signed-off-by: Christian Göttsche Reviewed-by: Stephen Smalley Signed-off-by: Paul Moore --- security/selinux/ss/policydb.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/security/selinux/ss/policydb.c b/security/selinux/ss/policydb.c index 6119df551f23..13d88e47db3c 100644 --- a/security/selinux/ss/policydb.c +++ b/security/selinux/ss/policydb.c @@ -2367,6 +2367,10 @@ static int filename_trans_read_helper(struct policydb *p, struct policy_file *fp if (rc) goto out; + rc = -EINVAL; + if (ebitmap_get_highest_set_bit(&datum->stypes) >= p->p_types.nprim) + goto out; + rc = next_entry(buf, fp, sizeof(u32)); if (rc) goto out; From 7e3e23305d13db5fa7cfaa2dbc9ab430218025c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6ttsche?= Date: Thu, 17 Sep 2026 16:13:17 +0200 Subject: [PATCH 10/13] selinux: check neveraudit in avc_xperms_audit_required() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit avc_audit_required() returns early for a neveraudit domain, while avc_xperms_audit_required() instead relies on security_compute_av() (and security_compute_av_user()) having already zeroed avd->auditallow and avd->auditdeny for such a domain. That leaves its "else if (result)" branch as the only way left to audit a neveraudit domain, and makes the two helpers read differently for the same rule. Add the same early return to avc_xperms_audit_required(), so both audit gates state the neveraudit rule directly and the extended permission audit path no longer depends on the auditallow/auditdeny mask having been zeroed elsewhere. No behavior changes today: avc_has_extended_perms() only reaches the "result" branch once the identically computed "denied" is already nonzero, so that branch was already unreachable in practice. Signed-off-by: Christian Göttsche Acked-by: Stephen Smalley Signed-off-by: Paul Moore --- security/selinux/avc.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/security/selinux/avc.c b/security/selinux/avc.c index d8c9dd6411d7..dd8905648b6a 100644 --- a/security/selinux/avc.c +++ b/security/selinux/avc.c @@ -389,6 +389,9 @@ static inline u32 avc_xperms_audit_required(u32 requested, { u32 denied, audited; + if (avd->flags & AVD_FLAGS_NEVERAUDIT) + return 0; + denied = requested & ~avd->allowed; if (unlikely(denied)) { audited = denied & avd->auditdeny; From d0e7866390ad1ae2cf24c8d3bf9856e44fd135b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6ttsche?= Date: Mon, 28 Sep 2026 18:52:53 +0200 Subject: [PATCH 11/13] selinux: bounds-check role and user membership bitmaps at load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit role->dominates, role->types and user->roles are read as ebitmaps but their set bits are never checked against the role and type symbol tables. A bit naming an undefined value is silently accepted, and an out-of-range one is used to index those tables later; in particular role_bounds_sanity_check() passes such a bit straight to sym_name(), reading p->sym_val_to_name[] out of bounds on a crafted bounded role. Validate the three bitmaps in policydb_index() once every symbol table is populated (roles are read before types, so this cannot be done in role_read()). Signed-off-by: Christian Göttsche Acked-by: Stephen Smalley Signed-off-by: Paul Moore --- security/selinux/ss/policydb.c | 52 ++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/security/selinux/ss/policydb.c b/security/selinux/ss/policydb.c index 13d88e47db3c..e0304297def4 100644 --- a/security/selinux/ss/policydb.c +++ b/security/selinux/ss/policydb.c @@ -707,6 +707,50 @@ static int sens_cat_index_check(void *key, void *datum, void *datap) return 0; } +static int role_index_check(void *key, void *datum, void *datap) +{ + const struct policydb *p = datap; + const struct role_datum *role = datum; + struct ebitmap_node *node; + u32 bit; + + ebitmap_for_each_positive_bit(&role->dominates, node, bit) { + if (!policydb_role_isvalid(p, bit + 1)) { + pr_err("SELinux: role %s declares dominance on undefined role %u\n", + (const char *)key, bit + 1); + return -EINVAL; + } + } + + ebitmap_for_each_positive_bit(&role->types, node, bit) { + if (!policydb_type_isvalid(p, bit + 1)) { + pr_err("SELinux: role %s authorizes undefined type %u\n", + (const char *)key, bit + 1); + return -EINVAL; + } + } + + return 0; +} + +static int user_index_check(void *key, void *datum, void *datap) +{ + const struct policydb *p = datap; + const struct user_datum *usr = datum; + struct ebitmap_node *node; + u32 bit; + + ebitmap_for_each_positive_bit(&usr->roles, node, bit) { + if (!policydb_role_isvalid(p, bit + 1)) { + pr_err("SELinux: user %s authorizes undefined role %u\n", + (const char *)key, bit + 1); + return -EINVAL; + } + } + + return 0; +} + /* clang-format off */ static int (*const index_f[SYM_NUM])(void *key, void *datum, void *datap) = { common_index, @@ -836,6 +880,14 @@ static int policydb_index(struct policydb *p) goto out; } + rc = hashtab_map(&p->p_roles.table, role_index_check, p); + if (rc) + goto out; + + rc = hashtab_map(&p->p_users.table, user_index_check, p); + if (rc) + goto out; + rc = 0; out: return rc; From 167d70651df872adcb37ab8317b837eb3a47d824 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6ttsche?= Date: Mon, 28 Sep 2026 18:52:51 +0200 Subject: [PATCH 12/13] selinux: validate user MLS range and default level at load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit user_read() reads each user's MLS range and default level but, unlike range transitions and security contexts, never validates them. Validate usr->range and usr->dfltlevel in user_index_check() when MLS is enabled, once the level and category symbol tables are populated. Signed-off-by: Christian Göttsche Acked-by: Stephen Smalley Signed-off-by: Paul Moore --- security/selinux/ss/policydb.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/security/selinux/ss/policydb.c b/security/selinux/ss/policydb.c index e0304297def4..e51293e3ca8c 100644 --- a/security/selinux/ss/policydb.c +++ b/security/selinux/ss/policydb.c @@ -748,6 +748,20 @@ static int user_index_check(void *key, void *datum, void *datap) } } + if (p->mls_enabled) { + if (!mls_range_isvalid(p, &usr->range)) { + pr_err("SELinux: user %s has an invalid MLS range\n", + (const char *)key); + return -EINVAL; + } + + if (!mls_level_isvalid(p, &usr->dfltlevel)) { + pr_err("SELinux: user %s has an invalid MLS default level\n", + (const char *)key); + return -EINVAL; + } + } + return 0; } From b73b967b0fc44707f39faa4f298f1f9d9b7bfac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6ttsche?= Date: Mon, 28 Sep 2026 18:52:52 +0200 Subject: [PATCH 13/13] selinux: validate permissive and neveraudit map types at load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The permissive and neveraudit maps are read as ebitmaps and indexed by type value, but their set bits are never checked. A bit that is not a defined, non-attribute type is silently accepted. Validate every set bit of both maps in policydb_index() with policydb_simpletype_isvalid(). Signed-off-by: Christian Göttsche Acked-by: Stephen Smalley Signed-off-by: Paul Moore --- security/selinux/ss/policydb.c | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/security/selinux/ss/policydb.c b/security/selinux/ss/policydb.c index e51293e3ca8c..fe0c0792583f 100644 --- a/security/selinux/ss/policydb.c +++ b/security/selinux/ss/policydb.c @@ -818,8 +818,9 @@ static inline void symtab_hash_eval(struct symtab *s) */ static int policydb_index(struct policydb *p) { + struct ebitmap_node *node; int i, rc; - u32 v; + u32 bit, v; if (p->mls_enabled) pr_debug( @@ -902,6 +903,23 @@ static int policydb_index(struct policydb *p) if (rc) goto out; + ebitmap_for_each_positive_bit(&p->permissive_map, node, bit) { + if (!policydb_simpletype_isvalid(p, bit)) { + pr_err("SELinux: permissive map refers to invalid type %u\n", + bit); + rc = -EINVAL; + goto out; + } + } + ebitmap_for_each_positive_bit(&p->neveraudit_map, node, bit) { + if (!policydb_simpletype_isvalid(p, bit)) { + pr_err("SELinux: neveraudit map refers to invalid type %u\n", + bit); + rc = -EINVAL; + goto out; + } + } + rc = 0; out: return rc;