diff mbox series

[v2] debug: Improve '%n' fortify detection (BZ 30932)

Message ID 20250314191240.1339994-1-adhemerval.zanella@linaro.org
State Accepted
Commit ed6a68bac7cd056abda9008019c71b167f0362dc
Headers show
Series [v2] debug: Improve '%n' fortify detection (BZ 30932) | expand

Commit Message

Adhemerval Zanella March 14, 2025, 7:09 p.m. UTC
The 7bb8045ec0 path made the '%n' fortify check ignore EMFILE errors
while trying to open /proc/self/maps, and this added a security
issue where EMFILE can be attacker-controlled thus making it
ineffective for some cases.

The EMFILE failure is reinstated but with a different error
message.  Also, to improve the false positive of the hardening for
the cases where no new files can be opened, the
_dl_readonly_area now uses  _dl_find_object to check if the
memory area is within a writable ELF segment.  The procfs method is
still used as fallback.

Checked on x86_64-linux-gnu and i686-linux-gnu.
--
Changes from v1:
* Rename readonly-area-arch to readonly-area-fallback.
* Handle found RW areas on _dl_readonly_area.
* Use l_relro_addr/l_relro_size in check_relro.
* Change variable names in tst-sprintf-fortify-rdonly-mod.c.
---
 Makeconfig                                    |   2 +-
 debug/Makefile                                |  14 ++
 debug/readonly-area.c                         |  23 +--
 debug/tst-sprintf-fortify-rdonly-dlopen.c     |   1 +
 debug/tst-sprintf-fortify-rdonly-mod.c        |  56 +++++++
 debug/tst-sprintf-fortify-rdonly.c            | 158 ++++++++++++++++--
 elf/Makefile                                  |   1 +
 elf/dl-readonly-area.c                        |  86 ++++++++++
 elf/rtld.c                                    |   1 +
 include/stdlib.h                              |  15 ++
 stdio-common/vfprintf-internal.c              |   9 +-
 stdio-common/vfprintf-process-arg.c           |  28 ++--
 sysdeps/generic/ldsodefs.h                    |  14 ++
 ...adonly-area.c => readonly-area-fallback.c} |  11 +-
 ...adonly-area.c => readonly-area-fallback.c} |  21 +--
 15 files changed, 375 insertions(+), 65 deletions(-)
 create mode 100644 debug/tst-sprintf-fortify-rdonly-dlopen.c
 create mode 100644 debug/tst-sprintf-fortify-rdonly-mod.c
 create mode 100644 elf/dl-readonly-area.c
 rename sysdeps/mach/{readonly-area.c => readonly-area-fallback.c} (90%)
 rename sysdeps/unix/sysv/linux/{readonly-area.c => readonly-area-fallback.c} (84%)

Comments

Arjun Shankar March 19, 2025, 10:14 p.m. UTC | #1
Hi Adhemerval,

> The 7bb8045ec0 path made the '%n' fortify check ignore EMFILE errors
> while trying to open /proc/self/maps, and this added a security
> issue where EMFILE can be attacker-controlled thus making it
> ineffective for some cases.
>
> The EMFILE failure is reinstated but with a different error
> message.  Also, to improve the false positive of the hardening for
> the cases where no new files can be opened, the
> _dl_readonly_area now uses  _dl_find_object to check if the
> memory area is within a writable ELF segment.  The procfs method is
> still used as fallback.
>
> Checked on x86_64-linux-gnu and i686-linux-gnu.
> --
> Changes from v1:
> * Rename readonly-area-arch to readonly-area-fallback.
> * Handle found RW areas on _dl_readonly_area.
> * Use l_relro_addr/l_relro_size in check_relro.
> * Change variable names in tst-sprintf-fortify-rdonly-mod.c.

Thanks!

There are three minor issues I've listed below, inline. TL;DR version:
1. The Makefile libsupport dependency can be dropped now.
2. The mach implementation function wasn't renamed with _fallback (but
the file was).
3. The comment typo (segmetns). I sort of vaguely wrote "segments" in
my earlier review but I meant the comment, rather than the error
message.

With these changes, the patch looks good to me:
Reviewed-by: Arjun Shankar <arjun@redhat.com>

Cheers,
Arjun

> ---
>  Makeconfig                                    |   2 +-
>  debug/Makefile                                |  14 ++
>  debug/readonly-area.c                         |  23 +--
>  debug/tst-sprintf-fortify-rdonly-dlopen.c     |   1 +
>  debug/tst-sprintf-fortify-rdonly-mod.c        |  56 +++++++
>  debug/tst-sprintf-fortify-rdonly.c            | 158 ++++++++++++++++--
>  elf/Makefile                                  |   1 +
>  elf/dl-readonly-area.c                        |  86 ++++++++++
>  elf/rtld.c                                    |   1 +
>  include/stdlib.h                              |  15 ++
>  stdio-common/vfprintf-internal.c              |   9 +-
>  stdio-common/vfprintf-process-arg.c           |  28 ++--
>  sysdeps/generic/ldsodefs.h                    |  14 ++
>  ...adonly-area.c => readonly-area-fallback.c} |  11 +-
>  ...adonly-area.c => readonly-area-fallback.c} |  21 +--
>  15 files changed, 375 insertions(+), 65 deletions(-)
>  create mode 100644 debug/tst-sprintf-fortify-rdonly-dlopen.c
>  create mode 100644 debug/tst-sprintf-fortify-rdonly-mod.c
>  create mode 100644 elf/dl-readonly-area.c
>  rename sysdeps/mach/{readonly-area.c => readonly-area-fallback.c} (90%)
>  rename sysdeps/unix/sysv/linux/{readonly-area.c => readonly-area-fallback.c} (84%)
>
> diff --git a/Makeconfig b/Makeconfig
> index aa547a443f..bf50406656 100644
> --- a/Makeconfig
> +++ b/Makeconfig
> @@ -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
> diff --git a/debug/Makefile b/debug/Makefile
> index 6a05205ce6..97d36438e8 100644
> --- a/debug/Makefile
> +++ b/debug/Makefile
> @@ -74,6 +74,7 @@ routines = \
>    readlink_chk \
>    readlinkat_chk \
>    readonly-area \
> +  readonly-area-fallback \

OK. Changed name.

>    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)
> +

The libsupport dependency can now be dropped.

>  # _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
> diff --git a/debug/readonly-area.c b/debug/readonly-area.c
> index 04b437ed8d..a35ef9934f 100644
> --- a/debug/readonly-area.c
> +++ b/debug/readonly-area.c
> @@ -16,18 +16,19 @@
>     <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;
> +  switch (GLRO(dl_readonly_area (ptr, size)))
> +    {
> +    case dl_readonly_area_rdonly:
> +      return readonly_noerror;
> +    case dl_readonly_area_writable:
> +      return readonly_area_writable;
> +    default:
> +      /* fallthrough */
> +    }
> +  return __readonly_area_fallback (ptr, size);

OK. dl_readonly_area now also returns an enum. We translate and return that.

>  }
> diff --git a/debug/tst-sprintf-fortify-rdonly-dlopen.c b/debug/tst-sprintf-fortify-rdonly-dlopen.c
> new file mode 100644
> index 0000000000..7da3f51f16
> --- /dev/null
> +++ b/debug/tst-sprintf-fortify-rdonly-dlopen.c
> @@ -0,0 +1 @@
> +#include "tst-sprintf-fortify-rdonly-mod.c"
> diff --git a/debug/tst-sprintf-fortify-rdonly-mod.c b/debug/tst-sprintf-fortify-rdonly-mod.c
> new file mode 100644
> index 0000000000..3655f27b32
> --- /dev/null
> +++ b/debug/tst-sprintf-fortify-rdonly-mod.c
> @@ -0,0 +1,56 @@
> +/* 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 <stdlib.h>

OK. Use stdlib.h instead of support.h so that we don't need libsupport.

> +
> +static const char *str2 = "F";
> +static char writeable_format[10] = "%s";
> +static char relro_format[10] __attribute__ ((section (".data.rel.ro"))) =
> +  "%s%n%s%n";

OK. Renamed variables.

> +
> +void
> +init_writable (void)
> +{
> +  strcpy (writeable_format + 2, "%n%s%n");
> +}

OK. Renamed function and variable use.

> +
> +int
> +sprintf_writable (int *n1, int *n2)
> +{
> +  char buf[128];
> +  return sprintf (buf, writeable_format, str2, n1, str2, n2);
> +}

OK. Same.

> +
> +int
> +sprintf_relro (int *n1, int *n2)
> +{
> +  char buf[128];
> +  return sprintf (buf, relro_format, str2, n1, str2, n2);
> +}

OK. Same.

> +
> +int
> +sprintf_writable_malloc (int *n1, int *n2)
> +{
> +  char buf[128];
> +  char *buf2_malloc = strdup (writeable_format);
> +  if (buf2_malloc == NULL)
> +    abort ();
> +  return sprintf (buf, buf2_malloc, str2, n1, str2, n2);
> +}

OK. same.

> diff --git a/debug/tst-sprintf-fortify-rdonly.c b/debug/tst-sprintf-fortify-rdonly.c
> index ba5a791020..fafc8340ea 100644
> --- a/debug/tst-sprintf-fortify-rdonly.c
> +++ b/debug/tst-sprintf-fortify-rdonly.c
> @@ -27,16 +27,64 @@
>  #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 sigjmp_buf chk_fail_buf;

OK. Changed type.

> +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 writeable_format[10] = "%s";
> +static char relro_format[10] __attribute__ ((section (".data.rel.ro"))) =
> + "%s%n%s%n";
> +
> +extern void init_writable (void);
> +extern int sprintf_writable (int *, int *);
> +extern int sprintf_relro (int *, int *);
> +extern int sprintf_writable_malloc (int *, int *);
> +
> +#define str(__x) # __x
> +void (*init_writable_dlopen)(void);
> +int (*sprintf_writable_dlopen)(int *, int *);
> +int (*sprintf_rdonly_dlopen)(int *, int *);
> +int (*sprintf_writable_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_writable_dlopen = xdlsym (h, str(init_writable));
> +    sprintf_writable_dlopen = xdlsym (h, str(sprintf_writable));
> +    sprintf_rdonly_dlopen = xdlsym (h, str(sprintf_relro));
> +    sprintf_writable_malloc_dlopen = xdlsym (h, str(sprintf_writable_malloc));
> +  }
> +
>    struct rlimit rl;
>    int max_fd = 24;
>
> @@ -63,20 +111,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;
> +  strcpy (writeable_format + 2, "%n%s%n");
> +  init_writable ();
> +  init_writable_dlopen ();
> +
> +  /* writeable_format 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, writeable_format, str2, &n1, str2, &n2);
> +    CHK_FAIL_END
> +  }
> +
> +  /* Same as before, but from an library.  */
> +  {
> +    int n1;
> +    int n2;
> +    CHK_FAIL_START
> +    sprintf_writable (&n1, &n2);
> +    CHK_FAIL_END
> +  }
> +
> +  {
> +    int n1;
> +    int n2;
> +    CHK_FAIL_START
> +    sprintf_writable_dlopen (&n1, &n2);
> +    CHK_FAIL_END
> +  }
> +
> +  /* relro_format 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, relro_format, 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_relro (&n1, &n2) != 2 || n1 != 1 || n2 != 2)
> +      FAIL_EXIT1 ("sprintf failed: %d %d", n1, n2);
> +  }
> +
> +  {
> +    int n1;
> +    int n2;
> +    if (sprintf_rdonly_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 (writeable_format);
> +    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_writable_malloc (&n1, &n2);
> +    CHK_FAIL_END
> +  }
> +
> +  {
> +    int n1;
> +    int n2;
> +    CHK_FAIL_START
> +    sprintf_writable_malloc_dlopen (&n1, &n2);
> +    CHK_FAIL_END
> +  }
> +
> +  return ret;
>  }
>
>  #include <support/test-driver.c>

OK. Function calls and variables renamed to be more readable.

> diff --git a/elf/Makefile b/elf/Makefile
> index 2bce1ed486..8203048a88 100644
> --- a/elf/Makefile
> +++ b/elf/Makefile
> @@ -72,6 +72,7 @@ dl-routines = \
>    dl-open \
>    dl-origin \
>    dl-printf \
> +  dl-readonly-area \
>    dl-reloc \
>    dl-runtime \
>    dl-scope \
> diff --git a/elf/dl-readonly-area.c b/elf/dl-readonly-area.c
> new file mode 100644
> index 0000000000..22769ec9d9
> --- /dev/null
> +++ b/elf/dl-readonly-area.c
> @@ -0,0 +1,86 @@
> +/* 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)
> +{
> +  if (l->l_relro_addr != 0)
> +    {
> +      uintptr_t relro_start = ALIGN_DOWN (l->l_addr + l->l_relro_addr,
> +                                         GLRO(dl_pagesize));
> +      uintptr_t relro_end = ALIGN_DOWN (l->l_addr + l->l_relro_addr
> +                                       + l->l_relro_size,
> +                                       GLRO(dl_pagesize));
> +      /* RELRO is caved out from a RW segment, so the next range is either
> +        RW or nonexistent.  */
> +      return relro_start <= start && end <= relro_end
> +       ? dl_readonly_area_rdonly : dl_readonly_area_writable;
> +
> +    }
> +  return dl_readonly_area_writable;
> +}

OK. Use l_relro_addr and l_relro_size instead of looping. The
comparisons look right.

> +
> +enum dl_readonly_area_error_type
> +_dl_readonly_area (const void *ptr, size_t size)

OK. New enum used instead of bool.

> +{
> +  struct dl_find_object dlfo;
> +  if (_dl_find_object ((void *)ptr, &dlfo) != 0)
> +    return dl_readonly_area_not_found;

OK. Use enum.

> +
> +  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 dl_readonly_area_writable;

OK. Use new enum.

> +
> +           if (from <= ptr_start && to >= ptr_end)

OK. Correct comparisons.

> +             return dl_readonly_area_rdonly;

OK. New enum.

> +           else if (from <= ptr_start)

OK. Correct comparison.

> +             size -= to - ptr_start;
> +           else if (to >= ptr_end)

OK. Correct comparison.

> +             size -= ptr_end - from;
> +           else
> +             size -= to - from;
> +
> +           if (size == 0)
> +             break;
> +         }
> +      }
> +
> +  return size == 0 ? dl_readonly_area_rdonly : dl_readonly_area_not_found;

OK. New enum.

> +}
> diff --git a/elf/rtld.c b/elf/rtld.c
> index 00b25c1a73..099c447e80 100644
> --- a/elf/rtld.c
> +++ b/elf/rtld.c
> @@ -371,6 +371,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
> diff --git a/include/stdlib.h b/include/stdlib.h
> index 57f4ab8545..b7147ba590 100644
> --- a/include/stdlib.h
> +++ b/include/stdlib.h
> @@ -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_fallback (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;
> diff --git a/stdio-common/vfprintf-internal.c b/stdio-common/vfprintf-internal.c
> index aa9708bff5..fa41e1b242 100644
> --- a/stdio-common/vfprintf-internal.c
> +++ b/stdio-common/vfprintf-internal.c
> @@ -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,
> diff --git a/stdio-common/vfprintf-process-arg.c b/stdio-common/vfprintf-process-arg.c
> index 8d20493766..ad1e0ea3ff 100644
> --- a/stdio-common/vfprintf-process-arg.c
> +++ b/stdio-common/vfprintf-process-arg.c
> @@ -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 segments detected ***\n");
> +       /* The format is not within ELF segmetns and opening /proc/self/maps

Sorry I wasn't clear about the typo here earlier. It was in the
comment: "segmetns". The "segment" in the libc_fatal message was
actually okay.

> +          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 ();
> diff --git a/sysdeps/generic/ldsodefs.h b/sysdeps/generic/ldsodefs.h
> index 19494b82ee..5b12a41a18 100644
> --- a/sysdeps/generic/ldsodefs.h
> +++ b/sysdeps/generic/ldsodefs.h
> @@ -276,6 +276,12 @@ struct audit_ifaces
>    struct audit_ifaces *next;
>  };
>
> +enum dl_readonly_area_error_type
> +{
> +  dl_readonly_area_rdonly,
> +  dl_readonly_area_writable,
> +  dl_readonly_area_not_found,
> +};

OK. New enum declared here.

>
>  /* Test whether given NAME matches any of the names of the given object.  */
>  extern int _dl_name_match_p (const char *__name, const struct link_map *__map)
> @@ -676,6 +682,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.  */
> +  enum dl_readonly_area_error_type (*_dl_readonly_area) (const void *, size_t);

OK. Change declaration to use enum.

> +
>    /* Dynamic linker operations used after static dlopen.  */
>    const struct dlfcn_hook *_dl_dlfcn_hook;
>
> @@ -1284,6 +1294,10 @@ 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 enum dl_readonly_area_error_type _dl_readonly_area (const void *ptr,
> +                                                          size_t size)
> +     attribute_hidden;

OK. Change declaration to use enum.

> +
>  /* Initialization which is normally done by the dynamic linker.  */
>  extern void _dl_non_dynamic_init (void)
>       attribute_hidden;
> diff --git a/sysdeps/mach/readonly-area.c b/sysdeps/mach/readonly-area-fallback.c
> similarity index 90%
> rename from sysdeps/mach/readonly-area.c
> rename to sysdeps/mach/readonly-area-fallback.c

OK. File renamed.

> index fb89413fe6..b1bb1cba39 100644
> --- a/sysdeps/mach/readonly-area.c
> +++ b/sysdeps/mach/readonly-area-fallback.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)

The function needs to be renamed as well.

>  {
>    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;
>  }
> diff --git a/sysdeps/unix/sysv/linux/readonly-area.c b/sysdeps/unix/sysv/linux/readonly-area-fallback.c
> similarity index 84%
> rename from sysdeps/unix/sysv/linux/readonly-area.c
> rename to sysdeps/unix/sysv/linux/readonly-area-fallback.c

OK. File renamed.

> index 62d2070c72..c93ad2a598 100644
> --- a/sysdeps/unix/sysv/linux/readonly-area.c
> +++ b/sysdeps/unix/sysv/linux/readonly-area-fallback.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_fallback (const void *ptr, size_t size)

OK. Function renamed.

>  {
>    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;
>  }
> --
> 2.43.0
>


--
Arjun Shankar
he/him/his
Florian Weimer March 21, 2025, 3:41 p.m. UTC | #2
* Adhemerval Zanella:

> The 7bb8045ec0 path made the '%n' fortify check ignore EMFILE errors
> while trying to open /proc/self/maps, and this added a security
> issue where EMFILE can be attacker-controlled thus making it
> ineffective for some cases.
>
> The EMFILE failure is reinstated but with a different error
> message.  Also, to improve the false positive of the hardening for
> the cases where no new files can be opened, the
> _dl_readonly_area now uses  _dl_find_object to check if the
> memory area is within a writable ELF segment.  The procfs method is
> still used as fallback.
>
> Checked on x86_64-linux-gnu and i686-linux-gnu.
> --
> Changes from v1:
> * Rename readonly-area-arch to readonly-area-fallback.
> * Handle found RW areas on _dl_readonly_area.
> * Use l_relro_addr/l_relro_size in check_relro.
> * Change variable names in tst-sprintf-fortify-rdonly-mod.c.

Adhemerval, are you looking for further comments on this patch?

Thanks,
Florian
Adhemerval Zanella March 21, 2025, 4:32 p.m. UTC | #3
On 21/03/25 12:41, Florian Weimer wrote:
> * Adhemerval Zanella:
> 
>> The 7bb8045ec0 path made the '%n' fortify check ignore EMFILE errors
>> while trying to open /proc/self/maps, and this added a security
>> issue where EMFILE can be attacker-controlled thus making it
>> ineffective for some cases.
>>
>> The EMFILE failure is reinstated but with a different error
>> message.  Also, to improve the false positive of the hardening for
>> the cases where no new files can be opened, the
>> _dl_readonly_area now uses  _dl_find_object to check if the
>> memory area is within a writable ELF segment.  The procfs method is
>> still used as fallback.
>>
>> Checked on x86_64-linux-gnu and i686-linux-gnu.
>> --
>> Changes from v1:
>> * Rename readonly-area-arch to readonly-area-fallback.
>> * Handle found RW areas on _dl_readonly_area.
>> * Use l_relro_addr/l_relro_size in check_relro.
>> * Change variable names in tst-sprintf-fortify-rdonly-mod.c.
> 
> Adhemerval, are you looking for further comments on this patch?

No, I was focusing on the tst-origin breakage before install this.
diff mbox series

Patch

diff --git a/Makeconfig b/Makeconfig
index aa547a443f..bf50406656 100644
--- a/Makeconfig
+++ b/Makeconfig
@@ -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
diff --git a/debug/Makefile b/debug/Makefile
index 6a05205ce6..97d36438e8 100644
--- a/debug/Makefile
+++ b/debug/Makefile
@@ -74,6 +74,7 @@  routines = \
   readlink_chk \
   readlinkat_chk \
   readonly-area \
+  readonly-area-fallback \
   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
diff --git a/debug/readonly-area.c b/debug/readonly-area.c
index 04b437ed8d..a35ef9934f 100644
--- a/debug/readonly-area.c
+++ b/debug/readonly-area.c
@@ -16,18 +16,19 @@ 
    <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;
+  switch (GLRO(dl_readonly_area (ptr, size)))
+    {
+    case dl_readonly_area_rdonly:
+      return readonly_noerror;
+    case dl_readonly_area_writable:
+      return readonly_area_writable;
+    default:
+      /* fallthrough */
+    }
+  return __readonly_area_fallback (ptr, size);
 }
diff --git a/debug/tst-sprintf-fortify-rdonly-dlopen.c b/debug/tst-sprintf-fortify-rdonly-dlopen.c
new file mode 100644
index 0000000000..7da3f51f16
--- /dev/null
+++ b/debug/tst-sprintf-fortify-rdonly-dlopen.c
@@ -0,0 +1 @@ 
+#include "tst-sprintf-fortify-rdonly-mod.c"
diff --git a/debug/tst-sprintf-fortify-rdonly-mod.c b/debug/tst-sprintf-fortify-rdonly-mod.c
new file mode 100644
index 0000000000..3655f27b32
--- /dev/null
+++ b/debug/tst-sprintf-fortify-rdonly-mod.c
@@ -0,0 +1,56 @@ 
+/* 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 <stdlib.h>
+
+static const char *str2 = "F";
+static char writeable_format[10] = "%s";
+static char relro_format[10] __attribute__ ((section (".data.rel.ro"))) =
+  "%s%n%s%n";
+
+void
+init_writable (void)
+{
+  strcpy (writeable_format + 2, "%n%s%n");
+}
+
+int
+sprintf_writable (int *n1, int *n2)
+{
+  char buf[128];
+  return sprintf (buf, writeable_format, str2, n1, str2, n2);
+}
+
+int
+sprintf_relro (int *n1, int *n2)
+{
+  char buf[128];
+  return sprintf (buf, relro_format, str2, n1, str2, n2);
+}
+
+int
+sprintf_writable_malloc (int *n1, int *n2)
+{
+  char buf[128];
+  char *buf2_malloc = strdup (writeable_format);
+  if (buf2_malloc == NULL)
+    abort ();
+  return sprintf (buf, buf2_malloc, str2, n1, str2, n2);
+}
diff --git a/debug/tst-sprintf-fortify-rdonly.c b/debug/tst-sprintf-fortify-rdonly.c
index ba5a791020..fafc8340ea 100644
--- a/debug/tst-sprintf-fortify-rdonly.c
+++ b/debug/tst-sprintf-fortify-rdonly.c
@@ -27,16 +27,64 @@ 
 #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 sigjmp_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 writeable_format[10] = "%s";
+static char relro_format[10] __attribute__ ((section (".data.rel.ro"))) =
+ "%s%n%s%n";
+
+extern void init_writable (void);
+extern int sprintf_writable (int *, int *);
+extern int sprintf_relro (int *, int *);
+extern int sprintf_writable_malloc (int *, int *);
+
+#define str(__x) # __x
+void (*init_writable_dlopen)(void);
+int (*sprintf_writable_dlopen)(int *, int *);
+int (*sprintf_rdonly_dlopen)(int *, int *);
+int (*sprintf_writable_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_writable_dlopen = xdlsym (h, str(init_writable));
+    sprintf_writable_dlopen = xdlsym (h, str(sprintf_writable));
+    sprintf_rdonly_dlopen = xdlsym (h, str(sprintf_relro));
+    sprintf_writable_malloc_dlopen = xdlsym (h, str(sprintf_writable_malloc));
+  }
+
   struct rlimit rl;
   int max_fd = 24;
 
@@ -63,20 +111,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;
+  strcpy (writeable_format + 2, "%n%s%n");
+  init_writable ();
+  init_writable_dlopen ();
+
+  /* writeable_format 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, writeable_format, str2, &n1, str2, &n2);
+    CHK_FAIL_END
+  }
+
+  /* Same as before, but from an library.  */
+  {
+    int n1;
+    int n2;
+    CHK_FAIL_START
+    sprintf_writable (&n1, &n2);
+    CHK_FAIL_END
+  }
+
+  {
+    int n1;
+    int n2;
+    CHK_FAIL_START
+    sprintf_writable_dlopen (&n1, &n2);
+    CHK_FAIL_END
+  }
+
+  /* relro_format 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, relro_format, 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_relro (&n1, &n2) != 2 || n1 != 1 || n2 != 2)
+      FAIL_EXIT1 ("sprintf failed: %d %d", n1, n2);
+  }
+
+  {
+    int n1;
+    int n2;
+    if (sprintf_rdonly_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 (writeable_format);
+    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_writable_malloc (&n1, &n2);
+    CHK_FAIL_END
+  }
+
+  {
+    int n1;
+    int n2;
+    CHK_FAIL_START
+    sprintf_writable_malloc_dlopen (&n1, &n2);
+    CHK_FAIL_END
+  }
+
+  return ret;
 }
 
 #include <support/test-driver.c>
diff --git a/elf/Makefile b/elf/Makefile
index 2bce1ed486..8203048a88 100644
--- a/elf/Makefile
+++ b/elf/Makefile
@@ -72,6 +72,7 @@  dl-routines = \
   dl-open \
   dl-origin \
   dl-printf \
+  dl-readonly-area \
   dl-reloc \
   dl-runtime \
   dl-scope \
diff --git a/elf/dl-readonly-area.c b/elf/dl-readonly-area.c
new file mode 100644
index 0000000000..22769ec9d9
--- /dev/null
+++ b/elf/dl-readonly-area.c
@@ -0,0 +1,86 @@ 
+/* 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)
+{
+  if (l->l_relro_addr != 0)
+    {
+      uintptr_t relro_start = ALIGN_DOWN (l->l_addr + l->l_relro_addr,
+					  GLRO(dl_pagesize));
+      uintptr_t relro_end = ALIGN_DOWN (l->l_addr + l->l_relro_addr
+					+ l->l_relro_size,
+					GLRO(dl_pagesize));
+      /* RELRO is caved out from a RW segment, so the next range is either
+	 RW or nonexistent.  */
+      return relro_start <= start && end <= relro_end
+	? dl_readonly_area_rdonly : dl_readonly_area_writable;
+
+    }
+  return dl_readonly_area_writable;
+}
+
+enum dl_readonly_area_error_type
+_dl_readonly_area (const void *ptr, size_t size)
+{
+  struct dl_find_object dlfo;
+  if (_dl_find_object ((void *)ptr, &dlfo) != 0)
+    return dl_readonly_area_not_found;
+
+  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 dl_readonly_area_writable;
+
+	    if (from <= ptr_start && to >= ptr_end)
+	      return dl_readonly_area_rdonly;
+	    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 ? dl_readonly_area_rdonly : dl_readonly_area_not_found;
+}
diff --git a/elf/rtld.c b/elf/rtld.c
index 00b25c1a73..099c447e80 100644
--- a/elf/rtld.c
+++ b/elf/rtld.c
@@ -371,6 +371,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
diff --git a/include/stdlib.h b/include/stdlib.h
index 57f4ab8545..b7147ba590 100644
--- a/include/stdlib.h
+++ b/include/stdlib.h
@@ -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_fallback (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;
diff --git a/stdio-common/vfprintf-internal.c b/stdio-common/vfprintf-internal.c
index aa9708bff5..fa41e1b242 100644
--- a/stdio-common/vfprintf-internal.c
+++ b/stdio-common/vfprintf-internal.c
@@ -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,
diff --git a/stdio-common/vfprintf-process-arg.c b/stdio-common/vfprintf-process-arg.c
index 8d20493766..ad1e0ea3ff 100644
--- a/stdio-common/vfprintf-process-arg.c
+++ b/stdio-common/vfprintf-process-arg.c
@@ -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 segments 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 ();
diff --git a/sysdeps/generic/ldsodefs.h b/sysdeps/generic/ldsodefs.h
index 19494b82ee..5b12a41a18 100644
--- a/sysdeps/generic/ldsodefs.h
+++ b/sysdeps/generic/ldsodefs.h
@@ -276,6 +276,12 @@  struct audit_ifaces
   struct audit_ifaces *next;
 };
 
+enum dl_readonly_area_error_type
+{
+  dl_readonly_area_rdonly,
+  dl_readonly_area_writable,
+  dl_readonly_area_not_found,
+};
 
 /* Test whether given NAME matches any of the names of the given object.  */
 extern int _dl_name_match_p (const char *__name, const struct link_map *__map)
@@ -676,6 +682,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.  */
+  enum dl_readonly_area_error_type (*_dl_readonly_area) (const void *, size_t);
+
   /* Dynamic linker operations used after static dlopen.  */
   const struct dlfcn_hook *_dl_dlfcn_hook;
 
@@ -1284,6 +1294,10 @@  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 enum dl_readonly_area_error_type _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;
diff --git a/sysdeps/mach/readonly-area.c b/sysdeps/mach/readonly-area-fallback.c
similarity index 90%
rename from sysdeps/mach/readonly-area.c
rename to sysdeps/mach/readonly-area-fallback.c
index fb89413fe6..b1bb1cba39 100644
--- a/sysdeps/mach/readonly-area.c
+++ b/sysdeps/mach/readonly-area-fallback.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;
 }
diff --git a/sysdeps/unix/sysv/linux/readonly-area.c b/sysdeps/unix/sysv/linux/readonly-area-fallback.c
similarity index 84%
rename from sysdeps/unix/sysv/linux/readonly-area.c
rename to sysdeps/unix/sysv/linux/readonly-area-fallback.c
index 62d2070c72..c93ad2a598 100644
--- a/sysdeps/unix/sysv/linux/readonly-area.c
+++ b/sysdeps/unix/sysv/linux/readonly-area-fallback.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_fallback (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;
 }