From b844405532fb53bc3da7d0d24d85f8d3bb31c465 Mon Sep 17 00:00:00 2001 From: Gennaro Prota Date: Thu, 30 Jul 2026 10:50:12 +0200 Subject: [PATCH] fix: XML archive closes a completed document even while unwinding The XML output archive writes the closing `` tag in its destructor, but skipped it whenever an exception was unwinding the stack (`uncaught_exceptions() > 0`). That guard cannot tell an interrupted serialization from a completed one: when a fully written archive happened to be destroyed during unwinding for an unrelated reason (e.g. a later throw), its closing tag was silently dropped, leaving a corrupt file that fails to load (and, via #220, terminates the reader). So, always try to add the closing tag when the document is complete. Fixes #188. --- include/boost/archive/basic_xml_oarchive.hpp | 10 ++++ .../boost/archive/impl/xml_oarchive_impl.ipp | 18 +++++- .../boost/archive/impl/xml_woarchive_impl.ipp | 14 ++++- test/Jamfile.v2 | 1 + test/test_xml_save_during_unwind.cpp | 58 +++++++++++++++++++ 5 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 test/test_xml_save_during_unwind.cpp diff --git a/include/boost/archive/basic_xml_oarchive.hpp b/include/boost/archive/basic_xml_oarchive.hpp index 41d80710e6..7fc6711f59 100644 --- a/include/boost/archive/basic_xml_oarchive.hpp +++ b/include/boost/archive/basic_xml_oarchive.hpp @@ -10,6 +10,7 @@ // basic_xml_oarchive.hpp // (C) Copyright 2002 Robert Ramey - http://www.rrsd.com . +// Copyright 2026 Gennaro Prota. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // http://www.boost.org/LICENSE_1_0.txt) @@ -78,6 +79,15 @@ class BOOST_SYMBOL_VISIBLE basic_xml_oarchive : BOOST_ARCHIVE_OR_WARCHIVE_DECL void end_preamble(); + // True once every element that was opened has been closed, i.e. the + // document is complete and its root element can be closed safely. Used + // by the derived destructor to tell an interrupted serialization (an + // element still open) apart from an unrelated exception unwinding past + // an already-complete archive. + bool document_complete() const { + return 0 == depth; + } + // Anything not an attribute and not a name-value pair is an // error and should be trapped here. template diff --git a/include/boost/archive/impl/xml_oarchive_impl.ipp b/include/boost/archive/impl/xml_oarchive_impl.ipp index 9f74c61d04..428de37446 100644 --- a/include/boost/archive/impl/xml_oarchive_impl.ipp +++ b/include/boost/archive/impl/xml_oarchive_impl.ipp @@ -2,6 +2,7 @@ // xml_oarchive_impl.ipp: // (C) Copyright 2002 Robert Ramey - http://www.rrsd.com . +// Copyright 2026 Gennaro Prota. // Distributed under the Boost Software License, Version 1.0. (See // accompanying file LICENSE_1_0.txt or copy at // http://www.boost.org/LICENSE_1_0.txt) @@ -20,6 +21,7 @@ namespace std{ #endif #include +#include #include #include @@ -129,10 +131,20 @@ xml_oarchive_impl::save_binary(const void *address, std::size_t count){ template BOOST_ARCHIVE_DECL xml_oarchive_impl::~xml_oarchive_impl(){ - if(boost::core::uncaught_exceptions() > 0) + // The closing root tag is written here, at destruction. Skip it only + // when serialization was genuinely interrupted, i.e. when an exception + // is unwinding the stack and an element is still open, so the document + // is already truncated. When the document is complete, we must still + // attempt to close it, even while unwinding. + if(boost::core::uncaught_exceptions() > 0 && ! this->document_complete()){ return; - if(0 == (this->get_flags() & no_header)){ - this->put("\n"); + } + if(0 == (this->get_flags() & no_header) && os.good()){ + BOOST_TRY { + this->put("\n"); + } + BOOST_CATCH(...) {} + BOOST_CATCH_END } } diff --git a/include/boost/archive/impl/xml_woarchive_impl.ipp b/include/boost/archive/impl/xml_woarchive_impl.ipp index dabaf23062..d1d00e76c5 100644 --- a/include/boost/archive/impl/xml_woarchive_impl.ipp +++ b/include/boost/archive/impl/xml_woarchive_impl.ipp @@ -2,6 +2,7 @@ // xml_woarchive_impl.ipp: // (C) Copyright 2002 Robert Ramey - http://www.rrsd.com . +// Copyright 2026 Gennaro Prota. // Distributed under the Boost Software License, Version 1.0. (See // accompanying file LICENSE_1_0.txt or copy at // http://www.boost.org/LICENSE_1_0.txt) @@ -32,6 +33,7 @@ namespace std{ #endif #include +#include #include #include @@ -138,10 +140,16 @@ xml_woarchive_impl::xml_woarchive_impl( template BOOST_WARCHIVE_DECL xml_woarchive_impl::~xml_woarchive_impl(){ - if(boost::core::uncaught_exceptions() > 0) + // See the note in xml_oarchive_impl::~xml_oarchive_impl. + if(boost::core::uncaught_exceptions() > 0 && ! this->document_complete()){ return; - if(0 == (this->get_flags() & no_header)){ - os << L""; + } + if(0 == (this->get_flags() & no_header) && os.good()){ + BOOST_TRY { + os << L""; + } + BOOST_CATCH(...) {} + BOOST_CATCH_END } } diff --git a/test/Jamfile.v2 b/test/Jamfile.v2 index 8f04401847..4bcbd9c91e 100644 --- a/test/Jamfile.v2 +++ b/test/Jamfile.v2 @@ -153,6 +153,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_save_during_unwind ] [ test-bsl-run test_xml_trailing_whitespace ] [ test-bsl-run test_xml_missing_nvp ] [ test-bsl-run test_mult_archive_types : : : [ requires std_wstreambuf ] ] diff --git a/test/test_xml_save_during_unwind.cpp b/test/test_xml_save_during_unwind.cpp new file mode 100644 index 0000000000..a291b8d8ca --- /dev/null +++ b/test/test_xml_save_during_unwind.cpp @@ -0,0 +1,58 @@ +/////////1/////////2/////////3/////////4/////////5/////////6/////////7/////////8 +// test_xml_save_during_unwind.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 #188. The XML output archive writes the closing +// tag in its destructor. It used to skip that write +// whenever an exception was unwinding the stack (uncaught_exceptions() > 0), +// which silently corrupted an otherwise complete archive when it happened to +// be destroyed during unwinding for an unrelated reason. A complete document +// must be closed regardless; only a genuinely interrupted one (an element still +// open) may be left unterminated. + +#include +#include +#include + +#include +#include +#include + +#include "test_tools.hpp" + +int test_main(int /* argc */, char * /* argv */ []){ + // Serialize a complete value, then throw an unrelated exception so the + // archive is destroyed while the stack unwinds. `os` is declared outside + // the try block so it outlives the archive and we can inspect what the + // destructor wrote. + std::ostringstream os; + try { + boost::archive::xml_oarchive oa(os); + const int x = 42; + oa << boost::serialization::make_nvp("x", x); + throw std::runtime_error("unrelated"); + } + catch(const std::runtime_error &){} + + const std::string content = os.str(); + + // The complete document must have been closed despite the unwinding. + BOOST_CHECK( + content.find("") != std::string::npos + ); + + // And it must load back cleanly. + int y = 0; + { + std::istringstream is(content); + boost::archive::xml_iarchive ia(is); + ia >> boost::serialization::make_nvp("x", y); + } + BOOST_CHECK(42 == y); + + return EXIT_SUCCESS; +}