Skip to content

Commit

Permalink
merge bitcoin#22937: Forbid calling unsafe fs::path(std::string) cons…
Browse files Browse the repository at this point in the history
…tructor and fs::path::string() method
  • Loading branch information
kwvg committed Aug 6, 2024
1 parent 23fe7e2 commit ecfac10
Show file tree
Hide file tree
Showing 47 changed files with 376 additions and 224 deletions.
20 changes: 10 additions & 10 deletions src/addrdb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ bool SerializeFileDB(const std::string& prefix, const fs::path& path, const Data
if (fileout.IsNull()) {
fileout.fclose();
remove(pathTmp);
return error("%s: Failed to open file %s", __func__, pathTmp.string());
return error("%s: Failed to open file %s", __func__, fs::PathToString(pathTmp));
}

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

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

CBanDB::CBanDB(fs::path ban_list_path)
: m_banlist_dat(ban_list_path.string() + ".dat"),
m_banlist_json(ban_list_path.string() + ".json")
: m_banlist_dat(ban_list_path + ".dat"),
m_banlist_json(ban_list_path + ".json")
{
}

Expand All @@ -143,7 +143,7 @@ bool CBanDB::Write(const banmap_t& banSet)
bool CBanDB::Read(banmap_t& banSet)
{
if (fs::exists(m_banlist_dat)) {
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);
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)));
}
// If the JSON banlist does not exist, then recreate it
if (!fs::exists(m_banlist_json)) {
Expand All @@ -155,15 +155,15 @@ bool CBanDB::Read(banmap_t& banSet)

if (!util::ReadSettings(m_banlist_json, settings, errors)) {
for (const auto& err : errors) {
LogPrintf("Cannot load banlist %s: %s\n", m_banlist_json.string(), err);
LogPrintf("Cannot load banlist %s: %s\n", fs::PathToString(m_banlist_json), err);
}
return false;
}

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

Expand Down Expand Up @@ -194,7 +194,7 @@ std::optional<bilingual_str> LoadAddrman(const std::vector<bool>& asmap, const A
} catch (const DbNotFoundError&) {
// Addrman can be in an inconsistent state after failure, reset it
addrman = std::make_unique<AddrMan>(asmap, /* deterministic */ false, /* consistency_check_ratio */ check_addrman);
LogPrintf("Creating peers.dat because the file was not found (%s)\n", path_addr);
LogPrintf("Creating peers.dat because the file was not found (%s)\n", fs::quoted(fs::PathToString(path_addr)));
DumpPeerAddresses(args, *addrman);
} catch (const DbInconsistentError& e) {
// Addrman has shown a tendency to corrupt itself even with graceful shutdowns on known-good
Expand All @@ -208,7 +208,7 @@ std::optional<bilingual_str> LoadAddrman(const std::vector<bool>& asmap, const A
} catch (const std::exception& e) {
addrman = nullptr;
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."),
e.what(), PACKAGE_BUGREPORT, path_addr);
e.what(), PACKAGE_BUGREPORT, fs::quoted(fs::PathToString(path_addr)));
}
return std::nullopt;
}
Expand All @@ -224,7 +224,7 @@ std::vector<CAddress> ReadAnchors(const fs::path& anchors_db_path)
std::vector<CAddress> anchors;
try {
DeserializeFileDB(anchors_db_path, anchors, CLIENT_VERSION | ADDRV2_FORMAT);
LogPrintf("Loaded %i addresses from %s\n", anchors.size(), anchors_db_path.filename());
LogPrintf("Loaded %i addresses from %s\n", anchors.size(), fs::quoted(fs::PathToString(anchors_db_path.filename())));
} catch (const std::exception&) {
anchors.clear();
}
Expand Down
2 changes: 1 addition & 1 deletion src/bitcoin-cli.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -818,7 +818,7 @@ static UniValue CallRPC(BaseRequestHandler* rh, const std::string& strMethod, co
if (failedToGetAuthCookie) {
throw std::runtime_error(strprintf(
"Could not locate RPC credentials. No authentication cookie could be found, and RPC password is not set. See -rpcpassword and -stdinrpcpass. Configuration file: (%s)",
GetConfigFile(gArgs.GetArg("-conf", BITCOIN_CONF_FILENAME)).string()));
fs::PathToString(GetConfigFile(gArgs.GetArg("-conf", BITCOIN_CONF_FILENAME)))));
} else {
throw std::runtime_error("Authorization failed: Incorrect rpcuser or rpcpassword");
}
Expand Down
18 changes: 9 additions & 9 deletions src/dbwrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ static leveldb::Options GetOptions(size_t nCacheSize)
}

CDBWrapper::CDBWrapper(const fs::path& path, size_t nCacheSize, bool fMemory, bool fWipe, bool obfuscate)
: m_name{path.stem().string()}
: m_name{fs::PathToString(path.stem())}
{
penv = nullptr;
readoptions.verify_checksums = true;
Expand All @@ -130,21 +130,21 @@ CDBWrapper::CDBWrapper(const fs::path& path, size_t nCacheSize, bool fMemory, bo
options.env = penv;
} else {
if (fWipe) {
LogPrintf("Wiping LevelDB in %s\n", path.string());
leveldb::Status result = leveldb::DestroyDB(path.string(), options);
LogPrintf("Wiping LevelDB in %s\n", fs::PathToString(path));
leveldb::Status result = leveldb::DestroyDB(fs::PathToString(path), options);
dbwrapper_private::HandleError(result);
}
TryCreateDirectories(path);
LogPrintf("Opening LevelDB in %s\n", path.string());
LogPrintf("Opening LevelDB in %s\n", fs::PathToString(path));
}
leveldb::Status status = leveldb::DB::Open(options, path.string(), &pdb);
leveldb::Status status = leveldb::DB::Open(options, fs::PathToString(path), &pdb);
dbwrapper_private::HandleError(status);
LogPrintf("Opened LevelDB successfully\n");

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

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

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

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

CDBWrapper::~CDBWrapper()
Expand Down
8 changes: 4 additions & 4 deletions src/flat-database.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ class CFlatDB
ssObj << hash;

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

// Write and commit header, data
try {
Expand All @@ -77,11 +77,11 @@ class CFlatDB

int64_t nStart = GetTimeMillis();
// open input file, and associate with CAutoFile
FILE *file = fopen(pathDB.string().c_str(), "rb");
FILE *file = fsbridge::fopen(pathDB, "rb");
CAutoFile filein(file, SER_DISK, CLIENT_VERSION);
if (filein.IsNull())
{
error("%s: Failed to open file %s", __func__, pathDB.string());
error("%s: Failed to open file %s", __func__, fs::PathToString(pathDB));
return FileError;
}

Expand Down
4 changes: 2 additions & 2 deletions src/flatfile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ FILE* FlatFileSeq::Open(const FlatFilePos& pos, bool read_only)
if (!file && !read_only)
file = fsbridge::fopen(path, "wb+");
if (!file) {
LogPrintf("Unable to open file %s\n", path.string());
LogPrintf("Unable to open file %s\n", fs::PathToString(path));
return nullptr;
}
if (pos.nPos && fseek(file, pos.nPos, SEEK_SET)) {
LogPrintf("Unable to seek to position %u of %s\n", pos.nPos, path.string());
LogPrintf("Unable to seek to position %u of %s\n", pos.nPos, fs::PathToString(path));
fclose(file);
return nullptr;
}
Expand Down
8 changes: 4 additions & 4 deletions src/fs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ namespace fsbridge {
FILE *fopen(const fs::path& p, const char *mode)
{
#ifndef WIN32
return ::fopen(p.string().c_str(), mode);
return ::fopen(p.c_str(), mode);
#else
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>,wchar_t> utf8_cvt;
return ::_wfopen(p.wstring().c_str(), utf8_cvt.from_bytes(mode).c_str());
Expand All @@ -47,7 +47,7 @@ static std::string GetErrorReason()

FileLock::FileLock(const fs::path& file)
{
fd = open(file.string().c_str(), O_RDWR);
fd = open(file.c_str(), O_RDWR);
if (fd == -1) {
reason = GetErrorReason();
}
Expand Down Expand Up @@ -244,9 +244,9 @@ void ofstream::close()
#else // __GLIBCXX__

#if BOOST_VERSION >= 107700
static_assert(sizeof(*BOOST_FILESYSTEM_C_STR(fs::path())) == sizeof(wchar_t),
static_assert(sizeof(*BOOST_FILESYSTEM_C_STR(boost::filesystem::path())) == sizeof(wchar_t),
#else
static_assert(sizeof(*fs::path().BOOST_FILESYSTEM_C_STR) == sizeof(wchar_t),
static_assert(sizeof(*boost::filesystem::path().BOOST_FILESYSTEM_C_STR) == sizeof(wchar_t),
#endif // BOOST_VERSION >= 107700
"Warning: This build is using boost::filesystem ofstream and ifstream "
"implementations which will fail to open paths containing multibyte "
Expand Down
132 changes: 131 additions & 1 deletion src/fs.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,132 @@

#include <boost/filesystem.hpp>
#include <boost/filesystem/fstream.hpp>
#include <tinyformat.h>

/** Filesystem operations and types */
namespace fs = boost::filesystem;
namespace fs {

using namespace boost::filesystem;

/**
* Path class wrapper to prepare application code for transition from
* boost::filesystem library to std::filesystem implementation. The main
* purpose of the class is to define fs::path::u8string() and fs::u8path()
* functions not present in boost. It also blocks calls to the
* fs::path(std::string) implicit constructor and the fs::path::string()
* method, which worked well in the boost::filesystem implementation, but have
* unsafe and unpredictable behavior on Windows in the std::filesystem
* implementation (see implementation note in \ref PathToString for details).
*/
class path : public boost::filesystem::path
{
public:
using boost::filesystem::path::path;

// Allow path objects arguments for compatibility.
path(boost::filesystem::path path) : boost::filesystem::path::path(std::move(path)) {}
path& operator=(boost::filesystem::path path) { boost::filesystem::path::operator=(std::move(path)); return *this; }
path& operator/=(boost::filesystem::path path) { boost::filesystem::path::operator/=(std::move(path)); return *this; }

// Allow literal string arguments, which are safe as long as the literals are ASCII.
path(const char* c) : boost::filesystem::path(c) {}
path& operator=(const char* c) { boost::filesystem::path::operator=(c); return *this; }
path& operator/=(const char* c) { boost::filesystem::path::operator/=(c); return *this; }
path& append(const char* c) { boost::filesystem::path::append(c); return *this; }

// Disallow std::string arguments to avoid locale-dependent decoding on windows.
path(std::string) = delete;
path& operator=(std::string) = delete;
path& operator/=(std::string) = delete;
path& append(std::string) = delete;

// Disallow std::string conversion method to avoid locale-dependent encoding on windows.
std::string string() const = delete;

// Define UTF-8 string conversion method not present in boost::filesystem but present in std::filesystem.
std::string u8string() const { return boost::filesystem::path::string(); }
};

// Define UTF-8 string conversion function not present in boost::filesystem but present in std::filesystem.
static inline path u8path(const std::string& string)
{
return boost::filesystem::path(string);
}

// Disallow implicit std::string conversion for system_complete to avoid
// locale-dependent encoding on windows.
static inline path system_complete(const path& p)
{
return boost::filesystem::system_complete(p);
}

// Disallow implicit std::string conversion for exists to avoid
// locale-dependent encoding on windows.
static inline bool exists(const path& p)
{
return boost::filesystem::exists(p);
}

// Allow explicit quoted stream I/O.
static inline auto quoted(const std::string& s)
{
return boost::io::quoted(s, '&');
}

// Allow safe path append operations.
static inline path operator+(path p1, path p2)
{
p1 += std::move(p2);
return p1;
}

/**
* Convert path object to byte string. On POSIX, paths natively are byte
* strings so this is trivial. On Windows, paths natively are Unicode, so an
* encoding step is necessary.
*
* The inverse of \ref PathToString is \ref PathFromString. The strings
* returned and parsed by these functions can be used to call POSIX APIs, and
* for roundtrip conversion, logging, and debugging. But they are not
* guaranteed to be valid UTF-8, and are generally meant to be used internally,
* not externally. When communicating with external programs and libraries that
* require UTF-8, fs::path::u8string() and fs::u8path() methods can be used.
* For other applications, if support for non UTF-8 paths is required, or if
* higher-level JSON or XML or URI or C-style escapes are preferred, it may be
* also be appropriate to use different path encoding functions.
*
* Implementation note: On Windows, the std::filesystem::path(string)
* constructor and std::filesystem::path::string() method are not safe to use
* here, because these methods encode the path using C++'s narrow multibyte
* encoding, which on Windows corresponds to the current "code page", which is
* unpredictable and typically not able to represent all valid paths. So
* std::filesystem::path::u8string() and std::filesystem::u8path() functions
* are used instead on Windows. On POSIX, u8string/u8path functions are not
* safe to use because paths are not always valid UTF-8, so plain string
* methods which do not transform the path there are used.
*/
static inline std::string PathToString(const path& path)
{
#ifdef WIN32
return path.u8string();
#else
static_assert(std::is_same<path::string_type, std::string>::value, "PathToString not implemented on this platform");
return path.boost::filesystem::path::string();
#endif
}

/**
* Convert byte string to path object. Inverse of \ref PathToString.
*/
static inline path PathFromString(const std::string& string)
{
#ifdef WIN32
return u8path(string);
#else
return boost::filesystem::path(string);
#endif
}
} // namespace fs

/** Bridge operations to C stdio */
namespace fsbridge {
Expand Down Expand Up @@ -103,4 +226,11 @@ namespace fsbridge {
#endif // WIN32 && __GLIBCXX__
};

// Disallow path operator<< formatting in tinyformat to avoid locale-dependent
// encoding on windows.
namespace tinyformat {
template<> inline void formatValue(std::ostream&, const char*, const char*, int, const boost::filesystem::path&) = delete;
template<> inline void formatValue(std::ostream&, const char*, const char*, int, const fs::path&) = delete;
} // namespace tinyformat

#endif // BITCOIN_FS_H
2 changes: 1 addition & 1 deletion src/i2p.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ void Session::GenerateAndSavePrivateKey(const Sock& sock)
if (!WriteBinaryFile(m_private_key_file,
std::string(m_private_key.begin(), m_private_key.end()))) {
throw std::runtime_error(
strprintf("Cannot save I2P private key to %s", m_private_key_file));
strprintf("Cannot save I2P private key to %s", fs::quoted(fs::PathToString(m_private_key_file))));
}
}

Expand Down
Loading

0 comments on commit ecfac10

Please sign in to comment.