You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Low priority, but out of curiosity, asked an agent to do some research and stub out some ideas for what that might entail (pasted into following comment).
It's wrong about various things, like the "storageProfile" being braid-wide rather than a distinction per disk. (tbf it was my fault; it asked me some clarifying questions but I just zoomed through them).
Add opt-in SSD/NVMe support through a pool-wide storage profile. The default
stays HDD-compatible. Setting braid.storageProfile = "flash" makes braid open
LUKS mappings with discard passthrough and mount btrfs with async discard. The
HDD profile gets an explicit nodiscard mount option so that no device in the
pool can ever trigger the kernel's auto-async-discard heuristic. The cryptsetup
security tradeoff is documented: discard can leak filesystem usage patterns
through encryption.
Research facts to preserve in the docs: btrfs supports discard=async since
kernel 5.6 and auto-enables it since 6.2 whenever any device in the pool
advertises discard support; an explicit nodiscard mount option is the only
way to suppress this for HDD pools that might contain a flash member or sit on
a thinly-provisioned backing device. dm-crypt advertises discard only when
opened with --allow-discards; cryptsetup status reports this as flags: discards.
Rust StorageProfile enum plus Config::storage_profile().
No CLI override and no auto-detection; the operator chooses the profile.
Update command construction:
Extend CryptsetupLuksOpen and CryptsetupLuksOpenKeyFile requests with allow_discards: bool.
In flash profile, render cryptsetup open ... --allow-discards ....
Keep existing --perf-no_read_workqueue and --perf-no_write_workqueue.
Do not add --persistent; the Nix config owns this policy, not LUKS
metadata.
Do not add discard flags to luksFormat, passphrase tests, key enrollment,
or header backups.
Update mount behavior:
HDD profile mounts as noatime,skip_balance,subvolid=5,nodiscard. The
explicit nodiscard is required: btrfs auto-enables async discard since
kernel 6.2 if any device in the pool advertises support, which would
otherwise leak through HDD pools that contain a flash member or sit on
thinly-provisioned storage.
Flash profile mounts with noatime,skip_balance,subvolid=5,discard=async.
Thread the profile through a single rendering chokepoint: base_mount_options(profile) in cli/src/cmd.rs. The Mount and MountWithOptionsto_argv branches consume the profile-derived options
so every caller (unlock, auto-unlock, add/bootstrap, recover, degraded
mounts, replace/recovery paths) inherits the right policy without
per-callsite logic.
Make the chokepoint structural: CmdRequest::Mount and CmdRequest::MountWithOptions carry a profile: StorageProfile field so
omitting the profile is a compile error.
Keep noatime for flash too; even without HDD spindown, it avoids extra
metadata writes.
Handle already-open mappers (structural at the LUKS boundary):
Extend parse_cryptsetup_status to parse optional flags: and expose discards: bool.
Promote MapperOwnership::Owned to carry the discards bit, e.g. Owned { discards: bool }, populated by classify_mapper_ownership from
the parsed status.
Thread StorageProfile (or a MapperReusePolicy derived from it) through ensure_luks_open, ensure_luks_open_with_key_file, and probe_mapper_open so the reuse rule is enforced exactly once, at the
helper that owns the unsafe reuse decision. Omitting the profile is a
compile error.
Flash profile: Owned { discards: false } returns a hard error from ensure_luks_open* with a message telling the operator to run braid lock and retry. Owned { discards: true } resolves to OpenOutcome::AlreadyOwned. probe_mapper_open fails closed in the same
case with the same braid lock remediation -- not "needs reopen" -- so
the planner does not try to dry-run an impossible cryptsetup open
against a mapper that is still active. Current planning (plan_open_pool
in cli/src/mount.rs:303 and the mapper_open: bool on PresentLuks in cli/src/types.rs:511) treats false as "schedule an
open", so probe-level failure is the only safe shape.
HDD profile: Owned { discards: _ } always resolves to OpenOutcome::AlreadyOwned; the discard bit is ignored. No behavior
change for HDD.
All current callsites of these helpers (in cli/src/mount.rs, cli/src/add.rs, cli/src/replace.rs, cli/src/recover.rs) thread the
profile from the already-loaded Config. Because the policy lives in the
helper, no callsite-level check is required, and a future callsite
cannot bypass the rule without making the type system unhappy.
Update docs:
Revise ADR-015 and Principle 11 from "HDD-only defaults" to "HDD default,
flash opt-in".
Document the flash profile in README and docs/guides/nixos-configuration.md.
Update unlock/mount docs to include the explicit profile-dependent mount
options: HDD = noatime,skip_balance,subvolid=5,nodiscard, flash = noatime,skip_balance,subvolid=5,discard=async.
Explicitly state why HDD uses nodiscard: it neutralizes the kernel's
auto-async-discard heuristic for pools that might contain flash members or
sit on thin-provisioned storage.
Explicitly state the security tradeoff from cryptsetup: discard can reveal
filesystem type, used space, and related usage patterns.
Test Plan
Rust unit tests:
Config parses default "hdd" and explicit "flash", rejects unknown
profiles.
cryptsetup status parser handles no flags: line, flags: discards, and
mixed flags.
HDD CryptsetupLuksOpen* argv has no --allow-discards.
Flash CryptsetupLuksOpen* argv includes --allow-discards.
Pin mount argv at the central rendering chokepoint (to_argv for CmdRequest::Mount and CmdRequest::MountWithOptions) across the full
matrix:
HDD plain mount argv contains nodiscard and not discard=async.
HDD degraded mount argv contains nodiscard,degraded and not discard=async.
Flash plain mount argv contains discard=async and not nodiscard.
Flash degraded mount argv contains discard=async,degraded and not nodiscard.
Pin the LUKS reuse rule at the helper boundary, not only at planning:
classify_mapper_ownership populates Owned { discards } from the
parsed cryptsetup status for both flags: discards and a missing flags: line.
Under flash profile, ensure_luks_open returns a hard error for Owned { discards: false } and the error message names braid lock as
the remediation.
Under flash profile, ensure_luks_open returns AlreadyOwned for Owned { discards: true }.
Under flash profile, ensure_luks_open_with_key_file matches the above
two cases. (Same boundary, separate entry point, separate test.)
Under HDD profile, both ensure_luks_open and ensure_luks_open_with_key_file return AlreadyOwned regardless of discards.
Under flash profile, probe_mapper_open returns a hard error for Owned { discards: false } with the braid lock remediation, not a
"needs reopen" classification.
plan_open_pool (unlock dry-run path in cli/src/mount.rs) surfaces
that probe error verbatim when run with the flash profile against a
mapper that is active but lacks discards. Pin both the dry-run
preview and the executed unlock at this seam so the remediation
message reaches the operator before any cryptsetup open is
planned.
Nix/module tests:
Default generated /etc/braid/config.json contains or implies storage_profile = "hdd".
Invalid profile fails Nix evaluation through the enum type.
Focused VM tests (each registered in flake.nixchecks):
flash-profile-unlock: flash profile, create a small pool, unlock it, then
assert cryptsetup status braid-disk1 includes flags: with discards, findmnt -o OPTIONS -n /mnt/storage includes discard=async and not nodiscard, and existing skip_balance and subvolid=5 assertions still
pass.
flash-profile-add-bootstrap: flash profile, run braid add on a fresh
pool (the non-unlock mount-producing seam), then assert the mounted btrfs
options include discard=async. This exercises a different mount path
from the unlock test and catches regressions where add/bootstrap or
recover/remount fail to thread the profile.
hdd-profile-mount-options: default (HDD) profile pool, unlock it, then
assert findmnt -o OPTIONS -n /mnt/storage includes nodiscard and not discard=async. Confirms the explicit nodiscard reaches mount argv and
the kernel does not auto-enable discard on the VM's backing devices.
Verification:
just test-rust
just test-vm flash-profile-unlock flash-profile-add-bootstrap hdd-profile-mount-options
nix flake check --no-build to confirm the three new checks are
registered and evaluate.
mdbook build docs
Assumptions
The feature is explicit opt-in, not drive auto-detection.
Flash profile enables LUKS discard passthrough by default.
Scrub scheduling stays monthly by default.
Mixed HDD/flash pools are not blocked; the profile is an operator policy
choice, and unsupported lower layers simply will not perform useful discard.
TUI/NVMe SMART parsing is already present, so this commit does not need a TUI
feature unless docs mention existing monitoring support.
Activity
danneu commentedon May 23, 2026
Add Explicit Flash Storage Profile
Summary
Add opt-in SSD/NVMe support through a pool-wide storage profile. The default
stays HDD-compatible. Setting
braid.storageProfile = "flash"makes braid openLUKS mappings with discard passthrough and mount btrfs with async discard. The
HDD profile gets an explicit
nodiscardmount option so that no device in thepool can ever trigger the kernel's auto-async-discard heuristic. The cryptsetup
security tradeoff is documented: discard can leak filesystem usage patterns
through encryption.
Research facts to preserve in the docs: btrfs supports
discard=asyncsincekernel 5.6 and auto-enables it since 6.2 whenever any device in the pool
advertises discard support; an explicit
nodiscardmount option is the onlyway to suppress this for HDD pools that might contain a flash member or sit on
a thinly-provisioned backing device. dm-crypt advertises discard only when
opened with
--allow-discards; cryptsetup status reports this asflags: discards.Key Changes
braid.storageProfile = "hdd" | "flash", default"hdd".storage_profile.StorageProfileenum plusConfig::storage_profile().CryptsetupLuksOpenandCryptsetupLuksOpenKeyFilerequests withallow_discards: bool.cryptsetup open ... --allow-discards ....--perf-no_read_workqueueand--perf-no_write_workqueue.--persistent; the Nix config owns this policy, not LUKSmetadata.
luksFormat, passphrase tests, key enrollment,or header backups.
noatime,skip_balance,subvolid=5,nodiscard. Theexplicit
nodiscardis required: btrfs auto-enables async discard sincekernel 6.2 if any device in the pool advertises support, which would
otherwise leak through HDD pools that contain a flash member or sit on
thinly-provisioned storage.
noatime,skip_balance,subvolid=5,discard=async.base_mount_options(profile)incli/src/cmd.rs. TheMountandMountWithOptionsto_argvbranches consume the profile-derived optionsso every caller (unlock, auto-unlock, add/bootstrap, recover, degraded
mounts, replace/recovery paths) inherits the right policy without
per-callsite logic.
CmdRequest::MountandCmdRequest::MountWithOptionscarry aprofile: StorageProfilefield soomitting the profile is a compile error.
noatimefor flash too; even without HDD spindown, it avoids extrametadata writes.
parse_cryptsetup_statusto parse optionalflags:and exposediscards: bool.MapperOwnership::Ownedto carry the discards bit, e.g.Owned { discards: bool }, populated byclassify_mapper_ownershipfromthe parsed status.
StorageProfile(or aMapperReusePolicyderived from it) throughensure_luks_open,ensure_luks_open_with_key_file, andprobe_mapper_openso the reuse rule is enforced exactly once, at thehelper that owns the unsafe reuse decision. Omitting the profile is a
compile error.
Owned { discards: false }returns a hard error fromensure_luks_open*with a message telling the operator to runbraid lockand retry.Owned { discards: true }resolves toOpenOutcome::AlreadyOwned.probe_mapper_openfails closed in the samecase with the same
braid lockremediation -- not "needs reopen" -- sothe planner does not try to dry-run an impossible
cryptsetup openagainst a mapper that is still active. Current planning (
plan_open_poolin
cli/src/mount.rs:303and themapper_open: boolonPresentLuksincli/src/types.rs:511) treatsfalseas "schedule anopen", so probe-level failure is the only safe shape.
Owned { discards: _ }always resolves toOpenOutcome::AlreadyOwned; the discard bit is ignored. No behaviorchange for HDD.
cli/src/mount.rs,cli/src/add.rs,cli/src/replace.rs,cli/src/recover.rs) thread theprofile from the already-loaded
Config. Because the policy lives in thehelper, no callsite-level check is required, and a future callsite
cannot bypass the rule without making the type system unhappy.
flash opt-in".
docs/guides/nixos-configuration.md.options: HDD =
noatime,skip_balance,subvolid=5,nodiscard, flash =noatime,skip_balance,subvolid=5,discard=async.nodiscard: it neutralizes the kernel'sauto-async-discard heuristic for pools that might contain flash members or
sit on thin-provisioned storage.
filesystem type, used space, and related usage patterns.
Test Plan
"hdd"and explicit"flash", rejects unknownprofiles.
cryptsetup statusparser handles noflags:line,flags: discards, andmixed flags.
CryptsetupLuksOpen*argv has no--allow-discards.CryptsetupLuksOpen*argv includes--allow-discards.to_argvforCmdRequest::MountandCmdRequest::MountWithOptions) across the fullmatrix:
nodiscardand notdiscard=async.nodiscard,degradedand notdiscard=async.discard=asyncand notnodiscard.discard=async,degradedand notnodiscard.classify_mapper_ownershippopulatesOwned { discards }from theparsed
cryptsetup statusfor bothflags: discardsand a missingflags:line.ensure_luks_openreturns a hard error forOwned { discards: false }and the error message namesbraid lockasthe remediation.
ensure_luks_openreturnsAlreadyOwnedforOwned { discards: true }.ensure_luks_open_with_key_filematches the abovetwo cases. (Same boundary, separate entry point, separate test.)
ensure_luks_openandensure_luks_open_with_key_filereturnAlreadyOwnedregardless ofdiscards.probe_mapper_openreturns a hard error forOwned { discards: false }with thebraid lockremediation, not a"needs reopen" classification.
plan_open_pool(unlock dry-run path incli/src/mount.rs) surfacesthat probe error verbatim when run with the flash profile against a
mapper that is active but lacks
discards. Pin both the dry-runpreview and the executed unlock at this seam so the remediation
message reaches the operator before any
cryptsetup openisplanned.
/etc/braid/config.jsoncontains or impliesstorage_profile = "hdd".braid.storageProfile = "flash"emits"storage_profile": "flash".flake.nixchecks):flash-profile-unlock: flash profile, create a small pool, unlock it, thenassert
cryptsetup status braid-disk1includesflags:withdiscards,findmnt -o OPTIONS -n /mnt/storageincludesdiscard=asyncand notnodiscard, and existingskip_balanceandsubvolid=5assertions stillpass.
flash-profile-add-bootstrap: flash profile, runbraid addon a freshpool (the non-unlock mount-producing seam), then assert the mounted btrfs
options include
discard=async. This exercises a different mount pathfrom the unlock test and catches regressions where add/bootstrap or
recover/remount fail to thread the profile.
hdd-profile-mount-options: default (HDD) profile pool, unlock it, thenassert
findmnt -o OPTIONS -n /mnt/storageincludesnodiscardand notdiscard=async. Confirms the explicitnodiscardreaches mount argv andthe kernel does not auto-enable discard on the VM's backing devices.
just test-rustjust test-vm flash-profile-unlock flash-profile-add-bootstrap hdd-profile-mount-optionsnix flake check --no-buildto confirm the three new checks areregistered and evaluate.
mdbook build docsAssumptions
choice, and unsupported lower layers simply will not perform useful discard.
feature unless docs mention existing monitoring support.