From 732069bf98556b11367a27b03a0e6dacb4e3e5fa Mon Sep 17 00:00:00 2001 From: meldrey Date: Tue, 28 Apr 2026 12:17:45 -0500 Subject: [PATCH] auth: start PAM authentication immediately without pre-collecting input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, CPam::init() hardcoded a "Password: " prompt and blocked on waitForInput() before ever calling pam_authenticate(). This meant non-interactive PAM modules (howdy face recognition via pam_python, FIDO2 devices) could never run until the user typed something and pressed Enter — defeating the purpose of passwordless auth. Now pam_authenticate() fires immediately on lock. Non-interactive modules run first without needing the PAM conversation. If they succeed (e.g. face match), the screen unlocks instantly with no keypress. If they fail, subsequent modules (pam_unix) trigger the conv() callback which blocks for password input at that point. The conv() callback no longer skips waitForInput() for the "initial" prompt, since input is no longer pre-collected. Every PAM prompt now properly blocks until the user submits input. Fixes #926 Related: #535 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/auth/Pam.cpp | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/auth/Pam.cpp b/src/auth/Pam.cpp index 813ab92..344046a 100644 --- a/src/auth/Pam.cpp +++ b/src/auth/Pam.cpp @@ -17,32 +17,28 @@ int conv(int num_msg, const struct pam_message** msg, struct pam_response** resp, void* appdata_ptr) { const auto CONVERSATIONSTATE = (CPam::SPamConversationState*)appdata_ptr; struct pam_response* pamReply = (struct pam_response*)calloc(num_msg, sizeof(struct pam_response)); - bool initialPrompt = true; for (int i = 0; i < num_msg; ++i) { switch (msg[i]->msg_style) { case PAM_PROMPT_ECHO_OFF: case PAM_PROMPT_ECHO_ON: { - const auto PROMPT = std::string(msg[i]->msg); - const auto PROMPTCHANGED = PROMPT != CONVERSATIONSTATE->prompt; + const auto PROMPT = std::string(msg[i]->msg); Log::logger->log(Log::INFO, "PAM_PROMPT: {}", PROMPT); - if (PROMPTCHANGED) - g_pHyprlock->enqueueForceUpdateTimers(); - - // Some pam configurations ask for the password twice for whatever reason (Fedora su for example) - // When the prompt is the same as the last one, I guess our answer can be the same. - if (!initialPrompt && PROMPTCHANGED) { - CONVERSATIONSTATE->prompt = PROMPT; - CONVERSATIONSTATE->waitForInput(); - } + // Update the prompt and wait for user input. Since + // pam_authenticate runs immediately (not after pre-collecting + // input), every prompt from a PAM module must block here. + // Non-interactive modules (face auth, FIDO2) never send + // prompts, so this only fires for password-based modules. + CONVERSATIONSTATE->prompt = PROMPT; + g_pHyprlock->enqueueForceUpdateTimers(); + CONVERSATIONSTATE->waitForInput(); // Needed for unlocks via SIGUSR1 if (g_pHyprlock->isUnlocked()) return PAM_CONV_ERR; pamReply[i].resp = strdup(CONVERSATIONSTATE->input.c_str()); - initialPrompt = false; } break; case PAM_ERROR_MSG: Log::logger->log(Log::ERR, "PAM: {}", msg[i]->msg); break; case PAM_TEXT_INFO: @@ -83,9 +79,11 @@ void CPam::init() { while (true) { resetConversation(); - // Initial input - m_sConversationState.prompt = "Password: "; - waitForInput(); + // Start PAM authentication immediately. Non-interactive modules + // (e.g. pam_python/howdy for face recognition, FIDO2) run first + // without needing user input. If they succeed, we unlock instantly. + // If they fail, pam_unix will trigger the conv() callback which + // blocks for password input at that point. // For grace or SIGUSR1 unlocks if (g_pHyprlock->isUnlocked())