diff --git a/elf/rtld.c b/elf/rtld.c
index 00df9acfca..9842f31ee4 100644
--- a/elf/rtld.c
+++ b/elf/rtld.c
@@ -2430,10 +2430,12 @@ process_dl_debug (struct dl_main_state *state, const char *dl_debug)
DL_DEBUG_SCOPES },
{ LEN_AND_STR ("tls"), "display TLS structures processing",
DL_DEBUG_TLS },
+ { LEN_AND_STR ("security"), "show security warnings for input files",
+ DL_DEBUG_SECURITY },
{ LEN_AND_STR ("all"), "all previous options combined",
DL_DEBUG_LIBS | DL_DEBUG_RELOC | DL_DEBUG_FILES | DL_DEBUG_SYMBOLS
| DL_DEBUG_BINDINGS | DL_DEBUG_VERSIONS | DL_DEBUG_IMPCALLS
- | DL_DEBUG_SCOPES | DL_DEBUG_TLS },
+ | DL_DEBUG_SCOPES | DL_DEBUG_TLS | DL_DEBUG_SECURITY },
{ LEN_AND_STR ("statistics"), "display relocation statistics",
DL_DEBUG_STATISTICS },
{ LEN_AND_STR ("unused"), "determined unused DSOs",
diff --git a/elf/tst-dl-debug-protect.sh b/elf/tst-dl-debug-protect.sh
new file mode 100644
index 0000000000..7a9ed1175d
--- /dev/null
+++ b/elf/tst-dl-debug-protect.sh
@@ -0,0 +1,55 @@
+#!/bin/sh
+# A script to run tests with LD_DEBUG=security and check
+# that output contains expected pattern.
+# 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
+# .
+
+# Arguments are from Makefile:
+# Path to the current build folder
+objpfx="$1"
+# Test wrapper command line
+wrapper="$2"
+# Dynamic loader command line
+loader="$3"
+# Test environment variables
+runenv="$4"
+# Grep pattern to look for in the test output
+pattern="$5"
+# Path to the test executable to run
+program="$6"
+
+output="${objpfx}`basename ${program}`.debug"
+rm -f "${output}".*
+
+eval "${wrapper}" \
+ LD_DEBUG=security LD_DEBUG_OUTPUT="${output}" ${runenv} \
+ "${loader}" "${program}"
+rc=$?
+
+if test $rc -eq 77; then
+ echo "Test is not supported"
+ rm -f "${output}".*
+ exit 77
+fi
+
+output=$(ls "${output}".*)
+cat "${output}"
+if ! grep -q "${pattern}" "${output}"; then
+ echo "Could not find expected '${pattern}' in LD_DEBUG_OUTPUT file"
+ exit 1
+fi
+rm -f "${output}"
diff --git a/manual/dynlink.texi b/manual/dynlink.texi
index a78a065af4..a388d7eeeb 100644
--- a/manual/dynlink.texi
+++ b/manual/dynlink.texi
@@ -392,6 +392,11 @@ Display information about Thread-Local Storage (TLS) handling, including TCB
allocation, deallocation, and reuse. This is useful for debugging issues
related to thread creation and lifecycle.
+@item security
+Display security warnings that are related to loading binaries that lack
+certain target-dependent hardening features. This may be useful for audit
+purposes.
+
@item all
All previous options combined.
diff --git a/sysdeps/aarch64/Makefile b/sysdeps/aarch64/Makefile
index ee3981e3f6..a87d02e648 100644
--- a/sysdeps/aarch64/Makefile
+++ b/sysdeps/aarch64/Makefile
@@ -97,6 +97,10 @@ tests += \
tst-bti-dlopen-imm \
tst-bti-dlopen-prot \
tst-bti-dlopen-transitive \
+ tst-bti-ld-debug-both \
+ tst-bti-ld-debug-dlopen \
+ tst-bti-ld-debug-exe \
+ tst-bti-ld-debug-shared \
tst-bti-permissive-dlopen \
tst-bti-permissive-imm \
tst-bti-permissive-transitive \
@@ -115,8 +119,12 @@ $(objpfx)tst-bti-dep-prot: $(objpfx)tst-bti-mod-prot.so
$(objpfx)tst-bti-mod.so: $(objpfx)tst-bti-mod-unprot.so
$(objpfx)tst-bti-permissive-imm: $(objpfx)tst-bti-mod-unprot.so
$(objpfx)tst-bti-permissive-transitive: $(objpfx)tst-bti-mod.so
+$(objpfx)tst-bti-ld-debug-shared: $(objpfx)tst-bti-mod.so
+$(objpfx)tst-bti-ld-debug-both: $(objpfx)tst-bti-mod-unprot.so
CFLAGS-tst-bti-abort-unprot.o += -mbranch-protection=none
+CFLAGS-tst-bti-ld-debug-exe.o += -mbranch-protection=none
+CFLAGS-tst-bti-ld-debug-both.o += -mbranch-protection=none
CFLAGS-tst-bti-mod-unprot.os += -mbranch-protection=none
tst-bti-abort-imm-ENV = GLIBC_TUNABLES=glibc.cpu.aarch64_bti=1
@@ -148,6 +156,12 @@ tests-static += \
tst-bti-abort-static-ENV = GLIBC_TUNABLES=glibc.cpu.aarch64_bti=1
CFLAGS-tst-bti-abort-static.o += -mbranch-protection=none
+$(objpfx)tst-bti-ld-debug-%.out: $(..)elf/tst-dl-debug-protect.sh $(objpfx)tst-bti-ld-debug-%
+ $(SHELL) $< $(objpfx) '$(test-wrapper-env)' '$(rtld-prefix)' \
+ '$(run-program-env) GLIBC_TUNABLES=glibc.cpu.aarch64_bti=0' \
+ 'security: not compatible with AArch64 BTI: $(objpfx)' \
+ $(objpfx)tst-bti-ld-debug-$* > $@; $(evaluate-test)
+
endif # ifeq (yes,$(have-test-bti))
endif
diff --git a/sysdeps/aarch64/dl-bti.c b/sysdeps/aarch64/dl-bti.c
index 1301ce2e73..5ef5e37c60 100644
--- a/sysdeps/aarch64/dl-bti.c
+++ b/sysdeps/aarch64/dl-bti.c
@@ -85,6 +85,16 @@ bti_failed (struct link_map *l, const char *program)
"failed to turn on BTI protection");
}
+static void
+bti_warning (struct link_map *l, const char *program)
+{
+ if (l->l_name[0] != '\0')
+ _dl_debug_printf ("security: not compatible with AArch64 BTI: %s\n",
+ l->l_name);
+ else if (__glibc_likely (program != NULL))
+ _dl_debug_printf ("security: not compatible with AArch64 BTI: %s\n",
+ program);
+}
/* Enable BTI for L and its dependencies. */
@@ -112,7 +122,12 @@ _dl_bti_check (struct link_map *l, const char *program)
if (is_rtld_link_map (dep->l_real))
continue;
#endif
- if (enforce_bti && !dep->l_mach.bti)
- bti_failed (dep, program);
+ if (!dep->l_mach.bti)
+ {
+ if (enforce_bti)
+ bti_failed (dep, program);
+ else if (__glibc_unlikely (GLRO(dl_debug_mask) & DL_DEBUG_SECURITY))
+ bti_warning (dep, program);
+ }
}
}
diff --git a/sysdeps/aarch64/dl-gcs.c b/sysdeps/aarch64/dl-gcs.c
index 1c6944562d..c195cbc465 100644
--- a/sysdeps/aarch64/dl-gcs.c
+++ b/sysdeps/aarch64/dl-gcs.c
@@ -50,6 +50,17 @@ fail (struct link_map *l, const char *program)
_dl_signal_error (0, l->l_name, "dlopen", "not GCS compatible");
}
+static void
+warn (struct link_map *l, const char *program)
+{
+ if (l->l_name[0] != '\0')
+ _dl_debug_printf ("security: not compatible with AArch64 GCS: %s\n",
+ l->l_name);
+ else if (__glibc_likely (program != NULL))
+ _dl_debug_printf ("security: not compatible with AArch64 GCS: %s\n",
+ program);
+}
+
static void
unsupported (void)
{
@@ -58,7 +69,7 @@ unsupported (void)
/* This function is called only when binary markings are not
ignored and GCS is supposed to be enabled. This occurs
- for the GCS_POLICY_ENFORCED and GCS_POLICY_ENFORCED policies. */
+ for the GCS_POLICY_ENFORCED and GCS_POLICY_OPTIONAL policies. */
static bool
check_gcs (struct link_map *l, const char *program, bool enforced)
{
@@ -70,6 +81,9 @@ check_gcs (struct link_map *l, const char *program, bool enforced)
/* Binary is marked, all good. */
if (l->l_mach.gcs)
return true;
+ /* Extra logging requested, print path to failed binary. */
+ if (__glibc_unlikely (GLRO(dl_debug_mask) & DL_DEBUG_SECURITY))
+ warn (l, program);
/* Binary is not marked and loaded via dlopen: abort. */
if (program == NULL)
fail (l, program);
diff --git a/sysdeps/aarch64/tst-bti-ld-debug-both.c b/sysdeps/aarch64/tst-bti-ld-debug-both.c
new file mode 100644
index 0000000000..4189ca931d
--- /dev/null
+++ b/sysdeps/aarch64/tst-bti-ld-debug-both.c
@@ -0,0 +1,3 @@
+/* This LD_DEBUG warning test allows to test a case when both the exe and
+ one of its dependencies are not marked with BTI. */
+#include "tst-bti-skeleton.c"
diff --git a/sysdeps/aarch64/tst-bti-ld-debug-dlopen.c b/sysdeps/aarch64/tst-bti-ld-debug-dlopen.c
new file mode 100644
index 0000000000..eece559cde
--- /dev/null
+++ b/sysdeps/aarch64/tst-bti-ld-debug-dlopen.c
@@ -0,0 +1,5 @@
+/* Test that when BTI is not enforced an LD_DEBUG warning is printed
+ when a library that does not have BTI marking is loaded via dlopen. */
+#define TEST_BTI_DLOPEN_MODULE "tst-bti-mod-unprot.so"
+#define TEST_BTI_EXPECT_DLOPEN 1
+#include "tst-bti-skeleton-dlopen.c"
diff --git a/sysdeps/aarch64/tst-bti-ld-debug-exe.c b/sysdeps/aarch64/tst-bti-ld-debug-exe.c
new file mode 100644
index 0000000000..99511e2775
--- /dev/null
+++ b/sysdeps/aarch64/tst-bti-ld-debug-exe.c
@@ -0,0 +1,35 @@
+/* Simple test for an executable without BTI marking.
+ 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
+ . */
+
+#include
+#include
+#include
+
+#include
+#include
+
+static int
+do_test (void)
+{
+ unsigned long hwcap2 = getauxval (AT_HWCAP2);
+ if ((hwcap2 & HWCAP2_BTI) == 0)
+ FAIL_UNSUPPORTED ("BTI is not supported by this system");
+ return 0;
+}
+
+#include
diff --git a/sysdeps/aarch64/tst-bti-ld-debug-shared.c b/sysdeps/aarch64/tst-bti-ld-debug-shared.c
new file mode 100644
index 0000000000..c28a9a5515
--- /dev/null
+++ b/sysdeps/aarch64/tst-bti-ld-debug-shared.c
@@ -0,0 +1,3 @@
+/* Test that when BTI is not enforced an LD_DEBUG warning is printed
+ when one of the shared library dependencies does not have BTI marking. */
+#include "tst-bti-skeleton.c"
diff --git a/sysdeps/generic/ldsodefs.h b/sysdeps/generic/ldsodefs.h
index 46f334184c..0fcc14999c 100644
--- a/sysdeps/generic/ldsodefs.h
+++ b/sysdeps/generic/ldsodefs.h
@@ -526,6 +526,7 @@ struct rtld_global_ro
/* DL_DEBUG_HELP is only used internally. */
#define DL_DEBUG_HELP (1 << 10)
#define DL_DEBUG_TLS (1 << 11)
+#define DL_DEBUG_SECURITY (1 << 12)
/* Platform name. */
EXTERN const char *_dl_platform;
diff --git a/sysdeps/unix/sysv/linux/aarch64/Makefile b/sysdeps/unix/sysv/linux/aarch64/Makefile
index 6ea6048822..a8477087d9 100644
--- a/sysdeps/unix/sysv/linux/aarch64/Makefile
+++ b/sysdeps/unix/sysv/linux/aarch64/Makefile
@@ -28,6 +28,10 @@ gcs-tests-dynamic = \
tst-gcs-dlopen-override \
tst-gcs-enforced \
tst-gcs-enforced-abort \
+ tst-gcs-ld-debug-both \
+ tst-gcs-ld-debug-dlopen \
+ tst-gcs-ld-debug-exe \
+ tst-gcs-ld-debug-shared \
tst-gcs-noreturn \
tst-gcs-optional-off \
tst-gcs-optional-on \
@@ -73,10 +77,9 @@ LDFLAGS-tst-gcs-optional-on += -Wl,-z,gcs=always
LDFLAGS-tst-gcs-optional-off += -Wl,-z,gcs=never
LDFLAGS-tst-gcs-override += -Wl,-z,gcs=never
-CFLAGS-tst-gcs-enforced-static-abort.o += -mbranch-protection=none
-
LDFLAGS-tst-gcs-disabled-static += -Wl,-z,gcs=always
LDFLAGS-tst-gcs-enforced-static += -Wl,-z,gcs=always
+LDFLAGS-tst-gcs-enforced-static-abort += -Wl,-z,gcs=never
LDFLAGS-tst-gcs-optional-static-on += -Wl,-z,gcs=always
LDFLAGS-tst-gcs-optional-static-off += -Wl,-z,gcs=never
LDFLAGS-tst-gcs-override-static += -Wl,-z,gcs=never
@@ -90,7 +93,7 @@ tst-gcs-override-ENV = GLIBC_TUNABLES=glibc.cpu.aarch64_gcs=3
tst-gcs-disabled-static-ENV = GLIBC_TUNABLES=glibc.cpu.aarch64_gcs=0
tst-gcs-enforced-static-ENV = GLIBC_TUNABLES=glibc.cpu.aarch64_gcs=1
-tst-gcs-enforced-static-abort-ENV = GLIBC_TUNABLES=glibc.cpu.aarch64_gcs=1:glibc.cpu.aarch64_bti=0
+tst-gcs-enforced-static-abort-ENV = GLIBC_TUNABLES=glibc.cpu.aarch64_gcs=1
tst-gcs-optional-static-on-ENV = GLIBC_TUNABLES=glibc.cpu.aarch64_gcs=2
tst-gcs-optional-static-off-ENV = GLIBC_TUNABLES=glibc.cpu.aarch64_gcs=2
tst-gcs-override-static-ENV = GLIBC_TUNABLES=glibc.cpu.aarch64_gcs=3
@@ -103,6 +106,9 @@ LDFLAGS-tst-gcs-shared-enforced-abort = -Wl,-z,gcs=always
LDFLAGS-tst-gcs-shared-optional = -Wl,-z,gcs=always
LDFLAGS-tst-gcs-shared-override = -Wl,-z,gcs=always
+LDFLAGS-tst-gcs-ld-debug-shared = -Wl,-z,gcs=always
+LDFLAGS-tst-gcs-ld-debug-dlopen = -Wl,-z,gcs=always
+
modules-names += \
tst-gcs-mod1 \
tst-gcs-mod2 \
@@ -114,6 +120,8 @@ $(objpfx)tst-gcs-shared-enforced-abort: $(objpfx)tst-gcs-mod1.so $(objpfx)tst-gc
$(objpfx)tst-gcs-shared-optional: $(objpfx)tst-gcs-mod1.so $(objpfx)tst-gcs-mod3.so
$(objpfx)tst-gcs-shared-override: $(objpfx)tst-gcs-mod1.so $(objpfx)tst-gcs-mod3.so
$(objpfx)tst-gcs-mod1.so: $(objpfx)tst-gcs-mod2.so
+$(objpfx)tst-gcs-ld-debug-both: $(objpfx)tst-gcs-mod2.so
+$(objpfx)tst-gcs-ld-debug-shared: $(objpfx)tst-gcs-mod1.so $(objpfx)tst-gcs-mod3.so
tst-gcs-shared-disabled-ENV = GLIBC_TUNABLES=glibc.cpu.aarch64_gcs=0
tst-gcs-shared-enforced-abort-ENV = GLIBC_TUNABLES=glibc.cpu.aarch64_gcs=1
@@ -125,6 +133,8 @@ LDFLAGS-tst-gcs-dlopen-enforced = -Wl,-z,gcs=always
LDFLAGS-tst-gcs-dlopen-optional-on = -Wl,-z,gcs=always
LDFLAGS-tst-gcs-dlopen-optional-off = -Wl,-z,gcs=never
LDFLAGS-tst-gcs-dlopen-override = -Wl,-z,gcs=always
+LDFLAGS-tst-gcs-ld-debug-exe = -Wl,-z,gcs=never
+LDFLAGS-tst-gcs-ld-debug-both = -Wl,-z,gcs=never
tst-gcs-dlopen-disabled-ENV = GLIBC_TUNABLES=glibc.cpu.aarch64_gcs=0
tst-gcs-dlopen-enforced-ENV = GLIBC_TUNABLES=glibc.cpu.aarch64_gcs=1
@@ -137,11 +147,18 @@ $(objpfx)tst-gcs-dlopen-enforced.out: $(objpfx)tst-gcs-mod2.so
$(objpfx)tst-gcs-dlopen-optional-on.out: $(objpfx)tst-gcs-mod2.so
$(objpfx)tst-gcs-dlopen-optional-off.out: $(objpfx)tst-gcs-mod2.so
$(objpfx)tst-gcs-dlopen-override.out: $(objpfx)tst-gcs-mod2.so
+$(objpfx)tst-gcs-ld-debug-dlopen.out: $(objpfx)tst-gcs-mod2.so
LDFLAGS-tst-gcs-noreturn = -Wl,-z,gcs=always
tst-gcs-noreturn-ENV = GLIBC_TUNABLES=glibc.cpu.aarch64_gcs=0
+$(objpfx)tst-gcs-ld-debug-%.out: $(..)elf/tst-dl-debug-protect.sh $(objpfx)tst-gcs-ld-debug-%
+ $(SHELL) $< $(objpfx) '$(test-wrapper-env)' '$(rtld-prefix)' \
+ '$(run-program-env) GLIBC_TUNABLES=glibc.cpu.aarch64_gcs=2' \
+ 'security: not compatible with AArch64 GCS: $(objpfx)' \
+ $(objpfx)tst-gcs-ld-debug-$* > $@; $(evaluate-test)
+
endif # ifeq ($(have-test-gcs),yes)
endif # ifeq ($(subdir),misc)
diff --git a/sysdeps/unix/sysv/linux/aarch64/tst-gcs-ld-debug-both.c b/sysdeps/unix/sysv/linux/aarch64/tst-gcs-ld-debug-both.c
new file mode 100644
index 0000000000..acb71156ec
--- /dev/null
+++ b/sysdeps/unix/sysv/linux/aarch64/tst-gcs-ld-debug-both.c
@@ -0,0 +1,38 @@
+/* Test that when GCS is optional an LD_DEBUG warning is printed when
+ both the executable and its shared library dependency do not have
+ GCS marking.
+ 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
+ . */
+
+#include "tst-gcs-helper.h"
+
+/* Defined in tst-gcs-mod2.c. */
+extern int fun2 (void);
+
+static int
+do_test (void)
+{
+ /* Check if GCS could possible by enabled. */
+ if (!(getauxval (AT_HWCAP) & HWCAP_GCS))
+ FAIL_UNSUPPORTED ("kernel or CPU does not support GCS");
+ bool gcs_enabled = __check_gcs_status ();
+ puts (gcs_enabled ? "GCS enabled" : "GCS not enabled");
+ TEST_VERIFY (!gcs_enabled);
+ return fun2();
+}
+
+#include
diff --git a/sysdeps/unix/sysv/linux/aarch64/tst-gcs-ld-debug-dlopen.c b/sysdeps/unix/sysv/linux/aarch64/tst-gcs-ld-debug-dlopen.c
new file mode 100644
index 0000000000..b90fa2ef80
--- /dev/null
+++ b/sysdeps/unix/sysv/linux/aarch64/tst-gcs-ld-debug-dlopen.c
@@ -0,0 +1,5 @@
+/* Test that when GCS is optional an LD_DEBUG warning is printed when
+ a library that does not have GCS marking is loaded via dlopen. */
+#define TEST_GCS_EXPECT_ENABLED 0
+#define TEST_GCS_EXPECT_DLOPEN 0
+#include "tst-gcs-dlopen.c"
diff --git a/sysdeps/unix/sysv/linux/aarch64/tst-gcs-ld-debug-exe.c b/sysdeps/unix/sysv/linux/aarch64/tst-gcs-ld-debug-exe.c
new file mode 100644
index 0000000000..d900c45aef
--- /dev/null
+++ b/sysdeps/unix/sysv/linux/aarch64/tst-gcs-ld-debug-exe.c
@@ -0,0 +1,4 @@
+/* Test that when GCS is optional an LD_DEBUG warning is printed when
+ the executable does not have GCS marking. */
+#define TEST_GCS_EXPECT_ENABLED 0
+#include "tst-gcs-skeleton.c"
diff --git a/sysdeps/unix/sysv/linux/aarch64/tst-gcs-ld-debug-shared.c b/sysdeps/unix/sysv/linux/aarch64/tst-gcs-ld-debug-shared.c
new file mode 100644
index 0000000000..b6cf4b19af
--- /dev/null
+++ b/sysdeps/unix/sysv/linux/aarch64/tst-gcs-ld-debug-shared.c
@@ -0,0 +1,4 @@
+/* Test that when GCS is optional an LD_DEBUG warning is printed when
+ one of the shared library dependencies does not have GCS marking. */
+#define TEST_GCS_EXPECT_ENABLED 0
+#include "tst-gcs-shared.c"