Skip to content

Conversation

ldionne
Copy link
Member

@ldionne ldionne commented Jun 17, 2025

These invariants are always expected to hold, however it's not always clear that they do. Adding explicit checks for these invariants inside non-trivial functions of basic_streambuf makes that clear.

@ldionne ldionne requested a review from a team as a code owner June 17, 2025 20:59
@llvmbot llvmbot added the libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi. label Jun 17, 2025
@llvmbot
Copy link
Member

llvmbot commented Jun 17, 2025

@llvm/pr-subscribers-libcxx

Author: Louis Dionne (ldionne)

Changes

These invariants are always expected to hold, however it's not always clear that they do. Adding explicit checks for these invariants inside non-trivial functions of basic_streambuf makes that clear.


Full diff: https://github.com/llvm/llvm-project/pull/144602.diff

1 Files Affected:

  • (modified) libcxx/include/streambuf (+40)
diff --git a/libcxx/include/streambuf b/libcxx/include/streambuf
index 585ae7af65aa8..7dc4e31cc2324 100644
--- a/libcxx/include/streambuf
+++ b/libcxx/include/streambuf
@@ -119,6 +119,7 @@ protected:
 #    include <__locale>
 #    include <__type_traits/is_same.h>
 #    include <__utility/is_valid_range.h>
+#    include <__utility/scope_guard.h>
 #    include <climits>
 #    include <ios>
 #    include <iosfwd>
@@ -178,18 +179,27 @@ public:
   // Get and put areas:
   // 27.6.2.2.3 Get area:
   inline _LIBCPP_HIDE_FROM_ABI_AFTER_V1 streamsize in_avail() {
+    __check_invariants();
+    auto __guard = std::__make_scope_guard([this] { this->__check_invariants(); });
+
     if (gptr() < egptr())
       return static_cast<streamsize>(egptr() - gptr());
     return showmanyc();
   }
 
   inline _LIBCPP_HIDE_FROM_ABI_AFTER_V1 int_type snextc() {
+    __check_invariants();
+    auto __guard = std::__make_scope_guard([this] { this->__check_invariants(); });
+
     if (sbumpc() == traits_type::eof())
       return traits_type::eof();
     return sgetc();
   }
 
   inline _LIBCPP_HIDE_FROM_ABI_AFTER_V1 int_type sbumpc() {
+    __check_invariants();
+    auto __guard = std::__make_scope_guard([this] { this->__check_invariants(); });
+
     if (gptr() == egptr())
       return uflow();
     int_type __c = traits_type::to_int_type(*gptr());
@@ -198,6 +208,9 @@ public:
   }
 
   inline _LIBCPP_HIDE_FROM_ABI_AFTER_V1 int_type sgetc() {
+    __check_invariants();
+    auto __guard = std::__make_scope_guard([this] { this->__check_invariants(); });
+
     if (gptr() == egptr())
       return underflow();
     return traits_type::to_int_type(*gptr());
@@ -207,6 +220,9 @@ public:
 
   // 27.6.2.2.4 Putback:
   inline _LIBCPP_HIDE_FROM_ABI_AFTER_V1 int_type sputbackc(char_type __c) {
+    __check_invariants();
+    auto __guard = std::__make_scope_guard([this] { this->__check_invariants(); });
+
     if (eback() == gptr() || !traits_type::eq(__c, *(gptr() - 1)))
       return pbackfail(traits_type::to_int_type(__c));
     this->gbump(-1);
@@ -214,6 +230,9 @@ public:
   }
 
   inline _LIBCPP_HIDE_FROM_ABI_AFTER_V1 int_type sungetc() {
+    __check_invariants();
+    auto __guard = std::__make_scope_guard([this] { this->__check_invariants(); });
+
     if (eback() == gptr())
       return pbackfail();
     this->gbump(-1);
@@ -222,6 +241,9 @@ public:
 
   // 27.6.2.2.5 Put area:
   inline _LIBCPP_HIDE_FROM_ABI_AFTER_V1 int_type sputc(char_type __c) {
+    __check_invariants();
+    auto __guard = std::__make_scope_guard([this] { this->__check_invariants(); });
+
     if (pptr() == epptr())
       return overflow(traits_type::to_int_type(__c));
     *pptr() = __c;
@@ -317,6 +339,9 @@ protected:
   virtual streamsize showmanyc() { return 0; }
 
   virtual streamsize xsgetn(char_type* __s, streamsize __n) {
+    __check_invariants();
+    auto __guard = std::__make_scope_guard([this] { this->__check_invariants(); });
+
     int_type __c;
     streamsize __i = 0;
     while (__i < __n) {
@@ -338,6 +363,9 @@ protected:
 
   virtual int_type underflow() { return traits_type::eof(); }
   virtual int_type uflow() {
+    __check_invariants();
+    auto __guard = std::__make_scope_guard([this] { this->__check_invariants(); });
+
     if (underflow() == traits_type::eof())
       return traits_type::eof();
     int_type __c = traits_type::to_int_type(*gptr());
@@ -350,6 +378,9 @@ protected:
 
   // 27.6.2.4.5 Put area:
   virtual streamsize xsputn(const char_type* __s, streamsize __n) {
+    __check_invariants();
+    auto __guard = std::__make_scope_guard([this] { this->__check_invariants(); });
+
     streamsize __i = 0;
     while (__i < __n) {
       if (pptr() >= epptr()) {
@@ -370,6 +401,15 @@ protected:
 
   virtual int_type overflow(int_type = traits_type::eof()) { return traits_type::eof(); }
 
+  // This function checks some invariants of the class (it isn't exhaustive).
+  _LIBCPP_HIDE_FROM_ABI void __check_invariants() const {
+    _LIBCPP_ASSERT_INTERNAL(pbase() <= pptr(), "this is an invariant of the class");
+    _LIBCPP_ASSERT_INTERNAL(pptr() <= epptr(), "this is an invariant of the class");
+
+    _LIBCPP_ASSERT_INTERNAL(eback() <= gptr(), "this is an invariant of the class");
+    _LIBCPP_ASSERT_INTERNAL(gptr() <= egptr(), "this is an invariant of the class");
+  }
+
 private:
   locale __loc_;
   char_type* __binp_ = nullptr;

@ldionne
Copy link
Member Author

ldionne commented Jun 17, 2025

I'm a bit conflicted: on the one hand these invariant checks are extremely useful (especially when debugging tricky issues), but on the other hand they're somewhat clunky. We could also technically do this in all methods of all classes in the library, and clearly we don't want to go down that route.

@philnik777 Do you have thoughts on this? I could also scale it down to just the two methods where checking these invariants is really helpful to debug the issue I'm looking into.

@ldionne ldionne force-pushed the review/stream-add-invariants branch from 2f49318 to 68b73da Compare July 3, 2025 15:27
Copy link
Contributor

@philnik777 philnik777 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this LGTM. I'd much rather have some other way to say "this is an invariant", but I don't know how we'd do it.

These invariants are always expected to hold, however it's not always
clear that they do. Adding explicit checks for these invariants inside
non-trivial functions of basic_streambuf makes that clear.
@ldionne ldionne force-pushed the review/stream-add-invariants branch from 68b73da to 83e20ab Compare August 1, 2025 17:28
@ldionne ldionne added the pending-ci Merging the PR is only pending completion of CI label Aug 1, 2025
Copy link
Contributor

@frederick-vs-ja frederick-vs-ja left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like that we should merge this now. CI failures are probably unrelated.

@ldionne
Copy link
Member Author

ldionne commented Aug 20, 2025

Sorry, this went under my radar. Merging.

@ldionne ldionne merged commit 2e2349e into llvm:main Aug 20, 2025
74 of 79 checks passed
@ldionne ldionne deleted the review/stream-add-invariants branch August 20, 2025 15:09
@Michael137
Copy link
Member

This unfortunately broke the LLDB macOS CI:

08:14:18  Unresolved Tests (16):
08:14:18    lldb-api :: commands/expression/import-std-module/array/TestArrayFromStdModule.py
08:14:18    lldb-api :: commands/expression/import-std-module/deque-basic/TestDequeFromStdModule.py
08:14:18    lldb-api :: commands/expression/import-std-module/deque-dbg-info-content/TestDbgInfoContentDequeFromStdModule.py
08:14:18    lldb-api :: commands/expression/import-std-module/forward_list-dbg-info-content/TestDbgInfoContentForwardListFromStdModule.py
08:14:18    lldb-api :: commands/expression/import-std-module/forward_list/TestForwardListFromStdModule.py
08:14:18    lldb-api :: commands/expression/import-std-module/iterator/TestIteratorFromStdModule.py
08:14:18    lldb-api :: commands/expression/import-std-module/list-dbg-info-content/TestDbgInfoContentListFromStdModule.py
08:14:18    lldb-api :: commands/expression/import-std-module/list/TestListFromStdModule.py
08:14:18    lldb-api :: commands/expression/import-std-module/non-module-type-separation/TestNonModuleTypeSeparation.py
08:14:18    lldb-api :: commands/expression/import-std-module/shared_ptr-dbg-info-content/TestSharedPtrDbgInfoContentFromStdModule.py
08:14:18    lldb-api :: commands/expression/import-std-module/shared_ptr/TestSharedPtrFromStdModule.py
08:14:18    lldb-api :: commands/expression/import-std-module/unique_ptr-dbg-info-content/TestUniquePtrDbgInfoContent.py
08:14:18    lldb-api :: commands/expression/import-std-module/unique_ptr/TestUniquePtrFromStdModule.py
08:14:18    lldb-api :: commands/expression/import-std-module/vector-of-vectors/TestVectorOfVectorsFromStdModule.py
08:14:18    lldb-api :: commands/expression/import-std-module/weak_ptr-dbg-info-content/TestDbgInfoContentWeakPtrFromStdModule.py
08:14:18    lldb-api :: commands/expression/import-std-module/weak_ptr/TestWeakPtrFromStdModule.py
08:14:18 

Same issue as #149477

We worked around it last time by not using a lambda in #149390. But that doesn't seem like a reasonable ask in this case?

I'll try to repro this locally and see if there's an LLDB fix for this

@Michael137
Copy link
Member

Michael137 commented Aug 22, 2025

@ldionne could we revert this for now until I fix/skip the tests? I know what the root cause is, just working towards a fix. But won't be landing it until monday. If I end up not fixing it by then we can skip the tests temporarily to unblock libc++

Also, curious why these tests didn't run in pre-merge CI. Need to check

EDIT: looks like we skip these tests on Linux :(

@Michael137
Copy link
Member

Michael137 commented Aug 22, 2025

Actually nvm, I'm working around it in: #154962

No need to revert

Michael137 added a commit that referenced this pull request Aug 22, 2025
This patch works around an assertion that we hit in the `LambdaExpr`
constructor when we call it from `ASTNodeImporter::VisitLambdaExpr` (see
#149477). The lambda that we
imported doesn't have the `NumCaptures` field accurately set to the one
on the source decl. This is because in `MinimalImport` mode, we skip
importing of lambda definitions:
https://github.com/llvm/llvm-project/blob/e21b0dd81928a3266df0e3ede008fb7a6676ff95/clang/lib/AST/ASTImporter.cpp#L2499

In practice we have seen this assertion occur in our `import-std-module`
test-suite when libc++ headers decide to use lambdas inside inline
function bodies (the latest failure being caused by
#144602).

To avoid running into this whenever libc++ decides to use lambdas in
headers, this patch skips `ASTImport` of lambdas alltogether. Ideally
this would bubble up to the user or log as an error, but we swallow the
`ASTImportError`s currently. The only way this codepath is hit is when
lambdas are used inside functions in defined in the expression
evaluator, or when importing AST nodes from Clang modules. Both of these
are very niche use-cases (for now), so a workaround seemed appropriate.
llvm-sync bot pushed a commit to arm/arm-toolchain that referenced this pull request Aug 22, 2025
…(#154962)

This patch works around an assertion that we hit in the `LambdaExpr`
constructor when we call it from `ASTNodeImporter::VisitLambdaExpr` (see
llvm/llvm-project#149477). The lambda that we
imported doesn't have the `NumCaptures` field accurately set to the one
on the source decl. This is because in `MinimalImport` mode, we skip
importing of lambda definitions:
https://github.com/llvm/llvm-project/blob/e21b0dd81928a3266df0e3ede008fb7a6676ff95/clang/lib/AST/ASTImporter.cpp#L2499

In practice we have seen this assertion occur in our `import-std-module`
test-suite when libc++ headers decide to use lambdas inside inline
function bodies (the latest failure being caused by
llvm/llvm-project#144602).

To avoid running into this whenever libc++ decides to use lambdas in
headers, this patch skips `ASTImport` of lambdas alltogether. Ideally
this would bubble up to the user or log as an error, but we swallow the
`ASTImportError`s currently. The only way this codepath is hit is when
lambdas are used inside functions in defined in the expression
evaluator, or when importing AST nodes from Clang modules. Both of these
are very niche use-cases (for now), so a workaround seemed appropriate.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi. pending-ci Merging the PR is only pending completion of CI
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants