Skip to content

Commit

Permalink
tinyformat: don't throw run-time errors for string literals
Browse files Browse the repository at this point in the history
Format string literals are partially validated at compile-time through
util::ConstevalFormatString. In almost all cases, invalid format
strings should not lead to crashing behaviour, so suppress any remaining
run-time format errors by returning the error as a string instead of
throwing a tinyformat::format_error.

Callsites that require throwing behaviour can still use tfm::format_raw.
  • Loading branch information
stickies-v committed Sep 26, 2024
1 parent 5d69f2d commit e48b847
Show file tree
Hide file tree
Showing 4 changed files with 37 additions and 9 deletions.
8 changes: 1 addition & 7 deletions src/logging.h
Original file line number Diff line number Diff line change
Expand Up @@ -249,13 +249,7 @@ template <typename... Args>
inline void LogPrintFormatInternal(std::string_view logging_function, std::string_view source_file, const int source_line, const BCLog::LogFlags flag, const BCLog::Level level, util::ConstevalFormatString<sizeof...(Args)> fmt, const Args&... args)
{
if (LogInstance().Enabled()) {
std::string log_msg;
try {
log_msg = tfm::format(fmt, args...);
} catch (tinyformat::format_error& fmterr) {
/* Original format string will have newline so don't add one here */
log_msg = "Error \"" + std::string{fmterr.what()} + "\" while formatting log message: " + fmt.fmt;
}
const auto log_msg{tfm::format(fmt, args...)};
LogInstance().LogPrintStr(log_msg, logging_function, source_file, source_line, flag, level);
}
}
Expand Down
14 changes: 14 additions & 0 deletions src/test/util_string_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <tinyformat.h>
#include <util/string.h>

#include <boost/test/unit_test.hpp>

#include <sstream>

using namespace util;

BOOST_AUTO_TEST_SUITE(util_string_tests)
Expand Down Expand Up @@ -83,4 +86,15 @@ BOOST_AUTO_TEST_CASE(ConstevalFormatString_NumSpec)
FailFmtWithError<1>("%1$", err_term);
}

BOOST_AUTO_TEST_CASE(tinyformat_ConstevalFormatString)
{
// ensure invalid format strings don't throw at run-time
BOOST_CHECK_EQUAL(tfm::format("%*c", "dummy"), R"(Error "tinyformat: Cannot convert from argument type to integer for use as variable width or precision" while formatting log message: "%*c")");
BOOST_CHECK_EQUAL(tfm::format("%2$*3$d", "dummy", "value"), R"(Error "tinyformat: Positional argument out of range" while formatting log message: "%2$*3$d")");
std::ostringstream oss;
tfm::format(oss, "%.*f", 5);
BOOST_CHECK_EQUAL(oss.view(), R"(Error "tinyformat: Too many conversion specifiers in format string" while formatting log message: "%.*f")");
}


BOOST_AUTO_TEST_SUITE_END()
21 changes: 19 additions & 2 deletions src/tinyformat.h
Original file line number Diff line number Diff line change
Expand Up @@ -1147,15 +1147,32 @@ TINYFORMAT_FOREACH_ARGNUM(TINYFORMAT_MAKE_FORMAT_FUNCS)
#endif

// Added for Bitcoin Core
/**
* Safer alternative to tfm::format_raw that does not throw string
* formatting errors at run-time.
*
* The `fmt` format string is partially checked at compile-time. At
* run-time, if any `tfm::format_error` occurs, the error is caught and
* the error message is returned instead, so the caller doesn't need to
* handle the error.
*/
template <typename... Args>
std::string format(util::ConstevalFormatString<sizeof...(Args)> fmt, const Args&... args)
{
return format_raw(fmt.fmt, args...);
try {
return tfm::format_raw(fmt.fmt, args...);
} catch (tinyformat::format_error& fmterr) {
return "Error \"" + std::string{fmterr.what()} + "\" while formatting log message: \"" + fmt.fmt + "\"";
}
}
template <typename... Args>
void format(std::ostream& out, util::ConstevalFormatString<sizeof...(Args)> fmt, const Args&... args)
{
return format_raw(out, fmt.fmt, args...);
try {
tfm::format_raw(out, fmt.fmt, args...);
} catch (tinyformat::format_error& fmterr) {
out << "Error \"" + std::string{fmterr.what()} + "\" while formatting log message: \"" + fmt.fmt + "\"";
}
}
} // namespace tinyformat

Expand Down
3 changes: 3 additions & 0 deletions test/lint/run-lint-format-strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
FALSE_POSITIVES = [
("src/clientversion.cpp", "strprintf(_(COPYRIGHT_HOLDERS), COPYRIGHT_HOLDERS_SUBSTITUTION)"),
("src/test/translation_tests.cpp", "strprintf(format, arg)"),
("src/test/util_string_tests.cpp", r'tfm::format("%*c", "dummy")'),
("src/test/util_string_tests.cpp", r'tfm::format("%2$*3$d", "dummy", "value")'),
("src/test/util_string_tests.cpp", r'tfm::format(oss, "%.*f", 5)'),
]


Expand Down

0 comments on commit e48b847

Please sign in to comment.