Skip to content

Conversation

@ItsShamed
Copy link

This PR fixes a logic bug that prevents SDDM from restarting the greeter when either the session exits with code 1, or the SDDM helper gets signal-killed.

Helper side

If the helper gets killed by a signal (currently handled are SIGTERM and SIGHUP), exit with Auth::HELPER_OTHER_ERROR. This will prevent the helper from exiting with code Auth::HELPER_AUTH_ERROR, which is wrong.
If the session exits with a bad code (!= 0), exit with Auth::HELPER_SESSION_ERROR. This will prevent the helper from exiting Auth::HELPER_AUTH_ERROR if the session deliberately ends with code 1.

Daemon side

DO NOT cast the exitCode passed by QProcess::finished as a Auth::HelperStatus if the exitStatus is not QProcess::NormalExit. On Linux QProcess::finished is like a wrapper around waitid(2) which will put the signal number in exitCode if it got killed by one.
The Qt Docs even advises against doing it:

exitCode is the exit code of the process (only valid for normal exits)

For issues like #2003, this will result in a "restart loop" (which is essentially the same original symptom if the greeter fails to start in the first place).

Yes for the other session related issues, these are not SDDM related issues per se, but I think it's better UX to restart the greeter rather than leaving the user on a blank tty if a session fails.

Signed-off-by: tsrk. <tsrk@tsrk.me>
@Vogtinator
Copy link
Contributor

There's actually one more case of this:

connect(this, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, &UserSession::finished);

That connects QProcess::finished(int exitCode, QProcess::ExitStatus exitStatus) to UserSession::finished(int), losing exitStatus and making exitCode potentially invalid. Could you fix that up as well?

@ItsShamed
Copy link
Author

ItsShamed commented Jul 30, 2025

Should be good now

Q_EMIT finished(Auth::HELPER_OTHER_ERROR, QProcess::NormalExit);

For this line, I think this is fine, as we explicitly specify an enum, and HelperApp filters exit codes anyway.
If this is still bothering, I can change to a straight exit(2) instead

diff --git a/src/helper/UserSession.cpp b/src/helper/UserSession.cpp
index 5fd378a..eb6b598 100644
--- a/src/helper/UserSession.cpp
+++ b/src/helper/UserSession.cpp
@@ -156,7 +156,7 @@ namespace SDDM {
                 }
             }
         } else {
-            Q_EMIT finished(Auth::HELPER_OTHER_ERROR, QProcess::NormalExit);
+            exit(Auth::HELPER_OTHER_ERROR);
         }
     }
 

Copy link
Contributor

@Vogtinator Vogtinator left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM otherwise. FWICT this is effectively a noop in the "happy path", so not that likely to cause regressions.

A QProcess::CrashExit happen in most cases when a signal killed it. And
returning from a signal-killed process on Linux makes QProcess:finished
return the signal, which is a really bad idea to cast into a random
enum.

Qt also mentions that `exitCode` is only valid for QProcess::NormalExit
invocations

Signed-off-by: tsrk. <tsrk@tsrk.me>
This will allow HelperApp to dinstinguish between a crash (most likely
by a signal) from a normal exit.

Signed-off-by: tsrk. <tsrk@tsrk.me>
@ItsShamed ItsShamed force-pushed the helper-do-not-exit-1 branch from fb774db to 7052ba8 Compare July 30, 2025 12:04
@ItsShamed
Copy link
Author

ItsShamed commented Jul 30, 2025

Actually, hold off merging this one. I might've actually introduced new issues where SDDM would restart for seemingly no reason (opposite problem). I'm poking around to see what the root cause might be.

@ItsShamed

This comment was marked as off-topic.

@ItsShamed ItsShamed force-pushed the helper-do-not-exit-1 branch from 49b5912 to 7522be5 Compare July 30, 2025 15:44
@Vogtinator
Copy link
Contributor

I went back on Qt Base to see the behavior of waiting for a child process and
https://github.com/qt/qtbase/blob/46dcf3abc2ab4c6fbf14b5e108c585ba65060806/src/corelib/io/qprocess_unix.cpp#L1312-L1316
is absolute dogshit nightmare. There are no checks of the return code and errno except for their case of waiting for EINTR (do {} while (ret = -1 && errno = EINTR)). Which means that this is total undefined behavior if errno is anything other than EINTR (for example ECHILD), and can lead to QProcess::finished passing around undefined values.

If you have a concrete case where this can fail, please report this to Qt upstream.

void HelperApp::sessionFinished(int status) {
exit(status);
void HelperApp::sessionFinished(int exitCode, QProcess::ExitStatus exitStatus) {
disconnect(m_session, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, &HelperApp::sessionFinished);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can never be reached unless something went really wrong: All paths in this method lead to exit, i.e. it will never return.

If exit somehow spawns an event loop again (exit handlers?), that's broken and will cause many "fun" issues due to accidental reentrancy.

This comment was marked as off-topic.

This comment was marked as off-topic.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reasoning is kinda stupid imho…

The reasoning is:

The fcntl can only fail if the file descriptor is invalid. That means QProcess passed a bad file descriptor, so the bug would be in QProcess.

i.e. if this actually happens, it's a bug in QProcess and you can report it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we might as well take precautions to prevent such cases.

IMO the root cause needs to be found and it's not in QProcessPrivate::waitForDeadChild.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyway, I can't reproduce the "crash with 0" bug anymore, but I'm still not confident since I still get double sessionFinished calls despite exits…

You can try to trace why each of those calls happen.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I just realized the exit we've been calling, is not the exit(2) syscall, but QCoreApplication::exit which does in fact returns, and I was able to reproduce the "crash with 0" bug. I concluded this had nothing to do with QProcess.

Sorry for this nothingburger, here is what I understood what happened:

  • When UserSession QProcessed::finished, we catch it in HelperApp::sessionFinished
  • If it's a QProcess::NormalExit, call QCoreApplication::exit:
    • This only signals the event loop that we requested an exit, and the return code we exit with
    • It actually returns in order to process more events
  • This means we accidentally fall through our second case of QProcess::CrashExit, believing we stumbled into one:
    • By calling this exit, this sets the event loop return code to 3
  • Our HelperApp then process all the remaining events
  • Finally, the event loop returns to the main thread
  • When main() reaches out of scope, it calls our destructor ~HelperApp()
  • Which in turns calls UserSession::stop().
  • However, the UserSession is already QProcess::NotRunning by this stage, so it emits another QProcess::finished(Auth::HELPER_SESSION_ERROR, QProcess::NormalExit).
    • HelperApp::sessionFinshed gets called again…
Sequence diagram
sequenceDiagram
    participant main as ::main
    participant HelperApp as SDDM::HelperApp
    participant Backend as SDDM::Backend
    participant UserSession as SDDM::UserSession

    main->>HelperApp: exec()
    Note over HelperApp: setUp() and doAuth()
    HelperApp->>Backend: openSession()
    Backend->>UserSession: start()

    Note over UserSession: Session goes on…
    UserSession->>HelperApp: QProcess::finished()
    Note over HelperApp: sessionFinished()
    opt QProcess::NormalExit
        alt exitCode not 0
            HelperApp->>HelperApp: exit(Auth::HELPER_SESSION_ERROR)
        else
            HelperApp->>HelperApp: exit(Auth::HELPER_SUCCES)
        end
    end
    Note right of HelperApp: Those RETURN and fall through (not exit syscall)
    HelperApp->>HelperApp: exit(Auth::HELPER_SESSION_ERROR)
    HelperApp-->HelperApp: Finish processing events
    HelperApp-->>main: Event loop returns with 3
    main->>main: Return 3
    Note over main: At SCOPE FINISH
    main->>HelperApp: ~HelperApp() (call destructor)
    HelperApp->>UserSession: stop()
    Note over UserSession: state() is QProcess::NotRunning
    UserSession->>HelperApp: QProcess::finished(3, 0)
    Note over HelperApp: Repeat sessionFinished() a second time
Loading

Should be pretty simple to fix:

  • We should either explicitly return in HelperApp::sessionFinished, or wrap the second case in an else branch
  • In the destructor, call m_session->stop() only if m_session->state() != QProcess::NotRunning.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I just realized the exit we've been calling, is not the exit(2) syscall, but QCoreApplication::exit which does in fact return

In that case it should probably be made a bit more explicit by calling QCoreApplication::exit() instead...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, the UserSession is already QProcess::NotRunning by this stage, so it emits another QProcess::finished(Auth::HELPER_SESSION_ERROR, QProcess::NormalExit).

That always seemed odd to me, maybe we can just remove it?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case it should probably be made a bit more explicit by calling QCoreApplication::exit() instead...

Done

That always seemed odd to me, maybe we can just remove it?

Replaced by a qInfo() log

…ctor

This avoids calling exit an unnecessary number of times, which can
potentially falsify the real return code, and cause restrat loops

Signed-off-by: tsrk. <tsrk@tsrk.me>
@ItsShamed ItsShamed force-pushed the helper-do-not-exit-1 branch from 7522be5 to f886081 Compare July 31, 2025 23:27
Signed-off-by: tsrk. <tsrk@tsrk.me>
Calling `UserSession::stop` results in a call to `QProcess::terminate`,
which sends a SIGTERM to the session. `QProcess` will catch that and
emit an excess `QProcess::finished(15, 1)` which we don't want to handle
since we are in the process of exiting.

Signed-off-by: tsrk. <tsrk@tsrk.me>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sddm-helper crashes with system freeze when ~/.profile exits with error SDDM Crashes on loginctl terminate-user $USER SDDM crashes on boot

2 participants