mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-14 19:25:44 +00:00
64 lines
3.4 KiB
Markdown
64 lines
3.4 KiB
Markdown
# SPIFFS regular-file reads and login replay state
|
|
|
|
Arduino-ESP32 SPIFFS can return a truthy directory handle from a read-open of a
|
|
nonexistent filename. `File::operator bool()` alone does not prove that a
|
|
regular file exists. A directory's `size()` and `read()` are zero.
|
|
|
|
The first privileged login on an upgraded G2 encountered this in
|
|
`ClientACL::writeClientLoginReplayCeiling()`: opening the not-yet-created
|
|
`/s_login_replay` appeared successful, then subtracting the eight-byte trailer
|
|
from size zero underflowed. Copying the supposed records failed and login was
|
|
rejected. Waiting or changing the repeater clock cannot fix that file-open bug.
|
|
|
|
## Fix
|
|
|
|
Use `mesh::openFileRead()` for regular-file reads. It checks existence and
|
|
rejects directory handles, while preserving real empty files. Replay record
|
|
counts also validate the minimum trailer size, record alignment, and maximum
|
|
count before subtraction. An existing replay file that becomes unreadable is
|
|
not treated as a new store.
|
|
|
|
No replay records are cleared and authentication is not weakened. Invalid or
|
|
unwritable replay state still fails closed. The first successful privileged
|
|
login creates a 44-byte file: one 36-byte identity/ceiling record plus its
|
|
eight-byte integrity trailer.
|
|
|
|
Setting the repeater clock must not clear this file. Admission compares the
|
|
sender's timestamp with that sender's saved boundary, not with the repeater's
|
|
current time. Clearing the boundary could make captured requests reusable. A
|
|
sender clock rollback is a separate condition and remains subject to the saved
|
|
boundary after this fix.
|
|
|
|
Companion directory enumeration uses the separate `openDirectory()` API,
|
|
which deliberately permits SPIFFS virtual directories. Regular-file reads are
|
|
also enforced for companion data, repeater/room logs, flood-rule verification,
|
|
and HTTP packet-log downloads (missing logs return 404; real empty logs 200).
|
|
|
|
## Audit boundary
|
|
|
|
The audit covered file opens and size arithmetic in `src` and `examples`.
|
|
Identity, region, clock, and common preference loaders already gate reads with
|
|
filesystem existence checks; SPIFFS's `exists()` explicitly excludes directory
|
|
handles. Relevant MQTT length subtraction follows validated headers and exact
|
|
reads. ESP32 OTA staging uses partition APIs, not these SPIFFS file handles.
|
|
Intentional directory enumeration must not be changed to a regular-file read.
|
|
|
|
## Regression checks
|
|
|
|
- `test/test_client_acl_spiffs.py` compiles the actual `ClientACL.cpp` against
|
|
a filesystem that reproduces the misleading missing-file directory handle.
|
|
It covers first creation, fresh/stale login, reboot ceilings, corrupt and
|
|
unreadable state, short writes, failed publication, and truncated sources.
|
|
- `test/test_regular_file_reads.py` executes production companion readers,
|
|
flood-file verification, and HTTP log delivery with the same filesystem
|
|
behavior, including platform-specific file API differences.
|
|
- `test/test_esp32_tinyusb_cooperative_output.py` exercises both real role log
|
|
pumps, including missing logs and bounded output.
|
|
- Native ACL transaction, login persistence, and client-path persistence suites
|
|
retain the existing security and recovery coverage.
|
|
|
|
The pre-fix actual ACL code was reproduced as `first_login=rejected`, one
|
|
missing read-open, and no replay file. The fixed code accepted that identical
|
|
scenario without a missing read-open and created the valid 44-byte record.
|
|
Host simulations do not replace a post-flash LoRa login test on the G2.
|