Skip to content

fix(acl): persist guest last_timestamp across reboot#2533

Open
swaits wants to merge 1 commit into
meshcore-dev:mainfrom
swaits:fix/persist-guest-replay-state
Open

fix(acl): persist guest last_timestamp across reboot#2533
swaits wants to merge 1 commit into
meshcore-dev:mainfrom
swaits:fix/persist-guest-replay-state

Conversation

@swaits
Copy link
Copy Markdown

@swaits swaits commented May 12, 2026

Summary

ClientACL::save in src/helpers/ClientACL.cpp skipped any entry whose permissions == 0 ("skip deleted entries, or by filter function"). Since PERM_ACL_GUEST is defined as 0, guest clients added via handleLoginReq had permissions = 0 and were dropped on every save. The result: after a power cycle the device lost each guest's last_timestamp, opening a one-shot replay window for any captured guest packet. This change drops the permissions == 0 skip; guests now persist alongside admin/read-only entries.

Background

Replay protection in MeshCore relies on per-client monotonic timestamps (ClientInfo::last_timestamp). handleLoginReq and onPeerDataRecv enforce sender_timestamp > client->last_timestamp (or >=, depending on path) to reject replays. That works perfectly while the device is up — but ClientACL::save filters out permissions == 0 entries, so after a reboot every guest's timestamp resets to zero and the first replayed packet wins.

The "skip deleted entries" rationale was true at the time the comment was written: it predates the current applyPermissions(PERM_ACL_GUEST) path which now physically shifts the entry out of clients[] rather than zeroing its permissions. With that explicit-delete code in place, any in-table permissions == 0 entry is a live guest client created by handleLoginReq, not a tombstone — verified by walking every site that writes c->permissions.

Change

src/helpers/ClientACL.cpp — drop the c->permissions == 0 filter in the save loop. The user-supplied filter callback (used by some callers to write only certain entry types) is retained.

Why this is the minimal fix

An alternative would be introducing a separate "tombstone vs guest" marker; that's more invasive and not justified by the observed semantics. Dropping the bogus filter restores the intended persistence model.

Risk / compatibility

  • File format: unchanged. acl.load already reads any entry regardless of permissions.
  • Storage cost: 109 bytes per persisted guest, bounded by MAX_CLIENTS (= 20 or 32 depending on build). Worst case ~3 KB extra flash usage on a fully-saturated device.
  • Behaviour: re-login by a known guest now reuses the existing slot (with its last_timestamp intact) instead of creating a fresh one — closing the replay window.

References

  • CWE-294 — Authentication Bypass by Capture-replay.
  • CWE-672 — Operation on a Resource after Expiration or Release (analogue: state expected to be persisted is silently discarded).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant