Dirty-filesystem detection + vfat pre-mount repair - #180
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
Adds dirty-filesystem detection, optional FAT repair before mounting, and user-facing warnings. It also includes an unrelated forced SMB reprovisioning change.
Changes:
- Detects exFAT/FAT16/FAT32 dirty state through raw reads.
- Runs
fsck.fat -abefore vfat mounts when available. - Displays dirty-volume warnings and forces SMB account provisioning.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
lib/universal_proxy/storage/probe.ex |
Implements dirty-bit parsing. |
lib/universal_proxy/storage/server.ex |
Propagates dirty state and forces SMB provisioning. |
lib/universal_proxy/storage/mount.ex |
Adds conditional vfat repair. |
lib/universal_proxy/storage/smbd.ex |
Adds forced provisioning support. |
lib/universal_proxy_web/components/storage.ex |
Renders dirty-volume warnings. |
test/support/storage_fixtures.ex |
Adds FAT and exFAT fixtures. |
test/universal_proxy/storage/probe_test.exs |
Tests dirty-bit parsing. |
test/universal_proxy/storage/server_test.exs |
Tests detection propagation and provisioning. |
test/universal_proxy/storage/mount_test.exs |
Tests vfat repair behavior. |
test/universal_proxy/storage/smbd_test.exs |
Tests forced provisioning. |
test/universal_proxy_web/live/overview_live_test.exs |
Tests warning presentation. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
A yanked stick's dirty FAT returned EIO to HA's backup writes with no warning. Probe now reads the FAT/exFAT dirty bits, the drawer surfaces "wasn't cleanly unmounted", and vfat gets fsck.fat -a before mount (when the image ships dosfstools, systems v0.1.9) matching ext4's flow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The kernel holds the FAT/exFAT dirty bit while a volume is mounted writable, so post-mount sniffs flagged healthy drives; the verdict is now captured pre-mount and retained (nil for adopted mounts). FAT32 with mirroring disabled reads FAT[1] from the active copy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bbangert
force-pushed
the
feat/dirty-fs-detection
branch
from
August 21, 2026 06:06
4bbb324 to
75669cf
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Suppressed comments (1)
lib/universal_proxy/storage/server.ex:1273
- This comment describes a fallback that does not occur:
fs_dirty?/2uses the mount verdict whenever a mount map exists, and mounted-device sniffing also copies that verdict. An adopted mount therefore remains unknown (nil), as intended; update the comment so it does not document the opposite behavior.
# The mount table records no such thing, and this mount was
# not sniffed by this process — the drawer falls back to the
# drive's own sniffed verdict.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The EIO with no warning
A USB stick was yanked while Home Assistant was writing a backup over the
SMB share. On the next plug-in the kernel logged
…and nothing else happened: the volume mounted read-write, the share came
up, and HA's next backup write got EIO from a filesystem whose metadata
was inconsistent. Nothing in the UI said the drive had a problem, and
unlike ext4 — which gets
fsck.ext4 -pbefore every mount — vfat got nopre-mount check at all, so the damage was never even repaired.
This adds both halves: read the dirty bit at plug-in and surface it, and
run the FAT checker before mounting when the image has one.
The dirty bit, per filesystem
Storage.Probestays pure — it is handed bytes, never a device.VolumeFlags, u16 LE at offset 106, bit 1 (VolumeDirty)dirty?/2answers directlyFAT[1], u32 LE atreserved_sectors * bytes_per_sector + 4; bit 27 (CLEAN_SHUT) — dirty when clearFAT[1], u16 LE atreserved_sectors * bytes_per_sector + 2; bit 15 — dirty when clear:error/nil, never guessedfalse— it isfscked on the way in, so the superblock is stale by the time anything renders it$Volumedirty flagfalse— the kernel mounts a dirty NTFS read-only, which the drawer already reportsBecause FAT32's flag is past the reserved sectors,
dirty?/2answers:unknownfor vfat anddirty_probe/1returns{:read, offset, length};the caller does the read and finishes with
dirty_at?/3.Probenevertouches a device.
fat_dirty_offset/1parses the BPB out of the head (bytes-per-sector u16at 11, reserved-sectors u16 at 14, plus the FAT spec's own cluster-count
type determination) and refuses implausible geometry. The FAT12 guard
matters: its 12-bit
FAT[1]straddles a byte boundary, so reading it asa u16 would invent a dirty flag out of
FAT[2]'s bits.One read seam instead of two
Storage.Server's:read_head_funseam (offset 0 only) is replaced by:read_at_fun—(device_path, offset, length), defaulting to:file.preadon a:rawhandle. Both reads the sniff needs go throughit, so a test drives the whole two-step handshake from a single fixture
image.
dirty?rides along on each sniffed drive/partition map and on themount payload; an unreadable second read lands on
nil("not known"),deliberately distinct from a
falsethat was really read off the volume.vfat repair, and the dosfstools dependency
Storage.Mountnow runsfsck.fat -a <device>before a vfat mount,symmetric with ext4's
fsck.ext4 -p: exit 0 and 1 (errors found andfixed) proceed, 2 and above abort the mount rather than risk the data.
fsck.fatis not in the image yet. dosfstools lands with the customsystems' v0.1.9; until then
Mountfinds neither/sbin/fsck.fatnor/usr/sbin/fsck.fatand mounts without a repair rather than failing everyFAT mount. Both paths are searched so the repair starts working the moment
the package appears, with no code change. Detection is unaffected — it
reads bytes, not binaries. exFAT gets no repair either way: no exFAT
checker is shipped at all.
The drawer
The filesystem section grows a
warningbadge ("Not cleanly unmounted")and one line: Wasn't cleanly unmounted — data may be corrupt. Repair
happens automatically at mount when possible; formatting to ext4 is the
durable fix. An adopted mount (one that outlived the Server process) has
no sniff behind it, so it falls back to the drive's own partition verdict.
Tests
+40, all host: 21 in
probe_test(clean/dirty fixtures for exFAT, FAT32and FAT16, the offset math, FAT12 and implausible-BPB refusals, and
ActiveFatalone not reading as dirty), 6 inmount_test(invocation/repaired/abort/skip-when-absent), 10 in
server_test(thesecond read through the seam,
nilon a short read, payload + broadcast,a raising seam), 3 in
overview_live_test.343 passedfor the storage +overview set,
1584 passedfor the full suite;mix format,compile --warnings-as-errorsand host dialyzer all clean.Not hardware-validated — the FAT paths want a real yanked stick on the
rpi3 before this is trusted in the field.
🤖 Generated with Claude Code