Skip to content

Commit e46bebb

Browse files
committed
Merge bitcoin/bitcoin#30546: util: Use consteval checked format string in FatalErrorf, LogConnectFailure
fa5bc45 util: Use compile-time check for LogConnectFailure (MarcoFalke) fa7087b util: Use compile-time check for FatalErrorf (MarcoFalke) faa62c0 util: Add ConstevalFormatString (MarcoFalke) fae7b83 lint: Remove forbidden functions from lint-format-strings.py (MarcoFalke) Pull request description: The `test/lint/lint-format-strings.py` was designed to count the number of format specifiers and assert that they are equal to the number of parameters passed to the format function. The goal seems reasonable, but the implementation has many problems: * It is written in Python, meaning that C++ code can not be parsed correctly. Currently it relies on brittle regex and string parsing. * Apart from the parsing errors, there are also many logic errors. For example, `count_format_specifiers` allows a mix of positional specifiers and non-positional specifiers, which can lead to runtime format bugs. Also, `count_format_specifiers` silently skipped over "special" format specifiers, which are valid in tinyformat, which again can lead to runtime format bugs being undetected. * The brittle logic has a history of breaking in pull requests that are otherwise fine. This causes the CI to fail and the pull request being blocked from progress until the bug in the linter is fixed, or the code is rewritten to work around the bug. * It is only run in the CI, or when the developer invokes the script. It would be better if the developer got the error message at compile-time, directly when writing the code. Fix all issues by using a `consteval` checked format string in `FatalErrorf` and `LogConnectFailure`. This is the first step toward bitcoin/bitcoin#30530 and a follow-up will apply the approach to the other places. ACKs for top commit: stickies-v: re-ACK fa5bc45 l0rinc: ACK fa5bc45 hodlinator: ACK fa5bc45 ryanofsky: Code review ACK fa5bc45 Tree-SHA512: d6189096b16083143687ed1b1559cf4f92f97dd87bc5d00673e44f4fb9fce7bb7b215cfdfc39b6e6a24f0b75a79a03ededce966639e554f7172e1fc22cf015ae
2 parents be768db + fa5bc45 commit e46bebb

File tree

8 files changed

+166
-20
lines changed

8 files changed

+166
-20
lines changed

src/index/base.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2017-2022 The Bitcoin Core developers
1+
// Copyright (c) 2017-present The Bitcoin Core developers
22
// Distributed under the MIT software license, see the accompanying
33
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
44

@@ -14,6 +14,7 @@
1414
#include <node/database_args.h>
1515
#include <node/interface_ui.h>
1616
#include <tinyformat.h>
17+
#include <util/string.h>
1718
#include <util/thread.h>
1819
#include <util/translation.h>
1920
#include <validation.h> // For g_chainman
@@ -27,7 +28,7 @@ constexpr auto SYNC_LOG_INTERVAL{30s};
2728
constexpr auto SYNC_LOCATOR_WRITE_INTERVAL{30s};
2829

2930
template <typename... Args>
30-
void BaseIndex::FatalErrorf(const char* fmt, const Args&... args)
31+
void BaseIndex::FatalErrorf(util::ConstevalFormatString<sizeof...(Args)> fmt, const Args&... args)
3132
{
3233
auto message = tfm::format(fmt, args...);
3334
node::AbortNode(m_chain->context()->shutdown, m_chain->context()->exit_status, Untranslated(message), m_chain->context()->warnings.get());

src/index/base.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2017-2022 The Bitcoin Core developers
1+
// Copyright (c) 2017-present The Bitcoin Core developers
22
// Distributed under the MIT software license, see the accompanying
33
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
44

@@ -7,6 +7,7 @@
77

88
#include <dbwrapper.h>
99
#include <interfaces/chain.h>
10+
#include <util/string.h>
1011
#include <util/threadinterrupt.h>
1112
#include <validationinterface.h>
1213

@@ -94,7 +95,7 @@ class BaseIndex : public CValidationInterface
9495
virtual bool AllowPrune() const = 0;
9596

9697
template <typename... Args>
97-
void FatalErrorf(const char* fmt, const Args&... args);
98+
void FatalErrorf(util::ConstevalFormatString<sizeof...(Args)> fmt, const Args&... args);
9899

99100
protected:
100101
std::unique_ptr<interfaces::Chain> m_chain;

src/netbase.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,8 @@ std::unique_ptr<Sock> CreateSockOS(int domain, int type, int protocol)
557557
std::function<std::unique_ptr<Sock>(int, int, int)> CreateSock = CreateSockOS;
558558

559559
template<typename... Args>
560-
static void LogConnectFailure(bool manual_connection, const char* fmt, const Args&... args) {
560+
static void LogConnectFailure(bool manual_connection, util::ConstevalFormatString<sizeof...(Args)> fmt, const Args&... args)
561+
{
561562
std::string error_message = tfm::format(fmt, args...);
562563
if (manual_connection) {
563564
LogPrintf("%s\n", error_message);

src/test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ add_executable(test_bitcoin
132132
txvalidation_tests.cpp
133133
txvalidationcache_tests.cpp
134134
uint256_tests.cpp
135+
util_string_tests.cpp
135136
util_tests.cpp
136137
util_threadnames_tests.cpp
137138
validation_block_tests.cpp

src/test/util_string_tests.cpp

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright (c) 2024-present The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <util/string.h>
6+
7+
#include <boost/test/unit_test.hpp>
8+
9+
using namespace util;
10+
11+
BOOST_AUTO_TEST_SUITE(util_string_tests)
12+
13+
// Helper to allow compile-time sanity checks while providing the number of
14+
// args directly. Normally PassFmt<sizeof...(Args)> would be used.
15+
template <unsigned NumArgs>
16+
inline void PassFmt(util::ConstevalFormatString<NumArgs> fmt)
17+
{
18+
// This was already executed at compile-time, but is executed again at run-time to avoid -Wunused.
19+
decltype(fmt)::Detail_CheckNumFormatSpecifiers(fmt.fmt);
20+
}
21+
template <unsigned WrongNumArgs>
22+
inline void FailFmtWithError(std::string_view wrong_fmt, std::string_view error)
23+
{
24+
using ErrType = const char*;
25+
auto check_throw{[error](const ErrType& str) { return str == error; }};
26+
BOOST_CHECK_EXCEPTION(util::ConstevalFormatString<WrongNumArgs>::Detail_CheckNumFormatSpecifiers(wrong_fmt), ErrType, check_throw);
27+
}
28+
29+
BOOST_AUTO_TEST_CASE(ConstevalFormatString_NumSpec)
30+
{
31+
PassFmt<0>("");
32+
PassFmt<0>("%%");
33+
PassFmt<1>("%s");
34+
PassFmt<0>("%%s");
35+
PassFmt<0>("s%%");
36+
PassFmt<1>("%%%s");
37+
PassFmt<1>("%s%%");
38+
PassFmt<0>(" 1$s");
39+
PassFmt<1>("%1$s");
40+
PassFmt<1>("%1$s%1$s");
41+
PassFmt<2>("%2$s");
42+
PassFmt<2>("%2$s 4$s %2$s");
43+
PassFmt<129>("%129$s 999$s %2$s");
44+
PassFmt<1>("%02d");
45+
PassFmt<1>("%+2s");
46+
PassFmt<1>("%.6i");
47+
PassFmt<1>("%5.2f");
48+
PassFmt<1>("%#x");
49+
PassFmt<1>("%1$5i");
50+
PassFmt<1>("%1$-5i");
51+
PassFmt<1>("%1$.5i");
52+
// tinyformat accepts almost any "type" spec, even '%', or '_', or '\n'.
53+
PassFmt<1>("%123%");
54+
PassFmt<1>("%123%s");
55+
PassFmt<1>("%_");
56+
PassFmt<1>("%\n");
57+
58+
// The `*` specifier behavior is unsupported and can lead to runtime
59+
// errors when used in a ConstevalFormatString. Please refer to the
60+
// note in the ConstevalFormatString docs.
61+
PassFmt<1>("%*c");
62+
PassFmt<2>("%2$*3$d");
63+
PassFmt<1>("%.*f");
64+
65+
auto err_mix{"Format specifiers must be all positional or all non-positional!"};
66+
FailFmtWithError<1>("%s%1$s", err_mix);
67+
68+
auto err_num{"Format specifier count must match the argument count!"};
69+
FailFmtWithError<1>("", err_num);
70+
FailFmtWithError<0>("%s", err_num);
71+
FailFmtWithError<2>("%s", err_num);
72+
FailFmtWithError<0>("%1$s", err_num);
73+
FailFmtWithError<2>("%1$s", err_num);
74+
75+
auto err_0_pos{"Positional format specifier must have position of at least 1"};
76+
FailFmtWithError<1>("%$s", err_0_pos);
77+
FailFmtWithError<1>("%$", err_0_pos);
78+
FailFmtWithError<0>("%0$", err_0_pos);
79+
FailFmtWithError<0>("%0$s", err_0_pos);
80+
81+
auto err_term{"Format specifier incorrectly terminated by end of string"};
82+
FailFmtWithError<1>("%", err_term);
83+
FailFmtWithError<1>("%1$", err_term);
84+
}
85+
86+
BOOST_AUTO_TEST_SUITE_END()

src/util/string.h

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
// Copyright (c) 2019-2022 The Bitcoin Core developers
1+
// Copyright (c) 2019-present The Bitcoin Core developers
22
// Distributed under the MIT software license, see the accompanying
33
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
44

55
#ifndef BITCOIN_UTIL_STRING_H
66
#define BITCOIN_UTIL_STRING_H
77

88
#include <span.h>
9+
#include <tinyformat.h>
910

1011
#include <array>
1112
#include <cstdint>
@@ -17,6 +18,67 @@
1718
#include <vector>
1819

1920
namespace util {
21+
/**
22+
* @brief A wrapper for a compile-time partially validated format string
23+
*
24+
* This struct can be used to enforce partial compile-time validation of format
25+
* strings, to reduce the likelihood of tinyformat throwing exceptions at
26+
* run-time. Validation is partial to try and prevent the most common errors
27+
* while avoiding re-implementing the entire parsing logic.
28+
*
29+
* @note Counting of `*` dynamic width and precision fields (such as `%*c`,
30+
* `%2$*3$d`, `%.*f`) is not implemented to minimize code complexity as long as
31+
* they are not used in the codebase. Usage of these fields is not counted and
32+
* can lead to run-time exceptions. Code wanting to use the `*` specifier can
33+
* side-step this struct and call tinyformat directly.
34+
*/
35+
template <unsigned num_params>
36+
struct ConstevalFormatString {
37+
const char* const fmt;
38+
consteval ConstevalFormatString(const char* str) : fmt{str} { Detail_CheckNumFormatSpecifiers(fmt); }
39+
constexpr static void Detail_CheckNumFormatSpecifiers(std::string_view str)
40+
{
41+
unsigned count_normal{0}; // Number of "normal" specifiers, like %s
42+
unsigned count_pos{0}; // Max number in positional specifier, like %8$s
43+
for (auto it{str.begin()}; it < str.end();) {
44+
if (*it != '%') {
45+
++it;
46+
continue;
47+
}
48+
49+
if (++it >= str.end()) throw "Format specifier incorrectly terminated by end of string";
50+
if (*it == '%') {
51+
// Percent escape: %%
52+
++it;
53+
continue;
54+
}
55+
56+
unsigned maybe_num{0};
57+
while ('0' <= *it && *it <= '9') {
58+
maybe_num *= 10;
59+
maybe_num += *it - '0';
60+
++it;
61+
};
62+
63+
if (*it == '$') {
64+
// Positional specifier, like %8$s
65+
if (maybe_num == 0) throw "Positional format specifier must have position of at least 1";
66+
count_pos = std::max(count_pos, maybe_num);
67+
if (++it >= str.end()) throw "Format specifier incorrectly terminated by end of string";
68+
} else {
69+
// Non-positional specifier, like %s
70+
++count_normal;
71+
++it;
72+
}
73+
// The remainder "[flags][width][.precision][length]type" of the
74+
// specifier is not checked. Parsing continues with the next '%'.
75+
}
76+
if (count_normal && count_pos) throw "Format specifiers must be all positional or all non-positional!";
77+
unsigned count{count_normal | count_pos};
78+
if (num_params != count) throw "Format specifier count must match the argument count!";
79+
}
80+
};
81+
2082
void ReplaceAll(std::string& in_out, const std::string& search, const std::string& substitute);
2183

2284
/** Split a string on any char found in separators, returning a vector.
@@ -173,4 +235,12 @@ template <typename T1, size_t PREFIX_LEN>
173235
}
174236
} // namespace util
175237

238+
namespace tinyformat {
239+
template <typename... Args>
240+
std::string format(util::ConstevalFormatString<sizeof...(Args)> fmt, const Args&... args)
241+
{
242+
return format(fmt.fmt, args...);
243+
}
244+
} // namespace tinyformat
245+
176246
#endif // BITCOIN_UTIL_STRING_H

test/lint/lint-format-strings.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,15 @@
1616
import sys
1717

1818
FUNCTION_NAMES_AND_NUMBER_OF_LEADING_ARGUMENTS = [
19-
'FatalErrorf,0',
20-
'fprintf,1',
2119
'tfm::format,1', # Assuming tfm::::format(std::ostream&, ...
22-
'LogConnectFailure,1',
2320
'LogError,0',
2421
'LogWarning,0',
2522
'LogInfo,0',
2623
'LogDebug,1',
2724
'LogTrace,1',
2825
'LogPrintf,0',
2926
'LogPrintLevel,2',
30-
'printf,0',
31-
'snprintf,2',
32-
'sprintf,1',
3327
'strprintf,0',
34-
'vfprintf,1',
35-
'vprintf,1',
36-
'vsnprintf,1',
37-
'vsprintf,1',
3828
'WalletLogPrintf,0',
3929
]
4030
RUN_LINT_FILE = 'test/lint/run-lint-format-strings.py'

test/lint/run-lint-format-strings.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,6 @@
1313
import sys
1414

1515
FALSE_POSITIVES = [
16-
("src/dbwrapper.cpp", "vsnprintf(p, limit - p, format, backup_ap)"),
17-
("src/index/base.cpp", "FatalErrorf(const char* fmt, const Args&... args)"),
18-
("src/index/base.h", "FatalErrorf(const char* fmt, const Args&... args)"),
19-
("src/netbase.cpp", "LogConnectFailure(bool manual_connection, const char* fmt, const Args&... args)"),
2016
("src/clientversion.cpp", "strprintf(_(COPYRIGHT_HOLDERS).translated, COPYRIGHT_HOLDERS_SUBSTITUTION)"),
2117
("src/test/translation_tests.cpp", "strprintf(format, arg)"),
2218
("src/validationinterface.cpp", "LogDebug(BCLog::VALIDATION, fmt \"\\n\", __VA_ARGS__)"),

0 commit comments

Comments
 (0)