On Thu, Oct 1, 2026 at 7:46 PM Jonathan Wakely <[email protected]> wrote:
> The bug report points out one out-of-bounds read and the second was
> found by an LLM. The testcase was generated by an LLM. The code fixes
> are artisanal handwritten code.
>
> Assisted-by: LLM
>
> libstdc++-v3/ChangeLog:
>
> PR libstdc++/127666
> * src/c++17/floating_from_chars.cc (pattern): Avoid reading past
> the end of the input. Remove unused variable.
> * testsuite/20_util/from_chars/127666.cc: New test.
> ---
>
> Tested x86_64-linux.
>
The assign(first, 0, len) is a really sneaky bug. LTGM.
>
> I'm not sure whether the mmap and sysconf effective targets are enough
> to ensure the new test works everywhere. If we get new FAILs for it we
> can adjust the test to not run on more targets.
>
> libstdc++-v3/src/c++17/floating_from_chars.cc | 9 ++-
> .../testsuite/20_util/from_chars/127666.cc | 70 +++++++++++++++++++
> 2 files changed, 76 insertions(+), 3 deletions(-)
> create mode 100644 libstdc++-v3/testsuite/20_util/from_chars/127666.cc
>
> diff --git a/libstdc++-v3/src/c++17/floating_from_chars.cc
> b/libstdc++-v3/src/c++17/floating_from_chars.cc
> index ed434b395288..c28fc9a459ef 100644
> --- a/libstdc++-v3/src/c++17/floating_from_chars.cc
> +++ b/libstdc++-v3/src/c++17/floating_from_chars.cc
> @@ -451,13 +451,16 @@ namespace
> // fmt has the value of one of the enumerators of chars_format.
> __glibcxx_assert(valid_fmt(fmt));
>
> - string_view res;
> -
> + // Fail if the input is empty or starts with '+'
> if (first == last || *first == '+') [[unlikely]]
> return nullptr;
>
> const int neg = (*first == '-');
>
> + // Fail if the input consists of a lone '-'
> + if (first + neg == last)
> + return nullptr;
> +
> if (std::memchr("iInN", (unsigned char)first[neg], 4))
> {
> ptrdiff_t len = last - first;
> @@ -489,7 +492,7 @@ namespace
> else // Only need 4 chars for "-NAN"
> len = neg + 3;
>
> - buf.assign(first, 0, len);
> + buf.assign(first, len);
> // prevent make_result correcting for "0x"
> fmt = chars_format::general;
> return buf.c_str();
> diff --git a/libstdc++-v3/testsuite/20_util/from_chars/127666.cc
> b/libstdc++-v3/testsuite/20_util/from_chars/127666.cc
> new file mode 100644
> index 000000000000..25989f2cfee8
> --- /dev/null
> +++ b/libstdc++-v3/testsuite/20_util/from_chars/127666.cc
> @@ -0,0 +1,70 @@
> +// { dg-do run { target c++17 } }
> +// { dg-require-effective-target mmap }
> +// { dg-require-effective-target sysconf }
> +// { dg-add-options ieee }
> +// { dg-additional-options "-DSKIP_LONG_DOUBLE" { target aarch64-*-rtems*
> aarch64-*-vxworks* x86_64-*-vxworks* } }
> +
> +// Bug 127666
> +// std::from_chars(first, last, long double&) reads past 'last' for
> "inf"/"nan"
> +
> +#include <charconv>
> +#include <string_view>
> +#include <cstring>
> +#include <sys/mman.h>
> +#include <unistd.h>
> +#include <testsuite_hooks.h>
> +
> +using namespace std::string_view_literals;
> +
> +// One past the last writable byte; the guard page starts here, so reading
> +// at or beyond this address faults.
> +static char* buf_end;
> +
> +template<typename T>
> +void
> +check(std::string_view input, std::errc ec, std::size_t len)
> +{
> + // Position the input against the guard page so an over-read faults.
> + char* const first = buf_end - input.size();
> + std::memcpy(first, input.data(), input.size());
> + T value = 1;
> + auto res = std::from_chars(first, buf_end, value);
> + VERIFY( res.ec == ec );
> + VERIFY( res.ptr == first + len );
> +}
> +
> +template<typename T>
> +void
> +test_type()
> +{
> + // The inf/nan forms used to read past the end via strlen.
> + check<T>("inf"sv, std::errc{}, 3);
> + check<T>("infinity"sv, std::errc{}, 8);
> + check<T>("nan"sv, std::errc{}, 3);
> + check<T>("nan(1)"sv, std::errc{}, 6);
> + check<T>("-nan(ab)"sv, std::errc{}, 8);
> +
> + // A lone sign used to read one byte past the end.
> + check<T>("-"sv, std::errc::invalid_argument, 0);
> + check<T>("+"sv, std::errc::invalid_argument, 0);
> +}
> +
> +int
> +main()
> +{
> + const long pg = ::sysconf(_SC_PAGESIZE);
> + VERIFY( pg > 0 );
> + char* const base = (char*) ::mmap(nullptr, 2 * pg, PROT_READ |
> PROT_WRITE,
> + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
> + VERIFY( base != MAP_FAILED );
> + VERIFY( ::mprotect(base + pg, pg, PROT_NONE) == 0 );
> + buf_end = base + pg;
> +
> + test_type<float>();
> + test_type<double>();
> +#ifndef SKIP_LONG_DOUBLE
> + test_type<long double>();
> +#endif
> +
> + ::munmap(base, 2 * pg);
> +}
> --
> 2.55.0
>
>