From c814ec78091486c8fb835d0878dc724dea265aad Mon Sep 17 00:00:00 2001 From: MaikVermeulen Date: Tue, 28 Apr 2026 16:38:20 +0200 Subject: [PATCH] [posix] improve settings file resilience against corruption and power loss (#12872) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [posix] truncate settings file to last valid offset on parse error When Init() encounters a corrupt entry, it currently truncates the entire file to 0 bytes, destroying all settings. Since the parse loop already knows the exact offset where corruption starts, truncate to that offset instead, preserving all entries that were successfully parsed. This prevents loss of the active operational dataset (and other settings) when only trailing bytes are corrupt — a common failure mode when power is lost during a write. If corruption starts at offset 0 (no valid entries), behavior is identical to the original code. * [posix] fsync parent directory after settings file rename SwapPersist() calls fsync() on the data file descriptor but does not sync the parent directory after rename(). On journaling filesystems (ext4, overlayfs), the rename metadata may not reach stable storage before a power loss. This can leave the old swap file in place, which triggers a parse error on the next Init(). Add a best-effort fsync() on the parent directory after the rename. This is non-fatal since the file data is already persisted; only the directory entry could lag behind. --- src/posix/platform/settings_file.cpp | 36 +++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/posix/platform/settings_file.cpp b/src/posix/platform/settings_file.cpp index e76c41a83..7ac721928 100644 --- a/src/posix/platform/settings_file.cpp +++ b/src/posix/platform/settings_file.cpp @@ -83,8 +83,9 @@ void SettingsFile::SetSettingsFileName(const char *aSettingsFileName) otError SettingsFile::Init(const char *aSettingsFileBaseName) { - otError error = OT_ERROR_NONE; - const char *directory = GetSettingsPath(); + otError error = OT_ERROR_NONE; + const char *directory = GetSettingsPath(); + off_t lastValidOffset = 0; OT_ASSERT(strlen(directory) < kMaxFileBasePathNameSize); OT_ASSERT((aSettingsFileBaseName != nullptr) && strlen(aSettingsFileBaseName) < kMaxFileBaseNameSize); @@ -110,6 +111,8 @@ otError SettingsFile::Init(const char *aSettingsFileBaseName) for (off_t size = lseek(mSettingsFd, 0, SEEK_END), offset = lseek(mSettingsFd, 0, SEEK_SET); offset < size;) { + lastValidOffset = offset; + uint16_t key; uint16_t length; ssize_t rval; @@ -127,7 +130,20 @@ otError SettingsFile::Init(const char *aSettingsFileBaseName) exit: if (error == OT_ERROR_PARSE) { - VerifyOrDie(ftruncate(mSettingsFd, 0) == 0, OT_EXIT_ERROR_ERRNO); + off_t fileSize = lseek(mSettingsFd, 0, SEEK_END); + + if (lastValidOffset > 0) + { + otLogCritPlat("Settings file corrupt at offset %jd of %jd bytes, truncating to preserve %jd bytes of " + "valid entries", + (intmax_t)lastValidOffset, (intmax_t)fileSize, (intmax_t)lastValidOffset); + } + else + { + otLogCritPlat("Settings file corrupt from start (%jd bytes), truncating entire file", (intmax_t)fileSize); + } + + VerifyOrDie(ftruncate(mSettingsFd, lastValidOffset) == 0, OT_EXIT_ERROR_ERRNO); } return error; @@ -389,6 +405,20 @@ void SettingsFile::SwapPersist(int aFd) VerifyOrDie(0 == fsync(aFd), OT_EXIT_ERROR_ERRNO); VerifyOrDie(0 == rename(swapFile, dataFile), OT_EXIT_ERROR_ERRNO); + // Best-effort: sync the parent directory so that the rename metadata + // reaches stable storage. Without this, a power loss between rename() + // and the next journal commit can lose the rename, leaving a partially- + // written swap file that causes a parse error on next Init(). + { + int dirFd = open(GetSettingsPath(), O_RDONLY | O_DIRECTORY | O_CLOEXEC); + + if (dirFd >= 0) + { + fsync(dirFd); + close(dirFd); + } + } + mSettingsFd = aFd; }