https://bugs.kde.org/show_bug.cgi?id=481808

--- Comment #36 from nyanpasu64 <[email protected]> ---
Created attachment 195053
  --> https://bugs.kde.org/attachment.cgi?id=195053&action=edit
journalctl log of a sleep-wake failed login

The source code lives at
https://invent.kde.org/plasma/kscreenlocker/-/tree/v6.7.4?ref_type=tags.
We can enable logging via:
nano ~/.config/QtProject/qtlogging.ini
[Rules]
kscreenlocker_greet=true

How does kscreenlocker set up authentication?

// auth.cpp
class PamAuthenticator : public QObject {
    QThread m_thread;
    PamWorker *d;
PamAuthenticator::PamAuthenticator(const QString &service, const QString &user,
NoninteractiveAuthenticatorTypes types, QObject *parent)
{
    d->moveToThread(&m_thread);

↑ This shows that PamWorker methods are invoked in a background thread, and run
concurrently with UI operations.

(worker) PamWorker::PamWorker()
    : m_conv({&PamWorker::converse, this})

(main)void PamAuthenticator::init(const QString &service, const QString &user)
{
    QMetaObject::invokeMethod(d, [this, service, user]() {
        d->start(service, user);
    });
(worker) void PamWorker::start(const QString &service, const QString &user)
{
    m_service = service;
    if (user.isEmpty())
        m_result = pam_start(qPrintable(service), nullptr, &m_conv, &m_handle);
    else
        m_result = pam_start(qPrintable(service), qPrintable(user), &m_conv,
&m_handle);
Aug 10 14:56:55 servlet kscreenlocker_greet[3848637]: [PAM worker
kde-fingerprint] start: successfully started
Aug 10 14:56:55 servlet kscreenlocker_greet[3848637]: [PAM worker
kde-smartcard] start: successfully started
Aug 10 14:56:55 servlet kscreenlocker_greet[3848637]: [PAM worker kde] start:
successfully started

↑ Each potential unlock thread calls pam_start(), passing in m_conv ->
PamWorker::converse().

(main) void PamAuthenticator::tryUnlock()
{
    m_unlocked = false;
    QMetaObject::invokeMethod(d, &PamWorker::authenticate);
(worker) void PamWorker::authenticate()
{
    qCDebug(KSCREENLOCKER_GREET, "[PAM worker %s] Authenticate: Starting
authentication", qUtf8Printable(m_service));
Aug 10 14:56:56 servlet kscreenlocker_greet[3848637]: [PAM worker
kde-fingerprint] Authenticate: Starting authentication
Aug 10 14:56:56 servlet kscreenlocker_greet[3848637]: [PAM worker kde]
Authenticate: Starting authentication
Aug 10 14:56:56 servlet kscreenlocker_greet[3848637]: [PAM worker
kde-smartcard] Authenticate: Starting authentication

↓ pam_authenticate() is a blocking call, that calls back into m_conv ->
PamWorker::converse(). Each call-back receives a fixed number of commands, not
an updatable stream.

    int rc = pam_authenticate(m_handle, 0); // PAM_SILENT);
    qCDebug(KSCREENLOCKER_GREET,
            "[PAM worker %s] Authenticate: Authentication done, result code: %d
(%s)",
            qUtf8Printable(m_service), rc, pam_strerror(m_handle, rc));
Aug 10 14:56:56 servlet kscreenlocker_greet[3848637]: [PAM worker
kde-smartcard] Authenticate: Authentication done, result code: 28 (Module is
unknown)
Aug 10 14:56:56 servlet kscreenlocker_greet[3848637]: [PAM worker
kde-fingerprint] Authenticate: Authentication done, result code: 28 (Module is
unknown)

↑ At this point only the "kde" authenticator (password prompt) is active.
pam_authenticate() calls into PamWorker::converse(): ↓
(worker) int PamWorker::converse(int n, const struct pam_message **msg, struct
pam_response **resp, void *data)
{
    PamWorker *c = static_cast<PamWorker *>(data);

    *resp = (calloc pam_response[n])
    auto responses = std::span<pam_response>{*resp, nSize};
    auto messages = std::span<const pam_message*>{msg, nSize};
    for (const auto &[pamMessage, pamResponse] : std::views::zip(messages,
responses)) {
        bool isSecret = false;
        switch (pamMessage->msg_style) {

There are two types of PAM prompts for user input. KDE's handling for them
is... awkward.

            qCDebug(KSCREENLOCKER_GREET,
                    "[PAM worker %s] Message: %s: %s",
                    qUtf8Printable(c->m_service),
                    (isSecret ? "Echo-off prompt" : "Echo-on prompt"),
                    qUtf8Printable(prompt));
Aug 10 14:56:56 servlet kscreenlocker_greet[3848637]: [PAM worker kde] Message:
Echo-off prompt: Password: 

            QEventLoop e;
            QObject::connect(c, &PamWorker::cancelled, &e, ...);
            qCDebug(KSCREENLOCKER_GREET, "[PAM worker %s] Starting nested event
loop to await response", qUtf8Printable(c->m_service));
Aug 10 14:56:56 servlet kscreenlocker_greet[3848637]: [PAM worker kde] Starting
nested event loop to await response
            // We are in a non-gui thread. It should be mostly fine to exec()
here.
            int rc = e.exec();

↑ This is the point we reach with the lock screen open. There is a wedged call
to pam_authenticate() -> PamWorker::converse(), that persists until we enter a
password or sleep the system. In the latter case... ↓
Aug 10 14:56:58 servlet systemd-logind[734]: suspend requested from client PID
3848637 ('kscreenlocker_g') (unit [email protected])...
Aug 10 14:56:58 servlet systemd-logind[734]: The system will suspend now!

(main) void PamAuthenticators::cancel()
{
    qCDebug(KSCREENLOCKER_GREET) << "PamAuthenticators: cancelling interactive
authenticator";
Aug 10 14:56:58 servlet kscreenlocker_greet[3848637]: PamAuthenticators:
cancelling interactive authenticator
    d->interactive->cancel();
[void PamAuthenticator::cancel()]
{
    ... QMetaObject::invokeMethod(d, &PamWorker::cancelled);
}
e.exec() calls into [QObject::connect(&PamWorker::cancelled)] → [&]() {
                qCDebug(KSCREENLOCKER_GREET, "[PAM worker %s] Received
cancellation, exiting with PAM_CONV_ERR", qUtf8Printable(c->m_service));
Aug 10 14:56:58 servlet kscreenlocker_greet[3848637]: [PAM worker kde] Received
cancellation, exiting with PAM_CONV_ERR
                e.exit(PAM_CONV_ERR);
            }

            return from [int rc = e.exec()] = PAM_CONV_ERR
            if (rc != 0) {
                qCDebug(KSCREENLOCKER_GREET, "[PAM worker %s] Nested event
loop's exit code was not zero, bailing", qUtf8Printable(c->m_service));
Aug 10 14:56:58 servlet kscreenlocker_greet[3848637]: [PAM worker kde] Nested
event loop's exit code was not zero, bailing
                return rc;
            }

↑ Our pam_conv callback returns PAM_CONV_ERR. It seems that pam_authenticate()
→ pam_unix.so did not appreciate us returning PAM_CONV_ERR, and returns
PAM_AUTHTOK_ERR=20 to the caller.
Aug 10 14:56:58 servlet kscreenlocker_greet[3848637]: [PAM worker kde]
Authenticate: Authentication done, result code: 20 (Authentication token
manipulation error)
Aug 10 14:56:58 servlet kscreenlocker_greet[3848637]: pam_unix(kde:auth):
unexpected response from failed conversation function
Aug 10 14:56:58 servlet kscreenlocker_greet[3848637]: PamAuthenticators:
Failure from interactive authenticator kde
Aug 10 14:56:58 servlet kscreenlocker_greet[3848637]: pam_unix(kde:auth):
conversation failed

Worse yet, when pam_unix.so receives an erroneous reply from our callback, it
doesn't just abort authentication, but libpam invokes pam_faillock.so in
authfail mode, registering our suspend attempt as a failed login.

I've attached my full journal log of authentication errors.

----

Which authentication flow did we trigger in libpam?
void PamWorker::start(const QString &service, const QString &user)
{
        m_result = pam_start(qPrintable(service), qPrintable(user), &m_conv,
&m_handle);
[int pam_start(const char *service_name, ...)];

Based on the log messages, we can infer service_name is "kde".
/usr/lib/pam.d/kde redirects to system-local-login,
/etc/pam.d/system-local-login redirects to system-login, and
/etc/pam.d/system-login specifies four modes each with a sequential list of
authenticator shared libraries. README.pam claims (consistent with my wild
poking-around in linux-pam/libpam):

> PAM configuration files have four types of entries for each service,
> however KScreenLocker only uses the "auth" entries.  Other programs
> using PAM may use other entries.

The auth entries are:
auth       required   pam_shells.so
auth       requisite  pam_nologin.so
auth       include    system-auth

/etc/pam.d/system-auth contains:
auth       required                    pam_faillock.so      preauth
-auth      [success=2 default=ignore]  pam_systemd_home.so
auth       [success=1 default=bad]     pam_unix.so          try_first_pass
nullok
auth       [default=die]               pam_faillock.so      authfail
...
auth       required                    pam_faillock.so      authsucc

So at this point we'd have to either configure pam_faillock
(https://linux.die.net/man/8/pam_faillock, in the linux-pam repo) to ignore
PAM_CONV_ERR as a failed unlock, or not *call* pam_authenticate() until the
user has already entered a password. The first may be difficult, and we'd need
to hide the stray "Unlocking failed" message another way (don't print an error
upon PAM_AUTHTOK_ERR? if we've called PamWorker::cancelled()?). The second may
break non-password unlock mechanisms the user installs into the kde →
system-local-login → system-login → system-auth chain, since we only check for
PAM authentication when the user enters a password and presses Enter (not if
they plug in a security key, bring a smartphone, or anything else.).

See also https://github.com/ChocolateLoverRaj/pam-any/issues/19, I experienced
something similar there over cancelling password prompts.

-- 
You are receiving this mail because:
You are watching all bug changes.

Reply via email to