diff --git a/csu/libc-start.c b/csu/libc-start.c
index 6f3d52e223..7a13961b18 100644
--- a/csu/libc-start.c
+++ b/csu/libc-start.c
@@ -307,6 +307,9 @@ LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
 
 #endif /* !SHARED  */
 
+  /* Make the thread executable if required.  */
+  GLRO(dl_check_executable_stack) ();
+
   /* Register the destructor of the dynamic linker if there is any.  */
   if (__glibc_likely (rtld_fini != NULL))
     __cxa_atexit ((void (*) (void *)) rtld_fini, NULL, NULL);
diff --git a/elf/dl-tunables.list b/elf/dl-tunables.list
index 0b6721bc51..c03c9967f0 100644
--- a/elf/dl-tunables.list
+++ b/elf/dl-tunables.list
@@ -138,7 +138,7 @@ glibc {
     execstack {
       type: INT_32
       minval: 0
-      maxval: 1
+      maxval: 2
       default: 1
     }
   }
diff --git a/elf/rtld.c b/elf/rtld.c
index 115f1da37f..573197a484 100644
--- a/elf/rtld.c
+++ b/elf/rtld.c
@@ -372,6 +372,7 @@ struct rtld_global_ro _rtld_global_ro attribute_relro =
     ._dl_error_free = _dl_error_free,
     ._dl_tls_get_addr_soft = _dl_tls_get_addr_soft,
     ._dl_libc_freeres = __rtld_libc_freeres,
+    ._dl_check_executable_stack = _dl_check_executable_stack,
   };
 /* If we would use strong_alias here the compiler would see a
    non-hidden definition.  This would undo the effect of the previous
diff --git a/elf/tst-rtld-list-tunables.exp b/elf/tst-rtld-list-tunables.exp
index 9f5990f340..8df6f5906e 100644
--- a/elf/tst-rtld-list-tunables.exp
+++ b/elf/tst-rtld-list-tunables.exp
@@ -13,6 +13,6 @@ glibc.malloc.top_pad: 0x20000 (min: 0x0, max: 0x[f]+)
 glibc.malloc.trim_threshold: 0x0 (min: 0x0, max: 0x[f]+)
 glibc.rtld.dynamic_sort: 2 (min: 1, max: 2)
 glibc.rtld.enable_secure: 0 (min: 0, max: 1)
-glibc.rtld.execstack: 1 (min: 0, max: 1)
+glibc.rtld.execstack: 1 (min: 0, max: 2)
 glibc.rtld.nns: 0x4 (min: 0x1, max: 0x10)
 glibc.rtld.optional_static_tls: 0x200 (min: 0x0, max: 0x[f]+)
diff --git a/manual/tunables.texi b/manual/tunables.texi
index 7f0246c789..5c96fae769 100644
--- a/manual/tunables.texi
+++ b/manual/tunables.texi
@@ -365,8 +365,11 @@ change the main stack permission if kernel starts with a non-executable stack.
 The @code{glibc.rtld.execstack} can be used to control whether an executable
 stack is allowed from the main program.  Setting the value to @code{0} disables
 the ABI auto-negotiation (meaning no executable stacks even if the ABI or ELF
-header requires it), while @code{1} enables auto-negotiation (although the
-program might not need an executable stack).
+header requires it), @code{1} enables auto-negotiation (although the program
+might not need an executable stack), while @code{2} forces and executable
+stack during initialization (this is provide for compatibility reasons, where
+the program requires to dynamically load modules with executable stacks with
+@code{dlopen}).
 
 When executable stacks are not allowed, and if the main program requires it,
 the loader will fail with an error message.
@@ -380,7 +383,8 @@ of hardware capabilities and kernel configuration.
 @strong{NB:} Trying to load a dynamic shared library with @code{dlopen} or
 @code{dlmopen} that requires an executable stack will always fail if the
 main program does not require an executable stack at loading time.  This
-is enforced regardless of the tunable value.
+can be disable by setting the tunable to @code{2}, where the stack is
+always executable.
 @end deftp
 
 @node Elision Tunables
diff --git a/sysdeps/generic/ldsodefs.h b/sysdeps/generic/ldsodefs.h
index 8465cbaa9b..caed31e17f 100644
--- a/sysdeps/generic/ldsodefs.h
+++ b/sysdeps/generic/ldsodefs.h
@@ -671,6 +671,8 @@ struct rtld_global_ro
   /* Called from __libc_shared to deallocate malloc'ed memory.  */
   void (*_dl_libc_freeres) (void);
 
+  int (*_dl_check_executable_stack) (void);
+
   /* Implementation of _dl_find_object.  The public entry point is in
      libc, and this is patched by __rtld_static_init to support static
      dlopen.  */
@@ -707,6 +709,8 @@ extern const ElfW(Phdr) *_dl_phdr;
 extern size_t _dl_phnum;
 #endif
 
+int _dl_check_executable_stack (void) attribute_hidden;
+
 /* This function changes the permission of the memory region pointed
    by STACK_ENDP to executable and update the internal memory protection
    flags for future thread stack creation.  */
diff --git a/sysdeps/unix/sysv/linux/Makefile b/sysdeps/unix/sysv/linux/Makefile
index 395d2d6593..8161b3d4c8 100644
--- a/sysdeps/unix/sysv/linux/Makefile
+++ b/sysdeps/unix/sysv/linux/Makefile
@@ -681,6 +681,25 @@ tests-special += \
   $(objpfx)tst-nolink-libc-2.out \
   # tests-special
 endif
+
+ifeq ($(have-z-execstack),yes)
+tests += \
+  tst-execstack-tunable \
+  tst-execstack-tunable-execstack \
+  # tests
+
+# Re-use the module with an executable stack from elf/Makefile
+$(objpfx)tst-execstack.out: $(objpfx)tst-execstack-mod.so
+$(objpfx)tst-execstack-execstack.out: $(objpfx)tst-execstack-mod.so
+
+# Also check if the tunable works on a program with an executable stack
+# (it should be a no-op).
+LDFLAGS-tst-execstack-tunable-execstack = -Wl,-z,execstack
+
+tst-execstack-tunable-ENV = GLIBC_TUNABLES=glibc.rtld.execstack=2
+tst-execstack-tunable-execstack-ENV = GLIBC_TUNABLES=glibc.rtld.execstack=2
+endif
+
 endif # $(subdir) == elf
 
 ifeq ($(subdir),rt)
diff --git a/sysdeps/unix/sysv/linux/dl-execstack.c b/sysdeps/unix/sysv/linux/dl-execstack.c
index 9791b339ca..a8fa21dfda 100644
--- a/sysdeps/unix/sysv/linux/dl-execstack.c
+++ b/sysdeps/unix/sysv/linux/dl-execstack.c
@@ -17,9 +17,10 @@
    <https://www.gnu.org/licenses/>.  */
 
 #include <ldsodefs.h>
+#include <dl-tunables.h>
 
-int
-_dl_make_stack_executable (void **stack_endp)
+static int
+make_stack_executable (void **stack_endp)
 {
   /* This gives us the highest/lowest page that needs to be changed.  */
   uintptr_t page = ((uintptr_t) *stack_endp
@@ -35,11 +36,31 @@ _dl_make_stack_executable (void **stack_endp)
 		  ) != 0)
     return errno;
 
+  /* Remember that we changed the permission.  */
+  GL(dl_stack_flags) |= PF_X;
+
+  return 0;
+}
+
+int
+_dl_make_stack_executable (void **stack_endp)
+{
+  int r = make_stack_executable (stack_endp);
+  if (r != 0)
+    return r;
+
   /* Clear the address.  */
   *stack_endp = NULL;
 
-  /* Remember that we changed the permission.  */
-  GL(dl_stack_flags) |= PF_X;
+  return r;
+}
 
+int
+_dl_check_executable_stack (void)
+{
+  if (TUNABLE_GET (glibc, rtld, execstack, int32_t, NULL) == 2)
+    /* We can't clean the __libc_stack_end because it is marked as RO
+       when this function is called.  */
+    return make_stack_executable (&__libc_stack_end);
   return 0;
 }
diff --git a/sysdeps/unix/sysv/linux/include/sys/personality.h b/sysdeps/unix/sysv/linux/include/sys/personality.h
new file mode 100644
index 0000000000..794047a488
--- /dev/null
+++ b/sysdeps/unix/sysv/linux/include/sys/personality.h
@@ -0,0 +1,12 @@
+#ifndef _INCLUDE_SYS_PERSONALITY_H
+#define _INCLUDE_SYS_PERSONALITY_H	1
+
+#include_next <sys/personality.h>
+
+# ifndef _ISOMAC
+
+extern __typeof (personality) __personality __THROW;
+hidden_proto (__personality)
+
+# endif /* _ISOMAC */
+#endif /* sys/sysinfo.h */
diff --git a/sysdeps/unix/sysv/linux/tst-execstack-tunable-execstack.c b/sysdeps/unix/sysv/linux/tst-execstack-tunable-execstack.c
new file mode 100644
index 0000000000..50e4fa5299
--- /dev/null
+++ b/sysdeps/unix/sysv/linux/tst-execstack-tunable-execstack.c
@@ -0,0 +1 @@
+#include "tst-execstack-tunable-skeleton.c"
diff --git a/sysdeps/unix/sysv/linux/tst-execstack-tunable-skeleton.c b/sysdeps/unix/sysv/linux/tst-execstack-tunable-skeleton.c
new file mode 100644
index 0000000000..e91f85edb7
--- /dev/null
+++ b/sysdeps/unix/sysv/linux/tst-execstack-tunable-skeleton.c
@@ -0,0 +1,39 @@
+/* Check glibc.rtld.execstack=2 makes the stack executable.
+
+   Copyright (C) 2025 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <support/check.h>
+#include <support/xdlfcn.h>
+
+static int
+do_test (void)
+{
+  void *h = dlopen ("tst-execstack-mod.so", RTLD_LAZY);
+  TEST_VERIFY (h != NULL);
+
+  void (*f)(void) = xdlsym (h, "tryme");
+  f ();
+
+  return 0;
+}
+
+#include <support/test-driver.c>
diff --git a/sysdeps/unix/sysv/linux/tst-execstack-tunable.c b/sysdeps/unix/sysv/linux/tst-execstack-tunable.c
new file mode 100644
index 0000000000..50e4fa5299
--- /dev/null
+++ b/sysdeps/unix/sysv/linux/tst-execstack-tunable.c
@@ -0,0 +1 @@
+#include "tst-execstack-tunable-skeleton.c"
