From patchwork Fri Mar 14 19:09:57 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Adhemerval Zanella X-Patchwork-Id: 873471 Delivered-To: patch@linaro.org Received: by 2002:a5d:4308:0:b0:38f:210b:807b with SMTP id h8csp879264wrq; Fri, 14 Mar 2025 12:13:11 -0700 (PDT) X-Forwarded-Encrypted: i=3; AJvYcCU4+9jmLYhQ95zmQ7KAB+/N8GfPINix30ceyxYC1fqfNdfCdMjLguViY5mq99Ryr18Ktv6Kdw==@linaro.org X-Google-Smtp-Source: AGHT+IG12457cETprkz9Unz/TRztlOI2y49BpOpuZd3otTPM8xHMPTfc2w1QyENMphOw9gL3UlMq X-Received: by 2002:a05:6102:2920:b0:4c3:6393:843f with SMTP id ada2fe7eead31-4c38311b9e0mr2606577137.5.1741979591686; Fri, 14 Mar 2025 12:13:11 -0700 (PDT) ARC-Seal: i=2; a=rsa-sha256; t=1741979591; cv=pass; d=google.com; s=arc-20240605; b=cpCOEKFbeEb0YkjgDUb4+ZaKj0DRa67+xI76DHRKTvsdnAb695wCZDgvOl6xlxuC+5 pydlZw9Zc7rC0xjSOhKvYwVNS87/VRcFnWVvbSnMcb2QMDv+5Rd3OcWTGvApL6AE8HJ1 fRLYagNS316dJWNeB8GBQ38KJiTJYNJnl7NjFmxOVdlnwJGs8m3bDHMbuqlfMzLiryOC /iNwoIo7aR7cSukR80yLJoGDJVTkwSR8offWXjTcyvCryJoWvgUTEGCGGLXzWayqAK3A OykmeXvCr6/+cCLGKIbiDFdGtCFYFQWOspt39SwXitlkBkTBV8beXuh/fCfmp9HWd8/h 5eOQ== ARC-Message-Signature: i=2; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605; h=errors-to:list-subscribe:list-help:list-post:list-archive :list-unsubscribe:list-id:precedence:content-transfer-encoding :mime-version:message-id:date:subject:cc:to:from:dkim-signature :dkim-filter:arc-filter:dmarc-filter:delivered-to:dkim-filter; bh=Zr1MRq1nCSSRAX9ipR2HLpqvE3n8D7E+MHfixsJw00k=; fh=atSnhVuc61BWQ+FX5+mHsLW0Q3PtWgdA9QXyQdAf8FI=; b=L37VwW7+OxLIJCceOfT1jcearH59trDOKUhjp3azbDJYhG5JuI63WBIk9s3idKL5tx vQq6Wtr4mJELOho7rboUC/XGaas+DE0JJ1/llyT6z0gjPHedHMW4Ae7IgoFc6liJebfp aBaEWc3h/8BXyp9ZNGNFDwvuQBWPzjO15reQqI62M7GLUdMjaYZHPWaleEGt3qr7k4sj ImLoHdSIfx7r8eqcM9dnJG1CnM/jzRmrpDeLWliz/pqt0CudJwe5FYxrzUQcRWOxlyMA NSndbYCPFwG4FsZhBMevdvu7ReMzQoFjJnZYLB9Ehqq1/w1zIi9D4E1bVuD18DAa7rk2 NtDA==; dara=google.com ARC-Authentication-Results: i=2; mx.google.com; dkim=pass header.i=@linaro.org header.s=google header.b=bTF9v8+D; arc=pass (i=1); spf=pass (google.com: domain of libc-alpha-bounces~patch=linaro.org@sourceware.org designates 2620:52:3:1:0:246e:9693:128c as permitted sender) smtp.mailfrom="libc-alpha-bounces~patch=linaro.org@sourceware.org"; dmarc=pass (p=NONE sp=NONE dis=NONE) header.from=linaro.org Return-Path: Received: from server2.sourceware.org (server2.sourceware.org. [2620:52:3:1:0:246e:9693:128c]) by mx.google.com with ESMTPS id ada2fe7eead31-4c374b0191dsi1322406137.488.2025.03.14.12.13.11 for (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Fri, 14 Mar 2025 12:13:11 -0700 (PDT) Received-SPF: pass (google.com: domain of libc-alpha-bounces~patch=linaro.org@sourceware.org designates 2620:52:3:1:0:246e:9693:128c as permitted sender) client-ip=2620:52:3:1:0:246e:9693:128c; Authentication-Results: mx.google.com; dkim=pass header.i=@linaro.org header.s=google header.b=bTF9v8+D; arc=pass (i=1); spf=pass (google.com: domain of libc-alpha-bounces~patch=linaro.org@sourceware.org designates 2620:52:3:1:0:246e:9693:128c as permitted sender) smtp.mailfrom="libc-alpha-bounces~patch=linaro.org@sourceware.org"; dmarc=pass (p=NONE sp=NONE dis=NONE) header.from=linaro.org Received: from server2.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id 2D1BC3857341 for ; Fri, 14 Mar 2025 19:13:11 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 2D1BC3857341 Authentication-Results: sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=linaro.org header.i=@linaro.org header.a=rsa-sha256 header.s=google header.b=bTF9v8+D X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from mail-pl1-x631.google.com (mail-pl1-x631.google.com [IPv6:2607:f8b0:4864:20::631]) by sourceware.org (Postfix) with ESMTPS id 6077A3857BA0 for ; Fri, 14 Mar 2025 19:12:46 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org 6077A3857BA0 Authentication-Results: sourceware.org; dmarc=pass (p=none dis=none) header.from=linaro.org Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=linaro.org ARC-Filter: OpenARC Filter v1.0.0 sourceware.org 6077A3857BA0 Authentication-Results: server2.sourceware.org; arc=none smtp.remote-ip=2607:f8b0:4864:20::631 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1741979566; cv=none; b=iLtXlA1DPCIY2bAXBSiTxFWId2bWkHjya+fD6pCeveAJBWQyOa+R+jEuId5t59IZ1vomyCuD33+jJQXYXU312fC0bVzNciICCHM+BjtJH16hU2B7wY0+bh1+LU3ifN8zpRvOIzqkCzfawCxXzi0K7p+/VWPt8pp2YizKzRgmY5o= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1741979566; c=relaxed/simple; bh=a4SWvwHmZ0zxgnb+peYz+uD8eJlUcFYT2Oj1/5Zw0/I=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=cTl2Tohdmbl7nXS3kWD+vTpUp3syPUXE6Xexdo6A04zW1s6v4iTV/dgXzEoPpNVh6DvHtQrjsJR8GZoZOR00DldexSzrWAWsvmzdA4rVmf4z2vUPGRCfZ790puxu3NGcakAFE+lv+eXnh31fjPoxf4R37L1FVWHhlmOoiiXInf0= ARC-Authentication-Results: i=1; server2.sourceware.org DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 6077A3857BA0 Received: by mail-pl1-x631.google.com with SMTP id d9443c01a7336-22548a28d0cso71532495ad.3 for ; Fri, 14 Mar 2025 12:12:46 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linaro.org; s=google; t=1741979565; x=1742584365; darn=sourceware.org; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:from:to:cc:subject:date:message-id:reply-to; bh=Zr1MRq1nCSSRAX9ipR2HLpqvE3n8D7E+MHfixsJw00k=; b=bTF9v8+DpLA7m6SOxr6b3+h0LfjVo4aB+ZhKcxkinbOVP4Oacre5pZUdiqK+8eFRRb oMM1cZwvldiOJx7C7/CieFYK9A2a91ojGM43XWPGLnjIUhLQmrdpr8lNJjWU8EV35AOu 0iz6IItc4ROTPZqE75UIeQbOTL79lfVjN2CY1CwHfiPrWALHsEOChl1qowIkYnRWwwyF ofs8j3uUtoIKngYpzP+fOky83tRpFclz2ZHWCZ2A5zPdONtNAoHkokI996HODgPldsAc mV0nvvEJr8J5gwxCBatnR7SzfJN+13xhFBNQSL8haz4RoZbC3P18glbTG6L29Ls4U5wM kaug== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1741979565; x=1742584365; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:x-gm-message-state:from:to:cc:subject:date:message-id :reply-to; bh=Zr1MRq1nCSSRAX9ipR2HLpqvE3n8D7E+MHfixsJw00k=; b=qZs9z88R1yX0GZJ+QJnH2AJInPI4ux5fhoFs4DZp5ey3Y2akqc/Kgn4Q9B6knAgFMV r20fJjCW6Gi3TRq8kiyMKs8JZ/kVjAwU+uZJkKR03wbDawmu5azcjxafomWebhGikw1E bEfZBvc55Q33rHF4N/hpAs69w7MYAWe4ZUNSjTmMYIj41qTB+RulHb5SaoeGfBxbr9az B/3sG9Sa4cZDbv3nfgpkEhWI9T4QO0HqMsnwJdmzw0dsyEXD9zILFiOXiRyGlXNtQWNq rtau5gA/zhgGsmtFkxfYXmEvt0105Ilq9kQ6WMExyPgIMgDI+Q7eUW+0FZ99Hcn9OoT3 WyeQ== X-Gm-Message-State: AOJu0Yw0GPtBLf1FXjSRbX6ycV1ztOzRHMRgchZ2V6j5Lf76lNOm6q4k CtG6echCzSQmH6UTmyiF1ihrKxfyNp9xB13lRPFP8AruAj3BKNWFdLILaqXlHZZMqpJmuLk7XQH 1 X-Gm-Gg: ASbGncvl50C4RF6tR8YyzolSOn1SSqOMIIPFK8SqxXWDoaahdAuBw58MYWgwEWzeZ65 rvRXcq+ItL5Re+68HYjahV1Y0TNjfpa1nK0/8Mrmns5fHbRZEiTl4TUWHNbRJBq5Rlnh6Kc4pG3 LS4k15dfqzFLye+UNNtl40s6mI6LxXWBFVBg09QB3Fw4YsabyQfz+EBGdiFc1syXOq0bpUXXaJn VcKrpcBdV5XBdq+e8KzrJwX8Qzr5C4UXscj7mvmcIfoLtDY77uzuBFRFtXTaCw5CKqBzQzNe3oR EbpHTaNcokUaIZE9E+vLsAnlXgs8YfqZ7uheWZx5h+Ywn89zZhvNs2/L X-Received: by 2002:a17:902:eccf:b0:224:f12:3734 with SMTP id d9443c01a7336-225e0a7b300mr50695765ad.30.1741979564795; Fri, 14 Mar 2025 12:12:44 -0700 (PDT) Received: from mandiga.. ([2804:1b3:a7c1:1ebf:e84d:1d9d:94eb:181]) by smtp.gmail.com with ESMTPSA id d9443c01a7336-225c6ba6d69sm31970185ad.158.2025.03.14.12.12.43 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Fri, 14 Mar 2025 12:12:44 -0700 (PDT) From: Adhemerval Zanella To: libc-alpha@sourceware.org Cc: =?utf-8?q?Volker_Wei=C3=9Fmann?= , Arjun Shankar Subject: [PATCH v2] debug: Improve '%n' fortify detection (BZ 30932) Date: Fri, 14 Mar 2025 16:09:57 -0300 Message-ID: <20250314191240.1339994-1-adhemerval.zanella@linaro.org> X-Mailer: git-send-email 2.43.0 MIME-Version: 1.0 X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libc-alpha-bounces~patch=linaro.org@sourceware.org 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%) 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 @@ . */ #include +#include -/* 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 + . */ + +#include +#include +#include + +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 #include #include +#include -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 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 + . */ + +#include + +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 #include -/* 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 #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; }