Skip to content

Commit ecfac10

Browse files
committed
merge bitcoin#22937: Forbid calling unsafe fs::path(std::string) constructor and fs::path::string() method
1 parent 23fe7e2 commit ecfac10

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+376
-224
lines changed

src/addrdb.cpp

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ bool SerializeFileDB(const std::string& prefix, const fs::path& path, const Data
5858
if (fileout.IsNull()) {
5959
fileout.fclose();
6060
remove(pathTmp);
61-
return error("%s: Failed to open file %s", __func__, pathTmp.string());
61+
return error("%s: Failed to open file %s", __func__, fs::PathToString(pathTmp));
6262
}
6363

6464
// Serialize
@@ -70,7 +70,7 @@ bool SerializeFileDB(const std::string& prefix, const fs::path& path, const Data
7070
if (!FileCommit(fileout.Get())) {
7171
fileout.fclose();
7272
remove(pathTmp);
73-
return error("%s: Failed to flush file %s", __func__, pathTmp.string());
73+
return error("%s: Failed to flush file %s", __func__, fs::PathToString(pathTmp));
7474
}
7575
fileout.fclose();
7676

@@ -122,8 +122,8 @@ void DeserializeFileDB(const fs::path& path, Data& data, int version)
122122
} // namespace
123123

124124
CBanDB::CBanDB(fs::path ban_list_path)
125-
: m_banlist_dat(ban_list_path.string() + ".dat"),
126-
m_banlist_json(ban_list_path.string() + ".json")
125+
: m_banlist_dat(ban_list_path + ".dat"),
126+
m_banlist_json(ban_list_path + ".json")
127127
{
128128
}
129129

@@ -143,7 +143,7 @@ bool CBanDB::Write(const banmap_t& banSet)
143143
bool CBanDB::Read(banmap_t& banSet)
144144
{
145145
if (fs::exists(m_banlist_dat)) {
146-
LogPrintf("banlist.dat ignored because it can only be read by " PACKAGE_NAME " version 19.x. Remove %s to silence this warning.\n", m_banlist_dat);
146+
LogPrintf("banlist.dat ignored because it can only be read by " PACKAGE_NAME " version 19.x. Remove %s to silence this warning.\n", fs::quoted(fs::PathToString(m_banlist_dat)));
147147
}
148148
// If the JSON banlist does not exist, then recreate it
149149
if (!fs::exists(m_banlist_json)) {
@@ -155,15 +155,15 @@ bool CBanDB::Read(banmap_t& banSet)
155155

156156
if (!util::ReadSettings(m_banlist_json, settings, errors)) {
157157
for (const auto& err : errors) {
158-
LogPrintf("Cannot load banlist %s: %s\n", m_banlist_json.string(), err);
158+
LogPrintf("Cannot load banlist %s: %s\n", fs::PathToString(m_banlist_json), err);
159159
}
160160
return false;
161161
}
162162

163163
try {
164164
BanMapFromJson(settings[JSON_KEY], banSet);
165165
} catch (const std::runtime_error& e) {
166-
LogPrintf("Cannot parse banlist %s: %s\n", m_banlist_json.string(), e.what());
166+
LogPrintf("Cannot parse banlist %s: %s\n", fs::PathToString(m_banlist_json), e.what());
167167
return false;
168168
}
169169

@@ -194,7 +194,7 @@ std::optional<bilingual_str> LoadAddrman(const std::vector<bool>& asmap, const A
194194
} catch (const DbNotFoundError&) {
195195
// Addrman can be in an inconsistent state after failure, reset it
196196
addrman = std::make_unique<AddrMan>(asmap, /* deterministic */ false, /* consistency_check_ratio */ check_addrman);
197-
LogPrintf("Creating peers.dat because the file was not found (%s)\n", path_addr);
197+
LogPrintf("Creating peers.dat because the file was not found (%s)\n", fs::quoted(fs::PathToString(path_addr)));
198198
DumpPeerAddresses(args, *addrman);
199199
} catch (const DbInconsistentError& e) {
200200
// Addrman has shown a tendency to corrupt itself even with graceful shutdowns on known-good
@@ -208,7 +208,7 @@ std::optional<bilingual_str> LoadAddrman(const std::vector<bool>& asmap, const A
208208
} catch (const std::exception& e) {
209209
addrman = nullptr;
210210
return strprintf(_("Invalid or corrupt peers.dat (%s). If you believe this is a bug, please report it to %s. As a workaround, you can move the file (%s) out of the way (rename, move, or delete) to have a new one created on the next start."),
211-
e.what(), PACKAGE_BUGREPORT, path_addr);
211+
e.what(), PACKAGE_BUGREPORT, fs::quoted(fs::PathToString(path_addr)));
212212
}
213213
return std::nullopt;
214214
}
@@ -224,7 +224,7 @@ std::vector<CAddress> ReadAnchors(const fs::path& anchors_db_path)
224224
std::vector<CAddress> anchors;
225225
try {
226226
DeserializeFileDB(anchors_db_path, anchors, CLIENT_VERSION | ADDRV2_FORMAT);
227-
LogPrintf("Loaded %i addresses from %s\n", anchors.size(), anchors_db_path.filename());
227+
LogPrintf("Loaded %i addresses from %s\n", anchors.size(), fs::quoted(fs::PathToString(anchors_db_path.filename())));
228228
} catch (const std::exception&) {
229229
anchors.clear();
230230
}

src/bitcoin-cli.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -818,7 +818,7 @@ static UniValue CallRPC(BaseRequestHandler* rh, const std::string& strMethod, co
818818
if (failedToGetAuthCookie) {
819819
throw std::runtime_error(strprintf(
820820
"Could not locate RPC credentials. No authentication cookie could be found, and RPC password is not set. See -rpcpassword and -stdinrpcpass. Configuration file: (%s)",
821-
GetConfigFile(gArgs.GetArg("-conf", BITCOIN_CONF_FILENAME)).string()));
821+
fs::PathToString(GetConfigFile(gArgs.GetArg("-conf", BITCOIN_CONF_FILENAME)))));
822822
} else {
823823
throw std::runtime_error("Authorization failed: Incorrect rpcuser or rpcpassword");
824824
}

src/dbwrapper.cpp

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ static leveldb::Options GetOptions(size_t nCacheSize)
116116
}
117117

118118
CDBWrapper::CDBWrapper(const fs::path& path, size_t nCacheSize, bool fMemory, bool fWipe, bool obfuscate)
119-
: m_name{path.stem().string()}
119+
: m_name{fs::PathToString(path.stem())}
120120
{
121121
penv = nullptr;
122122
readoptions.verify_checksums = true;
@@ -130,21 +130,21 @@ CDBWrapper::CDBWrapper(const fs::path& path, size_t nCacheSize, bool fMemory, bo
130130
options.env = penv;
131131
} else {
132132
if (fWipe) {
133-
LogPrintf("Wiping LevelDB in %s\n", path.string());
134-
leveldb::Status result = leveldb::DestroyDB(path.string(), options);
133+
LogPrintf("Wiping LevelDB in %s\n", fs::PathToString(path));
134+
leveldb::Status result = leveldb::DestroyDB(fs::PathToString(path), options);
135135
dbwrapper_private::HandleError(result);
136136
}
137137
TryCreateDirectories(path);
138-
LogPrintf("Opening LevelDB in %s\n", path.string());
138+
LogPrintf("Opening LevelDB in %s\n", fs::PathToString(path));
139139
}
140-
leveldb::Status status = leveldb::DB::Open(options, path.string(), &pdb);
140+
leveldb::Status status = leveldb::DB::Open(options, fs::PathToString(path), &pdb);
141141
dbwrapper_private::HandleError(status);
142142
LogPrintf("Opened LevelDB successfully\n");
143143

144144
if (gArgs.GetBoolArg("-forcecompactdb", false)) {
145-
LogPrintf("Starting database compaction of %s\n", path.string());
145+
LogPrintf("Starting database compaction of %s\n", fs::PathToString(path));
146146
pdb->CompactRange(nullptr, nullptr);
147-
LogPrintf("Finished database compaction of %s\n", path.string());
147+
LogPrintf("Finished database compaction of %s\n", fs::PathToString(path));
148148
}
149149

150150
// The base-case obfuscation key, which is a noop.
@@ -161,10 +161,10 @@ CDBWrapper::CDBWrapper(const fs::path& path, size_t nCacheSize, bool fMemory, bo
161161
Write(OBFUSCATE_KEY_KEY, new_key);
162162
obfuscate_key = new_key;
163163

164-
LogPrintf("Wrote new obfuscate key for %s: %s\n", path.string(), HexStr(obfuscate_key));
164+
LogPrintf("Wrote new obfuscate key for %s: %s\n", fs::PathToString(path), HexStr(obfuscate_key));
165165
}
166166

167-
LogPrintf("Using obfuscation key for %s: %s\n", path.string(), HexStr(obfuscate_key));
167+
LogPrintf("Using obfuscation key for %s: %s\n", fs::PathToString(path), HexStr(obfuscate_key));
168168
}
169169

170170
CDBWrapper::~CDBWrapper()

src/flat-database.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@ class CFlatDB
5151
ssObj << hash;
5252

5353
// open output file, and associate with CAutoFile
54-
FILE *file = fopen(pathDB.string().c_str(), "wb");
54+
FILE *file = fsbridge::fopen(pathDB, "wb");
5555
CAutoFile fileout(file, SER_DISK, CLIENT_VERSION);
5656
if (fileout.IsNull())
57-
return error("%s: Failed to open file %s", __func__, pathDB.string());
57+
return error("%s: Failed to open file %s", __func__, fs::PathToString(pathDB));
5858

5959
// Write and commit header, data
6060
try {
@@ -77,11 +77,11 @@ class CFlatDB
7777

7878
int64_t nStart = GetTimeMillis();
7979
// open input file, and associate with CAutoFile
80-
FILE *file = fopen(pathDB.string().c_str(), "rb");
80+
FILE *file = fsbridge::fopen(pathDB, "rb");
8181
CAutoFile filein(file, SER_DISK, CLIENT_VERSION);
8282
if (filein.IsNull())
8383
{
84-
error("%s: Failed to open file %s", __func__, pathDB.string());
84+
error("%s: Failed to open file %s", __func__, fs::PathToString(pathDB));
8585
return FileError;
8686
}
8787

src/flatfile.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,11 @@ FILE* FlatFileSeq::Open(const FlatFilePos& pos, bool read_only)
4141
if (!file && !read_only)
4242
file = fsbridge::fopen(path, "wb+");
4343
if (!file) {
44-
LogPrintf("Unable to open file %s\n", path.string());
44+
LogPrintf("Unable to open file %s\n", fs::PathToString(path));
4545
return nullptr;
4646
}
4747
if (pos.nPos && fseek(file, pos.nPos, SEEK_SET)) {
48-
LogPrintf("Unable to seek to position %u of %s\n", pos.nPos, path.string());
48+
LogPrintf("Unable to seek to position %u of %s\n", pos.nPos, fs::PathToString(path));
4949
fclose(file);
5050
return nullptr;
5151
}

src/fs.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ namespace fsbridge {
2525
FILE *fopen(const fs::path& p, const char *mode)
2626
{
2727
#ifndef WIN32
28-
return ::fopen(p.string().c_str(), mode);
28+
return ::fopen(p.c_str(), mode);
2929
#else
3030
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>,wchar_t> utf8_cvt;
3131
return ::_wfopen(p.wstring().c_str(), utf8_cvt.from_bytes(mode).c_str());
@@ -47,7 +47,7 @@ static std::string GetErrorReason()
4747

4848
FileLock::FileLock(const fs::path& file)
4949
{
50-
fd = open(file.string().c_str(), O_RDWR);
50+
fd = open(file.c_str(), O_RDWR);
5151
if (fd == -1) {
5252
reason = GetErrorReason();
5353
}
@@ -244,9 +244,9 @@ void ofstream::close()
244244
#else // __GLIBCXX__
245245

246246
#if BOOST_VERSION >= 107700
247-
static_assert(sizeof(*BOOST_FILESYSTEM_C_STR(fs::path())) == sizeof(wchar_t),
247+
static_assert(sizeof(*BOOST_FILESYSTEM_C_STR(boost::filesystem::path())) == sizeof(wchar_t),
248248
#else
249-
static_assert(sizeof(*fs::path().BOOST_FILESYSTEM_C_STR) == sizeof(wchar_t),
249+
static_assert(sizeof(*boost::filesystem::path().BOOST_FILESYSTEM_C_STR) == sizeof(wchar_t),
250250
#endif // BOOST_VERSION >= 107700
251251
"Warning: This build is using boost::filesystem ofstream and ifstream "
252252
"implementations which will fail to open paths containing multibyte "

src/fs.h

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,132 @@
1313

1414
#include <boost/filesystem.hpp>
1515
#include <boost/filesystem/fstream.hpp>
16+
#include <tinyformat.h>
1617

1718
/** Filesystem operations and types */
18-
namespace fs = boost::filesystem;
19+
namespace fs {
20+
21+
using namespace boost::filesystem;
22+
23+
/**
24+
* Path class wrapper to prepare application code for transition from
25+
* boost::filesystem library to std::filesystem implementation. The main
26+
* purpose of the class is to define fs::path::u8string() and fs::u8path()
27+
* functions not present in boost. It also blocks calls to the
28+
* fs::path(std::string) implicit constructor and the fs::path::string()
29+
* method, which worked well in the boost::filesystem implementation, but have
30+
* unsafe and unpredictable behavior on Windows in the std::filesystem
31+
* implementation (see implementation note in \ref PathToString for details).
32+
*/
33+
class path : public boost::filesystem::path
34+
{
35+
public:
36+
using boost::filesystem::path::path;
37+
38+
// Allow path objects arguments for compatibility.
39+
path(boost::filesystem::path path) : boost::filesystem::path::path(std::move(path)) {}
40+
path& operator=(boost::filesystem::path path) { boost::filesystem::path::operator=(std::move(path)); return *this; }
41+
path& operator/=(boost::filesystem::path path) { boost::filesystem::path::operator/=(std::move(path)); return *this; }
42+
43+
// Allow literal string arguments, which are safe as long as the literals are ASCII.
44+
path(const char* c) : boost::filesystem::path(c) {}
45+
path& operator=(const char* c) { boost::filesystem::path::operator=(c); return *this; }
46+
path& operator/=(const char* c) { boost::filesystem::path::operator/=(c); return *this; }
47+
path& append(const char* c) { boost::filesystem::path::append(c); return *this; }
48+
49+
// Disallow std::string arguments to avoid locale-dependent decoding on windows.
50+
path(std::string) = delete;
51+
path& operator=(std::string) = delete;
52+
path& operator/=(std::string) = delete;
53+
path& append(std::string) = delete;
54+
55+
// Disallow std::string conversion method to avoid locale-dependent encoding on windows.
56+
std::string string() const = delete;
57+
58+
// Define UTF-8 string conversion method not present in boost::filesystem but present in std::filesystem.
59+
std::string u8string() const { return boost::filesystem::path::string(); }
60+
};
61+
62+
// Define UTF-8 string conversion function not present in boost::filesystem but present in std::filesystem.
63+
static inline path u8path(const std::string& string)
64+
{
65+
return boost::filesystem::path(string);
66+
}
67+
68+
// Disallow implicit std::string conversion for system_complete to avoid
69+
// locale-dependent encoding on windows.
70+
static inline path system_complete(const path& p)
71+
{
72+
return boost::filesystem::system_complete(p);
73+
}
74+
75+
// Disallow implicit std::string conversion for exists to avoid
76+
// locale-dependent encoding on windows.
77+
static inline bool exists(const path& p)
78+
{
79+
return boost::filesystem::exists(p);
80+
}
81+
82+
// Allow explicit quoted stream I/O.
83+
static inline auto quoted(const std::string& s)
84+
{
85+
return boost::io::quoted(s, '&');
86+
}
87+
88+
// Allow safe path append operations.
89+
static inline path operator+(path p1, path p2)
90+
{
91+
p1 += std::move(p2);
92+
return p1;
93+
}
94+
95+
/**
96+
* Convert path object to byte string. On POSIX, paths natively are byte
97+
* strings so this is trivial. On Windows, paths natively are Unicode, so an
98+
* encoding step is necessary.
99+
*
100+
* The inverse of \ref PathToString is \ref PathFromString. The strings
101+
* returned and parsed by these functions can be used to call POSIX APIs, and
102+
* for roundtrip conversion, logging, and debugging. But they are not
103+
* guaranteed to be valid UTF-8, and are generally meant to be used internally,
104+
* not externally. When communicating with external programs and libraries that
105+
* require UTF-8, fs::path::u8string() and fs::u8path() methods can be used.
106+
* For other applications, if support for non UTF-8 paths is required, or if
107+
* higher-level JSON or XML or URI or C-style escapes are preferred, it may be
108+
* also be appropriate to use different path encoding functions.
109+
*
110+
* Implementation note: On Windows, the std::filesystem::path(string)
111+
* constructor and std::filesystem::path::string() method are not safe to use
112+
* here, because these methods encode the path using C++'s narrow multibyte
113+
* encoding, which on Windows corresponds to the current "code page", which is
114+
* unpredictable and typically not able to represent all valid paths. So
115+
* std::filesystem::path::u8string() and std::filesystem::u8path() functions
116+
* are used instead on Windows. On POSIX, u8string/u8path functions are not
117+
* safe to use because paths are not always valid UTF-8, so plain string
118+
* methods which do not transform the path there are used.
119+
*/
120+
static inline std::string PathToString(const path& path)
121+
{
122+
#ifdef WIN32
123+
return path.u8string();
124+
#else
125+
static_assert(std::is_same<path::string_type, std::string>::value, "PathToString not implemented on this platform");
126+
return path.boost::filesystem::path::string();
127+
#endif
128+
}
129+
130+
/**
131+
* Convert byte string to path object. Inverse of \ref PathToString.
132+
*/
133+
static inline path PathFromString(const std::string& string)
134+
{
135+
#ifdef WIN32
136+
return u8path(string);
137+
#else
138+
return boost::filesystem::path(string);
139+
#endif
140+
}
141+
} // namespace fs
19142

20143
/** Bridge operations to C stdio */
21144
namespace fsbridge {
@@ -103,4 +226,11 @@ namespace fsbridge {
103226
#endif // WIN32 && __GLIBCXX__
104227
};
105228

229+
// Disallow path operator<< formatting in tinyformat to avoid locale-dependent
230+
// encoding on windows.
231+
namespace tinyformat {
232+
template<> inline void formatValue(std::ostream&, const char*, const char*, int, const boost::filesystem::path&) = delete;
233+
template<> inline void formatValue(std::ostream&, const char*, const char*, int, const fs::path&) = delete;
234+
} // namespace tinyformat
235+
106236
#endif // BITCOIN_FS_H

src/i2p.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ void Session::GenerateAndSavePrivateKey(const Sock& sock)
340340
if (!WriteBinaryFile(m_private_key_file,
341341
std::string(m_private_key.begin(), m_private_key.end()))) {
342342
throw std::runtime_error(
343-
strprintf("Cannot save I2P private key to %s", m_private_key_file));
343+
strprintf("Cannot save I2P private key to %s", fs::quoted(fs::PathToString(m_private_key_file))));
344344
}
345345
}
346346

0 commit comments

Comments
 (0)