From 141266ad2353f1a4ef37b73152407a53480d430d Mon Sep 17 00:00:00 2001 From: Edgar Costa Date: Mon, 27 Apr 2026 15:26:47 -0400 Subject: [PATCH 1/3] fix(generic_files): split flint_sprintf off from flint_vsnprintf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `flint_vsprintf` previously routed through `flint_vsnprintf` with a size of `INT_MAX`, but on 32-bit glibc `vsnprintf(dst, n, ...)` silently drops output past the first character once `n` exceeds about 16 MB. The result on i386 and armhf was that `flint_sprintf("x%wd", 1)` produced `"x"` instead of `"x1"`. This broke `fmpz_mpoly_set_str_pretty` (which builds variable names via `flint_sprintf("x%wd", i + 1)`): every variable became the literal string `"x"`, the parser then failed prefix matching, returned `-1`, and left the polynomial malformed. Tests that exercise the parser then saw a zero/garbage polynomial where they expected a deterministic input — the symptom reported in #2646: mpoly_test_irreducible FAIL: check 8 variable example fmpz_mpoly_compose_fmpz_mpoly Check non-example 1 nmod_mpoly_compose_nmod_mpoly Check non-example 1 Fix: give `flint_sprintf` its own sink in a new `io_vsprintf.c` that calls the system `vsprintf` directly. `sprintf` semantics already require the caller to provide a sufficiently large buffer, so no length bound is needed and the glibc edge case is avoided. Also add a regression test that catches the precise failure mode (`flint_sprintf("x%wd", n)` round-trip plus a few `WORD_MIN/MAX` cases and a mixed-`%w` format) and is registered in `src/test/main.c`. Verified by reverting only the `io_vsnprintf.c`/`io_vsprintf.c` changes: the new test reports `flint_sprintf("x%wd", 1) gave "x" expected "x1"` on i386, then passes once the fix is restored. Full `make check` passes on i386, amd64, and armhf (under qemu-arm-static). Closes #2646. --- src/generic_files/io_vsnprintf.c | 17 ---- src/generic_files/io_vsprintf.c | 136 +++++++++++++++++++++++++++++++ src/test/main.c | 1 + src/test/t-io.c | 52 ++++++++++++ 4 files changed, 189 insertions(+), 17 deletions(-) create mode 100644 src/generic_files/io_vsprintf.c diff --git a/src/generic_files/io_vsnprintf.c b/src/generic_files/io_vsnprintf.c index 8d849f1cf2..4f6c968e91 100644 --- a/src/generic_files/io_vsnprintf.c +++ b/src/generic_files/io_vsnprintf.c @@ -163,20 +163,3 @@ int flint_snprintf(char * s, size_t n, const char * str, ...) return ret; } - -int flint_vsprintf(char * s, const char * str, va_list vlist) -{ - return flint_vsnprintf(s, INT_MAX, str, vlist); -} - -int flint_sprintf(char * s, const char * str, ...) -{ - va_list vlist; - int ret; - - va_start(vlist, str); - ret = flint_vsprintf(s, str, vlist); - va_end(vlist); - - return ret; -} diff --git a/src/generic_files/io_vsprintf.c b/src/generic_files/io_vsprintf.c new file mode 100644 index 0000000000..cb4051f126 --- /dev/null +++ b/src/generic_files/io_vsprintf.c @@ -0,0 +1,136 @@ +/* + Copyright (C) 2026 Lars Göttgens + Copyright (C) 2026 Edgar Costa + + This file is part of FLINT. + + FLINT is free software: you can redistribute it and/or modify it under + the terms of the GNU Lesser General Public License (LGPL) as published + by the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. See . +*/ + +#include /* isdigit */ +#include /* intmax_t */ +#include +#include /* memcpy, strncmp and strchr */ +#include +#include /* wchar_t and wint_t */ +#include "nmod_types.h" +#include "fmpz.h" +#include "fmpz_mod.h" +#include "fmpq_types.h" +#include "fmpq.h" +#include "arf_types.h" +#include "arb.h" +#include "acb.h" +#include "gr.h" +#include "gr_vec.h" +#include "gr_poly.h" +#include "gr_ore_poly.h" +#include "gr_mat.h" +#include "gr_mat/impl.h" + +/* + Sink for flint_sprintf-style functions: writes into a caller-supplied + buffer with no length bound. Uses system vsprintf rather than vsnprintf + because some 32-bit glibc versions corrupt output when vsnprintf is + called with a very large size_t (issue #2646). +*/ +typedef struct +{ + char * buf; + size_t used; +} flint_vsprintf_out; + +static void flint_vsprintf_init(flint_vsprintf_out * out, char * buf) +{ + out->buf = buf; + out->used = 0; + out->buf[0] = '\0'; +} + +static int flint_vsprintf_vprintf(flint_vsprintf_out * out, const char * fmt, va_list ap) +{ + va_list ap_copy; + int res; + + va_copy(ap_copy, ap); + res = vsprintf(out->buf + out->used, fmt, ap_copy); + va_end(ap_copy); + + if (res > 0) + out->used += (size_t) res; + + return res; +} + +static int flint_vsprintf_printf(flint_vsprintf_out * out, const char * fmt, ...) +{ + va_list ap; + int res; + + va_start(ap, fmt); + res = flint_vsprintf_vprintf(out, fmt, ap); + va_end(ap); + + return res; +} + +static size_t flint_vsprintf_write(const void * buf, size_t len, flint_vsprintf_out * out) +{ + memcpy(out->buf + out->used, buf, len); + out->used += len; + out->buf[out->used] = '\0'; + return len; +} + +static int flint_vsprintf_putc(int ch, flint_vsprintf_out * out) +{ + out->buf[out->used] = (char) ch; + out->used++; + out->buf[out->used] = '\0'; + return (unsigned char) ch; +} + +#define FLINT_VPRINTF_FUNCTION flint_vsprintf +#define FLINT_VPRINTF_FUNCTION_ARGS char * s +#define FLINT_VPRINTF_OUT_T flint_vsprintf_out +#define FLINT_VPRINTF_INIT(out) flint_vsprintf_init((out), s) +#define FLINT_VPRINTF_PRINTF(out, ...) flint_vsprintf_printf((out), __VA_ARGS__) +#define FLINT_VPRINTF_VPRINTF(out, fmt, ap) flint_vsprintf_vprintf((out), (fmt), (ap)) +#define FLINT_VPRINTF_WRITE(buf, len, out) flint_vsprintf_write((buf), (len), (out)) +#define FLINT_VPRINTF_PUTC(ch, out) flint_vsprintf_putc((ch), (out)) +#define FLINT_VPRINTF_PUTC_ERRVAL (-1) +#define FLINT_VPRINTF_GR_STREAM_INIT(gr_out, out) gr_stream_init_str((gr_out)) +#define FLINT_VPRINTF_GR_STREAM_FLUSH(gr_out, out) \ + do { \ + FLINT_VPRINTF_WRITE((gr_out)->s, (gr_out)->len, (out)); \ + flint_free((gr_out)->s); \ + } while (0) + +#include "io_vprintf_impl.h" + +#undef FLINT_VPRINTF_FUNCTION +#undef FLINT_VPRINTF_FUNCTION_ARGS +#undef FLINT_VPRINTF_OUT_T +#undef FLINT_VPRINTF_INIT +#undef FLINT_VPRINTF_PRINTF +#undef FLINT_VPRINTF_VPRINTF +#undef FLINT_VPRINTF_WRITE +#undef FLINT_VPRINTF_PUTC +#undef FLINT_VPRINTF_PUTC_ERRVAL +#undef FLINT_VPRINTF_GR_STREAM_INIT +#undef FLINT_VPRINTF_GR_STREAM_FLUSH + +int flint_sprintf(char * s, const char * str, ...) +{ + va_list vlist; + int ret; + + va_start(vlist, str); + ret = flint_vsprintf(s, str, vlist); + va_end(vlist); + + return ret; +} diff --git a/src/test/main.c b/src/test/main.c index 4b7f480b7a..c293dfdd9a 100644 --- a/src/test/main.c +++ b/src/test/main.c @@ -36,6 +36,7 @@ test_struct tests[] = TEST_FUNCTION(flint_ctz), TEST_FUNCTION(flint_fprintf), TEST_FUNCTION(flint_snprintf), + TEST_FUNCTION(flint_sprintf), TEST_FUNCTION(flint_printf), TEST_FUNCTION(memory_manager), TEST_FUNCTION(sdiv_qrnnd), diff --git a/src/test/t-io.c b/src/test/t-io.c index a11dd0b3ba..2e50ff7f86 100644 --- a/src/test/t-io.c +++ b/src/test/t-io.c @@ -1435,6 +1435,58 @@ TEST_FUNCTION_START(flint_snprintf, state) TEST_FUNCTION_END(state); } +TEST_FUNCTION_START(flint_sprintf, state) +{ + /* Regression test for #2646: on 32-bit glibc, vsnprintf called with a + very large size_t (such as INT_MAX) silently drops output past the + first character, causing flint_sprintf("x%wd", n) to produce "x" + instead of "x". */ + char buf[64]; + int got; + slong i; + + for (i = 0; i < 10; i++) + { + char expected[16]; + memset(buf, 'Z', sizeof(buf)); + got = flint_sprintf(buf, "x%wd", i + 1); + snprintf(expected, sizeof(expected), "x%ld", (long) (i + 1)); + if (got < 0) + TEST_FUNCTION_FAIL( + "Negative return value from flint_sprintf for i=%wd.\n", i); + if (strcmp(buf, expected) != 0) + TEST_FUNCTION_FAIL( + "flint_sprintf(\"x%%wd\", %wd) gave \"%s\" expected \"%s\"\n", + i + 1, buf, expected); + } + + { + slong values[] = {0, 1, -1, 12345, -12345, WORD_MIN, WORD_MAX}; + size_t ix; + for (ix = 0; ix < sizeof(values) / sizeof(values[0]); ix++) + { + char expected[64]; + memset(buf, 'Z', sizeof(buf)); + got = flint_sprintf(buf, "[%wd]", values[ix]); + snprintf(expected, sizeof(expected), "[%ld]", (long) values[ix]); + if (got < 0 || strcmp(buf, expected) != 0) + TEST_FUNCTION_FAIL( + "flint_sprintf(\"[%%wd]\", %wd) gave \"%s\" expected \"%s\"\n", + values[ix], buf, expected); + } + } + + { + memset(buf, 'Z', sizeof(buf)); + got = flint_sprintf(buf, "a%wd b%wu c%wx", (slong) -7, (ulong) 42, (ulong) 0xab); + if (got < 0 || strcmp(buf, "a-7 b42 cab") != 0) + TEST_FUNCTION_FAIL( + "flint_sprintf with multiple %%w specifiers: got \"%s\"\n", buf); + } + + TEST_FUNCTION_END(state); +} + #if HAVE_UNISTD_H && FLINT_COVERAGE #include From 273acae4e8ebeb92d0aeb9ca793a52d3c9d4c789 Mon Sep 17 00:00:00 2001 From: Edgar Costa Date: Mon, 27 Apr 2026 16:33:42 -0400 Subject: [PATCH 2/3] test(io): cast slong to long long in flint_sprintf expected values Caught by the MinGW64 (LLP64) CI run on the previous commit (141266ad23): slong on Windows is `long long` (64-bit) but `long` is 32-bit, so the expected-value computation `snprintf(expected, ..., "%ld", (long) values[ix])` truncated WORD_MIN/WORD_MAX to 0 and the test reported a false failure "flint_sprintf(\"[%wd]\", -9223372036854775808) gave ... expected \"[0]\"". Cast to `long long` and use `%lld` instead, which fits slong on every supported platform (slong is `long` on LP64 and `long long` on LLP64). --- src/test/t-io.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/t-io.c b/src/test/t-io.c index 2e50ff7f86..3e7efc7f46 100644 --- a/src/test/t-io.c +++ b/src/test/t-io.c @@ -1450,7 +1450,7 @@ TEST_FUNCTION_START(flint_sprintf, state) char expected[16]; memset(buf, 'Z', sizeof(buf)); got = flint_sprintf(buf, "x%wd", i + 1); - snprintf(expected, sizeof(expected), "x%ld", (long) (i + 1)); + snprintf(expected, sizeof(expected), "x%lld", (long long) (i + 1)); if (got < 0) TEST_FUNCTION_FAIL( "Negative return value from flint_sprintf for i=%wd.\n", i); @@ -1468,7 +1468,7 @@ TEST_FUNCTION_START(flint_sprintf, state) char expected[64]; memset(buf, 'Z', sizeof(buf)); got = flint_sprintf(buf, "[%wd]", values[ix]); - snprintf(expected, sizeof(expected), "[%ld]", (long) values[ix]); + snprintf(expected, sizeof(expected), "[%lld]", (long long) values[ix]); if (got < 0 || strcmp(buf, expected) != 0) TEST_FUNCTION_FAIL( "flint_sprintf(\"[%%wd]\", %wd) gave \"%s\" expected \"%s\"\n", From daa4b0df84c0a900f8751648f5bf5f14225f214f Mon Sep 17 00:00:00 2001 From: Edgar Costa Date: Mon, 27 Apr 2026 16:58:23 -0400 Subject: [PATCH 3/3] docs(history): add 3.6.0-dev entry for #2646 fix --- doc/source/history.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/doc/source/history.rst b/doc/source/history.rst index 8b24fe569a..479082e09c 100644 --- a/doc/source/history.rst +++ b/doc/source/history.rst @@ -7,6 +7,17 @@ FLINT version history ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +FLINT 3.6.0 (in development) +------------------------------------------------------------------------------- + +Bug fixes + +* Fix ``flint_sprintf`` producing truncated output (e.g. ``"x"`` instead of + ``"x1"``) on 32-bit glibc, which broke ``mpoly_test_irreducible`` and the + ``compose_mpoly`` tests on i386/armhf + [EC, `#2648 `_]. + + 2026-04-24 -- FLINT 3.5.0 -------------------------------------------------------------------------------