Utility::FormatDateTime(): handle invalid format strings on Windows

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
This commit is contained in:
Julian Brost 2024-08-21 12:08:04 +02:00
parent 0285028689
commit d5b3ffaa6d
2 changed files with 40 additions and 0 deletions

View File

@ -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.

View File

@ -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);