Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion configs/airootfs/root/configurator
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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" |
Expand Down
129 changes: 129 additions & 0 deletions configs/airootfs/usr/local/bin/omarchy-iso-install
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,134 @@ 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.
# 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 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
# The same values context.py accepts for the same file.
[[ ${encrypt,,} == true || ${encrypt,,} == yes || ${encrypt,,} == 1 ]]
}

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

# 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:
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 luks.py on sys.path (tried lib/disk/luks.py and lib/luks.py)" >&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.
# 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
import re
import sys

path = Path(sys.argv[1])
memory = sys.argv[2]
text = path.read_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*)['\"][^'\"]*['\"]",
)
refreshed, n = already.subn(rf"\g<1>'{memory}'", text, count=1)
if n == 1:
path.write_text(refreshed)
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
52 changes: 52 additions & 0 deletions configs/airootfs/usr/local/bin/omarchy-luks-pbkdf-memory
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions configs/profiledef.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
72 changes: 72 additions & 0 deletions test/unit/luks-pbkdf-memory-test.sh
Original file line number Diff line number Diff line change
@@ -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"