fix(persistence): atomic + owner-only settings/address-book writes

settings.json and addressbook.json were written in place with a bare ofstream —
a crash or power loss mid-write truncated the file, and on the next launch the
parse failure silently reset every preference (hidden/favorite addresses, labels,
pool workers, language, theme, lite-server list) because the next save overwrote
the corrupt file with defaults.

Add Platform::writeFileAtomically() (temp file -> fsync -> atomic rename; dir
fsync on POSIX, MoveFileEx on Windows; optional owner-only 0600) and route both
saves through it. On a parse failure, quarantine the unreadable settings file to
settings.json.corrupt-<ts> instead of clobbering it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 14:16:10 -05:00
parent 7195c25376
commit 20b22410e9
4 changed files with 122 additions and 21 deletions

View File

@@ -11,8 +11,10 @@
#include <nlohmann/json.hpp>
#include <fstream>
#include <filesystem>
#include <ctime>
#include "../util/logger.h"
#include "../util/platform.h"
#ifdef _WIN32
#include <shlobj.h>
@@ -291,6 +293,17 @@ bool Settings::load(const std::string& path)
return true;
} catch (const std::exception& e) {
DEBUG_LOGF("Failed to parse settings: %s\n", e.what());
// The file exists but is unparseable (truncated/corrupt). Quarantine it so the
// next save() doesn't silently overwrite it with defaults — the user's data stays
// recoverable. Proceed with in-memory defaults.
file.close();
std::error_code ec;
const std::string quarantine =
path + ".corrupt-" + std::to_string(static_cast<long long>(std::time(nullptr)));
fs::rename(path, quarantine, ec);
if (!ec) {
DEBUG_LOGF("Quarantined corrupt settings to %s\n", quarantine.c_str());
}
return false;
}
}
@@ -411,17 +424,11 @@ bool Settings::save(const std::string& path)
}
try {
// Ensure directory exists
fs::path p(path);
fs::create_directories(p.parent_path());
std::ofstream file(path);
if (!file.is_open()) {
return false;
}
file << j.dump(4);
return true;
// Atomic + durable: write to a temp file, fsync, then rename over the real file.
// A crash mid-write can no longer truncate settings.json (which would silently
// reset every preference on the next launch). Owner-only (0600) — it carries the
// lite-server list and address metadata.
return util::Platform::writeFileAtomically(path, j.dump(4), /*restrictPermissions=*/true);
} catch (const std::exception& e) {
DEBUG_LOGF("Failed to save settings: %s\n", e.what());
return false;