Skip to content

Dirty-filesystem detection + vfat pre-mount repair - #180

Merged
bbangert merged 2 commits into
mainfrom
feat/dirty-fs-detection
Aug 21, 2026
Merged

Dirty-filesystem detection + vfat pre-mount repair#180
bbangert merged 2 commits into
mainfrom
feat/dirty-fs-detection

Conversation

@bbangert

Copy link
Copy Markdown
Owner

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

FAT-fs (sda1): Volume was not properly unmounted. Some data may be corrupt. Please run fsck.

…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 -p before every mount — vfat got no
pre-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.Probe stays pure — it is handed bytes, never a device.

fs where the flag lives verdict
exFAT boot sector VolumeFlags, u16 LE at offset 106, bit 1 (VolumeDirty) in the 4 KiB head; dirty?/2 answers directly
FAT32 FAT[1], u32 LE at reserved_sectors * bytes_per_sector + 4; bit 27 (CLEAN_SHUT) — dirty when clear outside the head, needs a second read
FAT16 FAT[1], u16 LE at reserved_sectors * bytes_per_sector + 2; bit 15 — dirty when clear second read (usually still inside the head)
FAT12 no clean-shutdown bit exists :error/nil, never guessed
ext4 superblock state false — it is fscked on the way in, so the superblock is stale by the time anything renders it
NTFS3 $Volume dirty flag false — the kernel mounts a dirty NTFS read-only, which the drawer already reports

Because FAT32's flag is past the reserved sectors, dirty?/2 answers
:unknown for vfat and dirty_probe/1 returns {:read, offset, length};
the caller does the read and finishes with dirty_at?/3. Probe never
touches a device.

fat_dirty_offset/1 parses the BPB out of the head (bytes-per-sector u16
at 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 as
a u16 would invent a dirty flag out of FAT[2]'s bits.

One read seam instead of two

Storage.Server's :read_head_fun seam (offset 0 only) is replaced by
:read_at_fun(device_path, offset, length), defaulting to
:file.pread on a :raw handle. Both reads the sniff needs go through
it, 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 the
mount payload; an unreadable second read lands on nil ("not known"),
deliberately distinct from a false that was really read off the volume.

vfat repair, and the dosfstools dependency

Storage.Mount now runs fsck.fat -a <device> before a vfat mount,
symmetric with ext4's fsck.ext4 -p: exit 0 and 1 (errors found and
fixed) proceed, 2 and above abort the mount rather than risk the data.

fsck.fat is not in the image yet. dosfstools lands with the custom
systems' v0.1.9; until then Mount finds neither /sbin/fsck.fat nor
/usr/sbin/fsck.fat and mounts without a repair rather than failing every
FAT 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 warning badge ("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, FAT32
and FAT16, the offset math, FAT12 and implausible-BPB refusals, and
ActiveFat alone not reading as dirty), 6 in mount_test
(invocation/repaired/abort/skip-when-absent), 10 in server_test (the
second read through the seam, nil on a short read, payload + broadcast,
a raising seam), 3 in overview_live_test. 343 passed for the storage +
overview set, 1584 passed for the full suite; mix format,
compile --warnings-as-errors and 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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 -a before 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.

Comment thread lib/universal_proxy/storage/server.ex
Comment thread lib/universal_proxy/storage/probe.ex
Comment thread lib/universal_proxy/storage/server.ex
bbangert and others added 2 commits August 21, 2026 05:48
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
bbangert force-pushed the feat/dirty-fs-detection branch from 4bbb324 to 75669cf Compare August 21, 2026 06:06
@bbangert
bbangert requested a balanced review from Copilot August 21, 2026 14:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?/2 uses 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.

@bbangert
bbangert merged commit ac4de54 into main Aug 21, 2026
7 checks passed
@bbangert
bbangert deleted the feat/dirty-fs-detection branch August 21, 2026 14:58
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.

2 participants