[PATCH v7 1/3] Add [v]aprintf(3)
Alejandro Colomar
alx@kernel.org
Wed Jun 3 20:37:47 GMT 2026
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.
> 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);
[...]
> > 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);
>
> > +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.
>
> > +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.)
> > + 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. */
[...]
> > 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?
> 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.
Have a lovely night!
Alex
--
<https://www.alejandro-colomar.es>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: signature.asc
Type: application/pgp-signature
Size: 833 bytes
Desc: not available
URL: <https://sourceware.org/pipermail/libc-alpha/attachments/20260603/13181fc9/attachment.sig>
More information about the Libc-alpha
mailing list