[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-Id: <20250719-memfd-exec-v1-3-0ef7feba5821@gmail.com>
Date: Sat, 19 Jul 2025 05:13:13 -0600
From: Abhinav Saxena <xandfury@...il.com>
To: Mickaël Salaün <mic@...ikod.net>,
Günther Noack <gnoack@...gle.com>,
Paul Moore <paul@...l-moore.com>, James Morris <jmorris@...ei.org>,
"Serge E. Hallyn" <serge@...lyn.com>, Shuah Khan <shuah@...nel.org>,
Nathan Chancellor <nathan@...nel.org>,
Nick Desaulniers <nick.desaulniers+lkml@...il.com>,
Bill Wendling <morbo@...gle.com>, Justin Stitt <justinstitt@...gle.com>
Cc: linux-security-module@...r.kernel.org, linux-kernel@...r.kernel.org,
linux-kselftest@...r.kernel.org, llvm@...ts.linux.dev,
Abhinav Saxena <xandfury@...il.com>
Subject: [PATCH RFC 3/4] landlock: add memfd exec LSM hooks and scoping
Implement LSM hooks to enforce memfd execution restrictions:
- hook_mmap_file: Prevent executable mapping of memfd files
- hook_file_mprotect: Block mprotect() adding PROT_EXEC to memfd
mappings
- hook_bprm_creds_for_exec: Prevent direct execution via execve()
family
- hook_file_alloc_security: Initialize memfd files with proper access
masks
All hooks use domain hierarchy checking to enforce scoped restrictions
with proper audit logging. This prevents multiple attack vectors:
- Direct mmap(PROT_EXEC) on memfd
- Two-stage mmap(PROT_READ) + mprotect(PROT_EXEC) bypass
- execve("/proc/self/fd/N") anonymous execution
Implement memfd execution access control in check_memfd_execute_access()
using hierarchy-aware domain checking
Signed-off-by: Abhinav Saxena <xandfury@...il.com>
---
security/landlock/cred.c | 14 ----
security/landlock/fs.c | 195 ++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 194 insertions(+), 15 deletions(-)
diff --git a/security/landlock/cred.c b/security/landlock/cred.c
index 0cb3edde4d18..356dad0b7e9b 100644
--- a/security/landlock/cred.c
+++ b/security/landlock/cred.c
@@ -43,25 +43,11 @@ static void hook_cred_free(struct cred *const cred)
landlock_put_ruleset_deferred(dom);
}
-#ifdef CONFIG_AUDIT
-
-static int hook_bprm_creds_for_exec(struct linux_binprm *const bprm)
-{
- /* Resets for each execution. */
- landlock_cred(bprm->cred)->domain_exec = 0;
- return 0;
-}
-
-#endif /* CONFIG_AUDIT */
-
static struct security_hook_list landlock_hooks[] __ro_after_init = {
LSM_HOOK_INIT(cred_prepare, hook_cred_prepare),
LSM_HOOK_INIT(cred_transfer, hook_cred_transfer),
LSM_HOOK_INIT(cred_free, hook_cred_free),
-#ifdef CONFIG_AUDIT
- LSM_HOOK_INIT(bprm_creds_for_exec, hook_bprm_creds_for_exec),
-#endif /* CONFIG_AUDIT */
};
__init void landlock_add_cred_hooks(void)
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index d86d21034f4c..e8b58f2fd87e 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -1880,7 +1880,24 @@ static int hook_file_alloc_security(struct file *const file)
* without going through the file_open hook, for example when using
* memfd_create(2).
*/
- landlock_file(file)->allowed_access = LANDLOCK_MASK_ACCESS_FS;
+ access_mask_t allowed_access = LANDLOCK_MASK_ACCESS_FS;
+ const struct landlock_cred_security *subject;
+ size_t layer;
+ static const struct access_masks memfd_scope = {
+ .scope = LANDLOCK_SCOPE_MEMFD_EXEC,
+ };
+
+ /* allow everything by default */
+ landlock_file(file)->allowed_access = allowed_access;
+
+ subject = landlock_get_applicable_subject(current_cred(), memfd_scope,
+ &layer);
+ if (subject && is_memfd_file(file)) {
+ /* Creator domain restricts memfd execution */
+ allowed_access &= ~LANDLOCK_ACCESS_FS_EXECUTE;
+ landlock_file(file)->allowed_access = allowed_access;
+ /* Store creator and audit... */
+ }
return 0;
}
@@ -2107,6 +2124,178 @@ static void hook_file_free_security(struct file *file)
landlock_put_ruleset_deferred(landlock_file(file)->fown_subject.domain);
}
+static bool
+check_memfd_execute_access(const struct file *file,
+ const struct landlock_cred_security **subject,
+ size_t *layer_plus_one)
+{
+ const struct landlock_ruleset *executor_domain, *creator_domain;
+ const struct landlock_cred_security *creator_subject;
+ static const struct access_masks memfd_scope = {
+ .scope = LANDLOCK_SCOPE_MEMFD_EXEC,
+ };
+ size_t creator_layer_plus_one = 0;
+ bool executor_scoped, creator_scoped, is_scoped;
+
+ *subject = NULL;
+ *layer_plus_one = 0;
+
+ /* Check scoping status for both executor and creator */
+ *subject = landlock_get_applicable_subject(current_cred(), memfd_scope,
+ layer_plus_one);
+ creator_subject = landlock_get_applicable_subject(
+ file->f_cred, memfd_scope, &creator_layer_plus_one);
+
+ executor_scoped = (*subject != NULL);
+ creator_scoped = (creator_subject != NULL);
+
+ if (!creator_scoped)
+ return true; /* No scoping enabled, allow execution */
+
+ /* Get domains for comparison */
+ executor_domain = executor_scoped ? (*subject)->domain : NULL;
+ creator_domain = creator_scoped ? creator_subject->domain :
+ landlock_cred(file->f_cred)->domain;
+
+ pr_info("MEMFD_DEBUG: executor_domain=%p, creator_domain=%p\n",
+ executor_domain, creator_domain);
+
+ /*
+ * Same-domain: deny to prevent read-to-execute bypass
+ * This prevents processes from bypassing execute restrictions
+ * by creating memfd in the same domain
+ */
+ if (executor_domain == creator_domain)
+ return false;
+
+ /*
+ * Cross-domain: use domain hierarchy checks to see if executor is
+ * scoped from creator domain_is_scoped() returns true when access
+ * should be DENIED
+ */
+ if (executor_scoped || creator_scoped) {
+ is_scoped = domain_is_scoped(executor_domain, creator_domain,
+ LANDLOCK_SCOPE_MEMFD_EXEC);
+ pr_info("MEMFD_DEBUG: Cross-domain: is_scoped=%d, returning=%d\n",
+ is_scoped, !is_scoped);
+ /* Return true (allow) when NOT scoped, false (deny) when scoped */
+ return !is_scoped;
+ }
+
+ return true;
+}
+
+static int hook_mmap_file(struct file *file, unsigned long reqprot,
+ unsigned long prot, unsigned long flags)
+{
+ const struct landlock_cred_security *subject;
+ size_t layer_plus_one;
+
+ /* Only check executable mappings */
+ if (!(prot & PROT_EXEC))
+ return 0;
+
+ /* Only restrict memfd files */
+ if (!is_memfd_file(file))
+ return 0;
+
+ /* Check if memfd execution is allowed */
+ if (check_memfd_execute_access(file, &subject, &layer_plus_one))
+ return 0;
+
+ /* Log denial for audit */
+ if (subject) {
+ landlock_log_denial(subject, &(struct landlock_request) {
+ .type = LANDLOCK_REQUEST_SCOPE_MEMFD_EXEC,
+ .audit = {
+ .type = LSM_AUDIT_DATA_ANONINODE,
+ .u.file = file,
+ },
+ .layer_plus_one = layer_plus_one,
+ });
+ }
+
+ return -EACCES;
+}
+
+static int hook_file_mprotect(struct vm_area_struct *vma, unsigned long reqprot,
+ unsigned long prot)
+{
+ const struct landlock_cred_security *subject;
+ size_t layer_plus_one;
+
+ /* Only check when adding execute permission */
+ if (!(prot & PROT_EXEC))
+ return 0;
+
+ /* Must have a file backing the VMA */
+ if (!vma || !vma->vm_file)
+ return 0;
+
+ /* Only restrict memfd files */
+ if (!is_memfd_file(vma->vm_file))
+ return 0;
+
+ /* Check if memfd execution is allowed */
+ if (check_memfd_execute_access(vma->vm_file, &subject, &layer_plus_one))
+ return 0;
+
+ /* Log denial for audit */
+ if (subject) {
+ landlock_log_denial(subject, &(struct landlock_request) {
+ .type = LANDLOCK_REQUEST_SCOPE_MEMFD_EXEC,
+ .audit = {
+ .type = LSM_AUDIT_DATA_ANONINODE,
+ .u.file = vma->vm_file,
+ },
+ .layer_plus_one = layer_plus_one,
+ });
+ }
+
+ return -EACCES;
+}
+
+static int hook_bprm_creds_for_exec(struct linux_binprm *bprm)
+{
+#ifdef CONFIG_AUDIT
+ /* Resets for each execution. */
+ landlock_cred(bprm->cred)->domain_exec = 0;
+#endif /* CONFIG_AUDIT */
+
+ const struct landlock_cred_security *subject;
+ size_t layer_plus_one;
+ struct file *file;
+
+ if (!bprm)
+ return 0;
+
+ file = bprm->file;
+ if (!file)
+ return 0;
+
+ /* Only restrict memfd files */
+ if (!is_memfd_file(file))
+ return 0;
+
+ /* Check if memfd execution is allowed */
+ if (check_memfd_execute_access(file, &subject, &layer_plus_one))
+ return 0;
+
+ /* Log denial for audit */
+ if (subject) {
+ landlock_log_denial(subject, &(struct landlock_request) {
+ .type = LANDLOCK_REQUEST_SCOPE_MEMFD_EXEC,
+ .audit = {
+ .type = LSM_AUDIT_DATA_ANONINODE,
+ .u.file = file,
+ },
+ .layer_plus_one = layer_plus_one,
+ });
+ }
+
+ return -EACCES; /* maybe we should return EPERM? */
+}
+
static struct security_hook_list landlock_hooks[] __ro_after_init = {
LSM_HOOK_INIT(inode_free_security_rcu, hook_inode_free_security_rcu),
@@ -2133,6 +2322,10 @@ static struct security_hook_list landlock_hooks[] __ro_after_init = {
LSM_HOOK_INIT(file_ioctl_compat, hook_file_ioctl_compat),
LSM_HOOK_INIT(file_set_fowner, hook_file_set_fowner),
LSM_HOOK_INIT(file_free_security, hook_file_free_security),
+
+ LSM_HOOK_INIT(mmap_file, hook_mmap_file),
+ LSM_HOOK_INIT(file_mprotect, hook_file_mprotect),
+ LSM_HOOK_INIT(bprm_creds_for_exec, hook_bprm_creds_for_exec),
};
__init void landlock_add_fs_hooks(void)
--
2.43.0
Powered by blists - more mailing lists