https://gcc.gnu.org/bugzilla/show_bug.cgi?id=125500

            Bug ID: 125500
           Summary: num_put::do_put(const void*) and
                    num_get::do_get(void*&) leak fmtflags when inner
                    iterator throws
           Product: gcc
           Version: 15.1.0
            Status: UNCONFIRMED
          Severity: normal
          Priority: P3
         Component: libstdc++
          Assignee: unassigned at gcc dot gnu.org
          Reporter: liweifriends at gmail dot com
  Target Milestone: ---

Created attachment 64582
  --> https://gcc.gnu.org/bugzilla/attachment.cgi?id=64582&action=edit
code to repor the issue

libstdc++'s std::num_put::do_put(const void*) and std::num_get::do_get(void*&)
  in libstdc++-v3/include/bits/locale_facets.tcc (around L1131 for do_get, and
  the matching do_put) both:

    (1) save io.flags() into a const local,
    (2) mutate ios_base flags to (hex | showbase) [do_put] or set basefield to
        hex [do_get],
    (3) call _M_insert_int / _M_extract_int through an iterator,
    (4) restore the saved flags by calling io.flags(saved) at the end.

  Step (4) only runs on the normal-return path. If the iterator throws — and
  this is well-formed: a streambuf whose overflow/underflow reports an I/O
  error is supposed to throw, and the inner machinery may itself throw
  bad_alloc — then the restore is skipped and the caller's ios_base is left
  in the implementation's intermediate state (hex | showbase for do_put,
  hex basefield for do_get).

  basic_ostream::operator<<(const void*) and the matching extractor wrap the
  call in a sentry that catches the exception, sets badbit, and swallows it.
  The caller therefore sees no exception at all — only a stream whose flags()
  are permanently hex|showbase (or hex basefield) for every subsequent
  formatted operation on the same stream. This is a silent violation of the
  basic exception guarantee: do_put / do_get's externally observable contract
  does not include mutating flags(), so any observable change to flags() after
  the call is a bug.

  Reproducer (attached): repro_gcc_void_ptr_flags_leak.cpp.

  Build & run:
    g++-15 -std=c++20 -O0 repro_gcc_void_ptr_flags_leak.cpp -o repro
    ./repro

  Actual output on Homebrew GCC 15.1.0:

    Baseline flags: dec|skipws  (hex=off, showbase=off, dec=ON)

    (a) Throwing iterator — do_put / do_get MUST leak (libstdc++ bug):
          do_put(const void*), budget=2     before: hex=off showbase=off dec=ON
  | after: hex=ON
  showbase=ON  dec=off  bad=1  | FAIL  (flags LEAKED -- bug reproduced)
          do_get(void*&),     budget=3      before: hex=off showbase=off dec=ON
  | after: hex=ON
  showbase=off dec=off  bad=1  | FAIL  (flags LEAKED -- bug reproduced)

    (b) Non-throwing iterator — control, flags MUST be restored:
          do_put(const void*), budget=1024  before: hex=off showbase=off dec=ON
  | after: hex=off
  showbase=off dec=ON   bad=0  | OK    (flags restored)
          do_get(void*&),     budget=1024   before: hex=off showbase=off dec=ON
  | after: hex=off
  showbase=off dec=ON   bad=0  | OK    (flags restored)

    (c) Downstream effect of a leaked do_put on the SAME stream:
      (e) After leaked do_put, formatting int 255 yields "0xff" (expected
"255"; "0xff" means flags were
  corrupted)

  Key observations:

  - Case (a) shows hex/showbase remain set on the caller's ios_base after
    the throwing call, even though no exception escapes operator<< / >>.
    The corruption is entirely silent — caller sees only badbit.
  - do_put leaks (hex | showbase); do_get leaks hex via basefield. Both
    also clear the dec bit as a side effect of the implementation's
    setf(hex, basefield) call.
  - Case (b) — same code path, non-throwing streambuf — leaves flags()
    fully intact. This confirms the difference is exception-path-only and
    not a sentry-side artifact.
  - Case (c) demonstrates the externally observable consequence: after the
    leaked do_put, the next formatted int on the same stream is printed as
    "0xff" instead of "255".

  Suggested fix: in locale_facets.tcc, replace the manual save/restore in
  both do_put(const void*) and do_get(void*&) with a small RAII guard, so
  the restore runs on both the normal-return path and the exception path.

  Versions:
    g++ -v:               gcc version 15.1.0 (Homebrew GCC 15.1.0)
    libstdc++ version:    shipped with GCC 15.1.0
    Target triple:        aarch64-apple-darwin24
    Flags:                -std=c++20  (reproduces identically at both -O0 and
-O2)
  • [Bug libstdc++/125500] New: num... liweifriends at gmail dot com via Gcc-bugs

Reply via email to