Skip to content

Commit

Permalink
bug meshtastic#4184 partial set of fixes:
Browse files Browse the repository at this point in the history
* Use SafeFile for atomic file writing (with xor checksum readback)
* Write db.proto last because it could be the largest file on the FS (and less critical)
* Don't keep a tmp file around while writing db.proto (because too big to fit two files in the filesystem)
  • Loading branch information
geeksville committed Aug 5, 2024
1 parent 66a4632 commit ba6353c
Show file tree
Hide file tree
Showing 6 changed files with 239 additions and 68 deletions.
99 changes: 99 additions & 0 deletions src/SafeFile.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#include "SafeFile.h"

#ifdef FSCom

// Only way to work on both esp32 and nrf52
static File openFile(const char *filename, bool fullAtomic)
{
if (!fullAtomic)
FSCom.remove(filename); // Nuke the old file to make space (ignore if it !exists)

String filenameTmp = filename;
filenameTmp += ".tmp";

return FSCom.open(filenameTmp.c_str(), FILE_O_WRITE);
}

SafeFile::SafeFile(const char *_filename, bool fullAtomic)
: filename(_filename), f(openFile(_filename, fullAtomic)), fullAtomic(fullAtomic)
{
}

size_t SafeFile::write(uint8_t ch)
{
if (!f)
return 0;

hash ^= ch;
return f.write(ch);
}

size_t SafeFile::write(const uint8_t *buffer, size_t size)
{
if (!f)
return 0;

for (size_t i = 0; i < size; i++) {
hash ^= buffer[i];
}
return f.write((uint8_t const *)buffer, size); // This nasty cast is _IMPORTANT_ otherwise the correct adafruit method does
// not get used (they made a mistake in their typing)
}

/**
* Atomically close the file (deleting any old versions) and readback the contents to confirm the hash matches
*
* @return false for failure
*/
bool SafeFile::close()
{
if (!f)
return false;

f.close();
if (!testReadback())
return false;

// brief window of risk here ;-)
if (fullAtomic && FSCom.exists(filename.c_str()) && !FSCom.remove(filename.c_str())) {
LOG_ERROR("Can't remove old pref file\n");
return false;
}

String filenameTmp = filename;
filenameTmp += ".tmp";
if (!renameFile(filenameTmp.c_str(), filename.c_str())) {
LOG_ERROR("Error: can't rename new pref file\n");
return false;
}

return true;
}

/// Read our (closed) tempfile back in and compare the hash
bool SafeFile::testReadback()
{
String filenameTmp = filename;
filenameTmp += ".tmp";
auto f2 = FSCom.open(filenameTmp.c_str(), FILE_O_READ);
if (!f2) {
LOG_ERROR("Can't open tmp file for readback\n");
return false;
}

int c = 0;
uint8_t test_hash = 0;
while ((c = f2.read()) >= 0) {
test_hash ^= (uint8_t)c;
}
f2.close();

if (test_hash != hash) {
LOG_ERROR("Readback failed hash mismatch\n");
return false;
}

return true;
}

#endif
45 changes: 45 additions & 0 deletions src/SafeFile.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#pragma once

#include "FSCommon.h"
#include "configuration.h"

/**
* This class provides 'safe'/paranoid file writing.
*
* Some of our filesystems (in particular the nrf52) may have bugs beneath our layer. Therefore we want to
* be very careful about how we write files. This class provides a restricted (Stream only) writing API for writing to files.
*
* Notably:
* - we keep a simple xor hash of all characters that were written.
* - We do not allow seeking (because we want to maintain our hash)
* - we provide an close() method which is similar to close but returns false if we were unable to successfully write the
* file. Also this method
* - atomically replaces any old version of the file on the disk with our new file (after first rereading the file from the disk
* to confirm the hash matches)
* - Some files are super huge so we can't do the full atomic rename/copy (because of filesystem size limits). If !fullAtomic
* then we still do the readback to verify file is valid so higher level code can handle failures.
*/
class SafeFile : public Print
{
public:
SafeFile(char const *filepath, bool fullAtomic = false);

virtual size_t write(uint8_t);
virtual size_t write(const uint8_t *buffer, size_t size);

/**
* Atomically close the file (deleting any old versions) and readback the contents to confirm the hash matches
*
* @return false for failure
*/
bool close();

private:
/// Read our (closed) tempfile back in and compare the hash
bool testReadback();

String filename;
File f;
bool fullAtomic;
uint8_t hash = 0;
};
126 changes: 78 additions & 48 deletions src/mesh/NodeDB.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "PowerFSM.h"
#include "RTC.h"
#include "Router.h"
#include "SafeFile.h"
#include "TypeConversions.h"
#include "error.h"
#include "main.h"
Expand Down Expand Up @@ -54,6 +55,27 @@ meshtastic_LocalModuleConfig moduleConfig;
meshtastic_ChannelFile channelFile;
meshtastic_OEMStore oemStore;

// These are not publically exposed - copied from InternalFileSystem.cpp
// #define FLASH_NRF52_PAGE_SIZE 4096
// #define LFS_FLASH_TOTAL_SIZE (7*FLASH_NRF52_PAGE_SIZE)
// #define LFS_BLOCK_SIZE 128

/// List all files in the FS and test write and readback.
/// Useful for filesystem stress testing - normally stripped from build by the linker.
void flashTest()
{
auto filesManifest = getFiles("/", 5);

uint32_t totalSize = 0;
for (size_t i = 0; i < filesManifest.size(); i++) {
LOG_INFO("File %s (size %d)\n", filesManifest[i].file_name, filesManifest[i].size_bytes);
totalSize += filesManifest[i].size_bytes;
}
LOG_INFO("%d files (total size %u)\n", filesManifest.size(), totalSize);
// LOG_INFO("Filesystem block size %u, total bytes %u", LFS_FLASH_TOTAL_SIZE, LFS_BLOCK_SIZE);
nodeDB->saveToDisk();
}

bool meshtastic_DeviceState_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_iter_t *field)
{
if (ostream) {
Expand Down Expand Up @@ -608,22 +630,30 @@ LoadFileResult NodeDB::loadProto(const char *filename, size_t protoSize, size_t

void NodeDB::loadFromDisk()
{
devicestate.version =
0; // Mark the current device state as completely unusable, so that if we fail reading the entire file from
// disk we will still factoryReset to restore things.

// static DeviceState scratch; We no longer read into a tempbuf because this structure is 15KB of valuable RAM
auto state = loadProto(prefFileName, sizeof(meshtastic_DeviceState) + MAX_NUM_NODES * sizeof(meshtastic_NodeInfo),
sizeof(meshtastic_DeviceState), &meshtastic_DeviceState_msg, &devicestate);

if (state != LoadFileResult::LOAD_SUCCESS) {
installDefaultDeviceState(); // Our in RAM copy might now be corrupt
// See https://github.com/meshtastic/firmware/issues/4184#issuecomment-2269390786
// It is very important to try and use the saved prefs even if we fail to read meshtastic_DeviceState. Because most of our
// critical config may still be valid (in the other files - loaded next).
// Also, if we did fail on reading we probably failed on the enormous (and non critical) nodeDB. So DO NOT install default
// device state.
// if (state != LoadFileResult::LOAD_SUCCESS) {
// installDefaultDeviceState(); // Our in RAM copy might now be corrupt
//} else {
if (devicestate.version < DEVICESTATE_MIN_VER) {
LOG_WARN("Devicestate %d is old, discarding\n", devicestate.version);
factoryReset();
} else {
if (devicestate.version < DEVICESTATE_MIN_VER) {
LOG_WARN("Devicestate %d is old, discarding\n", devicestate.version);
factoryReset();
} else {
LOG_INFO("Loaded saved devicestate version %d, with nodecount: %d\n", devicestate.version,
devicestate.node_db_lite.size());
meshNodes = &devicestate.node_db_lite;
numMeshNodes = devicestate.node_db_lite.size();
}
LOG_INFO("Loaded saved devicestate version %d, with nodecount: %d\n", devicestate.version,
devicestate.node_db_lite.size());
meshNodes = &devicestate.node_db_lite;
numMeshNodes = devicestate.node_db_lite.size();
}
meshNodes->resize(MAX_NUM_NODES);

Expand Down Expand Up @@ -693,36 +723,29 @@ void NodeDB::loadFromDisk()
}

/** Save a protobuf from a file, return true for success */
bool NodeDB::saveProto(const char *filename, size_t protoSize, const pb_msgdesc_t *fields, const void *dest_struct)
bool NodeDB::saveProto(const char *filename, size_t protoSize, const pb_msgdesc_t *fields, const void *dest_struct,
bool fullAtomic)
{
bool okay = false;
#ifdef FSCom
// static DeviceState scratch; We no longer read into a tempbuf because this structure is 15KB of valuable RAM
String filenameTmp = filename;
filenameTmp += ".tmp";
auto f = FSCom.open(filenameTmp.c_str(), FILE_O_WRITE);
if (f) {
LOG_INFO("Saving %s\n", filename);
pb_ostream_t stream = {&writecb, &f, protoSize};
auto f = SafeFile(filename, fullAtomic);

if (!pb_encode(&stream, fields, dest_struct)) {
LOG_ERROR("Error: can't encode protobuf %s\n", PB_GET_ERROR(&stream));
} else {
okay = true;
}
f.flush();
f.close();
LOG_INFO("Saving %s\n", filename);
pb_ostream_t stream = {&writecb, static_cast<Print *>(&f), protoSize};

// brief window of risk here ;-)
if (FSCom.exists(filename) && !FSCom.remove(filename)) {
LOG_WARN("Can't remove old pref file\n");
}
if (!renameFile(filenameTmp.c_str(), filename)) {
LOG_ERROR("Error: can't rename new pref file\n");
}
if (!pb_encode(&stream, fields, dest_struct)) {
LOG_ERROR("Error: can't encode protobuf %s\n", PB_GET_ERROR(&stream));
} else {
okay = true;
}

bool writeSucceeded = f.close();

if (!okay || !writeSucceeded) {
LOG_ERROR("Can't write prefs\n");
#ifdef ARCH_NRF52
// kevinh FIXME - I'm removing all this because if we know that the filesystem is toast at this point the only thing we
// _can_ do is to reformat and fill with our valid info from RAM. To do otherwise risks losing valuable user config data.
#if 0 // def ARCH_NRF52
static uint8_t failedCounter = 0;
failedCounter++;
if (failedCounter >= 2) {
Expand All @@ -742,32 +765,32 @@ bool NodeDB::saveProto(const char *filename, size_t protoSize, const pb_msgdesc_
return okay;
}

void NodeDB::saveChannelsToDisk()
bool NodeDB::saveChannelsToDisk()
{
#ifdef FSCom
FSCom.mkdir("/prefs");
#endif
saveProto(channelFileName, meshtastic_ChannelFile_size, &meshtastic_ChannelFile_msg, &channelFile);
return saveProto(channelFileName, meshtastic_ChannelFile_size, &meshtastic_ChannelFile_msg, &channelFile);
}

void NodeDB::saveDeviceStateToDisk()
bool NodeDB::saveDeviceStateToDisk()
{
#ifdef FSCom
FSCom.mkdir("/prefs");
#endif
saveProto(prefFileName, sizeof(devicestate) + numMeshNodes * meshtastic_NodeInfoLite_size, &meshtastic_DeviceState_msg,
&devicestate);
// Note: if MAX_NUM_NODES=100 and meshtastic_NodeInfoLite_size=166, so will be approximately 17KB
// Because so huge we _must_ not use fullAtomic, because the filesystem is probably too small to hold two copies of this
return saveProto(prefFileName, sizeof(devicestate) + numMeshNodes * meshtastic_NodeInfoLite_size, &meshtastic_DeviceState_msg,
&devicestate, false);
}

void NodeDB::saveToDisk(int saveWhat)
bool NodeDB::saveToDisk(int saveWhat)
{
bool success = true;

#ifdef FSCom
FSCom.mkdir("/prefs");
#endif
if (saveWhat & SEGMENT_DEVICESTATE) {
saveDeviceStateToDisk();
}

if (saveWhat & SEGMENT_CONFIG) {
config.has_device = true;
config.has_display = true;
Expand All @@ -777,7 +800,7 @@ void NodeDB::saveToDisk(int saveWhat)
config.has_network = true;
config.has_bluetooth = true;

saveProto(configFileName, meshtastic_LocalConfig_size, &meshtastic_LocalConfig_msg, &config);
success &= saveProto(configFileName, meshtastic_LocalConfig_size, &meshtastic_LocalConfig_msg, &config);
}

if (saveWhat & SEGMENT_MODULECONFIG) {
Expand All @@ -794,12 +817,19 @@ void NodeDB::saveToDisk(int saveWhat)
moduleConfig.has_audio = true;
moduleConfig.has_paxcounter = true;

saveProto(moduleConfigFileName, meshtastic_LocalModuleConfig_size, &meshtastic_LocalModuleConfig_msg, &moduleConfig);
success &=
saveProto(moduleConfigFileName, meshtastic_LocalModuleConfig_size, &meshtastic_LocalModuleConfig_msg, &moduleConfig);
}

if (saveWhat & SEGMENT_CHANNELS) {
saveChannelsToDisk();
success &= saveChannelsToDisk();
}

if (saveWhat & SEGMENT_DEVICESTATE) {
success &= saveDeviceStateToDisk();
}

return success;
}

const meshtastic_NodeInfoLite *NodeDB::readNextMeshNode(uint32_t &readIndex)
Expand Down Expand Up @@ -1059,4 +1089,4 @@ void recordCriticalError(meshtastic_CriticalErrorCode code, uint32_t address, co
LOG_ERROR("A critical failure occurred, portduino is exiting...");
exit(2);
#endif
}
}
8 changes: 5 additions & 3 deletions src/mesh/NodeDB.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ class NodeDB
NodeDB();

/// write to flash
void saveToDisk(int saveWhat = SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS),
/// @return true if the save was successful
bool saveToDisk(int saveWhat = SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS),
saveChannelsToDisk(), saveDeviceStateToDisk();

/** Reinit radio config if needed, because either:
Expand Down Expand Up @@ -130,7 +131,8 @@ class NodeDB

LoadFileResult loadProto(const char *filename, size_t protoSize, size_t objSize, const pb_msgdesc_t *fields,
void *dest_struct);
bool saveProto(const char *filename, size_t protoSize, const pb_msgdesc_t *fields, const void *dest_struct);
bool saveProto(const char *filename, size_t protoSize, const pb_msgdesc_t *fields, const void *dest_struct,
bool fullAtomic = true);

void installRoleDefaults(meshtastic_Config_DeviceConfig_Role role);

Expand Down Expand Up @@ -228,4 +230,4 @@ extern uint32_t error_address;
ModuleConfig_RangeTestConfig_size + ModuleConfig_SerialConfig_size + ModuleConfig_StoreForwardConfig_size + \
ModuleConfig_TelemetryConfig_size + ModuleConfig_size)

// Please do not remove this comment, it makes trunk and compiler happy at the same time.
// Please do not remove this comment, it makes trunk and compiler happy at the same time.
Loading

0 comments on commit ba6353c

Please sign in to comment.