From 3de08c3bde1d622b04887172af2bc14870bacc9a Mon Sep 17 00:00:00 2001 From: Vincent Richard Date: Thu, 22 Oct 2026 14:38:03 +0200 Subject: [PATCH 1/2] CI: add a Linux build job with ASan, UBSan and LSan Build the library and tests with -fsanitize=address,undefined, and make UndefinedBehaviorSanitizer errors fatal, so that memory errors, leaks and undefined behaviour reached by the test suite fail the build. Leaks in third-party libraries are suppressed in tests/lsan.supp. --- .github/workflows/build.yml | 21 +++++++++++++++++++++ tests/lsan.supp | 4 ++++ 2 files changed, 25 insertions(+) create mode 100644 tests/lsan.supp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0cbefbc2..7338792d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,6 +30,27 @@ jobs: sudo make install make test + build-linux-sanitizers: + name: build-linux-sanitizers (asan, ubsan) + runs-on: ubuntu-26.04 + steps: + - name: install-deps + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends cmake g++ libcppunit-dev libgnutls28-dev libgsasl-dev libicu-dev make pkg-config + - name: clone-vmime + uses: actions/checkout@v4 + - name: buildall + env: + ASAN_OPTIONS: detect_leaks=1 + LSAN_OPTIONS: suppressions=${{ github.workspace }}/tests/lsan.supp + UBSAN_OPTIONS: print_stacktrace=1 + run: | + set -ex + cmake . -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-sanitize-recover=all -fno-omit-frame-pointer" -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined" -DCMAKE_SHARED_LINKER_FLAGS="-fsanitize=address,undefined" -DVMIME_SENDMAIL_PATH:STRING="/usr/sbin/sendmail" -DVMIME_HAVE_TLS_SUPPORT:BOOL=ON -DVMIME_TLS_SUPPORT_LIB:STRING=gnutls -DVMIME_CHARSETCONV_LIB:STRING=icu -DVMIME_BUILD_TESTS=ON -DVMIME_BUILD_SAMPLES=OFF + make "-j$(nproc)" + ctest --output-on-failure + build-cygwin: name: build-cygwin (openssl, iconv) runs-on: windows-latest diff --git a/tests/lsan.supp b/tests/lsan.supp new file mode 100644 index 00000000..30440dcd --- /dev/null +++ b/tests/lsan.supp @@ -0,0 +1,4 @@ +# LeakSanitizer suppressions for leaks in third-party libraries + +# Buffer allocated by glibc in newlocale(), called by std::locale and libp11-kit +leak:newlocale From 5b40fe5decd03eb8da3a2dd23036aa7f1b7cdae1 Mon Sep 17 00:00:00 2001 From: Vincent Richard Date: Thu, 22 Oct 2026 14:38:23 +0200 Subject: [PATCH 2/2] IMAP: fix memory leaks when parsing a response fails with an exception IMAPParser::get() allocated the component, and only freed it if parsing returned false. If parsing threw instead (eg. when reading a literal from the socket failed), the component and all the partially parsed components above it were leaked. Hold it in a std::unique_ptr until parsing succeeds. Also fix the IMAPParser tests which did not free the parsed responses. --- src/vmime/net/imap/IMAPParser.hpp | 12 ++++-------- tests/net/imap/IMAPParserTest.cpp | 14 +++++++------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/vmime/net/imap/IMAPParser.hpp b/src/vmime/net/imap/IMAPParser.hpp index 4c239dbe..0ea2d4de 100644 --- a/src/vmime/net/imap/IMAPParser.hpp +++ b/src/vmime/net/imap/IMAPParser.hpp @@ -4767,8 +4767,7 @@ class VMIME_EXPORT IMAPParser : public object { template TYPE* get(string& line, size_t* currentPos) { - component* resp = new TYPE; - return internalGet (resp, line, currentPos); + return internalGet (std::unique_ptr (new TYPE), line, currentPos); } /** Parse a token which takes 2 arguments and advance. @@ -4788,14 +4787,13 @@ class VMIME_EXPORT IMAPParser : public object { template TYPE* getWithArgs(string& line, size_t* currentPos, ARG1_TYPE arg1, ARG2_TYPE arg2) { - component* resp = new TYPE(arg1, arg2); - return internalGet (resp, line, currentPos); + return internalGet (std::unique_ptr (new TYPE(arg1, arg2)), line, currentPos); } private: template - TYPE* internalGet(component* resp, string& line, size_t* currentPos) { + TYPE* internalGet(std::unique_ptr resp, string& line, size_t* currentPos) { const size_t oldPos = *currentPos; @@ -4803,12 +4801,10 @@ class VMIME_EXPORT IMAPParser : public object { *currentPos = oldPos; - delete resp; - return NULL; } - return static_cast (resp); + return resp.release(); } const string lastLine() const { diff --git a/tests/net/imap/IMAPParserTest.cpp b/tests/net/imap/IMAPParserTest.cpp index dab31e2b..e671fee3 100644 --- a/tests/net/imap/IMAPParserTest.cpp +++ b/tests/net/imap/IMAPParserTest.cpp @@ -81,7 +81,7 @@ VMIME_TEST_SUITE_BEGIN(IMAPParserTest) parser->setTimeoutHandler(vmime::make_shared ()); parser->setStrict(false); - VASSERT_NO_THROW("non-strict mode", parser->readResponse(*tag)); + VASSERT_NO_THROW("non-strict mode", std::unique_ptr (parser->readResponse(*tag))); } } @@ -113,7 +113,7 @@ VMIME_TEST_SUITE_BEGIN(IMAPParserTest) parser->setTimeoutHandler(toh); parser->setStrict(false); - VASSERT_NO_THROW("non-strict mode", parser->readResponse(*tag)); + VASSERT_NO_THROW("non-strict mode", std::unique_ptr (parser->readResponse(*tag))); ++(*tag); @@ -158,7 +158,7 @@ VMIME_TEST_SUITE_BEGIN(IMAPParserTest) parser->setTimeoutHandler(vmime::make_shared ()); parser->setStrict(false); - VASSERT_NO_THROW("non-strict mode", parser->readResponse(*tag)); + VASSERT_NO_THROW("non-strict mode", std::unique_ptr (parser->readResponse(*tag))); } } @@ -181,7 +181,7 @@ VMIME_TEST_SUITE_BEGIN(IMAPParserTest) parser->setSocket(socket); parser->setTimeoutHandler(toh); - VASSERT_NO_THROW("parse", parser->readResponse(*tag)); + VASSERT_NO_THROW("parse", std::unique_ptr (parser->readResponse(*tag))); } // Support for NIL boundary, for mail.ru IMAP server: @@ -218,7 +218,7 @@ VMIME_TEST_SUITE_BEGIN(IMAPParserTest) parser->setTimeoutHandler(vmime::make_shared ()); parser->setStrict(false); - VASSERT_NO_THROW("non-strict mode", parser->readResponse(*tag)); + VASSERT_NO_THROW("non-strict mode", std::unique_ptr (parser->readResponse(*tag))); } } @@ -253,7 +253,7 @@ VMIME_TEST_SUITE_BEGIN(IMAPParserTest) parser->setTimeoutHandler(vmime::make_shared ()); parser->setStrict(false); - VASSERT_NO_THROW("non-strict mode", parser->readResponse(*tag)); + VASSERT_NO_THROW("non-strict mode", std::unique_ptr (parser->readResponse(*tag))); } } @@ -373,7 +373,7 @@ VMIME_TEST_SUITE_BEGIN(IMAPParserTest) parser->setTimeoutHandler(toh); parser->setStrict(false); - VASSERT_NO_THROW("non-strict mode", parser->readResponse(*tag)); + VASSERT_NO_THROW("non-strict mode", std::unique_ptr (parser->readResponse(*tag))); } }