From fa3135aaf70a4874b730527fa06d2ee03d4c9b09 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Mon, 20 Jul 2026 15:32:50 -0500 Subject: fix(installer): guard the user-password chpasswd set -e is off, so a chpasswd failure silently left the primary user with no password and no log entry, leaving an unloggable account on a fresh install. I extracted set_user_password, which aborts via error_fatal on a chpasswd failure. A fake-chpasswd test pins that the guard fires. --- archsetup | 10 ++++- tests/installer-steps/test_set_user_password.py | 56 +++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 tests/installer-steps/test_set_user_password.py diff --git a/archsetup b/archsetup index 9eea404..cefd9f2 100755 --- a/archsetup +++ b/archsetup @@ -1151,6 +1151,14 @@ EOF } ### Create User +set_user_password() { + # $1 = username; reads $password. set -e is off (line 21), so a bare + # `chpasswd` that failed would silently leave the user with no password and + # no log entry. Guard it: a failure aborts loudly instead. + echo "$1:$password" | chpasswd 2>> "$logfile" \ + || error_fatal "setting password for $1" "$?" "set it by hand: passwd $1" +} + create_user() { display "title" "User Creation" @@ -1165,7 +1173,7 @@ create_user() { error_fatal "adding user '$username'" "$?" "run the useradd manually to see why it failed" display "task" "assigning the password" - echo "$username:$password" | chpasswd # any text is allowable! be careful! + set_user_password "$username" unset password # clear from memory after use display "task" "adding to appropriate groups" diff --git a/tests/installer-steps/test_set_user_password.py b/tests/installer-steps/test_set_user_password.py new file mode 100644 index 0000000..9f21b3a --- /dev/null +++ b/tests/installer-steps/test_set_user_password.py @@ -0,0 +1,56 @@ +"""Test that the primary user's password is set under a guard. + +archsetup runs with `set -e` OFF (line 21), so an unguarded `chpasswd` that +fails silently leaves the primary user with no password and no log entry -- an +unloggable account on a fresh install. set_user_password guards the chpasswd +with error_fatal so a failure aborts loudly instead of passing unnoticed. + +Method: sed-extract set_user_password from the real `archsetup`, run it with a +fake chpasswd (controlled exit) and a fake error_fatal recorder. Assert the +guard fires on failure and stays quiet on success. + +Run from repo root: + python3 -m unittest tests.installer-steps.test_set_user_password +""" + +import os +import subprocess +import textwrap +import unittest + +REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +ARCHSETUP = os.path.join(REPO_ROOT, "archsetup") + + +def run_set_password(chpasswd_rc): + """Run set_user_password with a fake chpasswd exiting chpasswd_rc.""" + script = textwrap.dedent(f"""\ + password="hunter2" + logfile=/dev/null + display() {{ :; }} + chpasswd() {{ cat >/dev/null; return {chpasswd_rc}; }} + error_fatal() {{ echo "FATAL: $1"; exit 9; }} + source <(sed -n '/^set_user_password() {{/,/^}}/p' "{ARCHSETUP}") + set_user_password "alice" + echo "REACHED-END" + """) + return subprocess.run( + ["bash", "-c", script], capture_output=True, text=True, timeout=10, + ) + + +class SetUserPassword(unittest.TestCase): + def test_success_does_not_abort(self): + r = run_set_password(0) + self.assertIn("REACHED-END", r.stdout) + self.assertNotIn("FATAL", r.stdout) + + def test_failure_aborts_with_error_fatal(self): + r = run_set_password(1) + self.assertIn("FATAL: setting password for alice", r.stdout, + "a chpasswd failure must abort via error_fatal") + self.assertNotIn("REACHED-END", r.stdout) + + +if __name__ == "__main__": + unittest.main() -- cgit v1.2.3