From e4b159f10b2629e42ab223be9f888621efdfe33b Mon Sep 17 00:00:00 2001 From: Gennaro Prota Date: Mon, 20 Jul 2026 11:05:45 +0200 Subject: [PATCH 1/2] Don't let the XML input archive destructor throw at end of input The xml input archive destructor calls `windup()` to consume the trailing tag; `windup()` calls `my_parse()`. Since Boost 1.66 (commit 64dc620), `my_parse()` threw `input_stream_error` whenever `get()` set `failbit`. But `get()` sets both `failbit` and `eofbit` at end of input, and the check tested `fail()` before `eof()`---so simply reaching the end while scanning for a tag threw, escaping the implicitly noexcept destructor and terminating the process. No closing tag is left to find in several cases: a truncated stream, an archive whose writer was never flushed, or a well-formed one whose closing tag an earlier failed read had already consumed. So, test `eof()` before `fail()` and return `false` for end of input---as it did before 1.66 (a genuine, non-EOF stream error still throws). As defense in depth, this also wraps the `windup()` call in both XML input destructors so no exception can escape them regardless. Fixes issue #99, the same defect as the already-closed #82. It also removes the terminate reported in #109, though that issue's broader broken-state-after-exception concern is a by-design limitation of the forward-only parse and isn't fixed. Refs #109. --- .../boost/archive/impl/xml_iarchive_impl.ipp | 9 ++- .../boost/archive/impl/xml_wiarchive_impl.ipp | 9 ++- src/basic_xml_grammar.ipp | 11 +++- test/Jamfile.v2 | 1 + test/test_xml_trailing_whitespace.cpp | 59 +++++++++++++++++++ 5 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 test/test_xml_trailing_whitespace.cpp diff --git a/include/boost/archive/impl/xml_iarchive_impl.ipp b/include/boost/archive/impl/xml_iarchive_impl.ipp index b0847d4a1b..2c7b046333 100644 --- a/include/boost/archive/impl/xml_iarchive_impl.ipp +++ b/include/boost/archive/impl/xml_iarchive_impl.ipp @@ -189,7 +189,14 @@ xml_iarchive_impl::~xml_iarchive_impl(){ if(boost::core::uncaught_exceptions() > 0) return; if(0 == (this->get_flags() & no_header)){ - gimpl->windup(is); + // windup() parses the trailing end tag; an exception must not escape + // this (implicitly noexcept) destructor and terminate the process. + // A stream error while consuming the trailer is not worth that. #99 + BOOST_TRY { + gimpl->windup(is); + } + BOOST_CATCH(...) {} + BOOST_CATCH_END } } } // namespace archive diff --git a/include/boost/archive/impl/xml_wiarchive_impl.ipp b/include/boost/archive/impl/xml_wiarchive_impl.ipp index 198dfafb98..546ab0d4c6 100644 --- a/include/boost/archive/impl/xml_wiarchive_impl.ipp +++ b/include/boost/archive/impl/xml_wiarchive_impl.ipp @@ -177,7 +177,14 @@ xml_wiarchive_impl::~xml_wiarchive_impl(){ if(boost::core::uncaught_exceptions() > 0) return; if(0 == (this->get_flags() & no_header)){ - gimpl->windup(is); + // windup() parses the trailing end tag; an exception must not escape + // this (implicitly noexcept) destructor and terminate the process. + // A stream error while consuming the trailer is not worth that. #99 + BOOST_TRY { + gimpl->windup(is); + } + BOOST_CATCH(...) {} + BOOST_CATCH_END } } diff --git a/src/basic_xml_grammar.ipp b/src/basic_xml_grammar.ipp index c0d5320c7f..ee9eced647 100644 --- a/src/basic_xml_grammar.ipp +++ b/src/basic_xml_grammar.ipp @@ -191,6 +191,15 @@ bool basic_xml_grammar::my_parse( for(;;){ CharType result; is.get(result); + // Reaching end of input while scanning for the next character is the + // normal way an archive ends (e.g. windup() consuming the trailer); + // it is not a stream error. get() sets both eofbit and failbit at end + // of stream, so test eof() *before* fail() -- otherwise the normal + // termination is misreported as input_stream_error, which is fatal + // when it surfaces in the (noexcept) archive destructor via windup(). + // See #99. + if(is.eof()) + return false; if(is.fail()){ boost::serialization::throw_exception( boost::archive::archive_exception( @@ -199,8 +208,6 @@ bool basic_xml_grammar::my_parse( ) ); } - if(is.eof()) - return false; arg += result; if(result == delimiter) break; diff --git a/test/Jamfile.v2 b/test/Jamfile.v2 index a7b521577c..efa84daabc 100644 --- a/test/Jamfile.v2 +++ b/test/Jamfile.v2 @@ -151,6 +151,7 @@ if ! $(BOOST_ARCHIVE_LIST) { [ test-bsl-run test_private_ctor ] [ test-bsl-run test_reset_object_address : A ] [ test-bsl-run test_void_cast ] + [ test-bsl-run test_xml_trailing_whitespace ] [ test-bsl-run test_mult_archive_types : : : [ requires std_wstreambuf ] ] [ test-bsl-run test_iterators : : : [ requires std_wstreambuf ] ] [ test-bsl-run test_iterators_base64 ] diff --git a/test/test_xml_trailing_whitespace.cpp b/test/test_xml_trailing_whitespace.cpp new file mode 100644 index 0000000000..b912a6c3bf --- /dev/null +++ b/test/test_xml_trailing_whitespace.cpp @@ -0,0 +1,59 @@ +/////////1/////////2/////////3/////////4/////////5/////////6/////////7/////////8 +// test_xml_trailing_whitespace.cpp + +// Copyright 2026 Gennaro Prota +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +// Regression test for issue #99. When an XML input archive is destroyed and +// its stream has no closing tag left -- only trailing whitespace before end +// of input (a truncated archive; the reported case had the stream "contain +// \r\n") -- windup() reaches end of input while scanning for the trailing +// tag. End of input there is normal termination, not a stream error, so the +// (implicitly noexcept) destructor must complete cleanly rather than throw, +// which used to terminate the process. + +#include +#include + +#include +#include +#include + +#include "test_tools.hpp" + +int test_main(int /* argc */, char * /* argv */ []){ + // Build a valid XML archive holding a single value. + std::string content; + { + std::ostringstream os; + { + boost::archive::xml_oarchive oa(os); + const int x = 42; + oa << boost::serialization::make_nvp("x", x); + } + content = os.str(); + } + + // Drop the closing document tag and leave only trailing whitespace, so the + // input archive's windup() reaches end of input at destruction instead of + // finding a tag. + const std::string::size_type close = content.rfind(""); + BOOST_REQUIRE(std::string::npos != close); + content.erase(close); + content += "\r\n"; + + int y = 0; + { + std::istringstream is(content); + boost::archive::xml_iarchive ia(is); + ia >> boost::serialization::make_nvp("x", y); + // `ia` is destroyed here with only trailing whitespace left. Before + // the fix, windup() threw input_stream_error out of the noexcept + // destructor and terminated the process. + } + BOOST_CHECK(42 == y); + + return EXIT_SUCCESS; +} From 71356187c5187754fd2775300c836f7d46e4dd38 Mon Sep 17 00:00:00 2001 From: Gennaro Prota Date: Mon, 20 Jul 2026 12:25:00 +0200 Subject: [PATCH 2/2] Add a regression test for reading a missing nvp (#109) Issue #109 is the same Boost 1.66 `my_parse` regression as #82 and #99, reached by a different path: reading an nvp that is not present fails and consumes the closing tag, so at destruction `windup()` meets end of input and (before the fix) threw out of the noexcept destructor, terminating the process. The `my_parse` fix already resolves it; this adds the guarding test. It reads a value, then a missing nvp (catching the exception), and lets the archive destruct: it aborts before the fix and passes after. Refs issue #109. --- test/Jamfile.v2 | 1 + test/test_xml_missing_nvp.cpp | 58 +++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 test/test_xml_missing_nvp.cpp diff --git a/test/Jamfile.v2 b/test/Jamfile.v2 index efa84daabc..6800c81d42 100644 --- a/test/Jamfile.v2 +++ b/test/Jamfile.v2 @@ -152,6 +152,7 @@ if ! $(BOOST_ARCHIVE_LIST) { [ test-bsl-run test_reset_object_address : A ] [ test-bsl-run test_void_cast ] [ test-bsl-run test_xml_trailing_whitespace ] + [ test-bsl-run test_xml_missing_nvp ] [ test-bsl-run test_mult_archive_types : : : [ requires std_wstreambuf ] ] [ test-bsl-run test_iterators : : : [ requires std_wstreambuf ] ] [ test-bsl-run test_iterators_base64 ] diff --git a/test/test_xml_missing_nvp.cpp b/test/test_xml_missing_nvp.cpp new file mode 100644 index 0000000000..054e61fec3 --- /dev/null +++ b/test/test_xml_missing_nvp.cpp @@ -0,0 +1,58 @@ +/////////1/////////2/////////3/////////4/////////5/////////6/////////7/////////8 +// test_xml_missing_nvp.cpp + +// Copyright 2026 Gennaro Prota +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +// Regression test for issue #109. Attempting to read an nvp that is not +// present throws archive_exception, which the caller may catch. The failed +// read consumes the closing tag, so at destruction windup() reaches end of +// input -- the same Boost 1.66 my_parse regression as #82/#99, reached by a +// different path. The input archive must still destruct without terminating. + +#include +#include + +#include +#include +#include +#include + +#include "test_tools.hpp" + +int test_main(int /* argc */, char * /* argv */ []){ + // Build a valid XML archive holding a single value. + std::string content; + { + std::ostringstream os; + { + boost::archive::xml_oarchive oa(os); + const int x = 42; + oa << boost::serialization::make_nvp("x", x); + } + content = os.str(); + } + + int x = 0; + bool caught = false; + { + std::istringstream is(content); + boost::archive::xml_iarchive ia(is); + ia >> boost::serialization::make_nvp("x", x); + try { + int y = 0; + ia >> boost::serialization::make_nvp("not_there", y); + } catch (const boost::archive::archive_exception &){ + caught = true; + } + // `ia` is destroyed here, after the caught exception. Before the fix, + // windup() hit end of input (the failed read consumed the closing + // tag) and threw out of the noexcept destructor, terminating. + } + BOOST_CHECK(42 == x); + BOOST_CHECK(caught); + + return EXIT_SUCCESS; +}