* [PATCH 0/3] mm/mlock: make zero length requests a no-op and reject wrapping ranges
@ 2026-10-05 6:46 Jose A. Perez de Azpillaga
2026-10-05 6:46 ` [PATCH 1/3] mm/mlock: make a zero length request a no-op whatever the alignment Jose A. Perez de Azpillaga
` (2 more replies)
0 siblings, 3 replies; 4+ messages in thread
From: Jose A. Perez de Azpillaga @ 2026-10-05 6:46 UTC (permalink / raw)
To: Andrew Morton, Liam R. Howlett, Lorenzo Stoakes,
David Hildenbrand, Shuah Khan
Cc: Vlastimil Babka, Jann Horn, Pedro Falcato, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, linux-mm, linux-kernel,
linux-kselftest, Jose A. Perez de Azpillaga
Follow-up to Park Tae-sun's patch [1], which was dropped. David asked
which of the cases it raised can actually be triggered, and for selftests
that show them. Two can.
Patch 1 makes a zero length mlock()/munlock() a no-op before the address
is rounded. Today mlock(addr + 5, 0) locks a page and munlock(addr + 5, 0)
unlocks one.
Patch 2 rejects ranges where start + len overflows, or where the end
overflows when rounded to a page. Today these get a silent success, act
on a single page, or get -ENOMEM/-EINVAL depending on CAP_IPC_LOCK.
Patch 3 adds both cases to mlock2-tests.
I could not find the commit that introduced either behaviour in the git
history. Both are already present in the earliest history I could find,
so they may predate the imported git history. There is therefore no
commit to use for a Fixes tag. I have not added Cc: stable since these
changes intentionally alter UAPI-visible behaviour. A man-pages patch
for the zero length case will follow.
The mlock-random-test failure CI reported on [1] came from that patch,
not the test: it assigned 0 to do_mlock()'s error, which has to stay
-ENOMEM for the over-limit case, so mlock() over RLIMIT_MEMLOCK without
CAP_IPC_LOCK returned 0 without locking anything. With [1] applied the
test fails 40 runs out of 40; with error set back to -ENOMEM after the
check_mlock_range() call it passes all 40, as it does with this series.
Tested on x86_64 (KASAN, lockdep) in QEMU: mlock2-tests 31/31 as root and
as nobody, 24/31 and 23/31 on mm-unstable; mlock-random-test and
on-fault-limit pass.
[1] https://lore.kernel.org/all/179066531783.50175.13377521828379196177@dgu.ac.kr/
Jose A. Perez de Azpillaga (3):
mm/mlock: make a zero length request a no-op whatever the alignment
mm/mlock: reject ranges that cannot be represented
selftests/mm: test mlock() and munlock() range normalisation
mm/mlock.c | 29 +++-
tools/testing/selftests/mm/mlock2-tests.c | 191 +++++++++++++++++++++-
2 files changed, 215 insertions(+), 5 deletions(-)
base-commit: 33eb75fed9eef7a57e3f77c37c160d3e9ec55f2e
--
2.55.0
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH 1/3] mm/mlock: make a zero length request a no-op whatever the alignment
2026-10-05 6:46 [PATCH 0/3] mm/mlock: make zero length requests a no-op and reject wrapping ranges Jose A. Perez de Azpillaga
@ 2026-10-05 6:46 ` Jose A. Perez de Azpillaga
2026-10-05 6:46 ` [PATCH 2/3] mm/mlock: reject ranges that cannot be represented Jose A. Perez de Azpillaga
2026-10-05 6:46 ` [PATCH 3/3] selftests/mm: test mlock() and munlock() range normalisation Jose A. Perez de Azpillaga
2 siblings, 0 replies; 4+ messages in thread
From: Jose A. Perez de Azpillaga @ 2026-10-05 6:46 UTC (permalink / raw)
To: Andrew Morton, Liam R. Howlett, Lorenzo Stoakes,
David Hildenbrand, Shuah Khan
Cc: Vlastimil Babka, Jann Horn, Pedro Falcato, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, linux-mm, linux-kernel,
linux-kselftest, Jose A. Perez de Azpillaga, Park Tae-sun
A zero length mlock() or munlock() at an unaligned address is rounded up
to a page, so mlock(addr + 5, 0) locks a page that was never asked for,
and munlock(addr + 5, 0) unlocks one the caller may still rely on. An
aligned zero length is already a no-op.
Return early for a zero length, before the rounding and before
can_do_mlock(). This is a uapi change: with RLIMIT_MEMLOCK=0 and no
CAP_IPC_LOCK, mlock(addr, 0) returns 0 instead of -EPERM, which is what
munlock(addr, 0) already returns. man 2 mlock does not say what a zero
length should do, and -EINVAL would break callers that pass one today and
get 0.
Reported-by: Park Tae-sun <ts930@dgu.ac.kr>
Link: https://lore.kernel.org/all/179066531783.50175.13377521828379196177@dgu.ac.kr/
Signed-off-by: Jose A. Perez de Azpillaga <azpijr@gmail.com>
---
mm/mlock.c | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/mm/mlock.c b/mm/mlock.c
index 4235a1518fc9..cc28e5d7413a 100644
--- a/mm/mlock.c
+++ b/mm/mlock.c
@@ -612,6 +612,9 @@ static __must_check int do_mlock(unsigned long start, size_t len,
start = untagged_addr(start);
+ if (!len)
+ return 0;
+
if (!can_do_mlock())
return -EPERM;
@@ -678,6 +681,9 @@ SYSCALL_DEFINE2(munlock, unsigned long, start, size_t, len)
start = untagged_addr(start);
+ if (!len)
+ return 0;
+
len = PAGE_ALIGN(len + (offset_in_page(start)));
start &= PAGE_MASK;
--
2.55.0
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH 2/3] mm/mlock: reject ranges that cannot be represented
2026-10-05 6:46 [PATCH 0/3] mm/mlock: make zero length requests a no-op and reject wrapping ranges Jose A. Perez de Azpillaga
2026-10-05 6:46 ` [PATCH 1/3] mm/mlock: make a zero length request a no-op whatever the alignment Jose A. Perez de Azpillaga
@ 2026-10-05 6:46 ` Jose A. Perez de Azpillaga
2026-10-05 6:46 ` [PATCH 3/3] selftests/mm: test mlock() and munlock() range normalisation Jose A. Perez de Azpillaga
2 siblings, 0 replies; 4+ messages in thread
From: Jose A. Perez de Azpillaga @ 2026-10-05 6:46 UTC (permalink / raw)
To: Andrew Morton, Liam R. Howlett, Lorenzo Stoakes,
David Hildenbrand, Shuah Khan
Cc: Vlastimil Babka, Jann Horn, Pedro Falcato, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, linux-mm, linux-kernel,
linux-kselftest, Jose A. Perez de Azpillaga, Park Tae-sun
A length within a page of ULONG_MAX wraps when mlock() and munlock()
round the range to pages. At an aligned address the length becomes 0 and
the call is a silent success; at an unaligned one it becomes a single
page, which is then locked or unlocked. An overflowing start + len is
only caught in apply_vma_lock_flags(), after the RLIMIT_MEMLOCK check, so
mlock() returns -ENOMEM without CAP_IPC_LOCK and -EINVAL with it.
Return -EINVAL, as man 2 mlock documents, when start + len overflows or
the end overflows when rounded up to a page. Doing this before
can_do_mlock() also avoids making the result depend on
RLIMIT_MEMLOCK/CAP_IPC_LOCK. Requests that get a silent success today
now fail.
Reported-by: Park Tae-sun <ts930@dgu.ac.kr>
Closes: https://lore.kernel.org/all/179066531783.50175.13377521828379196177@dgu.ac.kr/
Signed-off-by: Jose A. Perez de Azpillaga <azpijr@gmail.com>
---
mm/mlock.c | 23 +++++++++++++++++++----
1 file changed, 19 insertions(+), 4 deletions(-)
diff --git a/mm/mlock.c b/mm/mlock.c
index cc28e5d7413a..a9e2b90c058f 100644
--- a/mm/mlock.c
+++ b/mm/mlock.c
@@ -608,6 +608,7 @@ static __must_check int do_mlock(unsigned long start, size_t len,
{
unsigned long locked;
unsigned long lock_limit;
+ unsigned long end;
int error = -ENOMEM;
start = untagged_addr(start);
@@ -615,12 +616,19 @@ static __must_check int do_mlock(unsigned long start, size_t len,
if (!len)
return 0;
+ /* Reject a wrapping range before can_do_mlock() and the rlimit check. */
+ end = start + len;
+ if (end < start)
+ return -EINVAL;
+ end = PAGE_ALIGN(end);
+ if (!end)
+ return -EINVAL;
+ start &= PAGE_MASK;
+ len = end - start;
+
if (!can_do_mlock())
return -EPERM;
- len = PAGE_ALIGN(len + (offset_in_page(start)));
- start &= PAGE_MASK;
-
lock_limit = rlimit(RLIMIT_MEMLOCK);
lock_limit >>= PAGE_SHIFT;
locked = len >> PAGE_SHIFT;
@@ -676,6 +684,7 @@ SYSCALL_DEFINE3(mlock2, unsigned long, start, size_t, len, int, flags)
SYSCALL_DEFINE2(munlock, unsigned long, start, size_t, len)
{
+ unsigned long end;
vma_flags_t flags = EMPTY_VMA_FLAGS;
int ret;
@@ -684,8 +693,14 @@ SYSCALL_DEFINE2(munlock, unsigned long, start, size_t, len)
if (!len)
return 0;
- len = PAGE_ALIGN(len + (offset_in_page(start)));
+ end = start + len;
+ if (end < start)
+ return -EINVAL;
+ end = PAGE_ALIGN(end);
+ if (!end)
+ return -EINVAL;
start &= PAGE_MASK;
+ len = end - start;
if (mmap_write_lock_killable(current->mm))
return -EINTR;
--
2.55.0
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH 3/3] selftests/mm: test mlock() and munlock() range normalisation
2026-10-05 6:46 [PATCH 0/3] mm/mlock: make zero length requests a no-op and reject wrapping ranges Jose A. Perez de Azpillaga
2026-10-05 6:46 ` [PATCH 1/3] mm/mlock: make a zero length request a no-op whatever the alignment Jose A. Perez de Azpillaga
2026-10-05 6:46 ` [PATCH 2/3] mm/mlock: reject ranges that cannot be represented Jose A. Perez de Azpillaga
@ 2026-10-05 6:46 ` Jose A. Perez de Azpillaga
2 siblings, 0 replies; 4+ messages in thread
From: Jose A. Perez de Azpillaga @ 2026-10-05 6:46 UTC (permalink / raw)
To: Andrew Morton, Liam R. Howlett, Lorenzo Stoakes,
David Hildenbrand, Shuah Khan
Cc: Vlastimil Babka, Jann Horn, Pedro Falcato, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, linux-mm, linux-kernel,
linux-kselftest, Jose A. Perez de Azpillaga
Add mlock2-tests cases for a zero length at aligned and unaligned
addresses, for a length that wraps when rounded to pages, and for an
overflowing start + len, checking the return value and VmLck. The
overflow cases expect -EINVAL with and without CAP_IPC_LOCK, so running
the test unprivileged also covers the errno that used to depend on it.
The wrapping cases assume the test is built for the kernel's word size: a
32-bit binary on a 64-bit kernel cannot pass a range that wraps.
Suggested-by: David Hildenbrand <david@kernel.org>
Signed-off-by: Jose A. Perez de Azpillaga <azpijr@gmail.com>
---
tools/testing/selftests/mm/mlock2-tests.c | 191 +++++++++++++++++++++-
1 file changed, 190 insertions(+), 1 deletion(-)
diff --git a/tools/testing/selftests/mm/mlock2-tests.c b/tools/testing/selftests/mm/mlock2-tests.c
index 144b550813a6..3dca349c4738 100644
--- a/tools/testing/selftests/mm/mlock2-tests.c
+++ b/tools/testing/selftests/mm/mlock2-tests.c
@@ -2,6 +2,7 @@
#define _GNU_SOURCE
#include <sys/mman.h>
#include <linux/mman.h>
+#include <limits.h>
#include <stdint.h>
#include <unistd.h>
#include <string.h>
@@ -91,6 +92,7 @@ static bool is_vmflag_set(unsigned long addr, const char *vmflag)
#define SIZE "Size:"
#define RSS "Rss:"
#define LOCKED "lo"
+#define VMLCK "VmLck:"
static unsigned long get_value_for_name(unsigned long addr, const char *name)
{
@@ -177,6 +179,27 @@ static int unlock_lock_check(char *map, bool mlock_supported)
return 1;
}
+static unsigned long get_vmlck(void)
+{
+ unsigned long vmlck;
+ char line[1024];
+ FILE *file;
+
+ file = fopen("/proc/self/status", "r");
+ if (!file)
+ ksft_exit_fail_msg("fopen: %s\n", strerror(errno));
+
+ while (fgets(line, sizeof(line), file)) {
+ if (sscanf(line, VMLCK "\t%lu kB", &vmlck) == 1) {
+ fclose(file);
+ return vmlck << 10;
+ }
+ }
+
+ fclose(file);
+ ksft_exit_fail_msg("cannot parse VmLck in /proc/self/status\n");
+}
+
static void test_mlock_lock(void)
{
char *map;
@@ -204,6 +227,167 @@ static void test_mlock_lock(void)
munmap(map, 2 * page_size);
}
+static void test_mlock_zero_length(void)
+{
+ unsigned long page_size = getpagesize();
+ unsigned long vmlck_before, vmlck_after;
+ char *map;
+
+ map = mmap(NULL, 2 * page_size, PROT_READ | PROT_WRITE,
+ MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
+ if (map == MAP_FAILED)
+ ksft_exit_fail_msg("mmap error: %s\n", strerror(errno));
+
+ vmlck_before = get_vmlck();
+ ksft_test_result(!mlock(map, 0),
+ "%s: zero length mlock() at an aligned address\n", __func__);
+ vmlck_after = get_vmlck();
+ ksft_test_result(vmlck_before == vmlck_after,
+ "%s: zero length mlock() at an aligned address locked nothing\n",
+ __func__);
+
+ vmlck_before = get_vmlck();
+ ksft_test_result(!mlock(map + 5, 0),
+ "%s: zero length mlock() at an unaligned address\n", __func__);
+ vmlck_after = get_vmlck();
+ ksft_test_result(vmlck_before == vmlck_after,
+ "%s: zero length mlock() at an unaligned address locked nothing\n",
+ __func__);
+
+ munmap(map, 2 * page_size);
+}
+
+static void test_munlock_zero_length(void)
+{
+ unsigned long page_size = getpagesize();
+ unsigned long vmlck_before, vmlck_after;
+ char *map;
+
+ map = mmap(NULL, 2 * page_size, PROT_READ | PROT_WRITE,
+ MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
+ if (map == MAP_FAILED)
+ ksft_exit_fail_msg("mmap error: %s\n", strerror(errno));
+
+ if (mlock(map, 2 * page_size)) {
+ munmap(map, 2 * page_size);
+ ksft_exit_fail_msg("mlock(): %s\n", strerror(errno));
+ }
+
+ vmlck_before = get_vmlck();
+ ksft_test_result(vmlck_before > 0,
+ "%s: the range to release is accounted as locked\n",
+ __func__);
+ ksft_test_result(!munlock(map + 5, 0),
+ "%s: zero length munlock() at an unaligned address\n", __func__);
+ vmlck_after = get_vmlck();
+ ksft_test_result(vmlck_before == vmlck_after,
+ "%s: zero length munlock() at an unaligned address released nothing\n",
+ __func__);
+
+ munmap(map, 2 * page_size);
+}
+
+static void test_mlock_length_wrap(void)
+{
+ unsigned long page_size = getpagesize();
+ unsigned long vmlck_before, vmlck_after;
+ char *map;
+ int ret;
+
+ map = mmap(NULL, 2 * page_size, PROT_READ | PROT_WRITE,
+ MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
+ if (map == MAP_FAILED)
+ ksft_exit_fail_msg("mmap error: %s\n", strerror(errno));
+
+ vmlck_before = get_vmlck();
+ errno = 0;
+ ret = mlock(map, ULONG_MAX);
+ ksft_test_result(ret == -1 && errno == EINVAL,
+ "%s: mlock() with a wrapping length is rejected\n",
+ __func__);
+
+ vmlck_after = get_vmlck();
+ ksft_test_result(vmlck_before == vmlck_after,
+ "%s: mlock() with a wrapping length locked nothing\n",
+ __func__);
+
+ vmlck_before = get_vmlck();
+ errno = 0;
+ ret = mlock(map + 5, ULONG_MAX);
+ ksft_test_result(ret == -1 && errno == EINVAL,
+ "%s: unaligned mlock() with a wrapping length is rejected\n",
+ __func__);
+
+ vmlck_after = get_vmlck();
+ ksft_test_result(vmlck_before == vmlck_after,
+ "%s: unaligned mlock() with a wrapping length locked nothing\n",
+ __func__);
+
+ munmap(map, 2 * page_size);
+}
+
+static void test_mlock_addr_overflow(void)
+{
+ unsigned long page_size = getpagesize();
+ unsigned long vmlck_before, vmlck_after;
+ size_t len;
+ char *map;
+ int ret;
+
+ map = mmap(NULL, 2 * page_size, PROT_READ | PROT_WRITE,
+ MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
+ if (map == MAP_FAILED)
+ ksft_exit_fail_msg("mmap error: %s\n", strerror(errno));
+
+ /* addr + len wraps to 0 */
+ len = (size_t)(0UL - (unsigned long)map);
+
+ /* -EINVAL with or without CAP_IPC_LOCK */
+ vmlck_before = get_vmlck();
+ errno = 0;
+ ret = mlock(map, len);
+ ksft_test_result(ret == -1 && errno == EINVAL,
+ "%s: mlock() with an overflowing addr + len is rejected\n",
+ __func__);
+
+ vmlck_after = get_vmlck();
+ ksft_test_result(vmlck_before == vmlck_after,
+ "%s: mlock() with an overflowing addr + len locked nothing\n",
+ __func__);
+
+ munmap(map, 2 * page_size);
+}
+
+static void test_munlock_range_overflow(void)
+{
+ unsigned long page_size = getpagesize();
+ size_t len;
+ char *map;
+
+ map = mmap(NULL, 2 * page_size, PROT_READ | PROT_WRITE,
+ MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
+ if (map == MAP_FAILED)
+ ksft_exit_fail_msg("mmap error: %s\n", strerror(errno));
+
+ errno = 0;
+ ksft_test_result(munlock(map, ULONG_MAX) == -1 && errno == EINVAL,
+ "%s: munlock() with a wrapping length is rejected\n",
+ __func__);
+
+ errno = 0;
+ ksft_test_result(munlock(map + 5, ULONG_MAX) == -1 && errno == EINVAL,
+ "%s: unaligned munlock() with a wrapping length is rejected\n",
+ __func__);
+
+ len = (size_t)(0UL - (unsigned long)map);
+ errno = 0;
+ ksft_test_result(munlock(map, len) == -1 && errno == EINVAL,
+ "%s: munlock() with an overflowing addr + len is rejected\n",
+ __func__);
+
+ munmap(map, 2 * page_size);
+}
+
static int onfault_check(char *map)
{
*map = 'a';
@@ -506,9 +690,14 @@ int main(int argc, char **argv)
munmap(map, size);
- ksft_set_plan(15);
+ ksft_set_plan(31);
test_mlock_lock();
+ test_mlock_zero_length();
+ test_munlock_zero_length();
+ test_mlock_length_wrap();
+ test_mlock_addr_overflow();
+ test_munlock_range_overflow();
test_mlock_onfault();
test_munlockall0();
test_munlockall1();
--
2.55.0
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-10-05 6:53 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-05 6:46 [PATCH 0/3] mm/mlock: make zero length requests a no-op and reject wrapping ranges Jose A. Perez de Azpillaga
2026-10-05 6:46 ` [PATCH 1/3] mm/mlock: make a zero length request a no-op whatever the alignment Jose A. Perez de Azpillaga
2026-10-05 6:46 ` [PATCH 2/3] mm/mlock: reject ranges that cannot be represented Jose A. Perez de Azpillaga
2026-10-05 6:46 ` [PATCH 3/3] selftests/mm: test mlock() and munlock() range normalisation Jose A. Perez de Azpillaga
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®