[plasmashell] [Bug 481808] Clicking "Sleep" on the lockscreen makes it also display a failed login attempt

nyanpasu64 <[email protected]>
Newsgroups gmane.comp.kde.devel.bugs
Message-ID <[email protected]/>
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.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.