From 5f4ac0ba98a1c7625df69b35ccb1c9781320f37e Mon Sep 17 00:00:00 2001 From: Vincenzo Palazzo Date: Fri, 7 Aug 2026 12:31:47 +0200 Subject: [PATCH 1/5] fix: cap LUKS Argon2id memory to avoid SIGKILL on live install cryptsetup default argon2id memory cost (~1 GiB) OOMs on constrained live/PXE boots, killing luksFormat with Signals.SIGKILL: 9. - Add omarchy-luks-pbkdf-memory with a fixed 256 MiB Omarchy default. Step down to the RFC 9106 64 MiB floor only when that default cannot fit, and refuse to format when even the floor cannot. MemAvailable is not a hardness knob: Argon2id attacker cost scales with memory squared times passes, so --iter-time does not make up for a lower --pbkdf-memory. - Use it in the protected-mode configurator luksFormat path, inside run_partition_execute's existing disk_abort_hook / 2>&1 pipeline. - Patch archinstall Luks2.encrypt before encrypted full-disk installs. Locate luks.py via sys.path (do not importlib.util.find_spec), require helper exit 0 plus numeric stdout, and fail closed if the patch misses. Reproduction - Desktop install of Omarchy 3.8.40 via LAN/PXE (USB boot unavailable) - Client: ASRock-like NIC MAC d0:50:99:5a:3e:5e, target disk /dev/sda - Live image ships archinstall on Python 3.14; full-disk encrypted layout - Installer progressed through keyring sync, wiped /dev/sda, created partitions, then died during LUKS format of /dev/sda2 Failure log (from installer screen) subprocess.CalledProcessError: Command ['cryptsetup', '--batch-mode', '--verbose', ... 'luksFormat', '/dev/sda2'] died with . Co-Authored-By: Claude Opus 4.6 (1M context) --- configs/airootfs/root/configurator | 24 ++++- .../usr/local/bin/omarchy-iso-install | 102 ++++++++++++++++++ .../usr/local/bin/omarchy-luks-pbkdf-memory | 52 +++++++++ test/unit/luks-pbkdf-memory-test.sh | 72 +++++++++++++ 4 files changed, 249 insertions(+), 1 deletion(-) create mode 100755 configs/airootfs/usr/local/bin/omarchy-luks-pbkdf-memory create mode 100755 test/unit/luks-pbkdf-memory-test.sh diff --git a/configs/airootfs/root/configurator b/configs/airootfs/root/configurator index 4cd3db3d..c79331c2 100644 --- a/configs/airootfs/root/configurator +++ b/configs/airootfs/root/configurator @@ -435,6 +435,21 @@ disk_abort_hook() { abort "$1" } +# Argon2id --pbkdf-memory (KiB) for live-ISO luksFormat. +# Requires omarchy-luks-pbkdf-memory to exit 0 with a numeric value; do not +# probe with `command -v` (bash reports non-executables as found). +luks_pbkdf_memory_kib() { + local out + if ! out=$(omarchy-luks-pbkdf-memory); then + return 1 + fi + if ! [[ $out =~ ^[0-9]+$ ]]; then + echo "omarchy-luks-pbkdf-memory produced non-numeric output: ${out}" >&2 + return 1 + fi + printf '%s\n' "$out" +} + # Save the configurator answers the installer consumes: git identity and # encryption choice as plain files, account details as archinstall credentials. # Deferred-provisioning installs defer the user to first boot: no credentials, and on encrypted @@ -730,11 +745,18 @@ run_partition_execute() { if [[ "$encrypt_installation" == "true" ]]; then step "Setting up LUKS2 on $root_partition_device" + local pbkdf_memory + if ! pbkdf_memory=$(luks_pbkdf_memory_kib); then + disk_abort_hook "Could not choose a safe LUKS Argon2id memory cost" + fi + say --foreground 8 "LUKS Argon2id memory cost: ${pbkdf_memory} KiB" # Kept as pipes rather than routed through disk_step: the passphrase must # not become an argv the process table can show. Folding stderr into # stdout still lands any error in the install log. if ! printf "%s" "$password" | - cryptsetup luksFormat --type luks2 --batch-mode "$root_partition_device" - 2>&1; then + cryptsetup luksFormat --type luks2 --batch-mode \ + --pbkdf argon2id --pbkdf-memory "$pbkdf_memory" --iter-time 2000 \ + "$root_partition_device" - 2>&1; then disk_abort_hook "Formatting LUKS2 on $root_partition_device failed" fi if ! printf "%s" "$password" | diff --git a/configs/airootfs/usr/local/bin/omarchy-iso-install b/configs/airootfs/usr/local/bin/omarchy-iso-install index 59aca95b..835c9f36 100755 --- a/configs/airootfs/usr/local/bin/omarchy-iso-install +++ b/configs/airootfs/usr/local/bin/omarchy-iso-install @@ -23,5 +23,107 @@ while [[ $# -gt 0 ]]; do esac done +# Patch archinstall's Luks2.encrypt before Python imports it. +# +# Full-disk encrypted installs still go through archinstall's cryptsetup +# luksFormat. Without --pbkdf-memory, argon2id defaults to ~1 GiB and the +# kernel OOM-kills cryptsetup on constrained live/PXE boots (SIGKILL: 9). +# Locate luks.py via sys.path (do not importlib.util.find_spec: that imports +# archinstall as a side effect). Fail closed on the encrypted path: a missed +# patch would reintroduce the SIGKILL. +install_is_encrypted() { + local encrypt=true + if [[ -n ${OMARCHY_INSTALL_ENCRYPT_FILE:-} && -r $OMARCHY_INSTALL_ENCRYPT_FILE ]]; then + encrypt=$(tr -d '[:space:]' <"$OMARCHY_INSTALL_ENCRYPT_FILE") + fi + [[ $encrypt == true ]] +} + +read_pbkdf_memory() { + local out + if ! out=$(omarchy-luks-pbkdf-memory); then + echo "omarchy-iso-install: omarchy-luks-pbkdf-memory failed" >&2 + return 1 + fi + if ! [[ $out =~ ^[0-9]+$ ]]; then + echo "omarchy-iso-install: omarchy-luks-pbkdf-memory produced non-numeric output: ${out}" >&2 + return 1 + fi + printf '%s\n' "$out" +} + +patch_archinstall_luks_pbkdf_memory() { + local luks_py pbkdf_memory + + if ! install_is_encrypted; then + return 0 + fi + + luks_py=$(python - <<'PY' +import sys +from pathlib import Path + +for entry in sys.path: + candidate = Path(entry) / "archinstall" / "lib" / "disk" / "luks.py" + if candidate.is_file(): + print(candidate) + sys.exit(0) +sys.exit(1) +PY + ) || { + echo "omarchy-iso-install: could not locate archinstall lib/disk/luks.py on sys.path" >&2 + exit 1 + } + + if [[ ! -f $luks_py ]]; then + echo "omarchy-iso-install: archinstall luks.py is not a file: ${luks_py}" >&2 + exit 1 + fi + if [[ ! -w $luks_py ]]; then + echo "omarchy-iso-install: archinstall luks.py is not writable: ${luks_py}" >&2 + exit 1 + fi + + pbkdf_memory=$(read_pbkdf_memory) || exit 1 + + echo "omarchy-iso-install: capping archinstall LUKS Argon2id memory to ${pbkdf_memory} KiB" >&2 + + # Insert --pbkdf-memory after the --iter-time value in the cmd list. + # Match the stable archinstall 3.x/4.x shape: + # '--iter-time', str(iter_time), *key_file_arg, '--use-urandom', + python - "$luks_py" "$pbkdf_memory" <<'PY' +from pathlib import Path +import re +import sys + +path = Path(sys.argv[1]) +memory = sys.argv[2] +text = path.read_text() + +already = re.search( + r"['\"]--iter-time['\"],\s*str\(iter_time\),\s*['\"]--pbkdf-memory['\"]", + text, +) +if already: + sys.exit(0) + +pattern = re.compile( + r"(['\"]--iter-time['\"],\s*str\(iter_time\),)", + re.MULTILINE, +) +replacement = rf"\1\n\t\t\t'--pbkdf-memory',\n\t\t\t'{memory}'," +new_text, n = pattern.subn(replacement, text, count=1) +if n != 1: + sys.stderr.write( + f"omarchy-iso-install: could not patch {path} for --pbkdf-memory " + f"(matches={n})\n" + ) + sys.exit(1) +path.write_text(new_text) +PY +} + +patch_archinstall_luks_pbkdf_memory + cd /usr/share/omarchy-iso exec python -m orchestrator.main diff --git a/configs/airootfs/usr/local/bin/omarchy-luks-pbkdf-memory b/configs/airootfs/usr/local/bin/omarchy-luks-pbkdf-memory new file mode 100755 index 00000000..8fa1307e --- /dev/null +++ b/configs/airootfs/usr/local/bin/omarchy-luks-pbkdf-memory @@ -0,0 +1,52 @@ +#!/bin/bash +# Print Omarchy's Argon2id --pbkdf-memory (KiB) for live-ISO LUKS formatting. +# +# cryptsetup's argon2id default (~1 GiB) is OOM-killed on constrained live/PXE +# boots (cryptsetup dies with SIGKILL 9). Omarchy uses a fixed 256 MiB default +# so every install gets the same KDF parameters. We only step down to the +# RFC 9106 64 MiB floor when that default cannot fit, and we refuse to format +# if even the floor cannot. +# +# Argon2id attacker cost scales with memory squared times passes. Lowering +# --pbkdf-memory is a real hardness reduction; --iter-time does not make up +# for it. That is why the default is a fixed Omarchy value rather than a +# function of whatever MemAvailable happens to be at format time. +# +# OMARCHY_LUKS_MEMINFO may point at an alternate meminfo file (used by tests). + +set -euo pipefail + +default_kib=262144 # 256 MiB — Omarchy Argon2id memory cost +floor_kib=65536 # 64 MiB — RFC 9106 memory-constrained recommendation + +# cryptsetup will try to lock --pbkdf-memory KiB. Leave at least as much +# again for the rest of the live session so the allocation cannot consume +# all of MemAvailable. +fits() { + local need=$1 avail=$2 + ((avail >= need * 2)) +} + +meminfo=${OMARCHY_LUKS_MEMINFO:-/proc/meminfo} +avail_kib=0 +if [[ -r $meminfo ]]; then + avail_kib=$(awk '/^MemAvailable:/ { print $2; exit }' "$meminfo" 2>/dev/null || echo 0) +fi + +if ! [[ $avail_kib =~ ^[0-9]+$ ]]; then + avail_kib=0 +fi + +if fits "$default_kib" "$avail_kib"; then + printf '%s\n' "$default_kib" + exit 0 +fi + +if fits "$floor_kib" "$avail_kib"; then + echo "omarchy-luks-pbkdf-memory: MemAvailable ${avail_kib} KiB cannot fit the ${default_kib} KiB Omarchy default; using ${floor_kib} KiB floor" >&2 + printf '%s\n' "$floor_kib" + exit 0 +fi + +echo "omarchy-luks-pbkdf-memory: MemAvailable ${avail_kib} KiB is too small to fit even the ${floor_kib} KiB Argon2id floor; refusing LUKS format" >&2 +exit 1 diff --git a/test/unit/luks-pbkdf-memory-test.sh b/test/unit/luks-pbkdf-memory-test.sh new file mode 100755 index 00000000..43f9b8ba --- /dev/null +++ b/test/unit/luks-pbkdf-memory-test.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# +# Drive omarchy-luks-pbkdf-memory against fake meminfo files. The helper must +# be invoked for every assertion; inlining the formula would let a stubbed +# helper (printf '65536\n') pass. + +set -euo pipefail + +ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) +helper="$ROOT/configs/airootfs/usr/local/bin/omarchy-luks-pbkdf-memory" + +pass() { + printf 'ok - %s\n' "$1" +} + +fail() { + local description="$1" + local detail="${2:-}" + printf 'not ok - %s\n' "$description" >&2 + [[ -n $detail ]] && printf '%s\n' "$detail" >&2 + exit 1 +} + +[[ -x $helper ]] || fail "helper is executable" "missing $helper" + +fake_meminfo=$(mktemp) +trap 'rm -f "$fake_meminfo"' EXIT + +run_helper() { + OMARCHY_LUKS_MEMINFO="$fake_meminfo" "$helper" +} + +# 4 GiB available: 2x the 256 MiB default fits. +printf 'MemAvailable: 4194304 kB\n' >"$fake_meminfo" +out=$(run_helper) || fail "default on plenty of RAM" "helper exited $?" +[[ $out == 262144 ]] || fail "default on plenty of RAM" "expected 262144, got $out" +pass "default 256 MiB when MemAvailable is 4 GiB" + +# 512 MiB available: 256 MiB default needs 512 MiB free (2x), so this is the +# exact threshold for the default. +printf 'MemAvailable: 524288 kB\n' >"$fake_meminfo" +out=$(run_helper) || fail "default at 2x threshold" "helper exited $?" +[[ $out == 262144 ]] || fail "default at 2x threshold" "expected 262144, got $out" +pass "default 256 MiB when MemAvailable is exactly 2x 256 MiB" + +# Just under 2x default (511 MiB): cannot fit 256 MiB, can fit 64 MiB floor. +printf 'MemAvailable: 523264 kB\n' >"$fake_meminfo" +out=$(run_helper 2>/dev/null) || fail "floor when default cannot fit" "helper exited $?" +[[ $out == 65536 ]] || fail "floor when default cannot fit" "expected 65536, got $out" +pass "64 MiB floor when MemAvailable cannot fit 256 MiB" + +# 128 MiB available: exact 2x floor threshold. +printf 'MemAvailable: 131072 kB\n' >"$fake_meminfo" +out=$(run_helper 2>/dev/null) || fail "floor at 2x threshold" "helper exited $?" +[[ $out == 65536 ]] || fail "floor at 2x threshold" "expected 65536, got $out" +pass "64 MiB floor when MemAvailable is exactly 2x 64 MiB" + +# 64 MiB available: floor itself cannot fit (would consume all free RAM). +printf 'MemAvailable: 65536 kB\n' >"$fake_meminfo" +if out=$(run_helper 2>/dev/null); then + fail "refuse when floor cannot fit" "helper printed $out instead of failing" +fi +pass "refuses to format when MemAvailable cannot fit the 64 MiB floor" + +# Unreadable / missing MemAvailable: treat as 0 and refuse. +printf 'MemTotal: 16777216 kB\n' >"$fake_meminfo" +if out=$(run_helper 2>/dev/null); then + fail "refuse without MemAvailable" "helper printed $out instead of failing" +fi +pass "refuses to format when meminfo has no MemAvailable" + +echo "luks-pbkdf-memory-test: ok" From 517d9605e7ea27867d4e5d32c0f2685203428d53 Mon Sep 17 00:00:00 2001 From: Omabot Date: Thu, 20 Aug 2026 04:21:22 -0700 Subject: [PATCH 2/5] Declare omarchy-luks-pbkdf-memory in file_permissions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bin/omarchy-iso-make runs lint_file_permissions before the build and exits when an executable under configs/airootfs/{usr/local/bin,root} is missing from profiledef.sh, so the new helper failed the ISO build outright. mkarchiso does not carry the source mode across, and the callers guard on `command -v`, which in bash succeeds for a non-executable file in PATH — so a helper that shipped 0644 would be invoked and die with status 126 rather than falling back to 262144. Co-Authored-By: Claude Opus 5 (1M context) Co-Authored-By: Codex XHigh --- configs/profiledef.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/configs/profiledef.sh b/configs/profiledef.sh index 268a3cea..9bdc0d31 100644 --- a/configs/profiledef.sh +++ b/configs/profiledef.sh @@ -40,6 +40,7 @@ file_permissions=( ["/usr/local/bin/omarchy-iso-cleanup-disk"]="0:0:755" ["/usr/local/bin/omarchy-install-dashboard"]="0:0:755" ["/usr/local/bin/omarchy-iso-install"]="0:0:755" + ["/usr/local/bin/omarchy-luks-pbkdf-memory"]="0:0:755" ["/usr/local/bin/omarchy-upload-log"]="0:0:755" ["/var/cache/omarchy/mirror/offline/"]="0:0:775" ) From 9e2cec81893b66a2e9be3d1dd6e296d66776fcfd Mon Sep 17 00:00:00 2001 From: Omabot Date: Thu, 20 Aug 2026 05:26:31 -0700 Subject: [PATCH 3/5] Decide the archinstall LUKS patch from the config archinstall reads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit install_is_encrypted() gated the patch on user_encrypt_installation.txt, but nothing about that file decides whether luksFormat runs: archinstall_adapter.is_encrypted() reads disk_config.disk_encryption out of user_configuration.json, and README says outright that the flag file drives the encrypted install's autologin and boot validation, "not the encryption". An autoinstall drive whose config carries a disk_encryption block while the flag file says false — or True, or 1, both of which context.py:109 accepts and this did not — skipped the patch and left cryptsetup at the ~1 GiB default, which is the SIGKILL this branch exists to prevent. Ask disk_config.disk_encryption first, exactly as phases_impl.py:1237 already does, and keep the flag file as the fallback for when the config cannot be read. Co-Authored-By: Claude Opus 5 (1M context) --- .../usr/local/bin/omarchy-iso-install | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/configs/airootfs/usr/local/bin/omarchy-iso-install b/configs/airootfs/usr/local/bin/omarchy-iso-install index 835c9f36..77b9d914 100755 --- a/configs/airootfs/usr/local/bin/omarchy-iso-install +++ b/configs/airootfs/usr/local/bin/omarchy-iso-install @@ -31,12 +31,27 @@ done # Locate luks.py via sys.path (do not importlib.util.find_spec: that imports # archinstall as a side effect). Fail closed on the encrypted path: a missed # patch would reintroduce the SIGKILL. +# Ask disk_config.disk_encryption, the same field archinstall_adapter.is_encrypted +# reads to decide whether luksFormat runs at all. The flag file is only a +# fallback: README calls it the autologin signal, not the encryption one. install_is_encrypted() { - local encrypt=true + local encryption_type=no_encryption encrypt=true + + if [[ -n ${OMARCHY_INSTALL_CONFIG:-} && -r $OMARCHY_INSTALL_CONFIG ]]; then + encryption_type=$(jq -r ' + .disk_config.disk_encryption + | if . == null then "no_encryption" else (.encryption_type // "luks") end + ' "$OMARCHY_INSTALL_CONFIG" 2>/dev/null) || encryption_type=luks + fi + if [[ $encryption_type != no_encryption ]]; then + return 0 + fi + if [[ -n ${OMARCHY_INSTALL_ENCRYPT_FILE:-} && -r $OMARCHY_INSTALL_ENCRYPT_FILE ]]; then encrypt=$(tr -d '[:space:]' <"$OMARCHY_INSTALL_ENCRYPT_FILE") fi - [[ $encrypt == true ]] + # The same values context.py accepts for the same file. + [[ ${encrypt,,} == true || ${encrypt,,} == yes || ${encrypt,,} == 1 ]] } read_pbkdf_memory() { From 9b6ee70b9769184271cd37b0eec674c6cba301ac Mon Sep 17 00:00:00 2001 From: Omabot Date: Thu, 20 Aug 2026 05:36:00 -0700 Subject: [PATCH 4/5] Refresh the patched Argon2id cost instead of keeping the first run's MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The already-patched branch exited before rewriting, so a second run of omarchy-iso-install in the same live session left whatever value the first run chose. The helper re-reads MemAvailable each time and can land on the other side of the 128 MiB floor between attempts, so a retry after a failed install could log "capping archinstall LUKS Argon2id memory to 262144 KiB" and hand archinstall the 65536 it wrote earlier — or the reverse, which is the SIGKILL this branch exists to prevent, arriving on the retry. Substitute the value in place: still idempotent, still one --pbkdf-memory, and the number in the log is the number archinstall uses. Co-Authored-By: Claude Opus 5 (1M context) Co-Authored-By: Codex XHigh --- configs/airootfs/usr/local/bin/omarchy-iso-install | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/configs/airootfs/usr/local/bin/omarchy-iso-install b/configs/airootfs/usr/local/bin/omarchy-iso-install index 77b9d914..cba50f4f 100755 --- a/configs/airootfs/usr/local/bin/omarchy-iso-install +++ b/configs/airootfs/usr/local/bin/omarchy-iso-install @@ -115,11 +115,15 @@ path = Path(sys.argv[1]) memory = sys.argv[2] text = path.read_text() -already = re.search( - r"['\"]--iter-time['\"],\s*str\(iter_time\),\s*['\"]--pbkdf-memory['\"]", - text, +# Already patched: refresh the value rather than keep the first run's. A retry +# in the same live session re-reads MemAvailable and can land on the other side +# of the floor, and the log would name a cost archinstall never used. +already = re.compile( + r"(['\"]--iter-time['\"],\s*str\(iter_time\),\s*['\"]--pbkdf-memory['\"],\s*)['\"][^'\"]*['\"]", ) -if already: +refreshed, n = already.subn(rf"\g<1>'{memory}'", text, count=1) +if n == 1: + path.write_text(refreshed) sys.exit(0) pattern = re.compile( From b4d3eaf20acadcaad98ed4701e7850eef7d26525 Mon Sep 17 00:00:00 2001 From: Vincenzo Palazzo Date: Thu, 20 Aug 2026 14:46:28 +0200 Subject: [PATCH 5/5] Locate archinstall Luks2 on both 3.x and 4.x paths The locator only walked lib/disk/luks.py, which is the 4.x home. 3.x kept the class at lib/luks.py, and the comment claiming a "3.x/4.x shape" for that path was wrong. The shipped ISO is 4.x so nothing was broken, but the patch is fail-closed: a miss aborts every encrypted install rather than quietly under-protecting one. Prefer 4.x, fall back to 3.x, still exit 1 when neither file is on sys.path. --- .../usr/local/bin/omarchy-iso-install | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/configs/airootfs/usr/local/bin/omarchy-iso-install b/configs/airootfs/usr/local/bin/omarchy-iso-install index cba50f4f..3f21e32a 100755 --- a/configs/airootfs/usr/local/bin/omarchy-iso-install +++ b/configs/airootfs/usr/local/bin/omarchy-iso-install @@ -74,19 +74,27 @@ patch_archinstall_luks_pbkdf_memory() { return 0 fi + # 4.x moved Luks2 to lib/disk/luks.py; 3.x kept it at lib/luks.py. Prefer + # 4.x (what the ISO ships) then 3.x so a locator miss does not abort an + # encrypted install on an older tree. Fail closed either way. luks_py=$(python - <<'PY' import sys from pathlib import Path +relatives = ( + Path("archinstall") / "lib" / "disk" / "luks.py", # 4.x + Path("archinstall") / "lib" / "luks.py", # 3.x +) for entry in sys.path: - candidate = Path(entry) / "archinstall" / "lib" / "disk" / "luks.py" - if candidate.is_file(): - print(candidate) - sys.exit(0) + for rel in relatives: + candidate = Path(entry) / rel + if candidate.is_file(): + print(candidate) + sys.exit(0) sys.exit(1) PY ) || { - echo "omarchy-iso-install: could not locate archinstall lib/disk/luks.py on sys.path" >&2 + echo "omarchy-iso-install: could not locate archinstall luks.py on sys.path (tried lib/disk/luks.py and lib/luks.py)" >&2 exit 1 } @@ -104,7 +112,7 @@ PY echo "omarchy-iso-install: capping archinstall LUKS Argon2id memory to ${pbkdf_memory} KiB" >&2 # Insert --pbkdf-memory after the --iter-time value in the cmd list. - # Match the stable archinstall 3.x/4.x shape: + # Same encrypt argv in 3.x (lib/luks.py) and 4.x (lib/disk/luks.py): # '--iter-time', str(iter_time), *key_file_arg, '--use-urandom', python - "$luks_py" "$pbkdf_memory" <<'PY' from pathlib import Path