Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ include(FeatureSummary)

# Options
option(BUILD_MAN_PAGES "Build man pages" OFF)
option(BUILD_TESTING "Build unit tests" OFF)
option(WERROR "Build with -Werror" OFF)

set(CMAKE_CXX_STANDARD 20)
Expand Down Expand Up @@ -163,6 +164,11 @@ add_subdirectory(data)
add_subdirectory(services)
add_subdirectory(src)

if(BUILD_TESTING)
enable_testing()
add_subdirectory(test)
endif()

# Display feature summary
feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES)

Expand Down
33 changes: 32 additions & 1 deletion README.zh_CN.md
Original file line number Diff line number Diff line change
@@ -1 +1,32 @@
TODO:
# DDM

`ddm` 项目是基于 `SDDM` 的显示管理器分支。

## 依赖项

检查 `debian/control` 中的构建时和运行时依赖项,或者使用 `cmake` 来检查缺失的所需依赖项。
## Building

常规的 CMake 构建步骤适用,简而言之:

```shell
$ cmake -Bbuild
$ cmake --build build
$ cmake --install build # 只有在你知道自己在做什么的情况下才这样做。
```

提供了一个 `debian` 文件夹,用于在 *deepin* Linux 桌面发行版下构建该软件包。 要构建该包,请使用以下命令:

```shell
$ sudo apt build-dep . # install build dependencies
$ dpkg-buildpackage -uc -us -nc -b # build binary package(s)
```

## 参与方式

- [通过 GitHub 提交代码](https://github.com/linuxdeepin/ddm/)
- [向 GitHub 问题或 GitHub 讨论中提交错误或建议](https://github.com/linuxdeepin/developer-center/issues/new/choose)

## 许可证

**ddm** 采用 GPL-2.0+ 许可证。有关详细信息,请参阅 REUSE 文件。
44 changes: 37 additions & 7 deletions src/daemon/Auth.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@

#include "DaemonApp.h"
#include "DdeSeatdControl.h"
#include "ForkExitGuard.h"
#include "Login1Manager.h"

Check warning on line 11 in src/daemon/Auth.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "Login1Manager.h" not found.
#include "Login1Session.h"

Check warning on line 12 in src/daemon/Auth.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "Login1Session.h" not found.
#include "SignalHandler.h"

Check warning on line 13 in src/daemon/Auth.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "SignalHandler.h" not found.
#include "TtyUtils.h"
#include "TreelandConnector.h"

Expand Down Expand Up @@ -281,6 +282,26 @@
return -1;
}

// Install the forked-child exit bypass here, in the daemon, while
// libc locks are still consistent and atexit() is safe to call.
//
// Exit handlers are inherited across fork(): without the bypass, a
// forked child calling exit() would run the daemon's cleanup
// handlers (e.g. libQt6DBus joining its dispatcher thread), which
// deadlock because those threads do not exist after fork(). The
// bypass is registered last, so it runs first (LIFO) in every
// descendant -- grandchildren forked by PAM modules (e.g.
// pam_gnome_keyring) and QProcess children included -- and _exit(0)s
// before any inherited handler is reached. The daemon itself is
// excluded via the owner PID check and keeps its regular cleanup.
//
// This must never be done from a pthread_atfork child handler:
// atexit() is not async-signal-safe and may block forever on libc
// internal locks inherited in a locked state from another thread of
// the forking process.
if (!ForkExitGuard::install())
qWarning() << "[Auth] Failed to install the forked-child exit bypass";

sessionLeaderPid = fork();
switch (sessionLeaderPid) {
case -1: {
Expand Down Expand Up @@ -319,23 +340,32 @@
env.insert(QStringLiteral("LOGNAME"), QString::fromLocal8Bit(pw->pw_name));
}

// Neither this process nor anything forked from it may run the
// exit handlers inherited from the daemon: they were registered
// before fork() and may wait on threads that no longer exist
// (e.g. libQt6DBus joining its dispatcher thread blocks forever
// after fork()). The session leader therefore terminates with
// _exit() directly, and descendants forked by PAM modules which
// call exit() (e.g. pam_gnome_keyring) are terminated by the
// ForkExitGuard handler the daemon installed before fork().

// Open session
auto sessionEnv = openSessionInternal(env);
if (!sessionEnv.has_value()) {
qCritical() << "[SessionLeader] Failed to open session. Exit now.";
exit(1);
_exit(1);
}
env = *sessionEnv;

// Retrieve XDG_SESSION_ID
xdgSessionId = env.value(QStringLiteral("XDG_SESSION_ID")).toInt();
if (xdgSessionId <= 0) {
qCritical() << "[SessionLeader] Invalid XDG_SESSION_ID from pam_open_session()";
exit(1);
_exit(1);
}
if (write(pipefd[1], &xdgSessionId, sizeof(int)) != sizeof(int)) {
qCritical() << "[SessionLeader] Failed to write XDG_SESSION_ID to parent process!";
exit(1);
_exit(1);
}

// RUN!!!
Expand All @@ -344,14 +374,14 @@
session.start(command, type, cookie);
if (!session.waitForStarted()) {
qCritical() << "[SessionLeader] Failed to start session process. Exit now.";
exit(1);
_exit(1);
}

// Send session PID to parent
sessionPid = session.processId();
if (write(pipefd[1], &sessionPid, sizeof(qint64)) != sizeof(qint64)) {
qCritical() << "[SessionLeader] Failed to write session PID to parent process!";
exit(1);
_exit(1);
}
qInfo() << "[SessionLeader] Session started with PID" << sessionPid;

Expand All @@ -360,11 +390,11 @@
// Handle session end
if (session.exitStatus() == QProcess::CrashExit) {
qCritical() << "[SessionLeader] Session process crashed. Exit now.";
exit(1);
_exit(1);
}
qInfo() << "[SessionLeader] Session process finished with exit code"
<< session.exitCode() << ". Exiting.";
exit(session.exitCode());
_exit(session.exitCode());
}
default: {
// Parent process
Expand Down
71 changes: 71 additions & 0 deletions src/daemon/ForkExitGuard.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (C) 2026 UnionTech Software Technology Co., Ltd.
// SPDX-License-Identifier: GPL-2.0-or-later

#ifndef DDM_FORKEXITGUARD_H
#define DDM_FORKEXITGUARD_H

#include <cstdlib>

Check warning on line 7 in src/daemon/ForkExitGuard.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <cstdlib> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <sys/types.h>

Check warning on line 8 in src/daemon/ForkExitGuard.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <sys/types.h> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <unistd.h>

Check warning on line 9 in src/daemon/ForkExitGuard.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <unistd.h> not found. Please note: Cppcheck does not need standard library headers to get proper results.

namespace DDM::ForkExitGuard {
/** PID of the process which installed the exit bypass. Forked children
* inherit this value unchanged, so inside a child it still refers to
* the original (daemon) process. -1 means "not installed". */
inline pid_t s_ownerPid{ -1 };

/**
* atexit(3) handler terminating every process except the one which
* installed it (i.e. every fork()ed descendant) immediately with
* _exit(0), so that exit() in a forked child skips all the other
* exit handlers.
*
* Exit handlers registered via atexit()/__cxa_atexit() (such as Qt's
* static cleanup, e.g. libQt6DBus joining its dispatcher thread) are
* inherited across fork(), but the threads and other resources they
* operate on are not: running them in a forked child can deadlock
* forever. Exit handlers run in LIFO order, so this handler bypasses
* every handler registered before the last install() call.
*/
inline void bypassInheritedCleanup() {
if (::getpid() != s_ownerPid)
::_exit(0);
}

/**
* Installs (or refreshes) the exit bypass for forked children.
*
* Must be called from the original daemon process before fork(),
* where all libc locks are in a consistent state. It must never be
* called from a pthread_atfork child handler or any other post-fork
* child context: atexit() is not async-signal-safe and may block
* forever acquiring libc internal locks which were inherited in a
* locked state from another thread of the forking process.
*
* Calling this again before every fork() re-registers the handler at
* the end of the exit handler list, so that it also runs before any
* handler registered since the previous install(). The duplicate
* registrations are harmless in the owner process, where the handler
* is a no-op and the regular cleanup runs unchanged.
*
* @return true on success, false if the handler could not be registered
*/
inline bool install() {
if (::atexit(bypassInheritedCleanup) != 0)
return false;
s_ownerPid = ::getpid();
return true;
}

/** Whether the exit bypass has been installed in this process image. */
inline bool isInstalled() {
return s_ownerPid != -1;
}

/** PID of the process which installed the bypass, -1 if not installed. */
inline pid_t ownerPid() {
return s_ownerPid;
}
}

#endif // DDM_FORKEXITGUARD_H
15 changes: 15 additions & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
find_package(Qt6 CONFIG REQUIRED Test)

add_executable(tst_forkexitguard
tst_forkexitguard.cpp
)

target_include_directories(tst_forkexitguard PRIVATE
"${CMAKE_SOURCE_DIR}/src/daemon"
)

target_link_libraries(tst_forkexitguard PRIVATE
Qt6::Test
)

add_test(NAME tst_forkexitguard COMMAND tst_forkexitguard)
Loading
Loading