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.
This commit is contained in:
Gennaro Prota
2026-07-20 11:05:45 +02:00
parent 64c2f11a66
commit db2610e1e6
5 changed files with 85 additions and 4 deletions
@@ -189,7 +189,14 @@ xml_iarchive_impl<Archive>::~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
@@ -177,7 +177,14 @@ xml_wiarchive_impl<Archive>::~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
}
}
+9 -2
View File
@@ -191,6 +191,15 @@ bool basic_xml_grammar<CharType>::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<CharType>::my_parse(
)
);
}
if(is.eof())
return false;
arg += result;
if(result == delimiter)
break;
+1
View File
@@ -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 ]
+59
View File
@@ -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 <sstream>
#include <string>
#include <boost/archive/xml_oarchive.hpp>
#include <boost/archive/xml_iarchive.hpp>
#include <boost/serialization/nvp.hpp>
#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_serialization>");
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;
}