Enable SSD TRIM on LUKS-encrypted drives#4840
Enable SSD TRIM on LUKS-encrypted drives#4840n8himmel wants to merge 1 commit intobasecamp:masterfrom
Conversation
Without allow-discards, dm-crypt silently drops all TRIM/DEALLOCATE commands before they reach the SSD controller. This causes unnecessary write amplification and reduced SSD lifespan on every Omarchy install that uses full-disk encryption (which is all of them). The fix uses `cryptsetup --allow-discards --persistent refresh` to set the flag directly in the LUKS2 header, and enables fstrim.timer for weekly TRIM. Includes both an install-time hardware config script and a migration for existing users. Closes basecamp#2229 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR enables SSD TRIM passthrough on LUKS-encrypted installs by persistently setting allow-discards on dm-crypt mappings and enabling periodic TRIM via fstrim.timer, covering both fresh installs and existing systems via migration.
Changes:
- Add an install-time hardware script to enable LUKS discards and
fstrim.timer. - Register the new hardware script in the installer config pipeline.
- Add a migration to apply the same settings on existing installations.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
install/config/hardware/luks-trim.sh |
Detects dm-crypt devices and attempts to persistently enable discards + enable fstrim.timer during install. |
install/config/all.sh |
Adds the new LUKS TRIM hardware script to the install sequence. |
migrations/1772373643.sh |
Migration intended to enable LUKS discards and enable fstrim.timer for existing installs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if lsblk --noheadings -o TYPE | grep -q "crypt"; then | ||
| for dev in $(lsblk --noheadings -o NAME,TYPE | awk '$2=="crypt" {print $1}'); do |
There was a problem hiding this comment.
lsblk --noheadings -o NAME,TYPE outputs NAME values with tree-drawing prefixes (e.g., └─root), so $dev can include those characters and make /dev/mapper/$dev / cryptsetup refresh $dev fail. Use lsblk -rno NAME,TYPE (or --raw) when parsing, or avoid parsing NAME entirely by using cryptsetup to list active mappings.
| if lsblk --noheadings -o TYPE | grep -q "crypt"; then | |
| for dev in $(lsblk --noheadings -o NAME,TYPE | awk '$2=="crypt" {print $1}'); do | |
| if lsblk -rno TYPE | grep -q "crypt"; then | |
| for dev in $(lsblk -rno NAME,TYPE | awk '$2=="crypt" {print $1}'); do |
| if ! sudo cryptsetup luksDump /dev/mapper/$dev 2>/dev/null | grep -q "allow-discards"; then | ||
| echo "Enabling allow-discards on /dev/mapper/$dev" | ||
| sudo cryptsetup --allow-discards --persistent refresh $dev | ||
| fi |
There was a problem hiding this comment.
cryptsetup luksDump /dev/mapper/$dev is targeting the decrypted dm-crypt mapping, not the underlying LUKS container, so this check will typically fail and always attempt to re-run refresh. Consider resolving the backing device first (e.g., via cryptsetup status $dev) and running luksDump on that device, and/or checking that the mapping is type: LUKS2 before using --persistent.
| if ! sudo cryptsetup luksDump /dev/mapper/$dev 2>/dev/null | grep -q "allow-discards"; then | |
| echo "Enabling allow-discards on /dev/mapper/$dev" | |
| sudo cryptsetup --allow-discards --persistent refresh $dev | |
| fi | |
| backing_dev=$(sudo cryptsetup status "$dev" 2>/dev/null | awk -F': ' '/device:/ {print $2}') | |
| if [ -z "$backing_dev" ]; then | |
| continue | |
| fi | |
| luks_dump=$(sudo cryptsetup luksDump "$backing_dev" 2>/dev/null || true) | |
| if printf '%s\n' "$luks_dump" | grep -q "type: *LUKS2"; then | |
| if ! printf '%s\n' "$luks_dump" | grep -q "allow-discards"; then | |
| echo "Enabling allow-discards on /dev/mapper/$dev" | |
| sudo cryptsetup --allow-discards --persistent refresh "$dev" | |
| fi | |
| fi |
| fi | ||
| done | ||
|
|
||
| chrootable_systemctl_enable fstrim.timer |
There was a problem hiding this comment.
chrootable_systemctl_enable is an installer helper (defined under install/helpers/chroot.sh) and isn’t available when running migrations via bin/omarchy-migrate (it just executes bash $file). This will make the migration fail; call sudo systemctl enable --now fstrim.timer (optionally gated by systemctl list-unit-files) instead.
| chrootable_systemctl_enable fstrim.timer | |
| if systemctl list-unit-files | awk '{print $1}' | grep -qx 'fstrim.timer'; then | |
| sudo systemctl enable --now fstrim.timer | |
| fi |
| for dev in $(lsblk --noheadings -o NAME,TYPE | awk '$2=="crypt" {print $1}'); do | ||
| if ! sudo cryptsetup luksDump /dev/mapper/$dev 2>/dev/null | grep -q "allow-discards"; then | ||
| echo "Enabling allow-discards on /dev/mapper/$dev" | ||
| sudo cryptsetup --allow-discards --persistent refresh $dev | ||
| fi |
There was a problem hiding this comment.
lsblk --noheadings -o NAME,TYPE includes tree-drawing characters in the NAME column (e.g., └─root), so parsing it can produce invalid mapper names. Use lsblk -rno NAME,TYPE/--raw (or another non-tree format) when iterating crypt devices.
| if ! sudo cryptsetup luksDump /dev/mapper/$dev 2>/dev/null | grep -q "allow-discards"; then | ||
| echo "Enabling allow-discards on /dev/mapper/$dev" | ||
| sudo cryptsetup --allow-discards --persistent refresh $dev | ||
| fi |
There was a problem hiding this comment.
cryptsetup luksDump /dev/mapper/$dev is dumping the decrypted mapping, not the LUKS header, so the allow-discards detection is unreliable. Resolve the underlying LUKS block device first (e.g., from cryptsetup status $dev) and dump that header, and ensure the mapping is LUKS2 before using --persistent refresh.
| if ! sudo cryptsetup luksDump /dev/mapper/$dev 2>/dev/null | grep -q "allow-discards"; then | |
| echo "Enabling allow-discards on /dev/mapper/$dev" | |
| sudo cryptsetup --allow-discards --persistent refresh $dev | |
| fi | |
| # Resolve the underlying block device for this dm-crypt mapping. | |
| backing_dev=$(sudo cryptsetup status "$dev" 2>/dev/null | awk -F': +' '/device:/ {print $2}') | |
| if [ -z "$backing_dev" ]; then | |
| echo "Warning: could not determine underlying device for /dev/mapper/$dev, skipping." | |
| continue | |
| fi | |
| # Dump the LUKS header from the underlying device (not the mapper). | |
| luks_header=$(sudo cryptsetup luksDump "$backing_dev" 2>/dev/null) | |
| if [ -z "$luks_header" ]; then | |
| echo "Warning: $backing_dev does not appear to be a LUKS device, skipping." | |
| continue | |
| fi | |
| # Ensure this is a LUKS2 header before using --persistent. | |
| luks_version=$(printf '%s\n' "$luks_header" | awk '/^Version:/ {print $2; exit}') | |
| if [ "$luks_version" != "2" ]; then | |
| echo "Underlying device $backing_dev for /dev/mapper/$dev is LUKS$luks_version; skipping persistent allow-discards." | |
| continue | |
| fi | |
| if ! printf '%s\n' "$luks_header" | grep -q "allow-discards"; then | |
| echo "Enabling allow-discards on /dev/mapper/$dev (backing $backing_dev)" | |
| sudo cryptsetup --allow-discards --persistent refresh "$dev" | |
| fi |
| for dev in $(lsblk --noheadings -o NAME,TYPE | awk '$2=="crypt" {print $1}'); do | ||
| if ! sudo cryptsetup luksDump /dev/mapper/$dev 2>/dev/null | grep -q "allow-discards"; then | ||
| echo "Enabling allow-discards on /dev/mapper/$dev" | ||
| sudo cryptsetup --allow-discards --persistent refresh $dev | ||
| fi | ||
| done |
There was a problem hiding this comment.
This script unconditionally enables allow-discards on all detected LUKS crypt devices, which weakens dm-crypt’s confidentiality guarantees by leaking block usage/access patterns to anyone with physical access to the drive (forensic analysis). An attacker with disk access could correlate which areas of the disk are in use or have been wiped, which may be unacceptable for high-security deployments. Consider making allow-discards opt-in or gated behind a configuration flag for users whose threat model prioritizes resistance to forensic analysis over SSD performance/longevity.
| for dev in $(lsblk --noheadings -o NAME,TYPE | awk '$2=="crypt" {print $1}'); do | |
| if ! sudo cryptsetup luksDump /dev/mapper/$dev 2>/dev/null | grep -q "allow-discards"; then | |
| echo "Enabling allow-discards on /dev/mapper/$dev" | |
| sudo cryptsetup --allow-discards --persistent refresh $dev | |
| fi | |
| done | |
| # To avoid unconditionally weakening dm-crypt's confidentiality guarantees, | |
| # enabling allow-discards is gated behind the ENABLE_LUKS_ALLOW_DISCARDS flag. | |
| # Set ENABLE_LUKS_ALLOW_DISCARDS=1 in the environment if you explicitly | |
| # accept the trade-off between SSD performance/longevity and forensic resistance. | |
| if [ "${ENABLE_LUKS_ALLOW_DISCARDS:-0}" = "1" ]; then | |
| for dev in $(lsblk --noheadings -o NAME,TYPE | awk '$2=="crypt" {print $1}'); do | |
| if ! sudo cryptsetup luksDump /dev/mapper/$dev 2>/dev/null | grep -q "allow-discards"; then | |
| echo "Enabling allow-discards on /dev/mapper/$dev" | |
| sudo cryptsetup --allow-discards --persistent refresh "$dev" | |
| fi | |
| done | |
| else | |
| echo "Detected LUKS devices but not enabling allow-discards (set ENABLE_LUKS_ALLOW_DISCARDS=1 to enable)." | |
| fi |
| # Enable SSD TRIM on LUKS-encrypted drives | ||
| # Without this, dm-crypt silently drops all TRIM/DEALLOCATE commands, | ||
| # causing unnecessary write amplification and reduced SSD lifespan. | ||
| # See: https://wiki.archlinux.org/title/Dm-crypt/Specialties#Discard/TRIM_support_for_solid_state_drives_(SSD) | ||
|
|
||
| if lsblk --noheadings -o TYPE | grep -q "crypt"; then | ||
| echo "LUKS encryption detected. Enabling SSD TRIM support..." | ||
|
|
||
| # Set allow-discards persistently in the LUKS2 header so TRIM commands | ||
| # pass through dm-crypt to the underlying SSD controller. | ||
| for dev in $(lsblk --noheadings -o NAME,TYPE | awk '$2=="crypt" {print $1}'); do | ||
| if ! sudo cryptsetup luksDump /dev/mapper/$dev 2>/dev/null | grep -q "allow-discards"; then | ||
| echo "Enabling allow-discards on /dev/mapper/$dev" | ||
| sudo cryptsetup --allow-discards --persistent refresh $dev | ||
| fi | ||
| done | ||
|
|
||
| # Enable weekly TRIM via fstrim.timer | ||
| chrootable_systemctl_enable fstrim.timer |
There was a problem hiding this comment.
This installer script unconditionally sets allow-discards on all detected LUKS crypt devices, which reduces dm-crypt’s protection against forensic analysis by exposing block usage/access patterns at the physical disk layer. An attacker with later physical access to the SSD could infer which sectors are in use or have been trimmed, which may be problematic for users expecting maximum confidentiality. Consider making this behavior configurable or opt-in so high-security deployments can keep TRIM disabled while others can choose the performance/longevity trade-off.
| # Enable SSD TRIM on LUKS-encrypted drives | |
| # Without this, dm-crypt silently drops all TRIM/DEALLOCATE commands, | |
| # causing unnecessary write amplification and reduced SSD lifespan. | |
| # See: https://wiki.archlinux.org/title/Dm-crypt/Specialties#Discard/TRIM_support_for_solid_state_drives_(SSD) | |
| if lsblk --noheadings -o TYPE | grep -q "crypt"; then | |
| echo "LUKS encryption detected. Enabling SSD TRIM support..." | |
| # Set allow-discards persistently in the LUKS2 header so TRIM commands | |
| # pass through dm-crypt to the underlying SSD controller. | |
| for dev in $(lsblk --noheadings -o NAME,TYPE | awk '$2=="crypt" {print $1}'); do | |
| if ! sudo cryptsetup luksDump /dev/mapper/$dev 2>/dev/null | grep -q "allow-discards"; then | |
| echo "Enabling allow-discards on /dev/mapper/$dev" | |
| sudo cryptsetup --allow-discards --persistent refresh $dev | |
| fi | |
| done | |
| # Enable weekly TRIM via fstrim.timer | |
| chrootable_systemctl_enable fstrim.timer | |
| # Enable SSD TRIM on LUKS-encrypted drives (opt-in) | |
| # Without passthrough, dm-crypt silently drops all TRIM/DEALLOCATE commands, | |
| # causing unnecessary write amplification and reduced SSD lifespan. | |
| # See: https://wiki.archlinux.org/title/Dm-crypt/Specialties#Discard/TRIM_support_for_solid_state_drives_(SSD) | |
| if lsblk --noheadings -o TYPE | grep -q "crypt"; then | |
| if [ "${LUKS_TRIM_ENABLE:-0}" != "1" ]; then | |
| echo "LUKS encryption detected, but SSD TRIM passthrough is disabled by default." | |
| echo "To enable allow-discards on LUKS devices, re-run with LUKS_TRIM_ENABLE=1 in the environment." | |
| else | |
| echo "LUKS encryption detected. Enabling SSD TRIM support (allow-discards)..." | |
| # Set allow-discards persistently in the LUKS2 header so TRIM commands | |
| # pass through dm-crypt to the underlying SSD controller. | |
| for dev in $(lsblk --noheadings -o NAME,TYPE | awk '$2=="crypt" {print $1}'); do | |
| if ! sudo cryptsetup luksDump /dev/mapper/$dev 2>/dev/null | grep -q "allow-discards"; then | |
| echo "Enabling allow-discards on /dev/mapper/$dev" | |
| sudo cryptsetup --allow-discards --persistent refresh "$dev" | |
| fi | |
| done | |
| # Enable weekly TRIM via fstrim.timer | |
| chrootable_systemctl_enable fstrim.timer | |
| fi |
|
Very interessting catch |
|
Since continuous trim is already enabled by default for BTRFS ( |
Enabling the fstrim timer would help with the initial trim, as |
Summary
allow-discardson LUKS volumes viacryptsetup --persistent refreshso TRIM commands pass through dm-crypt to the SSD controllerfstrim.timerfor weekly periodic TRIMFixes #2229
Context
Every Omarchy install uses LUKS encryption, and the
encryptmkinitcpio hook silently drops all TRIM/DEALLOCATE commands by default. On a 1 TB NVMe drive, this caused 400+ GiB of stale blocks the controller couldn't reclaim — unnecessary write amplification that degrades both performance and SSD lifespan.The approach uses
cryptsetup --allow-discards --persistent refreshrather than kernel parameter modification, writing the flag directly into the LUKS2 header. This is the method recommended by the Arch Wiki and works regardless of boot configuration.Security note:
allow-discardsleaks block usage patterns to physical disk forensics. For most users, the performance/longevity benefits far outweigh this risk.Files changed
install/config/hardware/luks-trim.sh— new install-time script (detects LUKS, enables discards + fstrim.timer)install/config/all.sh— registers the new hardware scriptmigrations/1772373643.sh— applies the same fix to existing installations on updateTest plan
lsblk --discardshows non-zero DISC-GRAN on crypt devicesystemctl is-enabled fstrim.timerreturns "enabled"omarchy-migrateand verify both changes applysudo fstrim -avsuccessfully trims the root partition after changes🤖 Generated with Claude Code