Skip to content

Commit e097619

Browse files
authored
[libc++] Improves UB handling in ios_base destructor. (#76525)
Destroying an ios_base object before it is properly initialized is undefined behavior. Unlike typical C++ classes the initialization is not done in the constructor, but in a dedicated init function. Due to virtual inheritance of the basic_ios object in ostream and friends this undefined behaviour can be triggered when inheriting from classes that can throw in their constructor and inheriting from ostream. Use the __loc_ member of ios_base as sentinel to detect whether the object has or has not been initialized. Addresses #57964
1 parent bd72ebd commit e097619

File tree

3 files changed

+94
-2
lines changed

3 files changed

+94
-2
lines changed

libcxx/include/ios

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,13 @@ public:
359359
}
360360

361361
protected:
362-
_LIBCPP_HIDE_FROM_ABI ios_base() { // purposefully does no initialization
362+
_LIBCPP_HIDE_FROM_ABI ios_base() : __loc_(nullptr) {
363+
// Purposefully does no initialization
364+
//
365+
// Except for the locale, this is a sentinel to avoid destroying
366+
// an uninitialized object. See
367+
// test/libcxx/input.output/iostreams.base/ios.base/ios.base.cons/dtor.uninitialized.pass.cpp
368+
// for the details.
363369
}
364370

365371
void init(void* __sb);
@@ -571,7 +577,9 @@ public:
571577
_LIBCPP_HIDE_FROM_ABI char_type widen(char __c) const;
572578

573579
protected:
574-
_LIBCPP_HIDE_FROM_ABI basic_ios() { // purposefully does no initialization
580+
_LIBCPP_HIDE_FROM_ABI basic_ios() {
581+
// purposefully does no initialization
582+
// since the destructor does nothing this does not have ios_base issues.
575583
}
576584
_LIBCPP_HIDE_FROM_ABI void init(basic_streambuf<char_type, traits_type>* __sb);
577585

libcxx/src/ios.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,10 @@ void ios_base::register_callback(event_callback fn, int index) {
195195
}
196196

197197
ios_base::~ios_base() {
198+
// Avoid UB when not properly initialized. See ios_base::ios_base for
199+
// more information.
200+
if (!__loc_)
201+
return;
198202
__call_callbacks(erase_event);
199203
locale& loc_storage = *reinterpret_cast<locale*>(&__loc_);
200204
loc_storage.~locale();
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
// UNSUPPORTED: no-exceptions
10+
11+
// The fix for issue 57964 requires an updated dylib due to explicit
12+
// instantiations. That means Apple backdeployment targets remain broken.
13+
// UNSUPPORTED: using-built-library-before-llvm-19
14+
15+
// <ios>
16+
17+
// class ios_base
18+
19+
// ~ios_base()
20+
//
21+
// Destroying a constructed ios_base object that has not been
22+
// initialized by basic_ios::init is undefined behaviour. This can
23+
// happen in practice, make sure the undefined behaviour is handled
24+
// gracefully.
25+
//
26+
//
27+
// [ios.base.cons]/1
28+
//
29+
// ios_base();
30+
// Effects: Each ios_base member has an indeterminate value after construction.
31+
// The object's members shall be initialized by calling basic_ios::init before
32+
// the object's first use or before it is destroyed, whichever comes first;
33+
// otherwise the behavior is undefined.
34+
//
35+
// [basic.ios.cons]/2
36+
//
37+
// basic_ios();
38+
// Effects: Leaves its member objects uninitialized. The object shall be
39+
// initialized by calling basic_ios::init before its first use or before it is
40+
// destroyed, whichever comes first; otherwise the behavior is undefined.
41+
//
42+
// ostream and friends have a basic_ios virtual base.
43+
// [class.base.init]/13
44+
// In a non-delegating constructor, initialization proceeds in the
45+
// following order:
46+
// - First, and only for the constructor of the most derived class
47+
// ([intro.object]), virtual base classes are initialized ...
48+
//
49+
// So in this example
50+
// struct Foo : AlwaysThrows, std::ostream {
51+
// Foo() : AlwaysThrows{}, std::ostream{nullptr} {}
52+
// };
53+
//
54+
// Here
55+
// - the ios_base object is constructed
56+
// - the AlwaysThrows object is constructed and throws an exception
57+
// - the AlwaysThrows object is destrodyed
58+
// - the ios_base object is destroyed
59+
//
60+
// The ios_base object is destroyed before it has been initialized and runs
61+
// into undefined behavior. By using __loc_ as a sentinel we can avoid
62+
// accessing uninitialized memory in the destructor.
63+
64+
#include <ostream>
65+
66+
struct AlwaysThrows {
67+
AlwaysThrows() { throw 1; }
68+
};
69+
70+
struct Foo : AlwaysThrows, std::ostream {
71+
Foo() : AlwaysThrows(), std::ostream(nullptr) {}
72+
};
73+
74+
int main(int, char**) {
75+
try {
76+
Foo foo;
77+
} catch (...) {
78+
};
79+
return 0;
80+
}

0 commit comments

Comments
 (0)