Repository navigation
hal/flash: read failures are errors, never an empty store; read contract stated - #189
Conversation
…r, not an empty store; a failed write leaves the store (failing)
…n existing store into an empty or truncated one; read contract stated
|
Consolidated review at cb25e2e: hold merge. Read-error propagation, oversized-file refusal and NVM fail-closed recovery work in the focused local tests. Four closure items follow; the last two are pre-existing adjacent defects found during the requested whole-family audit, not newly introduced regressions. 1. [P1] Fix the Linux build failure in the new tests. test_hal_flash_posix.c:84–87: GCC sees a potentially 255-byte 2. [P1] Persist the renamed directory entry before reporting durable success. hal_flash_posix.c:178–189 syncs the temp file but returns OK immediately after rename. The public HAL/NVM durability promise is stronger than that: file fsync does not ensure the directory entry reached storage; the Linux fsync manual explicitly requires a directory fsync as well. The PR body discloses the limitation, but that does not satisfy the public contract. Obtain/check the containing-directory descriptor, sync it after successful rename, and account for any parent directory created by this operation (or require/prevalidate an already durable parent). Align the error contract: failures before publication preserve the old store; a directory-sync failure after rename returns ERR with durability uncertain and the new store possibly visible, rather than falsely promising that every error preserves the old contents. Do not attempt an unproven rollback after publication. Tests should verify operation order, exactly one HAL observation per failed operation, pre-rename failure preservation/cleanup, directory-sync failure returning ERR, and NVM retry behavior. These tests establish call/error handling, not a claimed physical power-cut proof. 3. [P1, pre-existing adjacent] Reject wrapped write/erase intervals before memory access. The write and erase checks add 4. [P2] Apply the new path refusal to the default HOME-derived path as well. hal_flash_posix.c:114–122 uses precision-limited Verification completed: isolated macOS build succeeded; focused ctest 3/3 (HAL flash, NVM, HAL observation) passed. No claim of green Linux CI or a clean sanitizer suite: both current Linux jobs fail compilation, and the independent boundary probe intentionally exposes the overflow failure. Source and developer checkout untouched; no merge. Please address these as one consolidated correction with the stated tests, then obtain exact-head green Linux build/TSan. No further optional redesign is requested here. |
…y entry synced after rename; wrapped ranges and shortened default paths are refused
|
All four items are addressed in 2593739. (1) The test paths are sized from their derivation: the temp-dir buffer is prefix + int pid + NUL, and the store paths add the longest suffix. They are built with a bounded |
|
Recheck at 2593739. Linux compile failure, overflow-safe range checks and both path-length branches are resolved. Directory-sync errors after publication now return an honest error without an unsafe rollback. One part of the original durability finding remains: first-use directory creation. Hold merge for that item. [P1] Do not return durable NVM success for a directory the HAL created without syncing its parent. hal_flash_posix.c:130–131 creates Here Closure: make the parent entry of each directory created by the HAL durable before reporting successful persistence. Account for creation during resolve_paths/read as well as during write. Preserve correct retries: if mkdir succeeds but syncing its parent fails, a later EEXIST must not bypass the still-needed parent sync. Keep one HAL error observation per failed HAL call, and propagate failure to NVM. Avoid changing unrelated code. Required targeted regressions: start with an existing HOME but no .embediq; run normal NVM load/set and verify the new directory’s parent is synced as well as the store directory, with success only after the necessary syncs. Repeat for an explicit path whose immediate parent is auto-created. Inject failure of the newly-created-directory parent sync: no false success; after recovery, retry must perform the needed sync even though mkdir now sees EEXIST, and preserve/reload the data. Keep pre-rename versus post-publication error semantics distinct. These check the durability protocol, not physical power-cut survival. Update the first-use disclaimer after implementation so the public contracts agree. The omitted NVM retry case was independently verified; it is not an additional blocker: with an already existing directory, the first identity provision hits the directory-fsync hook, returns ERR, then provision retry writes once and the identity reloads correctly: Source of both probes is saved in the review workspace as Verification: focused local ctest 3/3 passed (HAL flash/NVM/HAL observation). Exact-head CI 41971926269: normal 35/35, TSan 35/35, 0 report files / 0 findings / 0 runtime aborts. No other blocking issue identified in this recheck. No source changes or merge performed. |
|
Added test_set_after_directory_sync_failure_retries_and_persists (test_fb_nvm.c). A directory-sync failure fails the persist with one NVM_PERSIST_FAIL carrying the HAL code, the published store already holds the new value, and the next persist carries the change, through set and through flush. It passes on the current head because it guards the contract rather than reproducing a regression. |
… first successful write; a failed parent sync is retried (failing)
…HAL creates is synced before the first successful write, and retried on failure
|
The recheck item is fixed in 7a06a72 (failing tests at 3d9c6ac). |
|
Recheck at 7a06a72: normal first use, same-store retry and NVM retry are now addressed, and exact-head CI is green. One confirmed path-switching defect remains in the new pending-directory bookkeeping; hold merge. [P1] Creating a second directory must not discard the first directory’s unsatisfied sync. hal_flash_posix.c:111–112 unconditionally overwrites the sole
Independent public-HAL probe against this exact head: A’s directory entry remains unconfirmed even though its retry reports durable success. Using different parents is essential: syncing a shared parent could incidentally make both entries durable. The added test uses B’s already-existing directory, so its mkdir never replaces the pending slot and does not catch this case. Recommended bounded fix: avoid pending-path bookkeeping by syncing the current store directory’s parent before each successful nonzero write/erase returns, in addition to syncing the store directory. This extra sync removes the lost-slot problem and dependence on the current lexical path matching an in-memory record; it needs no unbounded table or allocation. Update the test-only introspection and implementation comments to match the chosen protocol. If retaining pending state instead, it must never overwrite/evict an unsynced entry; inability to retain it must fail safely, not produce false success. Closure regression: extend path-switching coverage to newly created A and B under distinct existing parents. Fail A’s parent sync, successfully create/write B, then retry A; prove A’s own parent is synced before HAL_OK. Also cover both parent syncs failing and subsequent retries in both orders if the implementation retains bookkeeping. Keep assertions about actual sync targets, one observation per failed call, unchanged no-side-effect reads and the existing NVM/retry coverage. Do not merely assert a global pending boolean. Verification: independent focused local ctest 3/3 passed (HAL flash, NVM, HAL observation). CI 41975010526: normal Linux 35/35, TSan 35/35, 0 report files / 0 findings / 0 runtime aborts. Those checks omit the failing sequence above. The other prior review items remain closed; no additional blocker identified in this recheck. No source edits or merge performed. |
… durable; every write syncs the parent (failing)
… and erase — no record of created directories
|
Recheck of 7a06a72 addressed at f43668f by removing the state rather than repairing it: every successful nonzero write or erase now syncs the store file, the store's directory and that directory's parent (computed with |
|
Final recheck at f43668f: no blocking findings; ready to merge. All prior review findings are closed. The write/erase helper now performs file sync, rename, store-directory sync and parent-directory sync before reporting successful persistence, without retaining a pending-directory slot. This removes the path-switching loss of retry state. Post-publication sync failure still returns HAL_ERR_IO with one observation and no unsafe rollback; a later nonzero write/erase repeats the protocol. Reads remain free of directory-creation/sync side effects, and the previously reviewed range/path/read-error protections remain intact. Independent reproduction of the previously failing distinct-parent sequence against the exact head now passes: The regression tests use distinct parents and verify the actual sync target in both directions; the every-write/erase test verifies reads do not increment the count. Using the post-write count as the read baseline is correct. Additional multi-pending-slot tests are no longer a closure requirement because that bookkeeping has been removed; the per-call failure/retry paths remain covered. Independent isolated local checks: focused ctest 3/3, HAL flash 74/74 assertions, NVM 34/34, HAL observation 19/19. Exact-head CI 42167032138: normal Linux 35/35, TSan 35/35, 0 report files / 0 distinct findings / 0 runtime aborts. No independent full-suite, sanitizer or physical power-cut rerun claimed this turn. These tests verify the sync protocol and error handling within the supported filesystem guarantees. No further revision requested for #189. Developer checkout untouched; no merge performed. |
The bug
The POSIX flash HAL's file reader returned "zero bytes" for every failed open and never checked for a read error, so
hal_flash_read()reported success with fabricated content, and
hal_flash_write()/hal_flash_erase()— which read the whole store before rewriting it —replaced an unreadable existing store with an empty one plus the new bytes. The NVM store's unreadable-flash protection therefore held only
against the test hook, not against a real read failure.
What breaks, and when
A transient filesystem error at boot, or a store the process cannot read, becomes a device that believes it has no configuration — and the
next write makes that permanent.
What changed and why it fixes it
An absent backing file is the one legitimate "empty" case (ENOENT). Every other open or read failure is HAL_ERR_IO, observed through the HAL
fault event; read returns it, and write and erase return it without touching the store. The read contract is now stated in the HAL header:
success means stored bytes (absent and erased regions read as the implementation's absent/erased value — callers detect "nothing stored" with
their own framing, as the NVM store does with magic and CRC), and HAL_ERR_IO means the store could not be read for any reason other than its
absence.
The write path checks flush, fsync and close as well as the write: a failure at any step up to and including the rename removes the temp
file and leaves the previous store in place. After the rename the containing directory is fsync'd, so the new directory entry is durable
too; the header now states this, and the error contract on either side of the publication point. A backing file larger than the
implementation holds is refused with HAL_ERR_INVALID instead of being rewritten truncated.
Ranges are validated by subtraction before any addition or pointer arithmetic, for read, write and erase alike: a wrapping interval such as
hal_flash_erase(1, SIZE_MAX)used to reachmemsetwith SIZE_MAX and is now HAL_ERR_INVALID with nothing touched; an out-of-capacity readis HAL_ERR_INVALID instead of silent zeros; and
len == 0within capacity is HAL_OK and does not rewrite the store. A store or temp path thatdoes not fit the path buffer is refused, never shortened: the HOME-derived default path is now checked like the explicit one, before any
directory is created.
A read never creates a directory. Every successful write or erase syncs the store file, the store's directory and that directory's parent,
in that order, before reporting success, so a directory created on first use (the default
$HOME/.embediq, or an explicit path's parent) isdurable by the first write that succeeds; a failed sync returns HAL_ERR_IO and is simply performed again by the next nonzero write or erase,
whichever store was written in between. No record of which directories the HAL created is kept.
Evidence
A new HAL test suite reproduces the failures with a real unreadable store (a directory as the backing path; a permission-denied file) — no
injection hook — and pins the absent/erased/short-read semantics; an NVM-level test shows a real read failure now puts the store into its
unreadable state and recovers after flush. Further tests cover the wrapping and boundary ranges, the zero-length policy, both path
refusals (nothing created), and a directory-sync failure after the rename (a host-only hook): the write reports HAL_ERR_IO with exactly one
observation and the new store published. Full suite under ThreadSanitizer: 0 findings; Linux CI build and TSan jobs green.
Limits stated
The POSIX implementation holds the whole store in a 64 KiB stack buffer for read-modify-write; a store larger than that is now refused
rather than truncated, and the buffer itself is unchanged here. A flush failure (disk full) during a write is handled but not unit-tested.
A directory-sync failure after the rename returns HAL_ERR_IO with the new contents already visible and their durability not guaranteed; no
rollback is attempted; the same holds when the store directory's parent cannot be synced. These tests establish call order
and error handling, not a physical power-cut proof.
🤖 Generated with Claude Code