Repository navigation
tls/nvm: completion belongs to its attempt; persist failures returned and retried - #186
Conversation
…t failures are returned and retried until durable (failing) Host-only seams, no behaviour change: a TLS hook between a connect attempt releasing its slot and invoking on_complete, and flash write failure injection with a write counter. Failing against the current HAL and NVM driver: - an attempt parked after releasing its slot while a second attempt on the same slot completes: the first completion is lost and the second callback runs twice; - a connected slot accepts a second connect_async; - a set, provision or flush whose flash write fails returns success with no event, and a provision retried after recovery writes nothing; - provisioning faults nothing when the identity write fails. A test-local definition stands in for the storage-failure event code until embediq_obs.h declares it.
…ing the slot; connected slots refuse connect; persist results returned, retried until durable TLS: the connect worker released the slot and then read on_complete and user_ctx from the reusable per-slot argument entry. A second attempt accepted in that interval replaced them: the first completion was lost and the second callback ran twice. The worker now copies its attempt before any blocking work and uses only the copy; nothing after the release touches the slot or the entry. A connected slot refuses connect_async with EMBEDIQ_TLS_ERR_BUSY instead of re-initialising the live socket and ssl context. NVM: a failed flash write was invisible. nvm_persist now returns whether the change (or a newer table) reached flash; set, delete, reset, provision and flush return it, and a failed write emits one EMBEDIQ_OBS_EVT_NVM_PERSIST_FAIL (0x6B) outside both locks. A lock failure returns an error without that event (the OSAL observes it). The skip test measures both distances back from the current generation, so a long run of failed writes never wraps into a false skip. provision with the key already set returns OK only once the table holding it is on flash: a real retry after a failed persist. Provisioning calls embediq_nvm_provision for the identity on every startup instead of skipping it when RAM already holds the value.
|
Reviewed PR #186 at [P1] Retry an unpersisted deletion even when the key is already absent from RAM. In
The TLS attempt-local callback/context fix and connected-slot BUSY guard look correct in the reviewed paths. The focused local TLS/NVM/provisioning tests passed (4/4); CI run 41548772849 is green at this exact head, with Linux 33/33 and TSan zero reports, distinct findings, and runtime aborts. Those checks do not cover deletion retries. No source changes or merge performed. |
…e must not leak or re-init the dead session (failing)
…ion teardown; nvm_load boot-only by contract
|
Fixed in 06014ed (test in 1fb4b0f, failing on aeead7a). |
…es to persist until flush reloads (failing)
…flash is never overwritten — persist refused until flush re-reads it
|
Consolidated follow-up review at 55335a0: the deletion retry, attempt-local completion, session teardown, and unreadable-flash refusal/recovery fixes are addressed in the reviewed paths. One blocker remains: active TLS configuration lifetime. [P1] Keep the active session’s entire configuration alive and immutable during configure(). hal_tls_posix.c lines 270–282 still frees/rebuilds A white-box lifetime probe, compiled with the exact build’s mbedTLS configuration, configures a valid CA, calls ssl_setup, arranges an active slot, then calls configure with invalid PEM. It shows the still-connected SSL context now points to the failed replacement configuration, with its original CA gone: This probe demonstrates mutation of the retained configuration, not a simulated network handshake or a claimed exploitable crash. It confirms the stated first limit is a correctness issue, not an acceptable release limitation under the existing “current session is unaffected” contract. Required closure: stage and validate replacement credentials/configuration separately, retain the active configuration and everything it references until the active SSL context is freed, then apply the replacement to the next session. A rejected reconfiguration must leave the active configuration intact. Add regressions for successful and failed reconfiguration while connected: both preserve active configuration/dependencies and ongoing traffic; a successful replacement is used by the next connection; repeat rotations/failures/reconnects/disconnects release all configurations/sessions exactly once. Verify both lifetime invariants and behavior; echo alone misses this defect. The second stated limit (erased flash semantics) is a nonblocking HAL contract clarification for future ports: a successful erased-region read should return erased bytes, not an I/O failure. POSIX already implements usable fresh-device behavior; retain the new fail-closed handling for real read errors. CI run 41722868070 is green at this exact head: Linux 33/33, TSan 0 reports / 0 distinct findings / 0 runtime aborts. I reviewed the accumulated PR and the latest failure paths; no other merge-blocking finding identified. This is the consolidated remaining fix and acceptance criteria. No source edits or merge performed. |
…fied or freed; staged replacement applies on the next connect (failing)
…a session is live; the active set outlives the context that references it
|
Fixed in b9b1346 (tests in 71594d1, failing on 55335a0). Each slot now holds two configuration sets: the active set — the one |
|
Final review at b9b1346: the active TLS configuration lifetime blocker is resolved. Each slot has separate active/scratch configuration storage; configure builds only the scratch set, failure frees scratch without changing active, and connect promotes a validated replacement only after the old session is freed. The retained configuration, credentials, entropy and RNG remain at stable addresses for the session lifetime. The documented failed-configure behavior on a disconnected slot is consistent with retaining the last valid configuration. My adapted independent lifetime probe now reports Exact-head CI 41727840441 passed Linux 33/33 and TSan 33/33, with 0 report files, 0 findings, 0 runtime aborts. No remaining merge-blocking code findings identified in the reviewed PR. All previously raised blockers are addressed. Proceeding with the user-authorized merge of this exact head into dev. |
The bugs (post-merge review of #183)
The TLS connect worker released the slot, then read its completion callback and user context from the reusable
per-slot argument table; a second attempt accepted in that interval replaced them, so the first completion was lost
and the second callback ran twice. NVM mutation and flush calls returned success when the flash write had failed, and a
provisioning retry found the value in RAM and returned success without writing it.
What breaks, and when
A completion delivered to the wrong object, or never; a device that believes it is provisioned and boots without its
identity after power loss.
What changed and why it fixes it
The connect worker copies its attempt — host, port, SNI, callback, context — before any blocking work and uses only
that copy; nothing after the slot is released touches shared storage. Persisting now returns whether the table
generation reached flash; every mutation API and flush return that result and a failed write emits one
NVM_PERSIST_FAIL event. Provisioning is idempotent on the value, not on the attempt: an already-provisioned key returns
success only once its value is on flash, retrying a persist that failed.
Deleting an absent key likewise returns success only once the table without it is on flash, and TLS session
teardown has one owner, so a peer-closed session is released before recv() returns and is never re-initialised over.
Each TLS slot keeps two configuration sets: configure() builds and validates the replacement apart from the active
set, which a live session references and which is never modified or freed until that session ends; the next
connect_async() promotes it.
Evidence
Nineteen new tests (fifteen failing before their fix, four covering staged-set replacement, discard, promotion after a failed handshake and mutual TLS) passing after; the reviewer's probes rebuilt: callbacks 0/2 → 1/1; failed provision
now returns an error and the retry performs exactly one flash write with the identity readable after reload.
Limits stated
A failed persist does not move the NVM Functional Block to FAULT; that policy (permanent failure, retry budget) is a
separate decision. The in-memory table keeps the change — getters return it — and the next successful persist carries
it. Factory reset that fails part-way leaves the remaining keys in place until reboot (pre-existing; recorded).
Loading the table from flash is boot-only by contract: a runtime reload would discard a concurrent writer's
unpersisted change, so it is not supported.
If flash cannot be read at boot the store starts empty and refuses every mutation (NVM_PERSIST_FAIL, state
FLASH_UNREADABLE) until embediq_nvm_flush() re-reads it, so unknown flash contents are never overwritten; an invalid
blob (bad magic or CRC) is replaced by the first successful write.
The POSIX TLS HAL holds two configuration sets per slot (memory: certificates and keys for both); a staged
replacement is discarded by a later rejected configure().
🤖 Generated with Claude Code