Skip to content
Merged
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ Edit `/etc/atomic.conf`:
| `MAPPER_NAME` | `root_crypt` | dm-crypt mapper name (fallback if auto-detection fails) |
| `KERNEL_PKG` | `linux` | Kernel package (linux/linux-lts/linux-zen) |
| `KERNEL_PARAMS` | *(security defaults)* | Kernel command line parameters |
| `CHROOT_COMMAND` | *(none)* | Default command in snapshot chroot (overrides built-in `pacman -Syu`) |
| `SBCTL_SIGN` | `0` | Sign UKI files with sbctl for Secure Boot (`0`=off, `1`=on) |
| `UPGRADE_GUARD` | `1` | Upgrade guard: block direct `pacman -Syu` (`0`=off, `1`=on) |
| `HOME_COPY_FILES` | *(empty)* | Files to copy into isolated home subvolumes (see [Home isolation](#home-isolation)) |
Expand Down
15 changes: 12 additions & 3 deletions bin/atomic-upgrade
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ show_help() {
cat << EOF
atomic-upgrade v${VERSION} - Atomic system upgrades for Arch Linux

Usage: atomic-upgrade [OPTIONS] [-- COMMAND...]
Usage: atomic-upgrade [OPTIONS] [-- CHROOT_COMMAND...]

Options:
-n, --dry-run Show what would be done
Expand Down Expand Up @@ -106,9 +106,18 @@ if [[ -n "$CUSTOM_TAG" ]]; then
fi
fi

# Default chroot command
# Default chroot command: use CHROOT_COMMAND from config (or default in common.sh).
# Note: CHROOT_COMMAND uses simple word splitting; args with spaces inside
# are not supported (same limitation as HOME_COPY_FILES).
if [[ ${#CHROOT_CMD[@]} -eq 0 ]]; then
CHROOT_CMD=(/usr/bin/pacman -Syu)
[[ -z "${CHROOT_COMMAND:-}" ]] && CHROOT_COMMAND="/usr/bin/pacman -Syu"
read -ra CHROOT_CMD <<< "$CHROOT_COMMAND"
fi

# Warn if CHROOT_COMMAND contains quotes (word splitting doesn't process them)
if [[ "$CHROOT_COMMAND" =~ [\'\"] ]]; then
echo "WARN: CHROOT_COMMAND contains quotes, but word splitting doesn't process them." >&2
echo "WARN: Use CLI instead: atomic-upgrade -- pacman -S 'nvidia dkms'" >&2
fi

# Verify required variables are set
Expand Down
10 changes: 8 additions & 2 deletions etc/atomic.conf
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
# /etc/atomic.conf

# Note: inline comments start at ' #' (space + hash).
# Values containing a literal ' #' will be truncated at that point.

#BTRFS_MOUNT="/run/atomic/temp_root"
#NEW_ROOT="/run/atomic/newroot"
#ESP="/efi"
Expand All @@ -7,8 +11,10 @@
#KERNEL_PKG="linux"
#KERNEL_PARAMS="rw slab_nomerge init_on_alloc=1 page_alloc.shuffle=1 pti=on vsyscall=none randomize_kstack_offset=on debugfs=off"

# Note: inline comments start at ' #' (space + hash).
# Values containing a literal ' #' will be truncated at that point.
# Default command to run in the snapshot chroot (overrides built-in default)
# Command-line -- CHROOT_COMMAND... overrides this setting
# Note: args with spaces inside are not supported (simple word splitting)
#CHROOT_COMMAND="/usr/bin/pacman -Syu"

# Sign UKI files with sbctl for Secure Boot (requires sbctl)
#SBCTL_SIGN=1
Expand Down
4 changes: 3 additions & 1 deletion lib/atomic/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ UPGRADE_GUARD=1
HOME_COPY_FILES=""
# Kernel security parameters
KERNEL_PARAMS="rw slab_nomerge init_on_alloc=1 page_alloc.shuffle=1 pti=on vsyscall=none randomize_kstack_offset=on debugfs=off"
# Default chroot command (overridden by config or CLI -- CHROOT_COMMAND...)
CHROOT_COMMAND="/usr/bin/pacman -Syu"

# ── Config loading (safe parser, no arbitrary code execution) ───────

Expand All @@ -46,7 +48,7 @@ load_config() {
fi

# Whitelist of allowed config keys
local -a allowed=(BTRFS_MOUNT NEW_ROOT ESP KEEP_GENERATIONS MAPPER_NAME KERNEL_PARAMS KERNEL_PKG SBCTL_SIGN UPGRADE_GUARD HOME_COPY_FILES)
local -a allowed=(BTRFS_MOUNT NEW_ROOT ESP KEEP_GENERATIONS MAPPER_NAME KERNEL_PARAMS KERNEL_PKG CHROOT_COMMAND SBCTL_SIGN UPGRADE_GUARD HOME_COPY_FILES)

while IFS='=' read -r key value; do
# Strip leading/trailing whitespace from key
Expand Down
9 changes: 9 additions & 0 deletions man/atomic.conf.5
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ device arguments.
Default: \f[B]rw slab_nomerge init_on_alloc=1 page_alloc.shuffle=1
pti=on vsyscall=none randomize_kstack_offset=on debugfs=off\f[R].
.TP
\f[B]COMMAND\f[R]
Default command to run in the snapshot chroot, overriding the built\-in
default of \f[B]pacman \-Syu\f[R].
Command\-line \f[B]\-\-\f[R] \f[I]COMMAND...\f[R] takes priority over
this setting.
Note: arguments with spaces inside are not supported due to simple word
splitting.
Default: \f[I](none)\f[R].
.TP
\f[B]SBCTL_SIGN\f[R]
Enable UKI signing with \f[B]sbctl\f[R](8) for Secure Boot.
Set to \f[B]1\f[R] to enable, \f[B]0\f[R] to disable.
Expand Down
6 changes: 6 additions & 0 deletions man/atomic.conf.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ rejected entirely.
page_alloc.shuffle=1 pti=on vsyscall=none randomize_kstack_offset=on
debugfs=off**.

**CHROOT_COMMAND**
: Default command to run in the snapshot chroot, overriding the built-in
default of **pacman -Syu**. Command-line **-- CHROOT_COMMAND...** takes priority
over this setting. Note: arguments with spaces inside are not supported
due to simple word splitting. Default: *(none)*.

**SBCTL_SIGN**
: Enable UKI signing with **sbctl**(8) for Secure Boot. Set to **1** to
enable, **0** to disable. When disabled, UKI files are built unsigned.
Expand Down
33 changes: 33 additions & 0 deletions tests/test_config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,40 @@ assert_eq "HOME_COPY_FILES absent → stays empty" "" "$HOME_COPY_FILES"
# Restore
HOME_COPY_FILES=""

# ── CHROOT_COMMAND config ───────────────────────────────────

section "CHROOT_COMMAND config"

# Test: CHROOT_COMMAND loaded from config
CONFIG_FILE="${TESTDIR}/chroot_command.conf"
cat > "$CONFIG_FILE" <<'EOF'
CHROOT_COMMAND=/usr/bin/pacman -Syu
EOF
CHROOT_COMMAND=""
load_config
assert_eq "CHROOT_COMMAND loaded from config" "/usr/bin/pacman -Syu" "$CHROOT_COMMAND"
CHROOT_COMMAND=""

# Test: CHROOT_COMMAND with quotes
CONFIG_FILE="${TESTDIR}/chroot_command_quoted.conf"
cat > "$CONFIG_FILE" <<'EOF'
CHROOT_COMMAND="/usr/bin/pacman -S nvidia"
EOF
CHROOT_COMMAND=""
load_config
assert_eq "CHROOT_COMMAND with double quotes" "/usr/bin/pacman -S nvidia" "$CHROOT_COMMAND"
CHROOT_COMMAND=""

# Test: CHROOT_COMMAND absent → stays empty
CONFIG_FILE="${TESTDIR}/chroot_command_absent.conf"
cat > "$CONFIG_FILE" <<'EOF'
KEEP_GENERATIONS=3
EOF
CHROOT_COMMAND=""
load_config
assert_eq "CHROOT_COMMAND absent → stays empty" "" "$CHROOT_COMMAND"

#
# ── config key whitespace trimming ─────────────

section "Config key whitespace trimming"
Expand Down
76 changes: 76 additions & 0 deletions tests/test_upgrade.sh
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,82 @@ assert_eq "default cmd count" "CHROOT_CMD_COUNT=2" "$(echo "$_out" | grep '^CHRO
assert_eq "default cmd[0]" "CHROOT_CMD_0=/usr/bin/pacman" "$(echo "$_out" | grep '^CHROOT_CMD_0=')"
assert_eq "default cmd[1]" "CHROOT_CMD_1=-Syu" "$(echo "$_out" | grep '^CHROOT_CMD_1=')"

# ── Word splitting limitation: args with spaces ────────────

section "Word splitting limitation (space in args)"

CONFIG_FILE="${TESTDIR}/word_split.conf"
cat > "$CONFIG_FILE" <<'EOF'
CHROOT_COMMAND=/usr/bin/pacman -S nvidia driver
EOF

_TEST_SCRIPT_WS="${TESTDIR}/atomic-upgrade-word-split"
sed \
-e 's|^\(export ATOMIC_UPGRADE=1\)$|\1\nCONFIG_FILE="${CONFIG_FILE:-/etc/atomic.conf}"|' \
-e 's/^\(\[\[ \$EUID -eq 0 \]\]\)/# \1/' \
-e '/^# Verify required variables are set$/i\
if [[ "${ATOMIC_EXIT_AFTER_PARSE:-}" == "1" ]]; then\
echo "PARSE_OK"\
echo "CHROOT_CMD_COUNT=${#CHROOT_CMD[@]}"\
for i in "${!CHROOT_CMD[@]}"; do\
echo "CHROOT_CMD_${i}=${CHROOT_CMD[$i]}"\
done\
exit 0\
fi\
' \
"$SCRIPT" > "$_TEST_SCRIPT_WS"
chmod +x "$_TEST_SCRIPT_WS"

run_cmd env CONFIG_FILE="$CONFIG_FILE" ATOMIC_EXIT_AFTER_PARSE=1 bash "$_TEST_SCRIPT_WS"
assert_eq "word split: PARSE_OK" "0" "$_rc"
assert_eq "word split: count is 4" "CHROOT_CMD_COUNT=4" "$(echo "$_out" | grep '^CHROOT_CMD_COUNT=')"
assert_eq "word split: arg[2]" "CHROOT_CMD_2=nvidia" "$(echo "$_out" | grep '^CHROOT_CMD_2=')"
assert_eq "word split: arg[3]" "CHROOT_CMD_3=driver" "$(echo "$_out" | grep '^CHROOT_CMD_3=')"

rm -f "$_TEST_SCRIPT_WS"

# ── CLI -- CHROOT_COMMAND priority over config CHROOT_COMMAND= ───────────

section "CLI -- CHROOT_COMMAND priority over config CHROOT_COMMAND="

CONFIG_FILE="${TESTDIR}/cli_priority.conf"
cat > "$CONFIG_FILE" <<'EOF'
CHROOT_COMMAND=/usr/bin/pacman -Syu
EOF

_TEST_SCRIPT_CLI="${TESTDIR}/atomic-upgrade-cli-priority"
sed \
-e 's|^\(export ATOMIC_UPGRADE=1\)$|\1\nCONFIG_FILE="${CONFIG_FILE:-/etc/atomic.conf}"|' \
-e 's/^\(\[\[ \$EUID -eq 0 \]\]\)/# \1/' \
-e '/^# Verify required variables are set$/i\
if [[ "${ATOMIC_EXIT_AFTER_PARSE:-}" == "1" ]]; then\
echo "PARSE_OK"\
echo "DRY_RUN=${DRY_RUN}"\
echo "CUSTOM_TAG=${CUSTOM_TAG}"\
echo "NO_GC=${NO_GC}"\
echo "SEPARATE_HOME=${SEPARATE_HOME}"\
echo "COPY_FILES=${COPY_FILES}"\
echo "CHROOT_CMD_COUNT=${#CHROOT_CMD[@]}"\
for i in "${!CHROOT_CMD[@]}"; do\
echo "CHROOT_CMD_${i}=${CHROOT_CMD[$i]}"\
done\
echo "CHROOT_COMMAND_FROM_CONFIG=${CHROOT_COMMAND:-}"\
exit 0\
fi\
' \
"$SCRIPT" > "$_TEST_SCRIPT_CLI"
chmod +x "$_TEST_SCRIPT_CLI"

run_cmd env CONFIG_FILE="$CONFIG_FILE" ATOMIC_EXIT_AFTER_PARSE=1 bash "$_TEST_SCRIPT_CLI" -- /usr/bin/pacman -S nvidia
assert_eq "CLI priority: PARSE_OK" "0" "$_rc"
assert_contains "CLI priority: uses CLI args" "CHROOT_CMD_0=/usr/bin/pacman" "$_out"
assert_contains "CLI priority: CLI arg[1]" "CHROOT_CMD_1=-S" "$_out"
assert_contains "CLI priority: CLI arg[2]" "CHROOT_CMD_2=nvidia" "$_out"
assert_eq "CLI priority: count is 3" "CHROOT_CMD_COUNT=3" "$(echo "$_out" | grep '^CHROOT_CMD_COUNT=')"
assert_contains "CLI priority: CHROOT_COMMAND loaded from config" "CHROOT_COMMAND_FROM_CONFIG=/usr/bin/pacman -Syu" "$_out"

rm -f "$_TEST_SCRIPT_CLI"

# ── GEN_ID format validation ────────────────────────────────

section "GEN_ID format validation"
Expand Down