[PATCH RFC 2/4] landlock: implement memfd detection

Abhinav Saxena posted 4 patches 2 months, 2 weeks ago
[PATCH RFC 2/4] landlock: implement memfd detection
Posted by Abhinav Saxena 2 months, 2 weeks ago
Add is_memfd_file() function to reliably detect memfd files by checking
for "memfd:" prefix in dentry names on shmem-backed files. This
distinguishes true memfd files from regular shmem files.

Move domain_is_scoped() to domain.c for reuse across subsystems.
Add comprehensive kunit tests for memfd detection edge cases.

Signed-off-by: Abhinav Saxena <xandfury@gmail.com>
---
 security/landlock/domain.c |  67 +++++++++++++++
 security/landlock/domain.h |   4 +
 security/landlock/fs.c     | 210 +++++++++++++++++++++++++++++++++++++++++++++
 security/landlock/task.c   |  67 ---------------
 4 files changed, 281 insertions(+), 67 deletions(-)

diff --git a/security/landlock/domain.c b/security/landlock/domain.c
index a647b68e8d06..993c299ca263 100644
--- a/security/landlock/domain.c
+++ b/security/landlock/domain.c
@@ -262,3 +262,70 @@ kunit_test_suite(test_suite);
 #endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
 
 #endif /* CONFIG_AUDIT */
+
+/**
+ * domain_is_scoped - Checks if the client domain is scoped in the same
+ *		      domain as the server.
+ *
+ * @client: IPC sender domain.
+ * @server: IPC receiver domain.
+ * @scope: The scope restriction criteria.
+ *
+ * Returns: True if the @client domain is scoped to access the @server,
+ * unless the @server is also scoped in the same domain as @client.
+ */
+bool domain_is_scoped(const struct landlock_ruleset *const client,
+		      const struct landlock_ruleset *const server,
+		      access_mask_t scope)
+{
+	int client_layer, server_layer;
+	const struct landlock_hierarchy *client_walker, *server_walker;
+
+	/* Quick return if client has no domain */
+	if (WARN_ON_ONCE(!client))
+		return false;
+
+	client_layer = client->num_layers - 1;
+	client_walker = client->hierarchy;
+	/*
+	 * client_layer must be a signed integer with greater capacity
+	 * than client->num_layers to ensure the following loop stops.
+	 */
+	BUILD_BUG_ON(sizeof(client_layer) > sizeof(client->num_layers));
+
+	server_layer = server ? (server->num_layers - 1) : -1;
+	server_walker = server ? server->hierarchy : NULL;
+
+	/*
+	 * Walks client's parent domains down to the same hierarchy level
+	 * as the server's domain, and checks that none of these client's
+	 * parent domains are scoped.
+	 */
+	for (; client_layer > server_layer; client_layer--) {
+		if (landlock_get_scope_mask(client, client_layer) & scope)
+			return true;
+
+		client_walker = client_walker->parent;
+	}
+	/*
+	 * Walks server's parent domains down to the same hierarchy level as
+	 * the client's domain.
+	 */
+	for (; server_layer > client_layer; server_layer--)
+		server_walker = server_walker->parent;
+
+	for (; client_layer >= 0; client_layer--) {
+		if (landlock_get_scope_mask(client, client_layer) & scope) {
+			/*
+			 * Client and server are at the same level in the
+			 * hierarchy. If the client is scoped, the request is
+			 * only allowed if this domain is also a server's
+			 * ancestor.
+			 */
+			return server_walker != client_walker;
+		}
+		client_walker = client_walker->parent;
+		server_walker = server_walker->parent;
+	}
+	return false;
+}
diff --git a/security/landlock/domain.h b/security/landlock/domain.h
index 7fb70b25f85a..21a9eea644bd 100644
--- a/security/landlock/domain.h
+++ b/security/landlock/domain.h
@@ -171,4 +171,8 @@ static inline void landlock_put_hierarchy(struct landlock_hierarchy *hierarchy)
 	}
 }
 
+bool domain_is_scoped(const struct landlock_ruleset *const client,
+		      const struct landlock_ruleset *const server,
+		      access_mask_t scope);
+
 #endif /* _SECURITY_LANDLOCK_DOMAIN_H */
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index da862fda329b..d86d21034f4c 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -18,6 +18,7 @@
 #include <linux/dcache.h>
 #include <linux/err.h>
 #include <linux/falloc.h>
+#include <linux/file.h>
 #include <linux/fs.h>
 #include <linux/init.h>
 #include <linux/kernel.h>
@@ -25,19 +26,26 @@
 #include <linux/list.h>
 #include <linux/lsm_audit.h>
 #include <linux/lsm_hooks.h>
+#include <linux/memfd.h>
+#include <linux/mm.h>
 #include <linux/mount.h>
 #include <linux/namei.h>
 #include <linux/path.h>
 #include <linux/pid.h>
 #include <linux/rcupdate.h>
 #include <linux/sched/signal.h>
+#include <linux/shmem_fs.h>
 #include <linux/spinlock.h>
 #include <linux/stat.h>
+#include <linux/syscalls.h>
+#include <linux/mman.h>
 #include <linux/types.h>
 #include <linux/wait_bit.h>
 #include <linux/workqueue.h>
+#include <uapi/linux/fcntl.h>
 #include <uapi/linux/fiemap.h>
 #include <uapi/linux/landlock.h>
+#include <linux/binfmts.h>
 
 #include "access.h"
 #include "audit.h"
@@ -1667,6 +1675,201 @@ get_required_file_open_access(const struct file *const file)
 	return access;
 }
 
+/**
+ * is_memfd_file - Check if file was created via memfd_create()
+ * @file: File to check
+ *
+ * Returns true if @file was created via memfd_create(), false otherwise.
+ *
+ * memfd files are shmem-backed files with "memfd:" prefix in their dentry name.
+ * This is the definitive way to distinguish memfd files from regular shmem
+ * files.
+ */
+static bool is_memfd_file(struct file *file)
+{
+	const struct dentry *dentry;
+	const unsigned char *name;
+	size_t name_len;
+
+	/* Fast path: basic validation */
+	if (unlikely(!file))
+		return false;
+
+	/* Must be shmem-backed first - this is the cheapest definitive check */
+	if (!shmem_file(file))
+		return false;
+
+#ifdef CONFIG_MEMFD_CREATE
+
+	/* Validate dentry and get name info */
+	dentry = file->f_path.dentry;
+	if (unlikely(!dentry))
+		return false;
+
+	name_len = dentry->d_name.len;
+	name = dentry->d_name.name;
+
+	/* memfd files always have "memfd:" prefix (6 characters) */
+	if (name_len < 6 || unlikely(!name))
+		return false;
+
+	/* Check for exact "memfd:" prefix */
+	return memcmp(name, "memfd:", 6) == 0;
+#else
+	return false;
+#endif
+}
+
+#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
+
+/*
+ * Test that is_memfd_file() returns false for NULL input
+ */
+static void test_memfd_null_file(struct kunit *test)
+{
+	KUNIT_EXPECT_FALSE(test, is_memfd_file(NULL));
+}
+
+/*
+ * Test that regular shmem files are correctly distinguished from memfd files
+ */
+static void test_shmem_vs_memfd_detection(struct kunit *test)
+{
+	struct file *shmem_files[4];
+	static const char *const names[] = {
+		"regular_shmem", "", "large_shmem",
+		"memfd_fake" /* This should NOT be detected as memfd */
+	};
+	static const size_t sizes[] = { 4096, 4096, 1024 * 1024, 8192 };
+	static const unsigned long vm_flags[] = { VM_NORESERVE, 0,
+						  VM_NORESERVE | VM_ACCOUNT,
+						  VM_NORESERVE };
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(shmem_files); i++) {
+		shmem_files[i] =
+			shmem_file_setup(names[i], sizes[i], vm_flags[i]);
+		KUNIT_ASSERT_FALSE(test, IS_ERR(shmem_files[i]));
+
+		/* All should be shmem-backed but NOT memfd */
+		KUNIT_EXPECT_TRUE(test, shmem_file(shmem_files[i]));
+		KUNIT_EXPECT_FALSE(test, is_memfd_file(shmem_files[i]));
+
+		/* Verify dentry name doesn't have memfd: prefix */
+		if (shmem_files[i]->f_path.dentry &&
+		    shmem_files[i]->f_path.dentry->d_name.name) {
+			const char *dentry_name =
+				shmem_files[i]->f_path.dentry->d_name.name;
+			KUNIT_EXPECT_TRUE(test,
+					  strlen(dentry_name) < 6 ||
+						  memcmp(dentry_name,
+							 "memfd:", 6) != 0);
+		}
+
+		fput(shmem_files[i]);
+	}
+}
+
+/*
+ * Test edge cases and boundary conditions
+ */
+static void test_memfd_detection_edge_cases(struct kunit *test)
+{
+	struct file *edge_case_files[3];
+	static const char *const tricky_names[] = {
+		"memf", /* Too short for memfd: prefix */
+		"memfd", /* Still too short */
+		"memfdx:test" /* Wrong prefix */
+	};
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(edge_case_files); i++) {
+		edge_case_files[i] =
+			shmem_file_setup(tricky_names[i], 4096, VM_NORESERVE);
+		KUNIT_ASSERT_FALSE(test, IS_ERR(edge_case_files[i]));
+
+		/* All should be shmem but NOT memfd due to incorrect naming */
+		KUNIT_EXPECT_TRUE(test, shmem_file(edge_case_files[i]));
+		KUNIT_EXPECT_FALSE(test, is_memfd_file(edge_case_files[i]));
+
+		fput(edge_case_files[i]);
+	}
+}
+
+/*
+ * Test detection consistency across multiple calls
+ */
+static void test_memfd_detection_consistency(struct kunit *test)
+{
+	struct file *file;
+	bool initial_result, subsequent_result;
+	int iteration;
+
+	file = shmem_file_setup("consistency_test", 4096, VM_NORESERVE);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(file));
+
+	/* Get initial detection result */
+	initial_result = is_memfd_file(file);
+
+	/* Multiple calls should return identical results */
+	for (iteration = 0; iteration < 10; iteration++) {
+		subsequent_result = is_memfd_file(file);
+		KUNIT_EXPECT_EQ(test, initial_result, subsequent_result);
+	}
+
+	fput(file);
+}
+
+#ifdef CONFIG_MEMFD_CREATE
+
+/*
+ * Test performance characteristics (ensure function is reasonably fast)
+ */
+static void test_memfd_detection_performance(struct kunit *test)
+{
+	struct file *files[5];
+	static const char *const names[] = { "perf1", "perf2", "perf3", "perf4",
+					     "perf5" };
+	ktime_t start_time, end_time;
+	s64 duration_ns;
+	int i, j;
+	const int iterations = 1000;
+
+	/* Set up test files */
+	for (i = 0; i < ARRAY_SIZE(files); i++) {
+		files[i] = shmem_file_setup(names[i], 4096, VM_NORESERVE);
+		KUNIT_ASSERT_FALSE(test, IS_ERR(files[i]));
+	}
+
+	/* Time the detection function */
+	start_time = ktime_get();
+
+	for (i = 0; i < iterations; i++) {
+		for (j = 0; j < ARRAY_SIZE(files); j++) {
+			bool result = is_memfd_file(files[j]);
+			(void)result; /* Suppress unused variable warning */
+		}
+	}
+
+	end_time = ktime_get();
+	duration_ns = ktime_to_ns(ktime_sub(end_time, start_time));
+
+	/* Cleanup */
+	for (i = 0; i < ARRAY_SIZE(files); i++)
+		fput(files[i]);
+
+	/* Performance check: should complete in reasonable time */
+	/* This is a sanity check - actual limits depend on system */
+	KUNIT_EXPECT_LT(test, duration_ns,
+			1000000000LL); /* Less than 1 second */
+
+	pr_info("memfd detection performance: %lld ns for %d iterations on %zu files\n",
+		duration_ns, iterations, ARRAY_SIZE(files));
+}
+
+#endif /* CONFIG_MEMFD_CREATE */
+#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
+
 static int hook_file_alloc_security(struct file *const file)
 {
 	/*
@@ -1949,6 +2152,13 @@ static struct kunit_case test_cases[] = {
 	KUNIT_CASE(test_is_eacces_with_none),
 	KUNIT_CASE(test_is_eacces_with_refer),
 	KUNIT_CASE(test_is_eacces_with_write),
+	KUNIT_CASE(test_memfd_null_file),
+	KUNIT_CASE(test_shmem_vs_memfd_detection),
+	KUNIT_CASE(test_memfd_detection_edge_cases),
+	KUNIT_CASE(test_memfd_detection_consistency),
+#ifdef CONFIG_MEMFD_CREATE
+	KUNIT_CASE(test_memfd_detection_performance),
+#endif
 	{}
 };
 /* clang-format on */
diff --git a/security/landlock/task.c b/security/landlock/task.c
index 2385017418ca..559a96a97ab6 100644
--- a/security/landlock/task.c
+++ b/security/landlock/task.c
@@ -165,73 +165,6 @@ static int hook_ptrace_traceme(struct task_struct *const parent)
 	return err;
 }
 
-/**
- * domain_is_scoped - Checks if the client domain is scoped in the same
- *		      domain as the server.
- *
- * @client: IPC sender domain.
- * @server: IPC receiver domain.
- * @scope: The scope restriction criteria.
- *
- * Returns: True if the @client domain is scoped to access the @server,
- * unless the @server is also scoped in the same domain as @client.
- */
-static bool domain_is_scoped(const struct landlock_ruleset *const client,
-			     const struct landlock_ruleset *const server,
-			     access_mask_t scope)
-{
-	int client_layer, server_layer;
-	const struct landlock_hierarchy *client_walker, *server_walker;
-
-	/* Quick return if client has no domain */
-	if (WARN_ON_ONCE(!client))
-		return false;
-
-	client_layer = client->num_layers - 1;
-	client_walker = client->hierarchy;
-	/*
-	 * client_layer must be a signed integer with greater capacity
-	 * than client->num_layers to ensure the following loop stops.
-	 */
-	BUILD_BUG_ON(sizeof(client_layer) > sizeof(client->num_layers));
-
-	server_layer = server ? (server->num_layers - 1) : -1;
-	server_walker = server ? server->hierarchy : NULL;
-
-	/*
-	 * Walks client's parent domains down to the same hierarchy level
-	 * as the server's domain, and checks that none of these client's
-	 * parent domains are scoped.
-	 */
-	for (; client_layer > server_layer; client_layer--) {
-		if (landlock_get_scope_mask(client, client_layer) & scope)
-			return true;
-
-		client_walker = client_walker->parent;
-	}
-	/*
-	 * Walks server's parent domains down to the same hierarchy level as
-	 * the client's domain.
-	 */
-	for (; server_layer > client_layer; server_layer--)
-		server_walker = server_walker->parent;
-
-	for (; client_layer >= 0; client_layer--) {
-		if (landlock_get_scope_mask(client, client_layer) & scope) {
-			/*
-			 * Client and server are at the same level in the
-			 * hierarchy. If the client is scoped, the request is
-			 * only allowed if this domain is also a server's
-			 * ancestor.
-			 */
-			return server_walker != client_walker;
-		}
-		client_walker = client_walker->parent;
-		server_walker = server_walker->parent;
-	}
-	return false;
-}
-
 static bool sock_is_scoped(struct sock *const other,
 			   const struct landlock_ruleset *const domain)
 {

-- 
2.43.0
Re: [PATCH RFC 2/4] landlock: implement memfd detection
Posted by Fan Wu 2 months, 2 weeks ago
On Sat, Jul 19, 2025 at 4:13 AM Abhinav Saxena <xandfury@gmail.com> wrote:
>
> Add is_memfd_file() function to reliably detect memfd files by checking
> for "memfd:" prefix in dentry names on shmem-backed files. This
> distinguishes true memfd files from regular shmem files.
>
> Move domain_is_scoped() to domain.c for reuse across subsystems.
> Add comprehensive kunit tests for memfd detection edge cases.
>
> Signed-off-by: Abhinav Saxena <xandfury@gmail.com>
> ---
>  security/landlock/domain.c |  67 +++++++++++++++
>  security/landlock/domain.h |   4 +
>  security/landlock/fs.c     | 210 +++++++++++++++++++++++++++++++++++++++++++++
>  security/landlock/task.c   |  67 ---------------
>  4 files changed, 281 insertions(+), 67 deletions(-)

...

>
> +/**
> + * is_memfd_file - Check if file was created via memfd_create()
> + * @file: File to check
> + *
> + * Returns true if @file was created via memfd_create(), false otherwise.
> + *
> + * memfd files are shmem-backed files with "memfd:" prefix in their dentry name.
> + * This is the definitive way to distinguish memfd files from regular shmem
> + * files.
> + */
> +static bool is_memfd_file(struct file *file)
> +{
> +       const struct dentry *dentry;
> +       const unsigned char *name;
> +       size_t name_len;
> +
> +       /* Fast path: basic validation */
> +       if (unlikely(!file))
> +               return false;
> +
> +       /* Must be shmem-backed first - this is the cheapest definitive check */
> +       if (!shmem_file(file))
> +               return false;
> +
> +#ifdef CONFIG_MEMFD_CREATE
> +
> +       /* Validate dentry and get name info */
> +       dentry = file->f_path.dentry;
> +       if (unlikely(!dentry))
> +               return false;
> +
> +       name_len = dentry->d_name.len;
> +       name = dentry->d_name.name;
> +
> +       /* memfd files always have "memfd:" prefix (6 characters) */
> +       if (name_len < 6 || unlikely(!name))
> +               return false;
> +
> +       /* Check for exact "memfd:" prefix */
> +       return memcmp(name, "memfd:", 6) == 0;
> +#else
> +       return false;
> +#endif

I was trying to do something similar early this year but didn't hear
feedback from the linux-mm folks.
https://lore.kernel.org/linux-security-module/20250129203932.22165-1-wufan@kernel.org/

I have considered this approach but didn't use it. My concern is,
potentially a malicious user can create a file in a shmem fs, e.g.
tmpfs , with the "memfd:" prefix, which can be used to bypass security
policy.
(Resending this message due to a misconfiguration with my email
client. Apologies for any inconvenience.)

-Fan
Re: [PATCH RFC 2/4] landlock: implement memfd detection
Posted by Abhinav Saxena 2 months, 2 weeks ago
Fan Wu <wufan@kernel.org> writes:

> On Sat, Jul 19, 2025 at 4:13 AM Abhinav Saxena <xandfury@gmail.com> wrote:
>>
>> Add is_memfd_file() function to reliably detect memfd files by checking
>> for “memfd:” prefix in dentry names on shmem-backed files. This
>> distinguishes true memfd files from regular shmem files.
>>
>> Move domain_is_scoped() to domain.c for reuse across subsystems.
>> Add comprehensive kunit tests for memfd detection edge cases.
>>
>> Signed-off-by: Abhinav Saxena <xandfury@gmail.com>
>> —
>>  security/landlock/domain.c |  67 +++++++++++++++
>>  security/landlock/domain.h |   4 +
>>  security/landlock/fs.c     | 210 +++++++++++++++++++++++++++++++++++++++++++++
>>  security/landlock/task.c   |  67 —————
>>  4 files changed, 281 insertions(+), 67 deletions(-)
>
> …
>
>>
>> +/**
>> + * is_memfd_file - Check if file was created via memfd_create()
>> + * @file: File to check
>> + *
>> + * Returns true if @file was created via memfd_create(), false otherwise.
>> + *
>> + * memfd files are shmem-backed files with “memfd:” prefix in their dentry name.
>> + * This is the definitive way to distinguish memfd files from regular shmem
>> + * files.
>> + */
>> +static bool is_memfd_file(struct file *file)
>> +{
>> +       const struct dentry *dentry;
>> +       const unsigned char *name;
>> +       size_t name_len;
>> +
>> +       /* Fast path: basic validation */
>> +       if (unlikely(!file))
>> +               return false;
>> +
>> +       /* Must be shmem-backed first - this is the cheapest definitive check */
>> +       if (!shmem_file(file))
>> +               return false;
>> +
>> +#ifdef CONFIG_MEMFD_CREATE
>> +
>> +       /* Validate dentry and get name info */
>> +       dentry = file->f_path.dentry;
>> +       if (unlikely(!dentry))
>> +               return false;
>> +
>> +       name_len = dentry->d_name.len;
>> +       name = dentry->d_name.name;
>> +
>> +       /* memfd files always have “memfd:” prefix (6 characters) */
>> +       if (name_len < 6 || unlikely(!name))
>> +               return false;
>> +
>> +       /* Check for exact “memfd:” prefix */
>> +       return memcmp(name, “memfd:”, 6) == 0;
>> +#else
>> +       return false;
>> +#endif
>
> I was trying to do something similar early this year but didn’t hear
> feedback from the linux-mm folks.
> <https://lore.kernel.org/linux-security-module/20250129203932.22165-1-wufan@kernel.org/>
>
> I have considered this approach but didn’t use it. My concern is,
> potentially a malicious user can create a file in a shmem fs, e.g.
> tmpfs , with the “memfd:” prefix, which can be used to bypass security
> policy.
> (Resending this message due to a misconfiguration with my email
> client. Apologies for any inconvenience.)
>
> -Fan

Hi Fan,

Thanks for your comments.

I agree that an LSM hook into memfd_create() would be a much better
solution. In the absence of such a function, do you think adding a
`d_unlinked(dentry)` check could serve as an additional verification?

I say things since I *think* that legitimate memfd files are always
unlinked while spoofed tmpfs files remain linked. I could be wrong
though.

In any case, we can test this approach using kprobes to validate
the behavior.

-Abhinav
Re: [PATCH RFC 2/4] landlock: implement memfd detection
Posted by Mickaël Salaün 2 weeks, 4 days ago
On Tue, Jul 22, 2025 at 03:56:38PM -0600, Abhinav Saxena wrote:
> Fan Wu <wufan@kernel.org> writes:
> 
> > On Sat, Jul 19, 2025 at 4:13 AM Abhinav Saxena <xandfury@gmail.com> wrote:
> >>
> >> Add is_memfd_file() function to reliably detect memfd files by checking
> >> for “memfd:” prefix in dentry names on shmem-backed files. This
> >> distinguishes true memfd files from regular shmem files.
> >>
> >> Move domain_is_scoped() to domain.c for reuse across subsystems.
> >> Add comprehensive kunit tests for memfd detection edge cases.
> >>
> >> Signed-off-by: Abhinav Saxena <xandfury@gmail.com>
> >> —
> >>  security/landlock/domain.c |  67 +++++++++++++++
> >>  security/landlock/domain.h |   4 +
> >>  security/landlock/fs.c     | 210 +++++++++++++++++++++++++++++++++++++++++++++
> >>  security/landlock/task.c   |  67 —————
> >>  4 files changed, 281 insertions(+), 67 deletions(-)
> >
> > …
> >
> >>
> >> +/**
> >> + * is_memfd_file - Check if file was created via memfd_create()
> >> + * @file: File to check
> >> + *
> >> + * Returns true if @file was created via memfd_create(), false otherwise.
> >> + *
> >> + * memfd files are shmem-backed files with “memfd:” prefix in their dentry name.
> >> + * This is the definitive way to distinguish memfd files from regular shmem
> >> + * files.
> >> + */
> >> +static bool is_memfd_file(struct file *file)
> >> +{
> >> +       const struct dentry *dentry;
> >> +       const unsigned char *name;
> >> +       size_t name_len;
> >> +
> >> +       /* Fast path: basic validation */
> >> +       if (unlikely(!file))
> >> +               return false;
> >> +
> >> +       /* Must be shmem-backed first - this is the cheapest definitive check */
> >> +       if (!shmem_file(file))
> >> +               return false;

In which case a memfd would not be detected by this function?
shmem_file_operations or hugetlbfs_file_operations are always set for
memfd right?

We could export memfd_get_seals() to stay consistent over time.

> >> +
> >> +#ifdef CONFIG_MEMFD_CREATE
> >> +
> >> +       /* Validate dentry and get name info */
> >> +       dentry = file->f_path.dentry;
> >> +       if (unlikely(!dentry))
> >> +               return false;
> >> +
> >> +       name_len = dentry->d_name.len;
> >> +       name = dentry->d_name.name;
> >> +
> >> +       /* memfd files always have “memfd:” prefix (6 characters) */
> >> +       if (name_len < 6 || unlikely(!name))
> >> +               return false;
> >> +
> >> +       /* Check for exact “memfd:” prefix */
> >> +       return memcmp(name, “memfd:”, 6) == 0;
> >> +#else
> >> +       return false;
> >> +#endif
> >
> > I was trying to do something similar early this year but didn’t hear
> > feedback from the linux-mm folks.
> > <https://lore.kernel.org/linux-security-module/20250129203932.22165-1-wufan@kernel.org/>
> >
> > I have considered this approach but didn’t use it. My concern is,
> > potentially a malicious user can create a file in a shmem fs, e.g.
> > tmpfs , with the “memfd:” prefix, which can be used to bypass security
> > policy.

In the case of Landlock that should not be a security issue but a
compatibility issue, which is not better.

> > (Resending this message due to a misconfiguration with my email
> > client. Apologies for any inconvenience.)
> >
> > -Fan
> 
> Hi Fan,
> 
> Thanks for your comments.
> 
> I agree that an LSM hook into memfd_create() would be a much better
> solution. In the absence of such a function, do you think adding a
> `d_unlinked(dentry)` check could serve as an additional verification?
> 
> I say things since I *think* that legitimate memfd files are always
> unlinked while spoofed tmpfs files remain linked. I could be wrong
> though.

We should just use the same checks used by the memfd-related
syscalls/operations to detect such file.

> 
> In any case, we can test this approach using kprobes to validate
> the behavior.
> 
> -Abhinav

Re: [PATCH RFC 2/4] landlock: implement memfd detection
Posted by Mickaël Salaün 2 weeks, 4 days ago
On Thu, Sep 18, 2025 at 04:07:48PM +0200, Mickaël Salaün wrote:
> On Tue, Jul 22, 2025 at 03:56:38PM -0600, Abhinav Saxena wrote:
> > Fan Wu <wufan@kernel.org> writes:
> > 
> > > On Sat, Jul 19, 2025 at 4:13 AM Abhinav Saxena <xandfury@gmail.com> wrote:
> > >>
> > >> Add is_memfd_file() function to reliably detect memfd files by checking
> > >> for “memfd:” prefix in dentry names on shmem-backed files. This
> > >> distinguishes true memfd files from regular shmem files.
> > >>
> > >> Move domain_is_scoped() to domain.c for reuse across subsystems.
> > >> Add comprehensive kunit tests for memfd detection edge cases.
> > >>
> > >> Signed-off-by: Abhinav Saxena <xandfury@gmail.com>
> > >> —
> > >>  security/landlock/domain.c |  67 +++++++++++++++
> > >>  security/landlock/domain.h |   4 +
> > >>  security/landlock/fs.c     | 210 +++++++++++++++++++++++++++++++++++++++++++++
> > >>  security/landlock/task.c   |  67 —————
> > >>  4 files changed, 281 insertions(+), 67 deletions(-)
> > >
> > > …
> > >
> > >>
> > >> +/**
> > >> + * is_memfd_file - Check if file was created via memfd_create()
> > >> + * @file: File to check
> > >> + *
> > >> + * Returns true if @file was created via memfd_create(), false otherwise.
> > >> + *
> > >> + * memfd files are shmem-backed files with “memfd:” prefix in their dentry name.
> > >> + * This is the definitive way to distinguish memfd files from regular shmem
> > >> + * files.
> > >> + */
> > >> +static bool is_memfd_file(struct file *file)
> > >> +{
> > >> +       const struct dentry *dentry;
> > >> +       const unsigned char *name;
> > >> +       size_t name_len;
> > >> +
> > >> +       /* Fast path: basic validation */
> > >> +       if (unlikely(!file))
> > >> +               return false;
> > >> +
> > >> +       /* Must be shmem-backed first - this is the cheapest definitive check */
> > >> +       if (!shmem_file(file))
> > >> +               return false;
> 
> In which case a memfd would not be detected by this function?
> shmem_file_operations or hugetlbfs_file_operations are always set for
> memfd right?
> 
> We could export memfd_get_seals() to stay consistent over time.
> 
> > >> +
> > >> +#ifdef CONFIG_MEMFD_CREATE
> > >> +
> > >> +       /* Validate dentry and get name info */
> > >> +       dentry = file->f_path.dentry;
> > >> +       if (unlikely(!dentry))
> > >> +               return false;
> > >> +
> > >> +       name_len = dentry->d_name.len;
> > >> +       name = dentry->d_name.name;
> > >> +
> > >> +       /* memfd files always have “memfd:” prefix (6 characters) */
> > >> +       if (name_len < 6 || unlikely(!name))
> > >> +               return false;
> > >> +
> > >> +       /* Check for exact “memfd:” prefix */
> > >> +       return memcmp(name, “memfd:”, 6) == 0;
> > >> +#else
> > >> +       return false;
> > >> +#endif
> > >
> > > I was trying to do something similar early this year but didn’t hear
> > > feedback from the linux-mm folks.
> > > <https://lore.kernel.org/linux-security-module/20250129203932.22165-1-wufan@kernel.org/>
> > >
> > > I have considered this approach but didn’t use it. My concern is,
> > > potentially a malicious user can create a file in a shmem fs, e.g.
> > > tmpfs , with the “memfd:” prefix, which can be used to bypass security
> > > policy.
> 
> In the case of Landlock that should not be a security issue but a
> compatibility issue, which is not better.
> 
> > > (Resending this message due to a misconfiguration with my email
> > > client. Apologies for any inconvenience.)
> > >
> > > -Fan
> > 
> > Hi Fan,
> > 
> > Thanks for your comments.
> > 
> > I agree that an LSM hook into memfd_create() would be a much better
> > solution. In the absence of such a function, do you think adding a

Hmm, we cannot use hook_file_alloc_security() to enforce restriction on
memfd creation because the inode mode is set after this call by
memfd.c:alloc_file().  So we should indeed add a new LSM hook.  That
would also improve security checks on file creation for other file
types.

> > `d_unlinked(dentry)` check could serve as an additional verification?
> > 
> > I say things since I *think* that legitimate memfd files are always
> > unlinked while spoofed tmpfs files remain linked. I could be wrong
> > though.
> 
> We should just use the same checks used by the memfd-related
> syscalls/operations to detect such file.
> 
> > 
> > In any case, we can test this approach using kprobes to validate
> > the behavior.
> > 
> > -Abhinav
>