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)