fix(startup): surface filesystem failures and verify Sapling param integrity

Three verified daemon-startup edge-case fixes centered on the config/params
filesystem path:

- F7: new non-throwing Platform::ensureDirectory(dir, outError) with one
  consistent "Cannot create <dir>: <reason>. Check permissions / free space."
  message. Replaces the unchecked/throwing create_directories sites at main.cpp
  (pre-init: log + Windows MessageBox + return 1), connection.cpp's
  autoDetectConfig (was the *throwing* overload -- could raise an uncaught
  filesystem_error through its callers; now sets the new
  ConnectionConfig::dir_error), and both app.cpp daemon-dir sites (surface via
  daemon_status_ + return false). The primary connect path (app_network.cpp)
  checks dir_error and shows it instead of mislabelling it "waiting for config".
  embedded_resources.cpp already checked its error_code, so it is left as-is.

- F6: verifySaplingParams() now hash-verifies each param against its pinned
  canonical SHA-256 (source of truth: scripts/build-lite-backend-artifact.sh)
  instead of only checking existence, so a truncated / corrupt-but-present param
  is rejected up front rather than failing later on a shielded operation. A
  <params_dir>/.sapling_verified marker keyed on size:mtime avoids re-hashing
  ~48MB on every startup. Logic extracted to the injectable, unit-testable
  verifySaplingParamsIn(dir, digests); reuses util::sha256Hex (no new hash impl).

- F5: startEmbeddedDaemon() now checks extractEmbeddedResources()'s return and
  the previously-dropped copy_file error_code in the daemon-binary fallback loop,
  aborting with a clear status (sb_daemon_extract_failed / sb_daemon_files_failed)
  instead of failing opaquely at spawn. An absent source file stays non-fatal.

Adds testPlatformEnsureDirectory and testVerifySaplingParams to test_phase4.cpp.
i18n keys added to i18n.cpp (English source of truth); the res/lang/*.json
back-fill via add_missing_translations.py is deferred to a single run at the end
of the batch. Progress tracked in docs/daemon-startup-hardening.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 11:07:32 -05:00
parent b3444e0a89
commit 2675b8ab93
10 changed files with 340 additions and 35 deletions

View File

@@ -1316,6 +1316,8 @@ void I18n::loadBuiltinEnglish()
strings_["sb_extracting_sapling"] = "Extracting Sapling parameters...";
strings_["sb_sapling_failed"] = "Failed to extract Sapling parameters.";
strings_["sb_sapling_not_found"] = "Sapling parameters not found.";
strings_["sb_daemon_extract_failed"] = "Failed to write daemon files — check free disk space and permissions.";
strings_["sb_daemon_files_failed"] = "Failed to write daemon files to %s — check free disk space and permissions.";
strings_["sb_dragonxd_running"] = "dragonxd running";
strings_["sb_dragonxd_stopping"] = "Stopping dragonxd...";
strings_["sb_dragonxd_stopped"] = "dragonxd stopped";

View File

@@ -126,6 +126,27 @@ bool Platform::openUrl(const std::string& url)
#endif
}
bool Platform::ensureDirectory(const std::string& dir, std::string* outError)
{
if (dir.empty()) {
if (outError) *outError = "Cannot create directory: empty path.";
return false;
}
std::error_code ec;
if (std::filesystem::is_directory(dir, ec)) return true;
ec.clear();
std::filesystem::create_directories(dir, ec);
if (ec) {
if (outError) {
*outError = "Cannot create " + dir + ": " + ec.message() +
". Check permissions / free space.";
}
DEBUG_LOGF("[ERROR] ensureDirectory failed for %s: %s\n", dir.c_str(), ec.message().c_str());
return false;
}
return true;
}
bool Platform::openFolder(const std::string& path, bool createIfMissing)
{
if (path.empty()) return false;

View File

@@ -128,6 +128,17 @@ public:
*/
static void ensureObsidianDragonSetup();
/**
* @brief Create a directory (and parents) if missing, with a clear error on failure.
*
* Uses the non-throwing std::error_code overload internally. On failure sets *outError
* (when non-null) to one consistent, user-facing message:
* "Cannot create <dir>: <reason>. Check permissions / free space."
*
* @return true if the directory exists (already did, or was just created).
*/
static bool ensureDirectory(const std::string& dir, std::string* outError = nullptr);
/**
* @brief Get total system RAM in megabytes
* @return Total physical RAM in MB, or 0 on failure