[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