Skip to content

fix(auth): prevent Qt atexit deadlock in PAM fork children#88

Open
felixonmars wants to merge 1 commit into
linuxdeepin:masterfrom
felixonmars:fix-freeze-on-arch
Open

fix(auth): prevent Qt atexit deadlock in PAM fork children#88
felixonmars wants to merge 1 commit into
linuxdeepin:masterfrom
felixonmars:fix-freeze-on-arch

Conversation

@felixonmars
Copy link
Copy Markdown
Member

@felixonmars felixonmars commented Mar 28, 2026

PAM modules (notably pam_gnome_keyring) may fork() internally and call exit() instead of _exit() in the fork child, e.g. when starting a daemon or communicating with an existing one. Since the DDM session leader inherits Qt's atexit handlers, exit() triggers libQt6DBus cleanup which tries to join its dispatcher thread. After fork(), only the calling thread survives, so the join blocks forever on a futex.

Fix this by registering a pthread_atfork child handler in the session leader, right before pam_open_session(). The handler pushes an atexit entry that calls _exit(0); since atexit handlers run in LIFO order, this runs first and terminates the grandchild without executing Qt's broken post-fork cleanup.

This fixes the immediate stuck after clicking login on Arch Linux.

Summary by Sourcery

Bug Fixes:

  • Prevent login from hanging when PAM modules fork and call exit() in the child by ensuring those processes terminate via _exit() instead of running Qt's atexit cleanup.

PAM modules (notably pam_gnome_keyring) may fork() internally and call
exit() instead of _exit() in the fork child, e.g. when starting a
daemon or communicating with an existing one. Since the DDM session
leader inherits Qt's atexit handlers, exit() triggers libQt6DBus
cleanup which tries to join its dispatcher thread. After fork(), only
the calling thread survives, so the join blocks forever on a futex.

Fix this by registering a pthread_atfork child handler in the session
leader, right before pam_open_session(). The handler pushes an atexit
entry that calls _exit(0); since atexit handlers run in LIFO order,
this runs first and terminates the grandchild without executing Qt's
broken post-fork cleanup.

This fixes the immediate stuck after clicking login on Arch Linux.
@deepin-ci-robot
Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: felixonmars

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Mar 28, 2026

Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Adds a pthread_atfork child handler in the DDM auth session setup so that PAM-spawned fork children exit via _exit(0) from an atexit handler, preventing Qt/DBus atexit thread-join deadlocks after fork.

Sequence diagram for PAM fork child exit handling with pthread_atfork

sequenceDiagram
    actor User
    participant DDM_SessionLeader
    participant PAM_Module
    participant PAM_Child
    participant C_Library_atexit

    User->>DDM_SessionLeader: Click login
    DDM_SessionLeader->>DDM_SessionLeader: Register pthread_atfork handlers
    Note over DDM_SessionLeader: child handler pushes atexit(_exit(0))
    DDM_SessionLeader->>PAM_Module: pam_open_session()

    PAM_Module->>PAM_Module: fork()
    PAM_Module->>PAM_Child: Create child process
    activate PAM_Child

    PAM_Child->>C_Library_atexit: Inherit atexit handlers
    PAM_Child->>PAM_Child: exit()
    PAM_Child->>C_Library_atexit: Run atexit handlers (LIFO)
    C_Library_atexit->>PAM_Child: Call _exit(0)
    deactivate PAM_Child

    Note over C_Library_atexit,DDM_SessionLeader: Qt/DBus atexit handlers are not executed
    DDM_SessionLeader->>User: Session opens successfully
Loading

File-Level Changes

Change Details Files
Install a pthread_atfork child handler before opening the PAM session to force PAM fork-children to terminate via _exit(0) from an atexit handler, avoiding Qt atexit deadlocks.
  • Include pthread.h in the auth implementation to access pthread_atfork
  • Before calling pam_open_session, register a pthread_atfork child handler whose child callback registers an atexit handler
  • The atexit handler calls _exit(0) so that, in PAM fork-children, this runs first (LIFO) and bypasses Qt/libQt6DBus atexit cleanup that would otherwise try to join a non-existent thread and deadlock
src/daemon/Auth.cpp

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • Registering a global pthread_atfork child handler here means all subsequent forks in this process (not just PAM internals) will inherit an atexit that unconditionally calls _exit(0), which may bypass expected cleanup or status codes for unrelated child processes; consider narrowing the scope or adding a guard to limit this behavior to the PAM-specific context.
  • The atexit handler installed in the fork child always exits with status 0, which can mask genuine failure conditions in PAM modules or other forked helpers; if you keep this approach, consider a strategy that preserves or communicates the intended exit status while still avoiding the Qt atexit deadlock.
  • pthread_atfork can fail (e.g., ENOMEM), and in that case the deadlock risk remains but without any indication; it may be worth checking the return value and logging or handling the failure path explicitly.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Registering a global pthread_atfork child handler here means *all* subsequent forks in this process (not just PAM internals) will inherit an atexit that unconditionally calls _exit(0), which may bypass expected cleanup or status codes for unrelated child processes; consider narrowing the scope or adding a guard to limit this behavior to the PAM-specific context.
- The atexit handler installed in the fork child always exits with status 0, which can mask genuine failure conditions in PAM modules or other forked helpers; if you keep this approach, consider a strategy that preserves or communicates the intended exit status while still avoiding the Qt atexit deadlock.
- pthread_atfork can fail (e.g., ENOMEM), and in that case the deadlock risk remains but without any indication; it may be worth checking the return value and logging or handling the failure path explicitly.

## Individual Comments

### Comment 1
<location path="src/daemon/Auth.cpp" line_range="279" />
<code_context>
+            // Register a pthread_atfork child handler here so that any
+            // grandchild processes spawned by PAM will have an atexit handler
+            // that runs _exit() first (LIFO), bypassing Qt's broken cleanup.
+            pthread_atfork(nullptr, nullptr, []() {
+                atexit([]() { _exit(0); });
+            });
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Consider handling the pthread_atfork return code and avoiding repeated registration.

`pthread_atfork` can fail (e.g., ENOMEM); right now that failure is silently ignored, so the workaround may never be installed and the caller can’t detect it. In addition, if this path is executed multiple times, each call adds another atfork handler that then adds another atexit handler in children, causing unbounded growth of handler lists. Please check and handle the `pthread_atfork` return value, and ensure registration happens only once per process (e.g., via `std::once_flag`).

Suggested implementation:

```cpp
            // Register a pthread_atfork child handler here so that any
            // grandchild processes spawned by PAM will have an atexit handler
            // that runs _exit() first (LIFO), bypassing Qt's broken cleanup.
            //
            // Use std::call_once to avoid registering multiple handlers in the
            // same process, and check the pthread_atfork return value.
            static std::once_flag pamAtforkOnce;
            std::call_once(pamAtforkOnce, [] {
                const int rc = pthread_atfork(
                    nullptr,
                    nullptr,
                    []() {
                        atexit([]() { _exit(0); });
                    }
                );
                if (rc != 0) {
                    // Best-effort logging; failure means the workaround is not installed.
                    qWarning() << "Failed to register pthread_atfork handler for PAM:"
                               << strerror(rc);
                }
            });

            // Open session
            auto sessionEnv = openSessionInternal(env);

```

To compile successfully, you will also need to:

1. Ensure the appropriate headers are included near the top of `src/daemon/Auth.cpp` (if they are not already):
   - `#include <mutex>` for `std::once_flag` and `std::call_once`.
   - `#include <cstring>` for `strerror`.
2. Ensure that `qWarning()` is available in this translation unit (it usually is via common Qt headers such as `<QDebug>` or similar). If not already present, add the appropriate Qt debug/logging include used elsewhere in this project.
3. If your project uses a different logging mechanism than `qWarning()`, replace the logging line inside the `if (rc != 0)` block with the project's standard error-logging macro or function.
</issue_to_address>

### Comment 2
<location path="src/daemon/Auth.cpp" line_range="276-277" />
<code_context>
+            // thread). After fork(), that thread doesn't exist, so the join
+            // blocks forever on a futex/semaphore.
+            //
+            // Register a pthread_atfork child handler here so that any
+            // grandchild processes spawned by PAM will have an atexit handler
+            // that runs _exit() first (LIFO), bypassing Qt's broken cleanup.
+            pthread_atfork(nullptr, nullptr, []() {
</code_context>
<issue_to_address>
**question (bug_risk):** Scope and lifetime of the atfork-based workaround may be broader than intended.

Since `pthread_atfork` is process-wide, this handler will run on every future `fork()`, not just those triggered indirectly by PAM, and will change termination behavior for all child processes by installing the `_exit(0)` atexit hook. If that broader effect isn’t intended, consider scoping the workaround more narrowly (e.g., via a helper process or only around the specific PAM operations known to fork) so other fork-using paths aren’t unintentionally altered.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/daemon/Auth.cpp
// Register a pthread_atfork child handler here so that any
// grandchild processes spawned by PAM will have an atexit handler
// that runs _exit() first (LIFO), bypassing Qt's broken cleanup.
pthread_atfork(nullptr, nullptr, []() {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): Consider handling the pthread_atfork return code and avoiding repeated registration.

pthread_atfork can fail (e.g., ENOMEM); right now that failure is silently ignored, so the workaround may never be installed and the caller can’t detect it. In addition, if this path is executed multiple times, each call adds another atfork handler that then adds another atexit handler in children, causing unbounded growth of handler lists. Please check and handle the pthread_atfork return value, and ensure registration happens only once per process (e.g., via std::once_flag).

Suggested implementation:

            // Register a pthread_atfork child handler here so that any
            // grandchild processes spawned by PAM will have an atexit handler
            // that runs _exit() first (LIFO), bypassing Qt's broken cleanup.
            //
            // Use std::call_once to avoid registering multiple handlers in the
            // same process, and check the pthread_atfork return value.
            static std::once_flag pamAtforkOnce;
            std::call_once(pamAtforkOnce, [] {
                const int rc = pthread_atfork(
                    nullptr,
                    nullptr,
                    []() {
                        atexit([]() { _exit(0); });
                    }
                );
                if (rc != 0) {
                    // Best-effort logging; failure means the workaround is not installed.
                    qWarning() << "Failed to register pthread_atfork handler for PAM:"
                               << strerror(rc);
                }
            });

            // Open session
            auto sessionEnv = openSessionInternal(env);

To compile successfully, you will also need to:

  1. Ensure the appropriate headers are included near the top of src/daemon/Auth.cpp (if they are not already):
    • #include <mutex> for std::once_flag and std::call_once.
    • #include <cstring> for strerror.
  2. Ensure that qWarning() is available in this translation unit (it usually is via common Qt headers such as <QDebug> or similar). If not already present, add the appropriate Qt debug/logging include used elsewhere in this project.
  3. If your project uses a different logging mechanism than qWarning(), replace the logging line inside the if (rc != 0) block with the project's standard error-logging macro or function.

Comment thread src/daemon/Auth.cpp
Comment on lines +276 to +277
// Register a pthread_atfork child handler here so that any
// grandchild processes spawned by PAM will have an atexit handler
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

question (bug_risk): Scope and lifetime of the atfork-based workaround may be broader than intended.

Since pthread_atfork is process-wide, this handler will run on every future fork(), not just those triggered indirectly by PAM, and will change termination behavior for all child processes by installing the _exit(0) atexit hook. If that broader effect isn’t intended, consider scoping the workaround more narrowly (e.g., via a helper process or only around the specific PAM operations known to fork) so other fork-using paths aren’t unintentionally altered.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a post-fork() deadlock during PAM session opening in DDM’s session leader by ensuring fork-children created inside PAM modules don’t run Qt’s atexit cleanup (notably QtDBus thread teardown) that can hang after fork().

Changes:

  • Add a pthread_atfork() child handler in the session leader before pam_open_session() work begins.
  • In the atfork child handler, register an atexit() handler that immediately terminates via _exit() to bypass Qt’s post-fork cleanup path.

Comment thread src/daemon/Auth.cpp
Comment on lines +279 to +281
pthread_atfork(nullptr, nullptr, []() {
atexit([]() { _exit(0); });
});
@deepin-bot
Copy link
Copy Markdown

deepin-bot Bot commented May 9, 2026

TAG Bot

New tag: 0.3.4
DISTRIBUTION: unstable
Suggest: synchronizing this PR through rebase #90

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.

3 participants