@@ -462,15 +462,33 @@ fn setup_accounts(config: &InstallConfig) -> Result<(), String> {
462462 let _ = run_cmd ( "mount" , & [ "--bind" , & src, & dst] ) ;
463463 }
464464
465- // Set root password
466- log:: log ( "Setting root password..." ) ;
467- if let Some ( ref pass) = config. root_password {
468- run_chroot_stdin ( "chpasswd" , & [ ] , & format ! ( "root:{}\n " , pass) ) ?;
469- } else {
470- run_chroot ( "passwd" , & [ "-d" , "root" ] ) ?;
465+ // Ensure SHA-512 password hashing — musl's crypt() does not support
466+ // yescrypt ($y$) which newer shadow packages may default to.
467+ let login_defs = format ! ( "{}/etc/login.defs" , TARGET_MNT ) ;
468+ if Path :: new ( & login_defs) . exists ( ) {
469+ if let Ok ( content) = fs:: read_to_string ( & login_defs) {
470+ let fixed: String = content
471+ . lines ( )
472+ . map ( |line| {
473+ if line. starts_with ( "ENCRYPT_METHOD" ) {
474+ "ENCRYPT_METHOD SHA512"
475+ } else {
476+ line
477+ }
478+ } )
479+ . collect :: < Vec < _ > > ( )
480+ . join ( "\n " ) ;
481+ let fixed = if content. ends_with ( '\n' ) && !fixed. ends_with ( '\n' ) {
482+ fixed + "\n "
483+ } else {
484+ fixed
485+ } ;
486+ let _ = fs:: write ( & login_defs, & fixed) ;
487+ log:: log ( "login.defs: ENCRYPT_METHOD set to SHA512" ) ;
488+ }
471489 }
472490
473- // Create user account
491+ // Create user account FIRST so all users exist before chpasswd runs
474492 log:: log ( & format ! ( "Creating user {}..." , config. username) ) ;
475493 run_chroot (
476494 "useradd" ,
@@ -484,17 +502,48 @@ fn setup_accounts(config: &InstallConfig) -> Result<(), String> {
484502 ] ,
485503 ) ?;
486504
487- // Set user password
488- if let Some ( ref pass) = config. user_password {
489- run_chroot_stdin (
490- "chpasswd" ,
491- & [ ] ,
492- & format ! ( "{}:{}\n " , config. username, pass) ,
493- ) ?;
494- } else {
505+ // Handle passwordless accounts
506+ if config. root_password . is_none ( ) {
507+ run_chroot ( "passwd" , & [ "-d" , "root" ] ) ?;
508+ }
509+ if config. user_password . is_none ( ) {
495510 run_chroot ( "passwd" , & [ "-d" , & config. username ] ) ?;
496511 }
497512
513+ // Set all passwords in a SINGLE chpasswd invocation to avoid
514+ // shadow file locking issues between separate calls.
515+ let mut pw_data = String :: new ( ) ;
516+ if let Some ( ref pass) = config. root_password {
517+ pw_data. push_str ( & format ! ( "root:{}\n " , pass) ) ;
518+ }
519+ if let Some ( ref pass) = config. user_password {
520+ pw_data. push_str ( & format ! ( "{}:{}\n " , config. username, pass) ) ;
521+ }
522+
523+ if !pw_data. is_empty ( ) {
524+ log:: log ( "Setting passwords via single chpasswd call..." ) ;
525+ run_chroot_stdin ( "chpasswd" , & [ ] , & pw_data) ?;
526+
527+ // Verify each password was set
528+ for user in & [ "root" , config. username . as_str ( ) ] {
529+ if is_shadow_locked ( user) {
530+ log:: log ( & format ! ( "WARNING: chpasswd did not set hash for {user}, trying usermod fallback..." ) ) ;
531+ let hash = generate_password_hash ( user, config) ?;
532+ if !hash. is_empty ( ) {
533+ run_chroot ( "usermod" , & [ "-p" , & hash, user] ) ?;
534+ if is_shadow_locked ( user) {
535+ return Err ( format ! ( "Failed to set password for {user}" ) ) ;
536+ }
537+ log:: log ( & format ! ( "OK: password set for {user} via usermod fallback" ) ) ;
538+ } else {
539+ return Err ( format ! ( "Failed to set password for {user} — no hash generation method available" ) ) ;
540+ }
541+ } else {
542+ log:: log ( & format ! ( "OK: password set for {user}" ) ) ;
543+ }
544+ }
545+ }
546+
498547 Ok ( ( ) )
499548}
500549
@@ -829,6 +878,43 @@ fn copy_glob_files_with_prefix(search_dir: &str, pattern: &str, staging: &str, t
829878 }
830879}
831880
881+ /// Check whether a user's shadow entry is still locked (!, !!, *, or empty).
882+ fn is_shadow_locked ( user : & str ) -> bool {
883+ let shadow_path = format ! ( "{}/etc/shadow" , TARGET_MNT ) ;
884+ if let Ok ( content) = fs:: read_to_string ( & shadow_path) {
885+ let prefix = format ! ( "{user}:" ) ;
886+ if let Some ( line) = content. lines ( ) . find ( |l| l. starts_with ( & prefix) ) {
887+ let hash = line. split ( ':' ) . nth ( 1 ) . unwrap_or ( "" ) ;
888+ return hash. is_empty ( )
889+ || hash == "!"
890+ || hash == "!!"
891+ || hash == "*"
892+ || hash. starts_with ( "!$" ) ;
893+ }
894+ }
895+ true
896+ }
897+
898+ /// Generate a SHA-512 password hash via openssl inside the chroot.
899+ fn generate_password_hash ( user : & str , config : & InstallConfig ) -> Result < String , String > {
900+ let pass = if user == "root" {
901+ config. root_password . as_deref ( ) . unwrap_or ( "" )
902+ } else {
903+ config. user_password . as_deref ( ) . unwrap_or ( "" )
904+ } ;
905+ if pass. is_empty ( ) {
906+ return Ok ( String :: new ( ) ) ;
907+ }
908+ let result = run_chroot_stdin ( "openssl" , & [ "passwd" , "-6" , "-stdin" ] , & format ! ( "{pass}\n " ) ) ;
909+ if let Ok ( output) = result {
910+ let hash = String :: from_utf8_lossy ( & output. stdout ) . trim ( ) . to_string ( ) ;
911+ if hash. starts_with ( "$6$" ) {
912+ return Ok ( hash) ;
913+ }
914+ }
915+ Err ( "openssl passwd not available or failed" . to_string ( ) )
916+ }
917+
832918fn find_first_existing ( candidates : & [ & str ] ) -> Option < String > {
833919 candidates
834920 . iter ( )
0 commit comments