Keep channel faults from disabling healthy Companion contacts

This commit is contained in:
mikecarper committed 2026-10-16 10:59:11 -07:00
1 parent 22c31c0246
commit 508dd23673
12 files changed
+572 -98

No files matched your search

+102 -71
View File
@@ -574,6 +574,7 @@ void DataStore::useVolatilePrimaryFS(FILESYSTEM& fs) {
_secondary_authority_unknown = false;
_identity_creation_blocked = false;
_contact_load_incomplete = false;
_channel_load_incomplete = false;
_prefs_load_incomplete = false;
}
@@ -835,6 +836,7 @@ bool DataStore::formatFileSystem() {
// failed erase must keep the boot's load quarantine latched.
if (success) {
_contact_load_incomplete = false;
_channel_load_incomplete = false;
#if MESH_CONTACT_CACHE
_cache_load_incomplete = false;
#endif
@@ -3132,10 +3134,11 @@ bool DataStore::hasPendingContactWrites() const {
#endif
}
// Share the repeated admission/persistence checks instead of duplicating them under LTO.
__attribute__((noinline))
bool DataStore::hasIncompleteContactLoad() const {
if (contactRecoveryPending()) return true;
#if !defined(NRF52_PLATFORM)
if (_channel_load_incomplete) return true;
#if !MESH_CONTACT_CACHE
if (_uncached_contact_load_incomplete) return true;
#endif
@@ -3164,10 +3167,15 @@ void DataStore::loadChannels(DataStoreHost* host) {
if (!file) continue;
static const uint32_t RECORD_SIZE = 4 + 32 + 32;
const size_t size = file.size();
if (size % RECORD_SIZE != 0 || size / RECORD_SIZE > MAX_GROUP_CHANNELS) {
if (size % RECORD_SIZE != 0) {
file.close();
break;
}
if (size / RECORD_SIZE > MAX_GROUP_CHANNELS) {
file.close();
_channel_load_incomplete = true;
return;
}
const uint8_t count = size / RECORD_SIZE;
ChannelDetails* loaded = count == 0 ? nullptr
: static_cast<ChannelDetails*>(malloc(sizeof(ChannelDetails) * count));
@@ -3209,10 +3217,11 @@ void DataStore::loadChannels(DataStoreHost* host) {
return;
#else
#if defined(NRF52_PLATFORM)
bool& incomplete = _contact_load_incomplete;
#else
bool& incomplete = _channel_load_incomplete;
// Contact/migration failures can still make the shared filesystem's
// authority unknowable. A channel-only failure must not set that veto.
if (_contact_load_incomplete) return;
#endif
bool& incomplete = _channel_load_incomplete;
if (incomplete) return;
FILESYSTEM* contacts_fs = _getContactsChannelsFS();
#if defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM)
@@ -3234,74 +3243,98 @@ void DataStore::loadChannels(DataStoreHost* host) {
#else
if (!contacts_fs->exists("/channels2")) return;
#endif
File file = openRead(contacts_fs, "/channels2");
if (!file) {
MESH_DEBUG_PRINTLN("DataStore: channels file open failed; storage quarantined");
incomplete = true;
return;
}
#if defined(NRF52_PLATFORM)
if (file.size() != channels_size) {
file.close();
MESH_DEBUG_PRINTLN("DataStore: channels file changed while opening; storage quarantined");
incomplete = true;
return;
}
#else
channels_size = file.size();
#endif
static const uint32_t CHANNEL_RECORD_SIZE = 4 + 32 + 32;
if ((channels_size % CHANNEL_RECORD_SIZE) != 0
|| channels_size / CHANNEL_RECORD_SIZE > MAX_GROUP_CHANNELS) {
file.close();
MESH_DEBUG_PRINTLN("DataStore: invalid channels file size; storage quarantined");
incomplete = true;
return;
}
const uint8_t channel_count =
static_cast<uint8_t>(channels_size / CHANNEL_RECORD_SIZE);
ChannelDetails* loaded = channel_count == 0 ? nullptr
: static_cast<ChannelDetails*>(
malloc(sizeof(ChannelDetails) * channel_count));
if (channel_count != 0 && loaded == nullptr) {
file.close();
MESH_DEBUG_PRINTLN(
"DataStore: no memory for channels snapshot; storage quarantined");
incomplete = true;
return;
}
bool success = true;
for (uint8_t channel_idx = 0; channel_idx < channel_count; channel_idx++) {
uint8_t unused[4];
success = file.read(unused, sizeof(unused)) == sizeof(unused)
&& file.read(reinterpret_cast<uint8_t*>(loaded[channel_idx].name),
sizeof(loaded[channel_idx].name))
== sizeof(loaded[channel_idx].name)
&& file.read(loaded[channel_idx].channel.secret,
sizeof(loaded[channel_idx].channel.secret))
== sizeof(loaded[channel_idx].channel.secret);
if (!success) break;
loaded[channel_idx].name[sizeof(loaded[channel_idx].name) - 1] = 0;
loaded[channel_idx].channel.tx_radio = mesh::decodeRadioTxPolicy(unused[0]);
}
file.close();
if (!success) {
free(loaded);
MESH_DEBUG_PRINTLN(
"DataStore: channels file read failed; storage quarantined");
incomplete = true;
return;
}
for (uint8_t channel_idx = 0; channel_idx < channel_count; channel_idx++) {
if (!host->onChannelLoaded(channel_idx, loaded[channel_idx])) {
free(loaded);
MESH_DEBUG_PRINTLN(
"DataStore: channel host refused durable record; storage quarantined");
const char* channel_path = "/channels2";
for (;;) {
File file = openRead(contacts_fs, channel_path);
if (!file) {
MESH_DEBUG_PRINTLN("DataStore: channels file open failed; storage quarantined");
incomplete = true;
return;
}
#if defined(NRF52_PLATFORM)
if (file.size() != channels_size) {
file.close();
MESH_DEBUG_PRINTLN("DataStore: channels file changed while opening; storage quarantined");
incomplete = true;
return;
}
#else
channels_size = file.size();
#endif
static const uint32_t CHANNEL_RECORD_SIZE = 4 + 32 + 32;
const bool malformed = (channels_size % CHANNEL_RECORD_SIZE) != 0;
if (malformed || channels_size / CHANNEL_RECORD_SIZE > MAX_GROUP_CHANNELS) {
file.close();
#if defined(RP2040_PLATFORM)
// A rename-gap backup takes precedence over replacing a malformed image.
// Read and admit that complete snapshot before changing either name.
if (malformed && channel_path[sizeof("/channels2") - 1] == 0
&& contacts_fs->exists("/channels2.bak")) {
channel_path = "/channels2.bak";
continue;
}
#endif
MESH_DEBUG_PRINTLN("DataStore: invalid channels file size");
incomplete = !malformed;
return;
}
const uint8_t channel_count =
static_cast<uint8_t>(channels_size / CHANNEL_RECORD_SIZE);
ChannelDetails* loaded = channel_count == 0 ? nullptr
: static_cast<ChannelDetails*>(
malloc(sizeof(ChannelDetails) * channel_count));
if (channel_count != 0 && loaded == nullptr) {
file.close();
MESH_DEBUG_PRINTLN(
"DataStore: no memory for channels snapshot; storage quarantined");
incomplete = true;
return;
}
bool success = true;
for (uint8_t channel_idx = 0; channel_idx < channel_count; channel_idx++) {
uint8_t unused[4];
success = file.read(unused, sizeof(unused)) == sizeof(unused)
&& file.read(reinterpret_cast<uint8_t*>(loaded[channel_idx].name),
sizeof(loaded[channel_idx].name))
== sizeof(loaded[channel_idx].name)
&& file.read(loaded[channel_idx].channel.secret,
sizeof(loaded[channel_idx].channel.secret))
== sizeof(loaded[channel_idx].channel.secret);
if (!success) break;
loaded[channel_idx].name[sizeof(loaded[channel_idx].name) - 1] = 0;
loaded[channel_idx].channel.tx_radio = mesh::decodeRadioTxPolicy(unused[0]);
}
file.close();
if (!success) {
free(loaded);
MESH_DEBUG_PRINTLN(
"DataStore: channels file read failed; storage quarantined");
incomplete = true;
return;
}
for (uint8_t channel_idx = 0; channel_idx < channel_count; channel_idx++) {
if (!host->onChannelLoaded(channel_idx, loaded[channel_idx])) {
free(loaded);
MESH_DEBUG_PRINTLN(
"DataStore: channel host refused durable record; storage quarantined");
incomplete = true;
return;
}
}
free(loaded);
#if defined(RP2040_PLATFORM)
if (channel_path[sizeof("/channels2") - 1] != 0) {
// Only the proven malformed target is retired. Failed promotion keeps
// the validated RAM table and protects the surviving backup from saves.
contacts_fs->remove("/channels2");
if (contacts_fs->exists("/channels2")
|| !mesh::ContactFileTransaction::recover(contacts_fs, "/channels2")
|| !contacts_fs->exists("/channels2")
|| contacts_fs->exists("/channels2.bak")) incomplete = true;
}
#endif
return;
}
free(loaded);
#endif
}
@@ -3310,9 +3343,7 @@ bool DataStore::saveChannels(DataStoreHost* host) {
if (!promoteCompanionRecoveryFile(_getContactsChannelsFS(), "/channels2",
_channel_recovery_source)) return false;
#endif
#if !defined(NRF52_PLATFORM)
if (_channel_load_incomplete) return false;
#endif
#if defined(NRF52_PLATFORM)
if (_contact_load_incomplete) return false;
#endif
+2 -1
View File
@@ -146,8 +146,8 @@ class DataStore
void invalidateAdvertWrite(const uint8_t key[], int key_len);
void retireAdvertWrite(const uint8_t key[], int key_len);
#endif
#if !defined(NRF52_PLATFORM)
bool _channel_load_incomplete = false;
#if !defined(NRF52_PLATFORM)
#if !MESH_CONTACT_CACHE
bool _uncached_contact_load_incomplete = false;
#endif
@@ -255,6 +255,7 @@ public:
bool flushContactWrites(DataStoreHost* host, bool (*filter)(const ContactInfo& c) = NULL);
bool hasPendingContactWrites() const;
bool hasIncompleteContactLoad() const;
bool hasIncompleteChannelLoad() const { return _channel_load_incomplete; }
void loadChannels(DataStoreHost* host);
bool saveChannels(DataStoreHost* host);
bool migrateToSecondaryFS();
+3 -2
View File
@@ -3696,8 +3696,9 @@ bool MyMesh::handleTxRoutingCommand(const char* command, char* reply, size_t rep
kind, parsed.channel ? "index" : "key:hex");
return true;
}
if (parsed.set && !canMutateContacts()) {
snprintf(reply, reply_size, "Error: contact/channel storage unavailable");
if (parsed.set && (!canMutateContacts()
|| (parsed.channel && _store->hasIncompleteChannelLoad()))) {
snprintf(reply, reply_size, "Error: storage unavailable");
return true;
}