nptl: Add TLS guard page between thread stack and static TLS [BZ #22850]

Message ID 20260604181737.3629828-1-adhemerval.zanella@linaro.org (mailing list archive)
State Under Review
Delegated to: Carlos O'Donell
Headers
Series nptl: Add TLS guard page between thread stack and static TLS [BZ #22850] |

Checks

Context Check Description
redhat-pt-bot/TryBot-apply_patch success Patch applied to master at the time it was sent
redhat-pt-bot/TryBot-32bit success Build for i686
linaro-tcwg-bot/tcwg_glibc_build--master-arm success Build passed
linaro-tcwg-bot/tcwg_glibc_build--master-aarch64 success Build passed
linaro-tcwg-bot/tcwg_glibc_check--master-aarch64 success Test passed
linaro-tcwg-bot/tcwg_glibc_check--master-arm success Test passed

Commit Message

Adhemerval Zanella June 4, 2026, 6:16 p.m. UTC
  A stack buffer overflow in a thread's usable stack area can silently
corrupt the thread's static TLS data (errno, thread-local variables,
struct pthread itself) because no guard page separates the two regions.
On architectures that store the stack-smashing canary in the TCB
(THREAD_SET_STACK_GUARD), an overflow can also corrupt __stack_chk_guard
itself, completely defeating SSP.

Every _STACK_GROWS_DOWN architecture is affected.  The severity depends
on whether the architecture stores __stack_chk_guard in the TCB:

* Architectures with THREAD_SET_STACK_GUARD — an overflow can overwrite
  the canary and defeat SSP entirely, in addition to corrupting TLS data:
  - x86 / x86_64, i386  (TLS_TCB_AT_TP)
  - s390 / s390x        (TLS_TCB_AT_TP)
  - sparc / sparc64     (TLS_TCB_AT_TP)
  - powerpc / powerpc64 (TLS_DTV_AT_TP)

* Architectures without THREAD_SET_STACK_GUARD — overflow corrupts TLS
  data but cannot defeat SSP via the canary:
  - aarch64, arm, riscv, mips, loongarch64, alpha, arc, csky, m68k,
    microblaze, or1k, sh (TLS_DTV_AT_TP)

hppa (_STACK_GROWS_UP) is not affected, _STACK_GROWS_UP puts the existing
stack guard between the usable stack and pd/TLS already; and
__stack_chk_guard is a global on hppa.

To harden against this, insert a guard page between the usable stack area
and the static TLS block.  The TLS guard is the same size as the existing
stack guard (guardsize) and is omitted when guardsize == 0 (user has
disabled stack guards).

The implementation is mostly on allocatestack.c:

  - Add tls_guard_start_position / tls_guard_end_position helpers in
    allocatestack.c for both TLS_TCB_AT_TP (guard sits page-aligned below
    the static TLS area) and TLS_DTV_AT_TP + _STACK_GROWS_DOWN (guard sits
    page-aligned below struct pthread).

  - Extend setup_stack_prot and adjust_stack_prot to install, resize, and
    remove the TLS guard alongside the existing stack guard, using
    MADV_GUARD_INSTALL where available and falling back to PROT_NONE.  The
    TLS guard size always equals guardsize, so no extra field is needed in
    struct pthread.

  - Update pthread_getattr_np to exclude the TLS guard overhead from the
    reported stacksize and stackaddr so that POSIX-visible values remain
    consistent.  With the TLS guard active, stackblock_size = S + 2*G and
    the reported stacksize = S (the user-requested size); this mirrors the
    treatment of the regular stack guard, which is also allocated on top of
    the requested size and not subtracted from it.

  - Gate the feature on the new ARCH_HAS_TLS_GUARD flag defined in each
    arch's pthreaddef.h, allowing architectures to opt out.  The
    hardening is enabled for all ABI but hppa.

== Alternatives considered ==

-mstack-protector-guard=global (BZ 26817): This GCC flag moves
__stack_chk_guard from a TLS slot to a global variable, preventing a stack
overflow from overwriting the canary.  It was originally added for kernel
support and it falls short for two reasons:

  1. Scope: it only protects the SSP canary.  A stack overflow can still
     silently corrupt errno, other thread-local variables, and other
     struct pthread fields (tid, joinstate, etc.).  The TLS guard page
     protects all of static TLS and struct pthread.

  2. ABI incompatibility: switching the canary source from a
     TLS-relative load to a global-variable load is an ABI-visible
     change.  Object files and DSOs compiled with the flag cannot safely
     be mixed with those compiled without it; a full recompile of all
     code (including third-party libraries) is required and it will
     require an update glibc to load the binary.  The TLS guard page
     approach requires no compiler changes and is fully transparent
     to existing binaries.

Tunable interface:

A tunable could allow programs to opt in or out of the TLS guard at
startup.  Although possible to extend it to use tunables, tying a security
harning to a tunable have some drawbacks:

  - Tunables are intended for performance and compatibility tradeoffs,
    not security mechanisms.  Exposing a hardening control through the
    same interface as, say, glibc.pthread.stack_cache_size conflates
    orthogonal concerns and makes auditing harder.

  - An attacker who can set environment variables (e.g. via
    GLIBC_TUNABLES or by exploiting a pre-exec privilege boundary) could
    disable the guard before glibc initialises, negating the protection.

  - Consistent, unconditional protection is more reliable than opt-in
    hardening whose coverage depends on deployment configuration.

The existing POSIX guardsize mechanism already provides a clean,
well-understood way to disable all guard protection: setting
guardsize == 0 suppresses both the stack guard and the TLS guard.

== Performance considerations ==

On kernels that support MADV_GUARD_INSTALL (Linux 6.13+), the TLS guard
is installed with an extra madvise call on the existing RW mapping.  The
stack block remains one VMA; no additional mappings are created and the
per-thread overhead is negligible.

On older kernels that fall back to PROT_NONE, the TLS guard requires two
extra mprotect calls per thread creation (one to mark the TLS guard
PROT_NONE, one to mark the TLS data region back to RW after the initial
all-PROT_NONE allocation).  This splits the stack block from 2 VMAs into
4 VMAs per thread.  The extra VMA pressure is the same as what the
existing stack guard already imposes and is bounded by the number of live
threads.

== Testing ==

- tst-tls-guard: new test that verifies the TLS guard is inaccessible,
  the stack below it is accessible, guardsize == 0 suppresses the guard,
  and behaviour is consistent across both MADV_GUARD_INSTALL and PROT_NONE
  implementations.   Run both in isolated subprocesses and in-process to
  exercise the stack cache (including guard resize on cache reuse).

- tst-guard1: updated assertions to account for stack-cache size
  distribution changes introduced by the extra guardsize allocation;
  exact-size cache reuse cannot be guaranteed when ARCH_HAS_TLS_GUARD is
  set, so the stacksize check uses >= rather than ==.

I fully check on aarch64-linux-gnu, x86_64-linux-gnu, arm-linux-gnu,
and i686-linux-gnu with a kernel with and without MADV_GUARD_INSTALL
support.

I also checked on some different kernels for powerpc64le, s390x,
mips64le, and riscv.
---
 NEWS                                 |   9 +
 elf/tst-dl-cache-long-path.c         |   2 +-
 nptl/Makefile                        |   1 +
 nptl/allocatestack.c                 | 391 ++++++++++++++++++++++++---
 nptl/pthread_getattr_np.c            |  30 +-
 nptl/tst-guard1.c                    |  23 ++
 nptl/tst-tls-guard.c                 | 323 ++++++++++++++++++++++
 sysdeps/aarch64/nptl/pthreaddef.h    |   3 +
 sysdeps/alpha/nptl/pthreaddef.h      |   3 +
 sysdeps/arc/nptl/pthreaddef.h        |   3 +
 sysdeps/arm/nptl/pthreaddef.h        |   3 +
 sysdeps/csky/nptl/pthreaddef.h       |   3 +
 sysdeps/hppa/nptl/pthreaddef.h       |   3 +
 sysdeps/loongarch/nptl/pthreaddef.h  |   3 +
 sysdeps/m68k/nptl/pthreaddef.h       |   3 +
 sysdeps/microblaze/nptl/pthreaddef.h |   3 +
 sysdeps/mips/nptl/pthreaddef.h       |   3 +
 sysdeps/or1k/nptl/pthreaddef.h       |   3 +
 sysdeps/powerpc/nptl/pthreaddef.h    |   3 +
 sysdeps/riscv/nptl/pthreaddef.h      |   3 +
 sysdeps/s390/nptl/pthreaddef.h       |   3 +
 sysdeps/sh/nptl/pthreaddef.h         |   3 +
 sysdeps/sparc/sparc32/pthreaddef.h   |   3 +
 sysdeps/sparc/sparc64/pthreaddef.h   |   3 +
 sysdeps/x86/nptl/pthreaddef.h        |   3 +
 25 files changed, 790 insertions(+), 43 deletions(-)
 create mode 100644 nptl/tst-tls-guard.c
  

Patch

diff --git a/NEWS b/NEWS
index e2173fa1aa7..1106537cacd 100644
--- a/NEWS
+++ b/NEWS
@@ -21,6 +21,15 @@  Major new features:
 * Static PIE is now supported for arm-*-linux-gnueabi.  It requires toolchain
   support to correctly set the expected linker options.
 
+* A guard page is now placed between a thread's usable stack and its static
+  TLS block (and struct pthread) on architectures that support it, hardening
+  against stack buffer overflows that would otherwise silently corrupt
+  thread-local storage, struct pthread, or the stack-smashing canary stored
+  in the TCB.  The TLS guard has the same size as the regular stack guard and
+  is suppressed when the guard size is set to zero.  As a result, for a given
+  requested stack size a thread may have up to one page less usable stack than
+  before, since the TLS guard is page-aligned below the static TLS block.
+
 Deprecated and removed features, and other changes affecting compatibility:
 
 * Although malloc and related functions currently return pointers
diff --git a/elf/tst-dl-cache-long-path.c b/elf/tst-dl-cache-long-path.c
index e7304934228..89209b80070 100644
--- a/elf/tst-dl-cache-long-path.c
+++ b/elf/tst-dl-cache-long-path.c
@@ -133,7 +133,7 @@  dlopen_via_cache (volatile char *pressure)
 
 /* Reduce the stack budget available to the dlopen call chain by
    STACK_PRESSURE bytes.  */
-enum { STACK_PRESSURE = 5 * 1024 };
+enum { STACK_PRESSURE = 3 * 1024 };
 
 static void
 __attribute_noinline__
diff --git a/nptl/Makefile b/nptl/Makefile
index 02862d1c04b..72edba04f64 100644
--- a/nptl/Makefile
+++ b/nptl/Makefile
@@ -387,6 +387,7 @@  tests-internal := \
   tst-sem13 \
   tst-setgetname \
   tst-signal7 \
+  tst-tls-guard \
   # tests-internal
 
 xtests = \
diff --git a/nptl/allocatestack.c b/nptl/allocatestack.c
index b2ecb001136..501f736be96 100644
--- a/nptl/allocatestack.c
+++ b/nptl/allocatestack.c
@@ -150,6 +150,125 @@  get_cached_stack (size_t *sizep, void **memp)
   return result;
 }
 
+/*
+  The thread stack layout depends on whether the ABI places the static TLS
+  (TLS_TCB_AT_TP or TLS_DTV_AT_TP), if the kernel supports lightweight guard
+  pages (MADV_GUARD_INSTALL), and if the ABI adds an extra TLS guard page
+  (ARCH_HAS_TLS_GUARD).
+
+  For TLS_TCB_AT_TP with !ARCH_HAS_TLS_GUARD:
+
+  * !MADV_GUARD_INSTALL (2 VMAs):
+
+  |------------|-------------------------------------------------------------|
+  | R--        | RW-                                                         |
+  |------------|-----------------------|--------------------|----------------|
+  |            |                       |                    | struct pthread |
+  | GUARD PAGE |        STACK          |                    |                |
+  |            |                       | initial-exec +     |----------------|
+  |            |                       | local-exec TLS     | tcbhead_t      |
+  |------------|-----------------------|--------------------|----------------|
+  low                                                       ^
+                                                            thread pointer
+  * MADV_GUARD_INSTALL (1 VMA):
+  |--------------------------------------------------------------------------|
+  | RW                                                                       |
+  |------------------------------------|--------------------|----------------|
+  |                                    |                    | struct pthread |
+  | (GUARD PAGE)        STACK          |                    |                |
+  |                                    | initial-exec +     |----------------|
+  |                                    | local-exec TLS     | tcbhead_t      |
+  |------------------------------------|--------------------|----------------|
+  low                                                       ^
+                                                            thread pointer
+
+  And with ARCH_HAS_TLS_GUARD:
+
+  * !MADV_GUARD_INSTALL (4 VMAs)
+
+  |------------|-----------------------|-----------|-------------------------|
+  | R--        | RW-                   | R--       | RW-                     |
+  |------------|-----------------------|-----------|---|-----------|---------|
+  |            |                       |           |   |           | struct  |
+  | GUARD PAGE |        STACK          | TLS GUARD |pad| init-exec | pthread |
+  |            |                       |           |   | + loc-exec|---------|
+  |            |                       |           |   | TLS       |tcbhead_t|
+  |------------|-----------------------|-----------|---|-----------|---------|
+  low                                  ^                           ^
+                                       page-aligned                TP
+
+  * MADV_GUARD_INSTALL (1 VMA):
+  |--------------------------------------------------------------------------|
+  | RW                                                                       |
+  |------------------------------------------------|---------------|---------|
+  |                                                |   |           | struct  |
+  | (GUARD PAGE)        STACK          (TLS GUARD) |pad|           | pthread |
+  |                                                |   | init-exec |---------|
+  |                                                |   | + loc-exec|tcbhead_t|
+  |------------------------------------------------|---------------|---------|
+  low                                              ^               ^
+                                                   page-aligned    TP
+
+
+  For TLS_DTV_AT_TP, the thread pointer (TP) points to the start of the static
+  TLS block, and struct pthread sits just before TP.  With !ARCH_HAS_TLS_GUARD:
+
+
+  * !MADV_GUARD_INSTALL (2 VMAs):
+
+  |------------|-------------------------------------------------------------|
+  | R--        | RW-                                                         |
+  |------------|----------------------|----------------|---------------------|
+  |            |                      | struct pthread | initial-exec        |
+  | GUARD PAGE |        STACK         |                | + local-exec TLS    |
+  |            |                      |                |                     |
+  |------------|-------------------------------------------------------------|
+  low                                                  ^                  high
+                                                       thread pointer
+
+  * MADV_GUARD_INSTALL (1 VMA):
+
+  |--------------------------------------------------------------------------|
+  | RW                                                                       |
+  |--------------------------------------------------------------------------|
+  |                                   |struct pthread | initial-exec         |
+  | (GUARD PAGE)        STACK         |               | + local-exec TLS     |
+  |                                   |               |                      |
+  |--------------------------------------------------------------------------|
+  low                                                 ^ thread pointer    high
+
+
+  And with ARCH_HAS_TLS_GUARD:
+
+  * !MADV_GUARD_INSTALL (4 VMAs):
+
+  |------------|-------------------------------------------------------------|
+  | R--        | RW-                  | R--       | RW-                      |
+  |------------|----------------------|-----------|--------------------------|
+  |            |                      |           | struct  | initial-exec   |
+  | GUARD PAGE |        STACK         | TLS GUARD | pthread | + local-exec   |
+  |            |                      |           |         |                |
+  |------------|-------------------------------------------------------------|
+  low                                             ^ thread pointer        high
+
+  * MADV_GUARD_INSTALL (1 VMA):
+
+  |--------------------------------------------------------------------------|
+  | RW                                                                       |
+  |--------------------------------------------------------------------------|
+  |                                               | struct  | initial-exec   |
+  | (GUARD PAGE)        STACK         (TLS GUARD) | pthread | + local-exec   |
+  |                                               |         |                |
+  |--------------------------------------------------------------------------|
+  low                                                 ^ thread pointer    high
+
+
+  - The TLS guard is placed between the usable stack and the static TLS block
+    and takes space from the thread stack.
+  - Its high end is page-aligned at or below the start of static TLS and it
+    is omitted when guardsize == 0.
+  - TLS_TCB_AT_TP only supports _STACK_GROWS_DOWN.  */
+
 /* Assume support for MADV_ADVISE_GUARD, setup_stack_prot will disable it
    and fallback to ALLOCATE_GUARD_PROT_NONE if the madvise call fails.  */
 static int allocate_stack_mode = ALLOCATE_GUARD_MADV_GUARD;
@@ -172,8 +291,7 @@  allocate_thread_stack (size_t size, size_t guardsize)
 
 
 /* Return the guard page position on allocated stack.  */
-static inline char *
-__attribute ((always_inline))
+static __always_inline char *
 guard_position (void *mem, size_t size, size_t guardsize, const struct pthread *pd,
 		size_t pagesize_m1)
 {
@@ -184,43 +302,128 @@  guard_position (void *mem, size_t size, size_t guardsize, const struct pthread *
 #endif
 }
 
+#if ARCH_HAS_TLS_GUARD
+/* For TLS_TCB_AT_TP the TLS guard sits between the stack and the static TLS
+   area.  Its end is page-aligned at or below the start of static TLS.
+
+   For TLS_DTV_AT_TP with a downward-growing stack the TLS guard sits between
+   the usable stack and struct pthread.  Its end is page-aligned at or below
+   the start of struct pthread.
+
+   Returns a pointer to the page-aligned end of the TLS guard (the first byte
+   above the guard region).  */
+static __always_inline char *
+tls_guard_end_position (const struct pthread *pd,
+			size_t tls_static_size_for_stack,
+			size_t pagesize_m1)
+{
+# if TLS_TCB_AT_TP
+  char *tls_start = (char *) (pd + 1) - tls_static_size_for_stack;
+  return (char *) ((uintptr_t) tls_start & ~pagesize_m1);
+# elif TLS_DTV_AT_TP
+#  if _STACK_GROWS_DOWN
+  return (char *) ((uintptr_t) pd & ~pagesize_m1);
+#  else
+#   error "TLS guard page does not support _STACK_GROWS_UP"
+#  endif
+# endif
+}
+
+static __always_inline char *
+tls_guard_start_position (const struct pthread *pd,
+			  size_t tls_static_size_for_stack,
+			  size_t guardsize,
+			  size_t pagesize_m1)
+{
+  return tls_guard_end_position (pd, tls_static_size_for_stack, pagesize_m1)
+    - guardsize;
+}
+#endif
+
 /* Setup the MEM thread stack of SIZE bytes with the required protection flags
-   along with a guard area of GUARDSIZE size.  It first tries with
-   MADV_GUARD_INSTALL, and then fallback to setup the guard area using the
-   extra PROT_NONE mapping.  Update PD with the type of guard area setup.  */
+   along with a guard area of GUARDSIZE size and, when ARCH_HAS_TLS_GUARD is
+   set, a TLS guard area of the same GUARDSIZE between the stack and the
+   static TLS block.
+
+   It first tries with MADV_GUARD_INSTALL, and then falls back to setting up
+   the guard areas using extra PROT_NONE mappings.  Updates PD with the type
+   of guard area setup.  */
 static inline bool
 setup_stack_prot (char *mem, size_t size, struct pthread *pd,
-		  size_t guardsize, size_t pagesize_m1)
+		  size_t guardsize, size_t tls_static_size_for_stack,
+		  size_t pagesize_m1)
 {
   if (__glibc_unlikely (guardsize == 0))
     return true;
 
   char *guard = guard_position (mem, size, guardsize, pd, pagesize_m1);
+
+#if ARCH_HAS_TLS_GUARD
+  /* guardsize > 0 is guaranteed by the early return above.  */
+  char *tls_guard = tls_guard_start_position (pd, tls_static_size_for_stack,
+					      guardsize, pagesize_m1);
+#endif
+
   if (atomic_load_relaxed (&allocate_stack_mode) == ALLOCATE_GUARD_MADV_GUARD)
     {
-      if (__madvise (guard, guardsize, MADV_GUARD_INSTALL) == 0)
+      bool stack_guard_ok = __madvise (guard, guardsize,
+				       MADV_GUARD_INSTALL) == 0;
+      bool tls_guard_ok =
+#if ARCH_HAS_TLS_GUARD
+	__madvise (tls_guard, guardsize, MADV_GUARD_INSTALL) == 0;
+#else
+        true;
+#endif
+
+      if (stack_guard_ok && tls_guard_ok)
 	{
 	  pd->stack_mode = ALLOCATE_GUARD_MADV_GUARD;
 	  return true;
 	}
 
+      /* At least one madvise failed; undo any successful madvise and fall
+	 back to PROT_NONE for both this allocation and future ones.  The
+	 stack memory is already RW (MADV_GUARD mode initial allocation), so
+	 we only need to PROT_NONE the guard regions.  */
+      if (stack_guard_ok)
+	__madvise (guard, guardsize, MADV_GUARD_REMOVE);
+#if ARCH_HAS_TLS_GUARD
+      if (tls_guard_ok)
+	__madvise (tls_guard, guardsize, MADV_GUARD_REMOVE);
+#endif
+
       /* If madvise fails it means the kernel does not support the guard
 	 advise (we assume that the syscall is available, guard is page-aligned
 	 and length is non negative).  The stack has already the expected
-	 protection flags, so it just need to PROT_NONE the guard area.  */
+	 protection flags, so it just need to PROT_NONE the guard areas.  */
       atomic_store_relaxed (&allocate_stack_mode, ALLOCATE_GUARD_PROT_NONE);
+
       if (__mprotect (guard, guardsize, PROT_NONE) != 0)
 	return false;
+#if ARCH_HAS_TLS_GUARD
+      if (__mprotect (tls_guard, guardsize, PROT_NONE) != 0)
+	return false;
+#endif
     }
   else
     {
       const int prot = GL(dl_stack_prot_flags);
       char *guardend = guard + guardsize;
 #if _STACK_GROWS_DOWN
-      /* As defined at guard_position, for architectures with downward stack
-	 the guard page is always at start of the allocated area.  */
+      /* For architectures with downward stack the guard page is always at the
+	 start of the allocated area.  */
+# if ARCH_HAS_TLS_GUARD
+      /* Make the stack area RW (between stack guard and TLS guard).  */
+      if (__mprotect (guardend, tls_guard - guardend, prot) != 0)
+	return false;
+      /* Make the pad + TLS + TCB area RW (after TLS guard).  */
+      char *tls_guardend = tls_guard + guardsize;
+      if (__mprotect (tls_guardend, (mem + size) - tls_guardend, prot) != 0)
+	return false;
+# else
       if (__mprotect (guardend, size - guardsize, prot) != 0)
 	return false;
+# endif /* ARCH_HAS_TLS_GUARD */
 #else
       size_t mprots1 = (uintptr_t) guard - (uintptr_t) mem;
       if (__mprotect (mem, mprots1, prot) != 0)
@@ -235,11 +438,14 @@  setup_stack_prot (char *mem, size_t size, struct pthread *pd,
   return true;
 }
 
-/* Update the guard area of the thread stack MEM of size SIZE with the new
-   GUARDISZE.  It uses the method defined by PD stack_mode.  */
+/* Update the guard areas of the thread stack MEM of size SIZE with the new
+   GUARDSIZE.  Uses the method defined by PD stack_mode.  When
+   ARCH_HAS_TLS_GUARD is set the TLS guard sits between the stack and struct
+   pthread and always has the same size as the stack guard.  */
 static inline bool
 adjust_stack_prot (char *mem, size_t size, struct pthread *pd,
-		   size_t guardsize, size_t pagesize_m1)
+		   size_t guardsize,
+		   size_t tls_static_size_for_stack, size_t pagesize_m1)
 {
   /* The required guard area is larger than the current one.  For
      _STACK_GROWS_DOWN it means the guard should increase as:
@@ -252,27 +458,60 @@  adjust_stack_prot (char *mem, size_t size, struct pthread *pd,
      |stack---------------------------|guard|-----|
      |stack--------------------|new guard---|-----|
 
-     Both madvise and mprotect allows overlap the required region,
+     With ARCH_HAS_TLS_GUARD the TLS guard also need to be increased
+     (TLS guard only supports _STACK_GROWS_DOWN):
+
+     |guard|----------------------|tls_guard|stack|
+     |new guard--|--------------|new guard--|stack|
+
+     Both madvise and mprotect allow overlapping the required region,
      so use the new guard placement with the new size.  */
   if (guardsize > pd->guardsize)
     {
       /* There was no need to previously setup a guard page, so we need
 	 to check whether the kernel supports guard advise.  */
       char *guard = guard_position (mem, size, guardsize, pd, pagesize_m1);
+
+#if ARCH_HAS_TLS_GUARD
+      /* guardsize > 0 is guaranteed by the outer condition.  */
+      char *tls_guard = tls_guard_start_position (pd, tls_static_size_for_stack,
+						  guardsize, pagesize_m1);
+#endif
+
       if (atomic_load_relaxed (&allocate_stack_mode)
 	  == ALLOCATE_GUARD_MADV_GUARD)
 	{
-	  if (__madvise (guard, guardsize, MADV_GUARD_INSTALL) == 0)
+	  bool stack_ok = __madvise (guard, guardsize,
+				     MADV_GUARD_INSTALL) == 0;
+	  bool tls_ok =
+#if ARCH_HAS_TLS_GUARD
+	    __madvise (tls_guard, guardsize, MADV_GUARD_INSTALL) == 0;
+#else
+	    true;
+#endif
+	  if (stack_ok && tls_ok)
 	    {
 	      pd->stack_mode = ALLOCATE_GUARD_MADV_GUARD;
 	      return true;
 	    }
+	  if (stack_ok)
+	    __madvise (guard, guardsize, MADV_GUARD_REMOVE);
+#if ARCH_HAS_TLS_GUARD
+	  if (tls_ok)
+	    __madvise (tls_guard, guardsize, MADV_GUARD_REMOVE);
+#endif
 	  atomic_store_relaxed (&allocate_stack_mode,
 				ALLOCATE_GUARD_PROT_NONE);
 	}
 
       pd->stack_mode = ALLOCATE_GUARD_PROT_NONE;
-      return __mprotect (guard, guardsize, PROT_NONE) == 0;
+      if (__mprotect (guard, guardsize, PROT_NONE) != 0)
+	return false;
+#if ARCH_HAS_TLS_GUARD
+      if (__mprotect (tls_guard, guardsize, PROT_NONE) != 0)
+	return false;
+#endif
+      return true;
     }
   /* The current guard area is larger than the required one.  For
      _STACK_GROWS_DOWN is means change the guard as:
@@ -285,28 +524,48 @@  adjust_stack_prot (char *mem, size_t size, struct pthread *pd,
      |stack---------------------|guard-------|---|
      |stack------------------------|new guard|---|
 
+     With ARCH_HAS_TLS_GUARD the TLS guard also need to be decreased
+     (TLS guard only supports _STACK_GROWS_DOWN):
+
+     |guard-------|----------|tls_guard-----|stack|
+     |new guard|------------------|new guard|stack|
+
      For ALLOCATE_GUARD_MADV_GUARD it means remove the slack area
      (disjointed region of guard and new guard), while for
      ALLOCATE_GUARD_PROT_NONE it requires to mprotect it with the stack
-     protection flags.  */
+     protection flags.  Same logic applies to the TLS guard.  */
   else if (pd->guardsize > guardsize)
     {
-      size_t slacksize = pd->guardsize - guardsize;
       if (pd->stack_mode == ALLOCATE_GUARD_MADV_GUARD)
 	{
+	  size_t slacksize = pd->guardsize - guardsize;
 	  void *slack =
 #if _STACK_GROWS_DOWN
 	    mem + guardsize;
 #else
 	    guard_position (mem, size, pd->guardsize, pd, pagesize_m1);
 #endif
-	  return __madvise (slack, slacksize, MADV_GUARD_REMOVE) == 0;
+	  bool stack_ok = __madvise (slack, slacksize, MADV_GUARD_REMOVE) == 0;
+
+#if ARCH_HAS_TLS_GUARD
+	  char *tls_guard_end =
+	    tls_guard_end_position (pd, tls_static_size_for_stack,
+				    pagesize_m1);
+	  char *old_tls_guard_start = tls_guard_end - pd->guardsize;
+	  bool tls_ok = __madvise (old_tls_guard_start, slacksize,
+				   MADV_GUARD_REMOVE) == 0;
+	  return stack_ok && tls_ok;
+#else
+	  return stack_ok;
+#endif
 	}
       else if (pd->stack_mode == ALLOCATE_GUARD_PROT_NONE)
 	{
 	  const int prot = GL(dl_stack_prot_flags);
+	  bool stack_ok = true;
 #if _STACK_GROWS_DOWN
-	  return __mprotect (mem + guardsize, slacksize, prot) == 0;
+	  size_t slacksize = pd->guardsize - guardsize;
+	  stack_ok = __mprotect (mem + guardsize, slacksize, prot) == 0;
 #else
 	  char *new_guard = (char *)(((uintptr_t) pd - guardsize)
 				     & ~pagesize_m1);
@@ -316,7 +575,20 @@  adjust_stack_prot (char *mem, size_t size, struct pthread *pd,
 	     to the nearest page the size difference might be zero.  */
 	  if (new_guard > old_guard
 	      && __mprotect (old_guard, new_guard - old_guard, prot) != 0)
-	    return false;
+	    stack_ok = false;
+#endif
+
+#if ARCH_HAS_TLS_GUARD
+	  size_t tls_slacksize = pd->guardsize - guardsize;
+	  char *tls_guard_end =
+	    tls_guard_end_position (pd, tls_static_size_for_stack,
+				    pagesize_m1);
+	  char *old_tls_guard_start = tls_guard_end - pd->guardsize;
+	  bool tls_ok = __mprotect (old_tls_guard_start, tls_slacksize,
+				    prot) == 0;
+	  return stack_ok && tls_ok;
+#else
+	  return stack_ok;
 #endif
 	}
     }
@@ -490,14 +762,29 @@  allocate_stack (const struct pthread_attr *attr, struct pthread **pdp,
       reported_guardsize = guardsize;
       if (guardsize > 0 && guardsize < ARCH_MIN_GUARD_SIZE)
 	guardsize = ARCH_MIN_GUARD_SIZE;
-      if (guardsize < attr->guardsize || size + guardsize < guardsize)
+      if (guardsize < attr->guardsize
+	  || INT_ADD_WRAPV (size, guardsize, &size))
 	/* Arithmetic overflow.  */
 	return EINVAL;
-      size += guardsize;
-      if (__builtin_expect (size < ((guardsize + tls_static_size_for_stack
-				     + MINIMAL_REST_STACK + pagesize_m1)
-				    & ~pagesize_m1),
-			    0))
+
+#if ARCH_HAS_TLS_GUARD
+      /* A TLS guard of the same size as the stack guard is placed between the
+	 stack and the static TLS (or struct pthread for TLS_DTV_AT_TP) to
+	 harden against stack buffer overflows that overwrite TLS data.  */
+      if (INT_ADD_WRAPV (size, guardsize, &size))
+	return EINVAL;
+#endif
+
+      size_t min_size;
+      if (INT_ADD_WRAPV (guardsize, tls_static_size_for_stack, &min_size)
+#if ARCH_HAS_TLS_GUARD
+	  || INT_ADD_WRAPV (min_size, guardsize, &min_size)
+#endif
+	  || INT_ADD_WRAPV (min_size, (size_t) MINIMAL_REST_STACK, &min_size)
+	  || INT_ADD_WRAPV (min_size, pagesize_m1, &min_size))
+	return EINVAL;
+      min_size &= ~pagesize_m1;
+      if (__glibc_unlikely (size < min_size))
 	/* The stack is too small (or the guard too large).  */
 	return EINVAL;
 
@@ -531,8 +818,9 @@  allocate_stack (const struct pthread_attr *attr, struct pthread **pdp,
 				   - TLS_PRE_TCB_SIZE);
 #endif
 
-	  /* Now mprotect the required region excluding the guard area.  */
-	  if (!setup_stack_prot (mem, size, pd, guardsize, pagesize_m1))
+	  /* Now mprotect the required region excluding the guard areas.  */
+	  if (!setup_stack_prot (mem, size, pd, guardsize,
+				 tls_static_size_for_stack, pagesize_m1))
 	    {
 	      __munmap (mem, size);
 	      return errno;
@@ -593,9 +881,10 @@  allocate_stack (const struct pthread_attr *attr, struct pthread **pdp,
 	     which will be read next.  */
 	}
 
-      /* Create or resize the guard area if necessary on an already
+      /* Create or resize the guard areas if necessary on an already
 	 allocated stack.  */
-      if (!adjust_stack_prot (mem, size, pd, guardsize, pagesize_m1))
+      if (!adjust_stack_prot (mem, size, pd, guardsize,
+			      tls_static_size_for_stack, pagesize_m1))
 	{
 	  lll_lock (GL (dl_stack_cache_lock), LLL_PRIVATE);
 
@@ -645,11 +934,23 @@  allocate_stack (const struct pthread_attr *attr, struct pthread **pdp,
 
   void *stacktop;
 
+  /* The stack begins before the TLS guard (if any), static TLS, and TCB.
+     When a TLS guard is in use, its start (page-aligned below the static TLS)
+     is the top of the usable stack.
+
+     With TLS_DTV_AT_TP and a downward-growing stack, struct pthread sits just
+     above the usable stack.  When a TLS guard is in use it occupies the pages
+     between the usable stack top and struct pthread.  */
+#if ARCH_HAS_TLS_GUARD
+  if (pd->guardsize > 0)
+    stacktop = tls_guard_start_position (pd, tls_static_size_for_stack,
+					 pd->guardsize, pagesize_m1);
+  else
+#endif
 #if TLS_TCB_AT_TP
-  /* The stack begins before the TCB and the static TLS block.  */
-  stacktop = ((char *) (pd + 1) - tls_static_size_for_stack);
+    stacktop = (char *) (pd + 1) - tls_static_size_for_stack;
 #elif TLS_DTV_AT_TP
-  stacktop = (char *) (pd - 1);
+    stacktop = (char *) (pd - 1);
 #endif
 
   *stacksize = stacktop - pd->stackblock;
@@ -673,8 +974,8 @@  allocate_stack (const struct pthread_attr *attr, struct pthread **pdp,
 static void
 name_stack_maps (struct pthread *pd, bool set)
 {
-  size_t adjust = pd->stack_mode == ALLOCATE_GUARD_PROT_NONE ?
-    pd->guardsize : 0;
+  enum allocate_stack_mode_t mode = pd->stack_mode;
+  size_t adjust = mode == ALLOCATE_GUARD_PROT_NONE ? pd->guardsize : 0;
 #if _STACK_GROWS_DOWN
   void *stack = pd->stackblock + adjust;
 #else
@@ -682,6 +983,24 @@  name_stack_maps (struct pthread *pd, bool set)
 #endif
   size_t stacksize = pd->stackblock_size - adjust;
 
+#if ARCH_HAS_TLS_GUARD
+  /* With PROT_NONE guards and ARCH_HAS_TLS_GUARD, the TLS guard and TLS
+     data are separate VMAs (mprotect splits the mapping).  Naming the full
+     [stack, stack+stacksize) region would apply the name to all three VMAs
+     (usable stack, TLS guard, TLS data), making the test count 3x too many
+     "pthread stack:" entries.  Limit the name to the usable stack only.
+     With MADV_GUARD_INSTALL the whole block remains one VMA, so no
+     adjustment is needed there.  */
+  if (mode == ALLOCATE_GUARD_PROT_NONE && pd->guardsize > 0)
+    {
+      size_t tls_static_size_for_stack = __nptl_tls_static_size_for_stack ();
+      size_t pagesize_m1 = __getpagesize () - 1;
+      char *tls_guard = tls_guard_start_position (pd, tls_static_size_for_stack,
+                                                  pd->guardsize, pagesize_m1);
+      stacksize = (char *) tls_guard - (char *) stack;
+    }
+#endif
+
   if (!set)
     __set_vma_name (stack, stacksize, " glibc: unused stack");
   else
diff --git a/nptl/pthread_getattr_np.c b/nptl/pthread_getattr_np.c
index b0d2343a59a..359fd8932ff 100644
--- a/nptl/pthread_getattr_np.c
+++ b/nptl/pthread_getattr_np.c
@@ -61,15 +61,33 @@  __pthread_getattr_np (pthread_t thread_id, pthread_attr_t *attr)
   /* The sizes are subject to alignment.  */
   if (__glibc_likely (thread->stackblock != NULL))
     {
-      /* The stack size reported to the user should not include the
-	 guard size.  */
-      iattr->stacksize = thread->stackblock_size - thread->guardsize;
+      /* The reported stack size excludes all guard pages.  The TLS guard
+	 (when ARCH_HAS_TLS_GUARD and guardsize > 0) is allocated on top of
+	 the user-requested size, just like the regular stack guard, so it is
+	 also excluded.  stackaddr is kept at the top of the user-requested
+	 region (excluding the TLS guard overhead).  */
+#if ARCH_HAS_TLS_GUARD
+      if (thread->guardsize > 0)
+	{
+	  iattr->stacksize = thread->stackblock_size - 2 * thread->guardsize;
+# if _STACK_GROWS_DOWN
+	  iattr->stackaddr = (char *) thread->stackblock
+			     + thread->stackblock_size - thread->guardsize;
+# else
+#  error "TLS guard page does not support _STACK_GROWS_UP"
+# endif
+	}
+      else
+#endif /* ARCH_HAS_TLS_GUARD */
+	{
+	  iattr->stacksize = thread->stackblock_size - thread->guardsize;
 #if _STACK_GROWS_DOWN
-      iattr->stackaddr = (char *) thread->stackblock
-			 + thread->stackblock_size;
+	  iattr->stackaddr = (char *) thread->stackblock
+			     + thread->stackblock_size;
 #else
-      iattr->stackaddr = (char *) thread->stackblock;
+	  iattr->stackaddr = (char *) thread->stackblock;
 #endif
+	}
     }
   else
     {
diff --git a/nptl/tst-guard1.c b/nptl/tst-guard1.c
index 3486cba9e4d..ec1a579d129 100644
--- a/nptl/tst-guard1.c
+++ b/nptl/tst-guard1.c
@@ -17,6 +17,7 @@ 
    <https://www.gnu.org/licenses/>.  */
 
 #include <array_length.h>
+#include <pthread.h>
 #include <pthreaddef.h>
 #include <setjmp.h>
 #include <stackinfo.h>
@@ -149,14 +150,36 @@  tf (void *closure)
 
   if (args != NULL)
     {
+	 /* With ARCH_HAS_TLS_GUARD the value may be larger when the thread
+	    reuses a cached stack that is bigger than strictly needed (because
+	    the TLS guard adds an extra guardsize to every allocation,
+	    shifting  the cached-size distribution so that an exact-fit hit is
+	    unlikely).
+
+	    Without ARCH_HAS_TLS_GUARD the test cases are designed to always
+	    reuse an exact-fit cached stack, so exact equality holds there.  */
+#if ARCH_HAS_TLS_GUARD
+      TEST_VERIFY (s.stacksize >= adjust_stacksize (args->stacksize));
+#else
       TEST_COMPARE (adjust_stacksize (args->stacksize), s.stacksize);
+#endif
       TEST_COMPARE (args->guardsize, s.guardsize);
     }
 
   /* Ensure we can access the stack area.  */
   TEST_COMPARE (try_read_buf (s.stack), true);
   TEST_COMPARE (try_read_buf (&s.stack[s.stacksize / 2]), true);
+  /* With ARCH_HAS_TLS_GUARD and a guard installed, the reported stacksize
+     spans the TLS guard area too (it is part of the overhead like the stack
+     guard).  The last byte may land inside the TLS guard when guardsize >=
+     tls_static_size; only check s.stacksize - 1 when no TLS guard is active
+     to avoid a spurious fault.  */
+#if ARCH_HAS_TLS_GUARD
+  if (s.guardsize == 0)
+    TEST_COMPARE (try_read_buf (&s.stack[s.stacksize - 1]), true);
+#else
   TEST_COMPARE (try_read_buf (&s.stack[s.stacksize - 1]), true);
+#endif
 
   /* Check if accessing the guard area results in SIGSEGV.  */
   if (s.guardsize > 0)
diff --git a/nptl/tst-tls-guard.c b/nptl/tst-tls-guard.c
new file mode 100644
index 00000000000..eb1e72366bf
--- /dev/null
+++ b/nptl/tst-tls-guard.c
@@ -0,0 +1,323 @@ 
+/* Tests for the TLS guard page between stack and static TLS.
+   Copyright (C) 2026 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/>.  */
+
+/* For architectures that set ARCH_HAS_TLS_GUARD, glibc inserts a guard page
+   between the thread stack and the static TLS / struct pthread area.
+   This test verifies:
+
+     1. The TLS guard is inaccessible (traps on read/write).
+     2. The stack area below the TLS guard is accessible.
+     3. When guardsize == 0 no TLS guard is added.
+     4. The behaviour is consistent across kernel support for
+        MADV_GUARD_INSTALL and the PROT_NONE fallback.  */
+
+#include <array_length.h>
+#include <pthread.h>
+#include <pthreaddef.h>
+#include <setjmp.h>
+#include <stackinfo.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <support/capture_subprocess.h>
+#include <support/check.h>
+#include <support/check_mem_access.h>
+#include <support/test-driver.h>
+#include <support/xsignal.h>
+#include <support/xthread.h>
+#include <support/xunistd.h>
+#include <tls.h>
+
+#if ARCH_HAS_TLS_GUARD
+/* Exported by ld.so (GLIBC_PRIVATE) for callers that need to know the
+   static TLS layout; used here to bound the TLS-guard scan tightly.  */
+extern void _dl_get_tls_static_info (size_t *sizep, size_t *alignp);
+
+static long int pagesz;
+
+/* Locate the TLS guard region by probing pages backward from STACK_TOP
+   (which equals stack + stacksize from pthread_attr_getstack, i.e.
+   the first byte above the TLS guard) down to SCAN_LIMIT.
+
+   Works for both MADV_GUARD_INSTALL (guard stays inside one RW VMA but
+   pages are inaccessible) and the PROT_NONE fallback (separate ---p VMA).
+
+   Returns 1 and fills the output pointers on success, 0 if no inaccessible
+   region is found within the scan range.  */
+static bool
+find_tls_guard (char *stack_top, char *scan_limit, long int pagesize,
+		size_t guardsize, char **guard_start_out,
+		size_t *guard_size_out)
+{
+  char *guard_end = NULL;
+  char *guard_start = NULL;
+  char *p = stack_top;
+  /* The TLS guard sits immediately below the page-aligned start of the
+     static TLS area, with size GUARDSIZE.  Scanning back by
+     (tls_static_size + guardsize) reaches the guard's low end; add a few
+     pages of margin to absorb the page-alignment rounding done when
+     computing the guard position.  This bounds the work even on large-TLS
+     workloads (vs. a fixed page count) and keeps it cheap: each probe
+     triggers a SIGSEGV + longjmp inside check_mem_access.  */
+  size_t tls_size = 0, tls_align = 0;
+  _dl_get_tls_static_info (&tls_size, &tls_align);
+  size_t scan_bytes = tls_size + guardsize + 4 * pagesize;
+  char *stop = stack_top - scan_bytes;
+  if (stop < scan_limit)
+    stop = scan_limit;
+
+  while (p > stop)
+    {
+      p -= pagesize;
+      if (!check_mem_access (p, false))
+        {
+          if (guard_end == NULL)
+            guard_end = p + pagesize;
+          guard_start = p;
+        }
+      else if (guard_end != NULL)
+        break;
+    }
+
+  if (guard_end == NULL)
+    return false;
+
+  *guard_start_out = guard_start;
+  *guard_size_out  = (size_t) (guard_end - guard_start);
+  return true;
+}
+
+struct thread_args
+{
+  bool expect_tls_guard;
+};
+
+static void *
+tf (void *closure)
+{
+  struct thread_args *args = closure;
+
+  pthread_attr_t attr;
+  TEST_VERIFY_EXIT (pthread_getattr_np (pthread_self (), &attr) == 0);
+
+  void *stack;
+  size_t stacksize;
+  TEST_VERIFY_EXIT (pthread_attr_getstack (&attr, &stack, &stacksize) == 0);
+
+  size_t guardsize;
+  TEST_VERIFY_EXIT (pthread_attr_getguardsize (&attr, &guardsize) == 0);
+  if (guardsize != 0 && guardsize < ARCH_MIN_GUARD_SIZE)
+    guardsize = ARCH_MIN_GUARD_SIZE;
+  pthread_attr_destroy (&attr);
+
+  /* Verify the stack area is accessible (start and midpoint are always in
+     the usable stack region below the TLS guard).  */
+  TEST_VERIFY (check_mem_access (stack, false));
+  TEST_VERIFY (check_mem_access ((char *) stack + stacksize / 4, false));
+
+  /* pthread_getattr_np reports stacksize as the user-requested size (both
+     the stack guard and the TLS guard are excluded) and stackaddr as the
+     top of that region.  For _STACK_GROWS_DOWN:
+
+       stack              == stackblock + guardsize  (low end of usable stack)
+       stack + stacksize + guardsize == end of the mmap'd allocation
+
+     Scan from the end of the allocation downward; the TLS guard is the
+     first inaccessible region encountered.  */
+  char *scan_top = (char *) stack + stacksize + guardsize;
+  char  *guard_start = NULL;
+  size_t guard_size  = 0;
+  bool found = find_tls_guard (scan_top, stack, pagesz, guardsize,
+			       &guard_start, &guard_size);
+
+  if (args->expect_tls_guard)
+    {
+      TEST_VERIFY_EXIT (found);
+
+      if (test_verbose)
+        printf ("debug: [tid=%jd] TLS guard at %p+%#zx, guardsize=%#zx\n",
+                (intmax_t) gettid (), guard_start, guard_size, guardsize);
+
+      /* The TLS guard should be the same size as the stack guard.  */
+      TEST_COMPARE (guard_size, guardsize);
+
+      /* Every byte of the TLS guard must be inaccessible.  */
+      TEST_VERIFY (!check_mem_access (guard_start, false));
+      TEST_VERIFY (!check_mem_access (guard_start, true));
+      TEST_VERIFY (!check_mem_access (guard_start + guard_size / 2, false));
+      TEST_VERIFY (!check_mem_access (guard_start + guard_size - 1, false));
+
+      /* The byte just below the TLS guard must be accessible (stack area).  */
+      TEST_VERIFY (check_mem_access (guard_start - 1, false));
+    }
+  else
+    /* With guardsize == 0 there must be no TLS guard.  */
+    TEST_VERIFY_EXIT (!found);
+
+  return NULL;
+}
+
+/* Test with default pthread attributes (guard present).  */
+static void
+do_test_default (void *closure)
+{
+  struct thread_args args = {
+    .expect_tls_guard = true
+  };
+  pthread_t t = xpthread_create (NULL, tf, &args);
+  void *status = xpthread_join (t);
+  TEST_VERIFY (status == 0);
+}
+
+/* Test with explicit guard size > 0 (guard present).  */
+static void
+do_test_with_guard (void *closure)
+{
+  pthread_attr_t attr;
+  xpthread_attr_init (&attr);
+  xpthread_attr_setstacksize (&attr, 2 * 1024 * 1024);
+  xpthread_attr_setguardsize (&attr, pagesz);
+
+  struct thread_args args = {
+    .expect_tls_guard = true
+  };
+  pthread_t t = xpthread_create (&attr, tf, &args);
+  void *status = xpthread_join (t);
+  TEST_VERIFY (status == 0);
+
+  xpthread_attr_destroy (&attr);
+}
+
+/* Test with guardsize == 0 (no TLS guard expected).  */
+static void
+do_test_no_guard (void *closure)
+{
+  pthread_attr_t attr;
+  xpthread_attr_init (&attr);
+  /* Use a larger stack so the thread still fits after removing the guard.  */
+  xpthread_attr_setstacksize (&attr, 2 * 1024 * 1024);
+  xpthread_attr_setguardsize (&attr, 0);
+
+  struct thread_args args = {
+    .expect_tls_guard = false
+  };
+  pthread_t t = xpthread_create (&attr, tf, &args);
+  void *status = xpthread_join (t);
+  TEST_VERIFY (status == 0);
+
+  xpthread_attr_destroy (&attr);
+}
+
+/* Test guard installation and expansion on a cached stack.
+
+   The stack cache lookup requires stackblock_size >= S + 2 * guardsize.
+   The first sub-test reserves an oversized stack with guardsize == 0 so
+   the cached entry is large enough to be reused by the guarded sub-tests
+   that follow.  Each later sub-test then exercises adjust_stack_prot:
+
+     - 0 -> guardsize_min: install guards on a cache entry that had none.
+
+     - guardsize_min -> guardsize_max: expand both guards on cache reuse.  */
+static void
+do_test_cache_reuse (void *closure)
+{
+  size_t base_stacksize = 2 * 1024 * 1024;
+  size_t guardsize_min = pagesz;
+  size_t guardsize_max = 2 * pagesz;
+  if (guardsize_min < ARCH_MIN_GUARD_SIZE)
+    guardsize_min = ARCH_MIN_GUARD_SIZE;
+  if (guardsize_max <= guardsize_min)
+    guardsize_max = guardsize_min + pagesz;
+
+  pthread_attr_t attr;
+  xpthread_attr_init (&attr);
+
+  /* First thread: no guard, with extra room so the cache entry can satisfy
+     the later requests.  */
+  xpthread_attr_setstacksize (&attr, base_stacksize + 2 * guardsize_max);
+  xpthread_attr_setguardsize (&attr, 0);
+  {
+    struct thread_args args = { .expect_tls_guard = false };
+    pthread_t t = xpthread_create (&attr, tf, &args);
+    xpthread_join (t);
+  }
+
+  /* Second thread: reuses the cached stack and installs a TLS guard.  */
+  xpthread_attr_setstacksize (&attr, base_stacksize);
+  xpthread_attr_setguardsize (&attr, guardsize_min);
+  {
+    struct thread_args args = { .expect_tls_guard = true };
+    pthread_t t = xpthread_create (&attr, tf, &args);
+    xpthread_join (t);
+  }
+
+  /* Third thread: reuses the cached stack and grows the TLS guard.  */
+  xpthread_attr_setguardsize (&attr, guardsize_max);
+  {
+    struct thread_args args = { .expect_tls_guard = true };
+    pthread_t t = xpthread_create (&attr, tf, &args);
+    xpthread_join (t);
+  }
+
+  xpthread_attr_destroy (&attr);
+}
+#endif /* ARCH_HAS_TLS_GUARD */
+
+static int
+do_test (void)
+{
+#if ARCH_HAS_TLS_GUARD
+  pagesz = sysconf (_SC_PAGESIZE);
+
+  static const struct
+  {
+    const char *descr;
+    void (*fn) (void *);
+  } tests[] = {
+    { "default attributes (TLS guard expected)",       do_test_default     },
+    { "explicit guard size > 0 (TLS guard expected)",  do_test_with_guard  },
+    { "guardsize == 0 (no TLS guard expected)",        do_test_no_guard    },
+    { "cache reuse: no-guard then with-guard",         do_test_cache_reuse },
+  };
+
+  /* Run each sub-test in a forked subprocess for isolation.  */
+  for (size_t i = 0; i < array_length (tests); i++)
+    {
+      printf ("info: fork: %s\n", tests[i].descr);
+      struct support_capture_subprocess r =
+        support_capture_subprocess (tests[i].fn, NULL);
+      support_capture_subprocess_check (&r, tests[i].descr, 0,
+                                        sc_allow_none);
+      support_capture_subprocess_free (&r);
+    }
+
+  /* Then run them without fork so the stack cache is exercised.  */
+  for (size_t i = 0; i < array_length (tests); i++)
+    {
+      printf ("info: %s\n", tests[i].descr);
+      tests[i].fn (NULL);
+    }
+
+  return 0;
+#else
+  puts ("SKIP: TLS guard not enabled for this architecture");
+  return EXIT_SUCCESS;
+#endif
+}
+
+#include <support/test-driver.c>
diff --git a/sysdeps/aarch64/nptl/pthreaddef.h b/sysdeps/aarch64/nptl/pthreaddef.h
index 93d3e267a43..49bd9cb7537 100644
--- a/sysdeps/aarch64/nptl/pthreaddef.h
+++ b/sysdeps/aarch64/nptl/pthreaddef.h
@@ -22,6 +22,9 @@ 
 /* Minimum guard size.  */
 #define ARCH_MIN_GUARD_SIZE (64 * 1024)
 
+/* TLS guard page between thread stack and static TLS.  */
+#define ARCH_HAS_TLS_GUARD 1
+
 /* Required stack pointer alignment at beginning.  */
 #define STACK_ALIGN 16
 
diff --git a/sysdeps/alpha/nptl/pthreaddef.h b/sysdeps/alpha/nptl/pthreaddef.h
index 72d2c281cc9..de378c118d9 100644
--- a/sysdeps/alpha/nptl/pthreaddef.h
+++ b/sysdeps/alpha/nptl/pthreaddef.h
@@ -21,6 +21,9 @@ 
 /* Minimum guard size.  */
 #define ARCH_MIN_GUARD_SIZE 0
 
+/* TLS guard page between thread stack and static TLS.  */
+#define ARCH_HAS_TLS_GUARD 1
+
 /* Required stack pointer alignment at beginning.  The ABI requires 16.  */
 #define STACK_ALIGN		16
 
diff --git a/sysdeps/arc/nptl/pthreaddef.h b/sysdeps/arc/nptl/pthreaddef.h
index 5c374b6bf27..f6e0cb9a5e4 100644
--- a/sysdeps/arc/nptl/pthreaddef.h
+++ b/sysdeps/arc/nptl/pthreaddef.h
@@ -22,6 +22,9 @@ 
 /* Minimum guard size.  */
 #define ARCH_MIN_GUARD_SIZE 0
 
+/* TLS guard page between thread stack and static TLS.  */
+#define ARCH_HAS_TLS_GUARD 1
+
 /* Required stack pointer alignment at beginning.  */
 #define STACK_ALIGN		4
 
diff --git a/sysdeps/arm/nptl/pthreaddef.h b/sysdeps/arm/nptl/pthreaddef.h
index a67721e7461..2d401e21d58 100644
--- a/sysdeps/arm/nptl/pthreaddef.h
+++ b/sysdeps/arm/nptl/pthreaddef.h
@@ -21,6 +21,9 @@ 
 /* Minimum guard size.  */
 #define ARCH_MIN_GUARD_SIZE 0
 
+/* TLS guard page between thread stack and static TLS.  */
+#define ARCH_HAS_TLS_GUARD 1
+
 /* Required stack pointer alignment at beginning.  SSE requires 16
    bytes.  */
 #define STACK_ALIGN		16
diff --git a/sysdeps/csky/nptl/pthreaddef.h b/sysdeps/csky/nptl/pthreaddef.h
index e57286423a3..819c4b8d40b 100644
--- a/sysdeps/csky/nptl/pthreaddef.h
+++ b/sysdeps/csky/nptl/pthreaddef.h
@@ -22,6 +22,9 @@ 
 /* Minimum guard size.  */
 #define ARCH_MIN_GUARD_SIZE 0
 
+/* TLS guard page between thread stack and static TLS.  */
+#define ARCH_HAS_TLS_GUARD 1
+
 /* Required stack pointer alignment at beginning.  */
 #define STACK_ALIGN		8
 
diff --git a/sysdeps/hppa/nptl/pthreaddef.h b/sysdeps/hppa/nptl/pthreaddef.h
index 5b54f4864f7..7640b271695 100644
--- a/sysdeps/hppa/nptl/pthreaddef.h
+++ b/sysdeps/hppa/nptl/pthreaddef.h
@@ -21,6 +21,9 @@ 
 /* Minimum guard size.  */
 #define ARCH_MIN_GUARD_SIZE 0
 
+/* TLS guard page between thread stack and static TLS.  */
+#define ARCH_HAS_TLS_GUARD 0
+
 /* Required stack pointer alignment at beginning.  */
 #define STACK_ALIGN		64
 
diff --git a/sysdeps/loongarch/nptl/pthreaddef.h b/sysdeps/loongarch/nptl/pthreaddef.h
index e500d60b777..06c74521510 100644
--- a/sysdeps/loongarch/nptl/pthreaddef.h
+++ b/sysdeps/loongarch/nptl/pthreaddef.h
@@ -22,6 +22,9 @@ 
 /* Minimum guard size.  */
 #define ARCH_MIN_GUARD_SIZE 0
 
+/* TLS guard page between thread stack and static TLS.  */
+#define ARCH_HAS_TLS_GUARD 1
+
 /* Required stack pointer alignment at beginning.  */
 #define STACK_ALIGN 16
 
diff --git a/sysdeps/m68k/nptl/pthreaddef.h b/sysdeps/m68k/nptl/pthreaddef.h
index d29a061b745..c99a1321afa 100644
--- a/sysdeps/m68k/nptl/pthreaddef.h
+++ b/sysdeps/m68k/nptl/pthreaddef.h
@@ -21,6 +21,9 @@ 
 /* Minimum guard size.  */
 #define ARCH_MIN_GUARD_SIZE 0
 
+/* TLS guard page between thread stack and static TLS.  */
+#define ARCH_HAS_TLS_GUARD 1
+
 /* Required stack pointer alignment at beginning.  */
 #define STACK_ALIGN		16
 
diff --git a/sysdeps/microblaze/nptl/pthreaddef.h b/sysdeps/microblaze/nptl/pthreaddef.h
index 10c7257a574..c735f34609b 100644
--- a/sysdeps/microblaze/nptl/pthreaddef.h
+++ b/sysdeps/microblaze/nptl/pthreaddef.h
@@ -25,6 +25,9 @@ 
 /* Minimum guard size.  */
 #define ARCH_MIN_GUARD_SIZE 0
 
+/* TLS guard page between thread stack and static TLS.  */
+#define ARCH_HAS_TLS_GUARD 1
+
 /* Required stack pointer alignment at beginning.  */
 #define STACK_ALIGN         16
 
diff --git a/sysdeps/mips/nptl/pthreaddef.h b/sysdeps/mips/nptl/pthreaddef.h
index 518e8486b91..5be67cea66b 100644
--- a/sysdeps/mips/nptl/pthreaddef.h
+++ b/sysdeps/mips/nptl/pthreaddef.h
@@ -21,6 +21,9 @@ 
 /* Minimum guard size.  */
 #define ARCH_MIN_GUARD_SIZE 0
 
+/* TLS guard page between thread stack and static TLS.  */
+#define ARCH_HAS_TLS_GUARD 1
+
 /* Required stack pointer alignment at beginning.  */
 #define STACK_ALIGN		16
 
diff --git a/sysdeps/or1k/nptl/pthreaddef.h b/sysdeps/or1k/nptl/pthreaddef.h
index cc92affca22..bce11d89e54 100644
--- a/sysdeps/or1k/nptl/pthreaddef.h
+++ b/sysdeps/or1k/nptl/pthreaddef.h
@@ -23,6 +23,9 @@ 
 /* Minimum guard size.  */
 #define ARCH_MIN_GUARD_SIZE 0
 
+/* TLS guard page between thread stack and static TLS.  */
+#define ARCH_HAS_TLS_GUARD 1
+
 /* Required stack pointer alignment at beginning.  */
 #define STACK_ALIGN 4
 
diff --git a/sysdeps/powerpc/nptl/pthreaddef.h b/sysdeps/powerpc/nptl/pthreaddef.h
index 5598dd3afc2..e0c14099947 100644
--- a/sysdeps/powerpc/nptl/pthreaddef.h
+++ b/sysdeps/powerpc/nptl/pthreaddef.h
@@ -21,6 +21,9 @@ 
 /* Minimum guard size.  */
 #define ARCH_MIN_GUARD_SIZE 0
 
+/* TLS guard page between thread stack and static TLS.  */
+#define ARCH_HAS_TLS_GUARD 1
+
 /* Required stack pointer alignment at beginning.  The ABI requires 16
    bytes (for both 32-bit and 64-bit PowerPC).  */
 #define STACK_ALIGN		16
diff --git a/sysdeps/riscv/nptl/pthreaddef.h b/sysdeps/riscv/nptl/pthreaddef.h
index 219ef54b538..68c2afb2eab 100644
--- a/sysdeps/riscv/nptl/pthreaddef.h
+++ b/sysdeps/riscv/nptl/pthreaddef.h
@@ -22,6 +22,9 @@ 
 /* Minimum guard size.  */
 #define ARCH_MIN_GUARD_SIZE 0
 
+/* TLS guard page between thread stack and static TLS.  */
+#define ARCH_HAS_TLS_GUARD 1
+
 /* Required stack pointer alignment at beginning.  */
 #define STACK_ALIGN		16
 
diff --git a/sysdeps/s390/nptl/pthreaddef.h b/sysdeps/s390/nptl/pthreaddef.h
index f1d413db3d1..9965f2593c5 100644
--- a/sysdeps/s390/nptl/pthreaddef.h
+++ b/sysdeps/s390/nptl/pthreaddef.h
@@ -21,6 +21,9 @@ 
 /* Minimum guard size.  */
 #define ARCH_MIN_GUARD_SIZE 0
 
+/* TLS guard page between thread stack and static TLS.  */
+#define ARCH_HAS_TLS_GUARD 1
+
 /* Required stack pointer alignment at beginning.  SSE requires 16
    bytes.  */
 #define STACK_ALIGN		16
diff --git a/sysdeps/sh/nptl/pthreaddef.h b/sysdeps/sh/nptl/pthreaddef.h
index ea6099a2e4c..677c126a5d8 100644
--- a/sysdeps/sh/nptl/pthreaddef.h
+++ b/sysdeps/sh/nptl/pthreaddef.h
@@ -21,6 +21,9 @@ 
 /* Minimum guard size.  */
 #define ARCH_MIN_GUARD_SIZE 0
 
+/* TLS guard page between thread stack and static TLS.  */
+#define ARCH_HAS_TLS_GUARD 1
+
 /* Required stack pointer alignment at beginning.  */
 #define STACK_ALIGN		8
 
diff --git a/sysdeps/sparc/sparc32/pthreaddef.h b/sysdeps/sparc/sparc32/pthreaddef.h
index 04676ef307d..8ddf5c21e61 100644
--- a/sysdeps/sparc/sparc32/pthreaddef.h
+++ b/sysdeps/sparc/sparc32/pthreaddef.h
@@ -21,6 +21,9 @@ 
 /* Minimum guard size.  */
 #define ARCH_MIN_GUARD_SIZE 0
 
+/* TLS guard page between thread stack and static TLS.  */
+#define ARCH_HAS_TLS_GUARD 1
+
 /* Required stack pointer alignment at beginning.  */
 #define STACK_ALIGN		16
 
diff --git a/sysdeps/sparc/sparc64/pthreaddef.h b/sysdeps/sparc/sparc64/pthreaddef.h
index 94192f8b75d..9e025c08537 100644
--- a/sysdeps/sparc/sparc64/pthreaddef.h
+++ b/sysdeps/sparc/sparc64/pthreaddef.h
@@ -21,6 +21,9 @@ 
 /* Minimum guard size.  */
 #define ARCH_MIN_GUARD_SIZE 0
 
+/* TLS guard page between thread stack and static TLS.  */
+#define ARCH_HAS_TLS_GUARD 1
+
 /* Required stack pointer alignment at beginning.  */
 #define STACK_ALIGN		16
 
diff --git a/sysdeps/x86/nptl/pthreaddef.h b/sysdeps/x86/nptl/pthreaddef.h
index 0764503f944..730453e6ce1 100644
--- a/sysdeps/x86/nptl/pthreaddef.h
+++ b/sysdeps/x86/nptl/pthreaddef.h
@@ -21,6 +21,9 @@ 
 /* Minimum guard size.  */
 #define ARCH_MIN_GUARD_SIZE 0
 
+/* TLS guard page between thread stack and static TLS.  */
+#define ARCH_HAS_TLS_GUARD 1
+
 /* Required stack pointer alignment at beginning.  SSE requires 16
    bytes.  */
 #define STACK_ALIGN		16