@@ -633,7 +633,7 @@ link-libc-printers-tests = $(link-libc-rpath) \
$(link-libc-tests-after-rpath-link)
# This is how to find at build-time things that will be installed there.
-rpath-dirs = math elf dlfcn nss nis rt resolv mathvec support misc
+rpath-dirs = math elf dlfcn nss nis rt resolv mathvec support misc debug
rpath-link = \
$(common-objdir):$(subst $(empty) ,:,$(patsubst ../$(subdir),.,$(rpath-dirs:%=$(common-objpfx)%)))
else # build-static
@@ -74,6 +74,7 @@ routines = \
readlink_chk \
readlinkat_chk \
readonly-area \
+ readonly-area-arch \
realpath_chk \
recv_chk \
recvfrom_chk \
@@ -179,9 +180,17 @@ CPPFLAGS-tst-longjmp_chk3.c += $(no-fortify-source) -D_FORTIFY_SOURCE=1
CPPFLAGS-tst-realpath-chk.c += $(no-fortify-source) -D_FORTIFY_SOURCE=2
CPPFLAGS-tst-chk-cancel.c += $(no-fortify-source) -D_FORTIFY_SOURCE=2
CFLAGS-tst-sprintf-fortify-rdonly.c += $(no-fortify-source) -D_FORTIFY_SOURCE=2
+CFLAGS-tst-sprintf-fortify-rdonly-mod.c += $(no-fortify-source) -D_FORTIFY_SOURCE=2
+CFLAGS-tst-sprintf-fortify-rdonly-dlopen.c += $(no-fortify-source) -D_FORTIFY_SOURCE=2
CFLAGS-tst-fortify-syslog.c += $(no-fortify-source) -D_FORTIFY_SOURCE=2
CFLAGS-tst-fortify-wide.c += $(no-fortify-source) -D_FORTIFY_SOURCE=2
+$(objpfx)tst-sprintf-fortify-rdonly: \
+ $(objpfx)tst-sprintf-fortify-rdonly-mod.so \
+ $(objpfx)tst-sprintf-fortify-rdonly-dlopen.so
+LDLIBS-tst-sprintf-fortify-rdonly-mod.so = $(libsupport)
+LDLIBS-tst-sprintf-fortify-rdonly-dlopen.so = $(libsupport)
+
# _FORTIFY_SOURCE tests.
# Auto-generate tests for _FORTIFY_SOURCE for different levels, compilers and
# preprocessor conditions based on tst-fortify.c.
@@ -302,6 +311,11 @@ tests-container += \
tst-fortify-syslog \
# tests-container
+modules-names += \
+ tst-sprintf-fortify-rdonly-dlopen \
+ tst-sprintf-fortify-rdonly-mod \
+ # modules-names
+
ifeq ($(have-ssp),yes)
tests += tst-ssp-1
endif
@@ -16,18 +16,13 @@
<https://www.gnu.org/licenses/>. */
#include <stdlib.h>
+#include <ldsodefs.h>
-/* Return 1 if the whole area PTR .. PTR+SIZE is not writable.
- Return -1 if it is writable. */
-
-int
+enum readonly_error_type
__readonly_area (const void *ptr, size_t size)
{
- /* We cannot determine in general whether memory is writable or not.
- This must be handled in a system-dependent manner. to not
- unconditionally break code we need to return here a positive
- answer. This disables this security measure but that is the
- price people have to pay for using systems without a real
- implementation of this interface. */
- return 1;
+ if (GLRO(dl_readonly_area (ptr, size)))
+ return readonly_noerror;
+
+ return __readonly_area_arch (ptr, size);
}
new file mode 100644
@@ -0,0 +1 @@
+#include "tst-sprintf-fortify-rdonly-mod.c"
new file mode 100644
@@ -0,0 +1,53 @@
+/* Testcase for BZ 30932.
+ 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 <stdio.h>
+#include <string.h>
+#include <support/support.h>
+
+static const char *str2 = "F";
+static char buf2[10] = "%s";
+static char buf3[10] __attribute__ ((section (".data.rel.ro"))) = "%s%n%s%n";
+
+void
+init_str2 (void)
+{
+ strcpy (buf2 + 2, "%n%s%n");
+}
+
+int
+sprintf_buf2 (int *n1, int *n2)
+{
+ char buf[128];
+ return sprintf (buf, buf2, str2, n1, str2, n2);
+}
+
+int
+sprintf_buf3 (int *n1, int *n2)
+{
+ char buf[128];
+ return sprintf (buf, buf3, str2, n1, str2, n2);
+}
+
+int
+sprintf_buf2_malloc (int *n1, int *n2)
+{
+ char buf[128];
+ char *buf2_malloc = xstrdup (buf2);
+ return sprintf (buf, buf2_malloc, str2, n1, str2, n2);
+}
@@ -27,16 +27,63 @@
#include <support/check.h>
#include <support/support.h>
#include <support/temp_file.h>
+#include <support/xdlfcn.h>
-jmp_buf chk_fail_buf;
-bool chk_fail_ok;
+static jmp_buf chk_fail_buf;
+static volatile int ret;
+static bool chk_fail_ok;
-const char *str2 = "F";
-char buf2[10] = "%s";
+static void
+handler (int sig)
+{
+ if (chk_fail_ok)
+ {
+ chk_fail_ok = false;
+ longjmp (chk_fail_buf, 1);
+ }
+ else
+ _exit (127);
+}
+
+#define FORTIFY_FAIL \
+ do { printf ("Failure on line %d\n", __LINE__); ret = 1; } while (0)
+#define CHK_FAIL_START \
+ chk_fail_ok = true; \
+ if (! sigsetjmp (chk_fail_buf, 1)) \
+ {
+#define CHK_FAIL_END \
+ chk_fail_ok = false; \
+ FORTIFY_FAIL; \
+ }
+
+static const char *str2 = "F";
+static char buf2[10] = "%s";
+static char buf3[10] __attribute__ ((section (".data.rel.ro"))) = "%s%n%s%n";
+
+extern void init_str2 (void);
+extern int sprintf_buf2 (int *, int *);
+extern int sprintf_buf3 (int *, int *);
+extern int sprintf_buf2_malloc (int *, int *);
+
+#define str(__x) # __x
+void (*init_str2_dlopen)(void);
+int (*sprintf_buf2_dlopen)(int *, int *);
+int (*sprintf_buf3_dlopen)(int *, int *);
+int (*sprintf_buf2_malloc_dlopen)(int *, int *);
static int
do_test (void)
{
+ set_fortify_handler (handler);
+
+ {
+ void *h = xdlopen ("tst-sprintf-fortify-rdonly-dlopen.so", RTLD_NOW);
+ init_str2_dlopen = xdlsym (h, str(init_str2));
+ sprintf_buf2_dlopen = xdlsym (h, str(sprintf_buf2));
+ sprintf_buf3_dlopen = xdlsym (h, str(sprintf_buf3));
+ sprintf_buf2_malloc_dlopen = xdlsym (h, str(sprintf_buf2_malloc));
+ }
+
struct rlimit rl;
int max_fd = 24;
@@ -63,20 +110,94 @@ do_test (void)
}
TEST_VERIFY_EXIT (nfiles != 0);
- /* When the format string is writable and contains %n,
- with -D_FORTIFY_SOURCE=2 it causes __chk_fail. However, if libc can not
- open procfs to check if the input format string in within a writable
- memory segment, the fortify version can not perform the check. */
- char buf[128];
- int n1;
- int n2;
-
strcpy (buf2 + 2, "%n%s%n");
- if (sprintf (buf, buf2, str2, &n1, str2, &n2) != 2
- || n1 != 1 || n2 != 2)
- FAIL_EXIT1 ("sprintf failed: %s %d %d", buf, n1, n2);
-
- return 0;
+ init_str2 ();
+ init_str2_dlopen ();
+
+ /* buf2 is at a writable part of .bss segment, so libc should be able to
+ check it without resorting to procfs. */
+ {
+ char buf[128];
+ int n1;
+ int n2;
+ CHK_FAIL_START
+ sprintf (buf, buf2, str2, &n1, str2, &n2);
+ CHK_FAIL_END
+ }
+
+ /* Same as before, but from an library. */
+ {
+ int n1;
+ int n2;
+ CHK_FAIL_START
+ sprintf_buf2 (&n1, &n2);
+ CHK_FAIL_END
+ }
+
+ {
+ int n1;
+ int n2;
+ CHK_FAIL_START
+ sprintf_buf2_dlopen (&n1, &n2);
+ CHK_FAIL_END
+ }
+
+ /* buf3 is at a readonly part of .bss segment, so '%n' in format input
+ should not trigger a fortify failure. */
+ {
+ char buf[128];
+ int n1;
+ int n2;
+ if (sprintf (buf, buf3, str2, &n1, str2, &n2) != 2
+ || n1 != 1 || n2 != 2)
+ FAIL_EXIT1 ("sprintf failed: %s %d %d", buf, n1, n2);
+ }
+
+ /* Same as before, but from an library. */
+ {
+ int n1;
+ int n2;
+ if (sprintf_buf3 (&n1, &n2) != 2 || n1 != 1 || n2 != 2)
+ FAIL_EXIT1 ("sprintf failed: %d %d", n1, n2);
+ }
+
+ {
+ int n1;
+ int n2;
+ if (sprintf_buf3_dlopen (&n1, &n2) != 2 || n1 != 1 || n2 != 2)
+ FAIL_EXIT1 ("sprintf failed: %d %d", n1, n2);
+ }
+
+ /* However if the format string is placed on a writable memory not covered
+ by ELF segments, libc needs to resort to procfs. */
+ {
+ char buf[128];
+ int n1;
+ int n2;
+ char *buf2_malloc = xstrdup (buf2);
+ CHK_FAIL_START
+ sprintf (buf, buf2_malloc, str2, &n1, str2, &n2);
+ CHK_FAIL_END
+ }
+
+ /* Same as before, but from an library. */
+ {
+ int n1;
+ int n2;
+ CHK_FAIL_START
+ sprintf_buf2_malloc (&n1, &n2);
+ CHK_FAIL_END
+ }
+
+ {
+ int n1;
+ int n2;
+ CHK_FAIL_START
+ sprintf_buf2_malloc_dlopen (&n1, &n2);
+ CHK_FAIL_END
+ }
+
+ return ret;
}
#include <support/test-driver.c>
@@ -72,6 +72,7 @@ dl-routines = \
dl-open \
dl-origin \
dl-printf \
+ dl-readonly-area \
dl-reloc \
dl-runtime \
dl-scope \
new file mode 100644
@@ -0,0 +1,84 @@
+/* Check if range is within a read-only from a loaded ELF object.
+ 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 <ldsodefs.h>
+
+static bool
+check_relro (const struct link_map *l, uintptr_t start, uintptr_t end)
+{
+ for (const ElfW(Phdr) *ph = l->l_phdr; ph < &l->l_phdr[l->l_phnum]; ++ph)
+ if (ph->p_type == PT_GNU_RELRO)
+ {
+ uintptr_t relro_start = ALIGN_DOWN (l->l_addr + ph->p_vaddr,
+ GLRO(dl_pagesize));
+ uintptr_t relro_end = ALIGN_DOWN (l->l_addr + ph->p_vaddr
+ + ph->p_memsz, GLRO(dl_pagesize));
+ /* RELRO is caved out from a RW segment, so the next range is either
+ RW or nonexistent. */
+ return relro_start < start && relro_end >= end;
+ }
+ return false;
+}
+
+bool
+_dl_readonly_area (const void *ptr, size_t size)
+{
+ struct dl_find_object dlfo;
+ if (_dl_find_object ((void *)ptr, &dlfo) != 0)
+ return false;
+
+ const struct link_map *l = dlfo.dlfo_link_map;
+ uintptr_t ptr_start = (uintptr_t) ptr;
+ uintptr_t ptr_end = ptr_start + size;
+
+ for (const ElfW(Phdr) *ph = l->l_phdr; ph < &l->l_phdr[l->l_phnum]; ++ph)
+ if (ph->p_type == PT_LOAD)
+ {
+ /* For segments with alignment larger than the page size,
+ _dl_map_segment allocates additional space that is mark as
+ PROT_NONE (so we can ignore). */
+ uintptr_t from = l->l_addr
+ + ALIGN_DOWN (ph->p_vaddr, GLRO(dl_pagesize));
+ uintptr_t to = l->l_addr
+ + ALIGN_UP (ph->p_vaddr + ph->p_filesz, GLRO(dl_pagesize));
+
+ /* Found an entry that at least partially covers the area. */
+ if (from < ptr_end && to > ptr_start)
+ {
+ if (ph->p_flags & PF_W)
+ return check_relro (l, ptr_start, ptr_end);
+
+ if ((ph->p_flags & PF_R) == 0)
+ return readonly_area_writable;
+
+ if (from < ptr_start && to >= ptr_end)
+ return true;
+ else if (from < ptr_start)
+ size -= to - ptr_start;
+ else if (to >= ptr_end)
+ size -= ptr_end - from;
+ else
+ size -= to - from;
+
+ if (size == 0)
+ break;
+ }
+ }
+
+ return size == 0;
+}
@@ -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_readonly_area = _dl_readonly_area,
};
/* If we would use strong_alias here the compiler would see a
non-hidden definition. This would undo the effect of the previous
@@ -368,6 +368,21 @@ struct abort_msg_s
extern struct abort_msg_s *__abort_msg;
libc_hidden_proto (__abort_msg)
+enum readonly_error_type
+{
+ readonly_noerror,
+ readonly_area_writable,
+ readonly_procfs_inaccessible,
+ readonly_procfs_open_fail,
+};
+
+extern enum readonly_error_type __readonly_area (const void *ptr,
+ size_t size)
+ attribute_hidden;
+extern enum readonly_error_type __readonly_area_arch (const void *ptr,
+ size_t size)
+ attribute_hidden;
+
# if IS_IN (rtld)
extern __typeof (unsetenv) unsetenv attribute_hidden;
extern __typeof (__strtoul_internal) __strtoul_internal attribute_hidden;
@@ -576,7 +576,8 @@ static const uint8_t jump_table[] =
/* Handle positional format specifiers. */
static void printf_positional (struct Xprintf_buffer *buf,
- const CHAR_T *format, int readonly_format,
+ const CHAR_T *format,
+ enum readonly_error_type readonly_format,
va_list ap, va_list *ap_savep,
int nspecs_done, const UCHAR_T *lead_str_end,
CHAR_T *work_buffer, int save_errno,
@@ -626,9 +627,7 @@ Xprintf_buffer (struct Xprintf_buffer *buf, const CHAR_T *format,
/* For the %m format we may need the current `errno' value. */
int save_errno = errno;
- /* 1 if format is in read-only memory, -1 if it is in writable memory,
- 0 if unknown. */
- int readonly_format = 0;
+ enum readonly_error_type readonly_format = readonly_noerror;
/* Initialize local variables. */
grouping = (const char *) -1;
@@ -1045,7 +1044,7 @@ do_positional:
static void
printf_positional (struct Xprintf_buffer * buf, const CHAR_T *format,
- int readonly_format,
+ enum readonly_error_type readonly_format,
va_list ap, va_list *ap_savep, int nspecs_done,
const UCHAR_T *lead_str_end,
CHAR_T *work_buffer, int save_errno,
@@ -324,16 +324,24 @@ LABEL (form_pointer):
LABEL (form_number):
if ((mode_flags & PRINTF_FORTIFY) != 0)
{
- if (! readonly_format)
- {
- extern int __readonly_area (const void *, size_t)
- attribute_hidden;
- readonly_format
- = __readonly_area (format, ((STR_LEN (format) + 1)
- * sizeof (CHAR_T)));
- }
- if (readonly_format < 0)
- __libc_fatal ("*** %n in writable segment detected ***\n");
+ if (readonly_format == readonly_noerror)
+ readonly_format = __readonly_area (format, ((STR_LEN (format) + 1)
+ * sizeof (CHAR_T)));
+ switch (readonly_format)
+ {
+ case readonly_area_writable:
+ __libc_fatal ("*** %n in writable segment detected ***\n");
+ /* The format is not within ELF segmetns and opening /proc/self/maps
+ failed because there are too many files. */
+ case readonly_procfs_open_fail:
+ __libc_fatal ("*** procfs could not open ***\n");
+ /* The /proc/self/maps can not be opened either because it is not
+ available or the process does not have the right permission. Since
+ it should not be attacker-controlled we can avoid failure. */
+ case readonly_procfs_inaccessible:
+ case readonly_noerror:
+ break;
+ }
}
/* Answer the count of characters written. */
void *ptrptr = process_arg_pointer ();
@@ -676,6 +676,10 @@ struct rtld_global_ro
dlopen. */
int (*_dl_find_object) (void *, struct dl_find_object *);
+ /* Implementation of _dl_readonly_area, used in fortify routines to check
+ if memory area is within a read-only ELF segment. */
+ bool (*_dl_readonly_area) (const void *, size_t);
+
/* Dynamic linker operations used after static dlopen. */
const struct dlfcn_hook *_dl_dlfcn_hook;
@@ -1280,6 +1284,9 @@ extern void _dl_show_scope (struct link_map *new, int from)
extern struct link_map *_dl_find_dso_for_object (const ElfW(Addr) addr);
rtld_hidden_proto (_dl_find_dso_for_object)
+extern bool _dl_readonly_area (const void *ptr, size_t size)
+ attribute_hidden;
+
/* Initialization which is normally done by the dynamic linker. */
extern void _dl_non_dynamic_init (void)
attribute_hidden;
similarity index 90%
rename from sysdeps/mach/readonly-area.c
rename to sysdeps/mach/readonly-area-arch.c
@@ -20,11 +20,8 @@
#include <stdint.h>
#include <mach.h>
-/* Return 1 if the whole area PTR .. PTR+SIZE is not writable.
- Return -1 if it is writable. */
-
-int
-__readonly_area (const char *ptr, size_t size)
+enum readonly_error_type
+__readonly_area_arch (const void *ptr, size_t size)
{
vm_address_t region_address = (uintptr_t) ptr;
vm_size_t region_length = size;
@@ -46,11 +43,11 @@ __readonly_area (const char *ptr, size_t size)
continue;
if (protection & VM_PROT_WRITE)
- return -1;
+ return readonly_area_writable;
if (region_address - (uintptr_t) ptr >= size)
break;
}
- return 1;
+ return readonly_noerror;
}
similarity index 84%
rename from sysdeps/unix/sysv/linux/readonly-area.c
rename to sysdeps/unix/sysv/linux/readonly-area-arch.c
@@ -23,11 +23,8 @@
#include <string.h>
#include "libio/libioP.h"
-/* Return 1 if the whole area PTR .. PTR+SIZE is not writable.
- Return -1 if it is writable. */
-
-int
-__readonly_area (const char *ptr, size_t size)
+enum readonly_error_type
+__readonly_area_arch (const void *ptr, size_t size)
{
const void *ptr_end = ptr + size;
@@ -42,11 +39,11 @@ __readonly_area (const char *ptr, size_t size)
to the /proc filesystem if it is set[ug]id. There has
been no willingness to change this in the kernel so
far. */
- || errno == EACCES
- /* Process has reached the maximum number of open files. */
- || errno == EMFILE)
- return 1;
- return -1;
+ || errno == EACCES)
+ return readonly_procfs_inaccessible;
+ /* Process has reached the maximum number of open files or another
+ unusual error. */
+ return readonly_procfs_open_fail;
}
/* We need no locking. */
@@ -98,7 +95,5 @@ __readonly_area (const char *ptr, size_t size)
fclose (fp);
free (line);
- /* If the whole area between ptr and ptr_end is covered by read-only
- VMAs, return 1. Otherwise return -1. */
- return size == 0 ? 1 : -1;
+ return size == 0 ? readonly_noerror : readonly_area_writable;
}