diff --git a/lib/base/utility.cpp b/lib/base/utility.cpp index e1bc42b8d..75b905fe1 100644 --- a/lib/base/utility.cpp +++ b/lib/base/utility.cpp @@ -4,6 +4,7 @@ #include "base/utility.hpp" #include "base/convert.hpp" #include "base/application.hpp" +#include "base/defer.hpp" #include "base/logger.hpp" #include "base/exception.hpp" #include "base/socket.hpp" @@ -1092,6 +1093,32 @@ String Utility::FormatDateTime(const char *format, double ts) } #endif /* _MSC_VER */ +#ifdef _MSC_VER + /* On Windows, the strftime() function family invokes an invalid parameter handler when the format string is + * invalid (see the "Remarks" section in their documentation). std::put_time() shows the same behavior as it + * uses _wcsftime_l() internally. The default invalid parameter handler may terminate the process, which can + * be a problem given that the format string can be specified by the user from the Icinga DSL. + * + * Thus, temporarily set a thread-local no-op handler to disable the default one allowing the program to + * continue. This then simply results in the function returning an error which then results in an exception as + * we ask the stream to throw one. + * + * See also: + * https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/strftime-wcsftime-strftime-l-wcsftime-l?view=msvc-170 + * https://learn.microsoft.com/en-us/cpp/c-runtime-library/parameter-validation?view=msvc-170 + * https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/set-invalid-parameter-handler-set-thread-local-invalid-parameter-handler?view=msvc-170 + */ + + auto oldHandler = _set_thread_local_invalid_parameter_handler( + [](const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, uintptr_t) { + // Intentionally do nothing to continue executing. + }); + + Defer resetHandler([oldHandler]() { + _set_thread_local_invalid_parameter_handler(oldHandler); + }); +#endif /* _MSC_VER */ + char buf[128]; size_t n = strftime(buf, sizeof(buf), format, &tmthen); // On error, n == 0 and an empty string is returned. diff --git a/test/base-utility.cpp b/test/base-utility.cpp index 5c0d358cd..8fe1814da 100644 --- a/test/base-utility.cpp +++ b/test/base-utility.cpp @@ -191,6 +191,19 @@ BOOST_AUTO_TEST_CASE(FormatDateTime) { // not really possible due to limitations in strftime() error handling, see comment in the implementation. BOOST_CHECK_EQUAL("", Utility::FormatDateTime(repeat("%Y", 1000).c_str(), ts)); + // Invalid format strings. + for (const char* format : {"%", "x % y", "x %! y"}) { + std::string result = Utility::FormatDateTime(format, ts); + + // Implementations of strftime() seem to either keep invalid format specifiers and return them in the output, or + // treat them as an error which our implementation currently maps to the empty string due to strftime() not + // properly reporting errors. If this limitation of our implementation is lifted, other behavior like throwing + // an exception would also be valid. + BOOST_CHECK_MESSAGE(result.empty() || result == format, + "FormatDateTime(" << std::quoted(format) << ", " << ts << ") = " << std::quoted(result) << + " should be one of [\"\", " << std::quoted(format) << "]"); + } + // Out of range timestamps. BOOST_CHECK_THROW(Utility::FormatDateTime("%Y", std::nextafter(time_t_limit::min(), -double_limit::infinity())), negative_overflow); BOOST_CHECK_THROW(Utility::FormatDateTime("%Y", std::nextafter(time_t_limit::max(), +double_limit::infinity())), positive_overflow);