# selftest_vlock.exp -- drive vlock(1) through a full lock/unlock cycle on a pty. # # Driven by tests/91-vlock.test, in the tagged-value shape 40-tcl.test # established: one "R- <0|1>" line per assertion, and ALL-VLOCK-OK only # when every one of them is 1. The caller passes the plaintext password whose # hash it just wrote into /etc/shadow as argv[0], so the secret has one home. # # Why expect is needed at all. vlock is a terminal locker: vlock-main refuses to # start unless isatty(0) agrees, then turns ECHO and ISIG off on that terminal # and reads it with select()+read() until the password is right. So there is no # shell pipeline that can drive it -- a pipe fails the isatty() check outright, # and a here-document could not answer a prompt that is only printed after the # first read returns. A pty is the only way in, and it must not be the console # this suite is reporting on: vlock on THAT terminal would lock the run itself, # with no second console to unlock it from. # # The loop under test is auth_loop() in vlock-main.c. Per iteration it prints # $VLOCK_CURRENT_MESSAGE (the wrapper exports it), waits for ENTER or ESC, then # prompts once for the password -- once, not twice, because --disable-root-password # compiles out the second "try as root" attempt. A wrong password prints # "vlock: Authentication error" and the whole iteration repeats; a right one # breaks the loop and exit(0)s. R4 is what proves the repeat is a real loop and # not a one-shot that happened to fail. set timeout 20 log_user 0 set PW [lindex $argv 0] proc has {hay needle} { expr {[string first $needle $hay] >= 0} } set fails 0 proc report {name cond} { global fails set v [expr {$cond ? 1 : 0}] if {!$v} { incr fails } puts "$name $v" } # Everything vlock writes goes to stderr, and the banner is prefixed by the # wrapper's ESC[H ESC[J clear-screen, so every assertion below is a substring # test on accumulated pty output rather than a line match. set all "" if {[catch {spawn /usr/bin/vlock} err]} { puts "R0-spawn 0" puts "VLOCK-FAILS spawn: $err" exit 1 } set vpid [exp_pid] # --- 1. the lock banner ------------------------------------------------------- # vlock-main is running, has taken the terminal, and has printed the message the # wrapper handed it in the environment. expect { -ex "Please press \[ENTER\] to unlock." { append all $expect_out(buffer) } timeout { append all $expect_out(buffer) } eof { append all $expect_out(buffer) } } report R1-banner [has $all "This TTY is now locked."] report R1-enter-hint [has $all "Please press \[ENTER\] to unlock."] # --- 2. the password prompt --------------------------------------------------- # wait_for_character("\n\033") consumes the ENTER; asprintf() builds the prompt # from the username get_username() resolved, so "root's" also proves that lookup. send "\r" expect { -ex "'s Password: " { append all $expect_out(buffer) } timeout { append all $expect_out(buffer) } eof { append all $expect_out(buffer) } } report R2-password-prompt [has $all "root's Password: "] # --- 3. a wrong password is rejected ------------------------------------------ # getspnam() found the entry, crypt() hashed the input against its salt, and the # comparison failed -- the path that needs libxcrypt to exist at all. send "wrong-$PW\r" set stage "" expect { -ex "Authentication error" { append stage $expect_out(buffer) } timeout { append stage $expect_out(buffer) } eof { append stage $expect_out(buffer) } } report R3-reject [has $stage "Authentication error"] # --- 4. and the lock survives it ---------------------------------------------- # The banner comes round again: auth_loop() is a loop, and a rejected password # leaves the terminal locked rather than falling through. set relock "" expect { -ex "Please press \[ENTER\] to unlock." { append relock $expect_out(buffer) } timeout { append relock $expect_out(buffer) } eof { append relock $expect_out(buffer) } } report R4-relock [has $relock "This TTY is now locked."] # --- 5. the right password unlocks -------------------------------------------- # strcmp(crypt(pw, hash), hash) matches, auth_loop() breaks, the atexit handler # restores the terminal and vlock exits 0. The status is the assertion: anything # else (a signal, a nonzero exit) means the unlock did not happen cleanly. send "\r" expect { -ex "'s Password: " { } timeout { } eof { } } send "$PW\r" set unlocked 0 set status -1 expect { eof { set unlocked 1 } timeout { } } if {$unlocked} { catch { set w [wait] ; set status [lindex $w 3] } } else { catch { exec kill -9 $vpid } catch { wait } } report R5-unlock-eof $unlocked report R5-unlock-status [expr {$status == 0}] if {$fails == 0} { puts "ALL-VLOCK-OK" } else { puts "VLOCK-FAILS $fails" }