[PATCH v7 1/3] Add [v]aprintf(3)

Adhemerval Zanella Netto adhemerval.zanella@linaro.org
Fri Jun 5 18:42:30 GMT 2026



On 03/06/26 17:37, Alejandro Colomar wrote:
> Hi Adhemerval,
> 
> On 2026-06-03T13:45:44-0300, Adhemerval Zanella Netto wrote:
>>> -  We don't need to fail with EOVERFLOW.  This initial implementation
>>>    still fails with EOVERFLOW, because it would require a lot of work
>>>    implementing it in a way that doesn't have such a failure point, but
>>>    the API has no inherent reasons to fail with EOVERFLOW.
>>
>> I think it should be disable by either extending __vasprintf_internal 
>> (with a flag that returns -1/EOVERFLOW to use on the current interface or 
>> functions where it should return int) or adding a new 
>> __vasprintf_internal_ex that:
>>
>> 1. Calls a new function simila to __printf_buffer_done (include/printf_buffer.h:195) 
>>    that return ptrdiff_t (or int64_).
>>
>> 2. Calls a new __printf_buffer_flush_asprintf (libio/vasprintf.c) that does 
>>    not bails early with EOVERFLOW once current_pos >= INT_MAX.
>>
>> But I do not think this is a hard requirement, nor a blocker for this patch 
>> (I am not sure if Florian agrees, so I would check with him).
> 
> He recently said he wants to block it for this, IIUC.  I'll work on
> a patch that applies on top of this one, after this one looks good to
> you.

Indeed, it seems Florian does want to get this fixed [1].

[1] https://inbox.sourceware.org/libc-alpha/87h5nmo5cz.fsf@oldenburg.str.redhat.com/

> 
>> But at the same 
>> time I think it would be worth exploring whether we can avoid this limitation.
>>
>> For the overflow case, I think we should properly document this in the manual
>> entry.
> 
> [...]
> 
>> Let's try to disintagle this patch.
> 
> Thanks for the review!
> 
>>  I saw a build failure for alpha,
>> powerpc (ppc64, ppc64le, ppc32), s390x, and sparcv9:
>>
>> $ alpha-glibc-linux-gnu-gcc ../sysdeps/ieee754/ldbl-opt/nldbl-compat.c [...]
>> ../sysdeps/ieee754/ldbl-opt/nldbl-compat.c:60:1: error: conflicting types for ‘__nldbl___aprintf’; have ‘int(const char *, ...)’
>>    60 | __nldbl___aprintf (const char *fmt, ...)
>>       | ^~~~~~~~~~~~~~~~~
> [...]
>>
>> The __nldbl___aprintf has the wrong return type — int instead of char *.
> 
> Ok.
> 
> 	diff --git c/sysdeps/ieee754/ldbl-opt/nldbl-compat.c i/sysdeps/ieee754/ldbl-opt/nldbl-compat.c
> 	index f5c69f7ff4d5..bfa0514b18f1 100644
> 	--- c/sysdeps/ieee754/ldbl-opt/nldbl-compat.c
> 	+++ i/sysdeps/ieee754/ldbl-opt/nldbl-compat.c
> 	@@ -55,7 +55,7 @@ libc_hidden_proto (__nldbl___isoc23_vfwscanf)
> 	    we don't need to split this into one file per function for the
> 	    sake of statically linked programs.  */
> 	 
> 	-int
> 	+char *
> 	 attribute_compat_text_section
> 	 __nldbl___aprintf (const char *fmt, ...)
> 	 {
> 
> [...]
>>> diff --git a/debug/aprintf_chk.c b/debug/aprintf_chk.c
>>> new file mode 100644
>>> index 000000000000..30f7811f20da
>>> --- /dev/null
>>> +++ b/debug/aprintf_chk.c
>>> @@ -0,0 +1,51 @@
> [...]
>>> +/* Write formatted output from FORMAT to a string allocated with malloc.  */
>>> +char *
>>> +___aprintf_chk (int flag, const char *fmt, ...)
>>> +{
>>> +  /* For flag > 0 (i.e. __USE_FORTIFY_LEVEL > 1) request that %n
>>> +     can only come from read-only format strings.  */
>>> +  unsigned int mode = (flag > 0) ? PRINTF_FORTIFY : 0;
>>> +  va_list ap;
>>> +  char *p;
>>> +
>>> +  va_start (ap, fmt);
>>> +  if (__vasprintf_internal (&p, fmt, ap, mode) < 0)
>>> +    p = NULL;
>>> +  va_end (ap);
>>> +
>>> +  return p;
>>> +}
>>
>> Adds some tests on debug/tst-fortify.c.
> 
> Ok.  Does this look good to you?
> 
> 	diff --git c/debug/tst-fortify.c i/debug/tst-fortify.c
> 	index ee83fca22467..2bc3e5eb1c47 100644
> 	--- c/debug/tst-fortify.c
> 	+++ i/debug/tst-fortify.c
> 	@@ -1026,34 +1026,48 @@ do_test (void)
> 	   strcpy (buf2 + 2, "%n%s%n");
> 	   /* When the format string is writable and contains %n,
> 	      with -D_FORTIFY_SOURCE=2 it causes __chk_fail.  */
> 	   CHK_FAIL2_START
> 	   if (asprintf (&my_ptr, buf2, str4, &n1, str5, &n1) != 14)
> 	     FAIL ();
> 	   else
> 	     free (my_ptr);
> 	   CHK_FAIL2_END
> 	 
> 	+  CHK_FAIL2_START
> 	+  my_ptr = aprintf (buf2, str4, &n1, str5, &n1);
> 	+  if (my_ptr == NULL)
> 	+    FAIL ();
> 	+  else
> 	+    free (my_ptr);
> 	+  CHK_FAIL2_END
> 	+
> 	   struct obstack obs;
> 	   obstack_init (&obs);
> 	   CHK_FAIL2_START
> 	   if (obstack_printf (&obs, buf2, str4, &n1, str5, &n1) != 14)
> 	     FAIL ();
> 	   CHK_FAIL2_END
> 	   obstack_free (&obs, NULL);
> 	 
> 	   my_ptr = NULL;
> 	   if (asprintf (&my_ptr, "%s%n%s%n", str4, &n1, str5, &n1) != 14)
> 	     FAIL ();
> 	   else
> 	     free (my_ptr);
> 	 
> 	+  my_ptr = aprintf ("%s%n%s%n", str4, &n1, str5, &n1);
> 	+  if (my_ptr == NULL)
> 	+    FAIL ();
> 	+  else
> 	+    free (my_ptr);
> 	+
> 	   obstack_init (&obs);
> 	   if (obstack_printf (&obs, "%s%n%s%n", str4, &n1, str5, &n1) != 14)
> 	     FAIL ();
> 	   obstack_free (&obs, NULL);
> 	 #endif
> 	 
> 	   if (freopen (temp_filename, "r", stdin) == NULL)
> 	     {
> 	       puts ("could not open temporary file");
> 	       exit (1);
> 
> [...]

It should suffice.

>>> diff --git a/libio/stdio.h b/libio/stdio.h
>>> index 3bf6a1f63203..81d7176c4535 100644
>>> --- a/libio/stdio.h
>>> +++ b/libio/stdio.h
>>> @@ -412,6 +412,19 @@ extern int asprintf (char **__restrict __ptr,
>>>       __THROWNL __attribute__ ((__format__ (__printf__, 2, 3))) __wur;
>>>  #endif
>>>  
>>> +#ifdef __USE_GNU
>>> +/* Write formatted output to a string dynamically allocated with `malloc'.  */
>>> +extern char *vaprintf (const char *__restrict __fmt, __gnuc_va_list __ap)
>>> +     __THROWNL __attribute__ ((__format__ (__printf__, 1, 0)))
>>> +     __attribute_malloc__;
>>
>> I think we should add __attr_dealloc_free, that one of the main advertises for
>> this symbol. Smae for asprintf and the __chk variants.
> 
> Agree.  I probably looked at strdup(3) for imitating it, and it seems it
> is lacking it.  Should we also add it to strdup(3) in string/string.h?
> 
> Anyway, here's a diff for aprintf(3):
> 
> 	diff --git i/include/stdio.h w/include/stdio.h
> 	index cfdec3681d51..721101a3ad04 100644
> 	--- i/include/stdio.h
> 	+++ w/include/stdio.h
> 	@@ -78,9 +78,9 @@ stdio_hidden_ldbl_proto (__, vfprintf_chk)
> 	 extern char *__fgets_unlocked_chk (char *buf, size_t size, int n, FILE *fp);
> 	 extern char *__fgets_chk (char *buf, size_t size, int n, FILE *fp);
> 	 extern char *__aprintf_chk (int, const char *, ...)
> 	-     __THROW __attribute_malloc__;
> 	+     __THROW __attribute_malloc__ __attr_dealloc_free;
> 	 extern char *__vaprintf_chk (int, const char *, __gnuc_va_list)
> 	-     __THROW __attribute_malloc__;
> 	+     __THROW __attribute_malloc__ __attr_dealloc_free;
> 	 stdio_hidden_ldbl_proto (__, vaprintf_chk)
> 	 extern int __asprintf_chk (char **, int, const char *, ...) __THROW;
> 	 extern int __vasprintf_chk (char **, int, const char *, __gnuc_va_list) __THROW;
> 	diff --git i/libio/bits/stdio2-decl.h w/libio/bits/stdio2-decl.h
> 	index cf52c46c31f6..d8d99fa1a9f3 100644
> 	--- i/libio/bits/stdio2-decl.h
> 	+++ w/libio/bits/stdio2-decl.h
> 	@@ -69,11 +69,11 @@ extern int __vdprintf_chk (int __fd, int __flag,
> 	 extern char *__aprintf_chk (int __flag,
> 				   const char *__restrict __fmt, ...)
> 	      __THROW __attribute__ ((__format__ (__printf__, 2, 3)))
> 	-     __attribute_malloc__;
> 	+     __attribute_malloc__ __attr_dealloc_free;
> 	 extern char *__vaprintf_chk (int __flag,
> 				    const char *__restrict __fmt, __gnuc_va_list __ap)
> 	      __THROW __attribute__ ((__format__ (__printf__, 2, 0)))
> 	-     __attribute_malloc__;
> 	+     __attribute_malloc__ __attr_dealloc_free;
> 	 extern int __asprintf_chk (char **__restrict __ptr, int __flag,
> 				   const char *__restrict __fmt, ...)
> 	      __THROW __attribute__ ((__format__ (__printf__, 3, 4))) __wur;
> 	diff --git i/libio/stdio.h w/libio/stdio.h
> 	index 81d7176c4535..8aa583eff8d5 100644
> 	--- i/libio/stdio.h
> 	+++ w/libio/stdio.h
> 	@@ -416,13 +416,13 @@ extern int asprintf (char **__restrict __ptr,
> 	 /* Write formatted output to a string dynamically allocated with `malloc'.  */
> 	 extern char *vaprintf (const char *__restrict __fmt, __gnuc_va_list __ap)
> 	      __THROWNL __attribute__ ((__format__ (__printf__, 1, 0)))
> 	-     __attribute_malloc__;
> 	+     __attribute_malloc__ __attr_dealloc_free;
> 	 extern char *__aprintf (const char *__restrict __fmt, ...)
> 	      __THROWNL __attribute__ ((__format__ (__printf__, 1, 2)))
> 	-     __attribute_malloc__;
> 	+     __attribute_malloc__ __attr_dealloc_free;
> 	 extern char *aprintf (const char *__restrict __fmt, ...)
> 	      __THROWNL __attribute__ ((__format__ (__printf__, 1, 2)))
> 	-     __attribute_malloc__;
> 	+     __attribute_malloc__ __attr_dealloc_free;
> 	 #endif
> 	 
> 	 #ifdef __USE_XOPEN2K8
> 	diff --git i/sysdeps/ieee754/ldbl-opt/nldbl-compat.h w/sysdeps/ieee754/ldbl-opt/nldbl-compat.h
> 	index 9da1c2d241b4..3a3c7baf0e21 100644
> 	--- i/sysdeps/ieee754/ldbl-opt/nldbl-compat.h
> 	+++ w/sysdeps/ieee754/ldbl-opt/nldbl-compat.h
> 	@@ -130,7 +130,7 @@ extern int __nldbl___vswprintf_chk (wchar_t *__restrict, size_t, int, size_t,
> 					    const wchar_t *__restrict, __gnuc_va_list)
> 	   __THROW;
> 	 extern char *__nldbl___vaprintf_chk (int, const char *, __gnuc_va_list)
> 	-  __THROW __attribute_malloc__;
> 	+  __THROW __attribute_malloc__ __attr_dealloc_free;
> 	 extern int __nldbl___vasprintf_chk (char **, int, const char *, __gnuc_va_list)
> 	   __THROW;
> 	 extern int __nldbl___vdprintf_chk (int, int, const char *, __gnuc_va_list);
> 

Looks ok.

>>
>>> +extern char *__aprintf (const char *__restrict __fmt, ...)
>>> +     __THROWNL __attribute__ ((__format__ (__printf__, 1, 2)))
>>> +     __attribute_malloc__;
>>
>> I think there is no need to export the __aprintf for a new symbol, we did it
>> on other symbols (like scanf) we can asm alias depending of the C standard
>> being used. Since this is a new symbol, we can just use the aprintf.
> 
> Can we use aprintf(3) internally within glibc?  Users might have
> defined aprintf() themselves, since it's non-standard.

Yes, but in this case there is also no need to export it on the installed headers.
We can do in on the internal one instead (at include/) and use a libc_hidden_{proto,def}
to avoid PLTs.

But I would only add it if/when we actually use it.

> 
>>
>>> +extern char *aprintf (const char *__restrict __fmt, ...)
>>> +     __THROWNL __attribute__ ((__format__ (__printf__, 1, 2)))
>>> +     __attribute_malloc__;
>>> +#endif
>>> +
>>>  #ifdef __USE_XOPEN2K8
>>>  /* Write formatted output to a file descriptor.  */
>>>  extern int vdprintf (int __fd, const char *__restrict __fmt,
>>> diff --git a/libio/tst-aprintf.c b/libio/tst-aprintf.c
>>> new file mode 100644
>>> index 000000000000..0c7d7a8024c3
>>> --- /dev/null
>>> +++ b/libio/tst-aprintf.c
>>> @@ -0,0 +1,73 @@
>>> +/* Test aprintf.
>>> +   Copyright (C) 2026 Free Software Foundation, Inc.
>>> +   This file is part of the GNU C Library.
>>> +
>>> +   The GNU C Library is free software; you can redistribute it and/or
>>> +   modify it under the terms of the GNU Lesser General Public
>>> +   License as published by the Free Software Foundation; either
>>> +   version 2.1 of the License, or (at your option) any later version.
>>> +
>>> +   The GNU C Library is distributed in the hope that it will be useful,
>>> +   but WITHOUT ANY WARRANTY; without even the implied warranty of
>>> +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
>>> +   Lesser General Public License for more details.
>>> +
>>> +   You should have received a copy of the GNU Lesser General Public
>>> +   License along with the GNU C Library; if not, see
>>> +   <https://www.gnu.org/licenses/>.  */
>>> +
>>> +#include <errno.h>
>>> +#include <stdlib.h>
>>> +#include <stdio.h>
>>> +#include <support/check.h>
>>> +#include <sys/resource.h>
>>> +#include <libc-diag.h>
>>> +
>>> +static int
>>> +do_test (void)
>>> +{
>>> +  char *buf;
>>> +
>>> +  /* Success */
>>> +  buf = aprintf ("foo %d", 42);
>>> +  TEST_COMPARE_STRING (buf, "foo 42");
>>> +  free(buf);
>>> +
>>> +  {
>>> +    /* -Wformat-overflow warns that the format would produce more than
>>> +       INT_MAX bytes; however, this function does not share this
>>> +       inherent limitation of other printf-like functions.  The current
>>> +       implementation fails with EOVERFLOW, because it's a trivial
>>> +       wrapper; however, in the future we should improve the function so
>>> +       that this doesn't fail anymore with EOVERFLOW.  When that is
>>> +       implemented, this test will stop failing.  */
>>> +    DIAG_PUSH_NEEDS_COMMENT;
>>> +    DIAG_IGNORE_NEEDS_COMMENT (8, "-Wformat-overflow=");
>>> +    buf = aprintf ("%2000000000d %2000000000d", 1, 2);
>>
>> Does it work on ILP32 ABIs (x32 and mips64-n32)?
> 
> It would probably ENOMEM instead of EOVERFLOW, I suspect.
> 
> I've done
> 
> 	diff --git i/libio/tst-aprintf.c w/libio/tst-aprintf.c
> 	index 0c7d7a8024c3..1db26505ee03 100644
> 	--- i/libio/tst-aprintf.c
> 	+++ w/libio/tst-aprintf.c
> 	@@ -53,7 +53,8 @@ do_test (void)
> 		that happens, this test will fail.  Just remove it and enable the
> 		test under '#if 0'.  */
> 	     TEST_VERIFY (buf == NULL);
> 	-    TEST_VERIFY (errno == EOVERFLOW);
> 	+    if (errno != ENOMEM)
> 	+      TEST_VERIFY (errno == EOVERFLOW);
> 	 #endif
> 	   }
> 
> (as is done is the asprintf(3) tests.)
> 

Ok.

>>> +    DIAG_POP_NEEDS_COMMENT;
>>> +#if 0
>>> +    if (buf == NULL)
>>> +      TEST_VERIFY (errno == ENOMEM);
>>> +#else
>>
>> Plase remove the '#if 0' path here.
> 
> Hmmm, okay.
> 
>>> +    /* We should eventually not fail with EOVERFLOW from aprintf.  When
>>> +       that happens, this test will fail.  Just remove it and enable the
>>> +       test under '#if 0'.  */
>>> +    TEST_VERIFY (buf == NULL);
>>> +    TEST_VERIFY (errno == EOVERFLOW);
>>> +#endif
> 
> [...]
>>> diff --git a/stdio-common/aprintf.c b/stdio-common/aprintf.c
>>> new file mode 100644
>>> index 000000000000..bee4966707ae
>>> --- /dev/null
>>> +++ b/stdio-common/aprintf.c
>>> @@ -0,0 +1,40 @@
>>> +/* Copyright (C) 2026 Free Software Foundation, Inc.
>>> +   This file is part of the GNU C Library.
>>> +
>>> +   The GNU C Library is free software; you can redistribute it and/or
>>> +   modify it under the terms of the GNU Lesser General Public
>>> +   License as published by the Free Software Foundation; either
>>> +   version 2.1 of the License, or (at your option) any later version.
>>> +
>>> +   The GNU C Library is distributed in the hope that it will be useful,
>>> +   but WITHOUT ANY WARRANTY; without even the implied warranty of
>>> +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
>>> +   Lesser General Public License for more details.
>>> +
>>> +   You should have received a copy of the GNU Lesser General Public
>>> +   License along with the GNU C Library; if not, see
>>> +   <https://www.gnu.org/licenses/>.  */
>>
>> Should we add a LGPL linking-exception clause, as other printf symbols?
> 
> I copied this from stdio-common/asprintf.c, IIRC.  I don't see the
> exception in the other files:
> 
> 	$ find stdio-common/ -type f \
> 		| grep printf \
> 		| xargs grep -i exception;
> 	$
> 
> The only file within stdio-common/ where I can see the exception is this
> one:
> 
> 	$ find stdio-common/ -type f \
> 		| grep -v Makefile \
> 		| xargs grep -i exception;
> 	stdio-common/isoc99_vsscanf.c:   As a special exception, if you link the code in this file with
> 	stdio-common/isoc99_vsscanf.c:   the GNU Lesser General Public License.  This exception does not
> 	stdio-common/isoc99_vsscanf.c:   This exception applies to code released by its copyright holders
> 	stdio-common/isoc99_vsscanf.c:   in files containing the exception.  */
> 
> [...]

Right, I am not sure why there are added on isoc99_vsscanf.c in first
place.

>>> diff --git a/sysdeps/ieee754/ldbl-opt/nldbl-compat.c b/sysdeps/ieee754/ldbl-opt/nldbl-compat.c
>>> index e239cd5bc5c4..f5c69f7ff4d5 100644
>>> --- a/sysdeps/ieee754/ldbl-opt/nldbl-compat.c
>>> +++ b/sysdeps/ieee754/ldbl-opt/nldbl-compat.c
> [...]
>>> @@ -1342,3 +1396,8 @@ compat_symbol (libc, __nldbl___fprintf_chk, __fprintf_chk, GLIBC_2_3_4);
>>>  compat_symbol (libc, __nldbl___vprintf_chk, __vprintf_chk, GLIBC_2_3_4);
>>>  compat_symbol (libc, __nldbl___vfprintf_chk, __vfprintf_chk, GLIBC_2_3_4);
>>>  #endif
>>> +#if LONG_DOUBLE_COMPAT(libc, GLIBC_2_4_4)
>>> +compat_symbol (libc, __nldbl_aprintf, aprintf, GLIBC_2_4_4);
>>> +compat_symbol (libc, __nldbl_vaprintf, vaprintf, GLIBC_2_4_4);
>>> +compat_symbol (libc, __nldbl___aprintf, __aprintf, GLIBC_2_4_4);
>>> +#endif
>>
>> I think you meant *2_44* here. But this whole block should not be required,
>> aprintf is a new symbol, so it has no historic ldbl == double ABI to be 
>> compatible with. 
> 
> So, should I drop all changes to
> sysdeps/ieee754/ldbl-opt/nldbl-compat.c?

I think so.  There is no need for compat symbol, although there is the need
to handle ABI that defined double == long double (I recall this is done by
other file in sysdeps/ieee754/ldbl-opt).

> 
>> New symbols do not get a compat_symbol at an older version (and compat_symbol 
>> at the current dev version is illegal anyway). Check the __isoc23_* additions, 
>> which use libc_hidden_def + the normal versioned export.
> 
> Would you mind naming a few __isoc23_* names?  I don't know those
> functions.

For instance __isoc23_vfscanf.  It is only declared at the installed header if
there is no REDIRECT support (asm alias).  Since for __asprintf there is no need
for alias at this moment, there is no need to add such symbol.



More information about the Libc-alpha mailing list