Skip to content

Enable SSD TRIM on LUKS-encrypted drives#4840

Open
n8himmel wants to merge 1 commit intobasecamp:masterfrom
n8himmel:enable-luks-trim
Open

Enable SSD TRIM on LUKS-encrypted drives#4840
n8himmel wants to merge 1 commit intobasecamp:masterfrom
n8himmel:enable-luks-trim

Conversation

@n8himmel
Copy link
Copy Markdown

@n8himmel n8himmel commented Mar 1, 2026

Summary

  • Enables allow-discards on LUKS volumes via cryptsetup --persistent refresh so TRIM commands pass through dm-crypt to the SSD controller
  • Enables fstrim.timer for weekly periodic TRIM
  • Includes install-time hardware detection script and migration for existing users

Fixes #2229

Context

Every Omarchy install uses LUKS encryption, and the encrypt mkinitcpio 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 refresh rather 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-discards leaks 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 script
  • migrations/1772373643.sh — applies the same fix to existing installations on update

Test plan

  • Fresh Omarchy install with LUKS: verify lsblk --discard shows non-zero DISC-GRAN on crypt device
  • Fresh install: verify systemctl is-enabled fstrim.timer returns "enabled"
  • Existing install: run omarchy-migrate and verify both changes apply
  • Non-LUKS install (if any): verify script is a no-op (no errors, no changes)
  • Verify sudo fstrim -av successfully trims the root partition after changes

🤖 Generated with Claude Code

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>
Copilot AI review requested due to automatic review settings March 1, 2026 14:01
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment thread migrations/1772373643.sh
Comment on lines +7 to +8
if lsblk --noheadings -o TYPE | grep -q "crypt"; then
for dev in $(lsblk --noheadings -o NAME,TYPE | awk '$2=="crypt" {print $1}'); do
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment thread migrations/1772373643.sh
Comment on lines +9 to +12
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
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment thread migrations/1772373643.sh
fi
done

chrootable_systemctl_enable fstrim.timer
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +15
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
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +15
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
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment thread migrations/1772373643.sh
Comment on lines +8 to +13
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
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +19
# 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
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
# 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

Copilot uses AI. Check for mistakes.
@woopstar
Copy link
Copy Markdown
Contributor

woopstar commented Mar 6, 2026

Very interessting catch

@woopstar
Copy link
Copy Markdown
Contributor

woopstar commented Mar 6, 2026

Maybe also look into https://wiki.archlinux.org/title/Dm-crypt/Specialties#Disable_workqueue_for_increased_solid_state_drive_(SSD)_performance

@yevshev
Copy link
Copy Markdown

yevshev commented Mar 6, 2026

Since continuous trim is already enabled by default for BTRFS ( discard=async default mount option), does adding periodic trim (fstrim.timer) provide any benefit? https://wiki.archlinux.org/title/Btrfs#SSD_TRIM

@zayatura
Copy link
Copy Markdown

Since continuous trim is already enabled by default for BTRFS ( discard=async default mount option), does adding periodic trim (fstrim.timer) provide any benefit? https://wiki.archlinux.org/title/Btrfs#SSD_TRIM

Enabling the fstrim timer would help with the initial trim, as discard=async only trims recently discarded blocks (e.g. deleted files) and does not send trim all unused space; the fstrim timer would. I did not check the pull request, but in case there is a one-time fstrim command, then this is not needed. Periodic fstrim would also trim any additional trim-capable device the user might have, such as additional SSDs and external SSDs.

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.

5 participants